feat(cloudtentacles): 后端核心数据结构多账号支持 - source-config/session-state 改造 + resolvePersistedCloudtentaclesContext 增加 sourceKey 参数
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,36 +11,101 @@ export function getCloudtentaclesSessionFilePath() {
|
|||||||
return CLOUDTENTACLES_SESSION_FILE_PATH
|
return CLOUDTENTACLES_SESSION_FILE_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible: returns the session for key='default'.
|
||||||
|
* Old callers that expect a single session object still work.
|
||||||
|
*/
|
||||||
export function getCloudtentaclesSessionState() {
|
export function getCloudtentaclesSessionState() {
|
||||||
return loadCloudtentaclesSessionStateFromFile()
|
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||||
|
return states.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a session by its sourceKey. Returns null if not found.
|
||||||
|
*/
|
||||||
|
export function getCloudtentaclesSessionStateByKey(sourceKey) {
|
||||||
|
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) return null
|
||||||
|
return states.sessions[key] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the entire session states map: { sessions: { 'default': {...}, ... } }.
|
||||||
|
*/
|
||||||
|
export function getAllCloudtentaclesSessionStates() {
|
||||||
|
return loadCloudtentaclesSessionStatesFromFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible save: accepts both old single-object format
|
||||||
|
* and new sessions-map format, normalizes, and persists.
|
||||||
|
*/
|
||||||
export function saveCloudtentaclesSessionState(rawValue) {
|
export function saveCloudtentaclesSessionState(rawValue) {
|
||||||
const normalized = normalizeCloudtentaclesSessionState(rawValue)
|
const normalized = normalizeSessionStatesFile(rawValue)
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||||
return normalized
|
return normalized.sessions['default'] || createDefaultCloudtentaclesSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a session for a specific sourceKey.
|
||||||
|
*/
|
||||||
|
export function saveCloudtentaclesSessionStateByKey(sourceKey, rawValue) {
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('saveCloudtentaclesSessionStateByKey: sourceKey is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const states = loadCloudtentaclesSessionStatesFromFile()
|
||||||
|
states.sessions[key] = normalizeCloudtentaclesSessionState(rawValue)
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
|
||||||
|
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(states, null, 2)}\n`, 'utf8')
|
||||||
|
return states.sessions[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible clear: clears the 'default' session only.
|
||||||
|
*/
|
||||||
export function clearCloudtentaclesSessionState() {
|
export function clearCloudtentaclesSessionState() {
|
||||||
|
return clearCloudtentaclesSessionStateByKey('default')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a session by sourceKey (sets it to default empty state).
|
||||||
|
*/
|
||||||
|
export function clearCloudtentaclesSessionStateByKey(sourceKey) {
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('clearCloudtentaclesSessionStateByKey: sourceKey is required')
|
||||||
|
}
|
||||||
|
|
||||||
const cleared = createDefaultCloudtentaclesSessionState()
|
const cleared = createDefaultCloudtentaclesSessionState()
|
||||||
saveCloudtentaclesSessionState(cleared)
|
saveCloudtentaclesSessionStateByKey(key, cleared)
|
||||||
return cleared
|
return cleared
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCloudtentaclesSessionStateFromFile() {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function loadCloudtentaclesSessionStatesFromFile() {
|
||||||
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
|
||||||
return createDefaultCloudtentaclesSessionState()
|
return createDefaultCloudtentaclesSessionStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8')
|
const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8')
|
||||||
return normalizeCloudtentaclesSessionState(JSON.parse(rawText))
|
return normalizeSessionStatesFile(JSON.parse(rawText))
|
||||||
} catch {
|
} catch {
|
||||||
return createDefaultCloudtentaclesSessionState()
|
return createDefaultCloudtentaclesSessionStates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a single session state item.
|
||||||
|
*/
|
||||||
function normalizeCloudtentaclesSessionState(rawValue) {
|
function normalizeCloudtentaclesSessionState(rawValue) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
|
||||||
@@ -55,6 +120,31 @@ function normalizeCloudtentaclesSessionState(rawValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the overall session states file format.
|
||||||
|
* Handles old format (single object without sessions key) by auto-wrapping
|
||||||
|
* into { sessions: { 'default': ... } }.
|
||||||
|
*/
|
||||||
|
function normalizeSessionStatesFile(rawValue) {
|
||||||
|
// Old format: { token: 'xxx', ... } (single object, no sessions key)
|
||||||
|
if (isPlainObject(rawValue) && !rawValue.sessions) {
|
||||||
|
return {
|
||||||
|
sessions: {
|
||||||
|
'default': normalizeCloudtentaclesSessionState(rawValue),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New format: { sessions: { 'default': {...}, ... } }
|
||||||
|
return {
|
||||||
|
sessions: isPlainObject(rawValue?.sessions)
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(rawValue.sessions).map(([k, v]) => [k, normalizeCloudtentaclesSessionState(v)])
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createDefaultCloudtentaclesSessionState() {
|
function createDefaultCloudtentaclesSessionState() {
|
||||||
return {
|
return {
|
||||||
token: '',
|
token: '',
|
||||||
@@ -67,6 +157,14 @@ function createDefaultCloudtentaclesSessionState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDefaultCloudtentaclesSessionStates() {
|
||||||
|
return {
|
||||||
|
sessions: {
|
||||||
|
'default': createDefaultCloudtentaclesSessionState(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeInteger(value, fallback) {
|
function normalizeInteger(value, fallback) {
|
||||||
const parsed = Number(value)
|
const parsed = Number(value)
|
||||||
return Number.isInteger(parsed) ? parsed : fallback
|
return Number.isInteger(parsed) ? parsed : fallback
|
||||||
|
|||||||
@@ -11,35 +11,128 @@ export function getCloudtentaclesSourcesFilePath() {
|
|||||||
return CLOUDTENTACLES_SOURCES_FILE_PATH
|
return CLOUDTENTACLES_SOURCES_FILE_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible: returns the source with key='default'.
|
||||||
|
* Old callers that expect a single source object still work.
|
||||||
|
*/
|
||||||
export function getCloudtentaclesSourceConfig() {
|
export function getCloudtentaclesSourceConfig() {
|
||||||
return loadCloudtentaclesSourceConfigFromFile()
|
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||||
|
const defaultSource = config.sources.find(s => s.key === 'default')
|
||||||
|
return defaultSource || normalizeCloudtentaclesSourceItem({ key: 'default' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a source item by its key. Returns null if not found.
|
||||||
|
*/
|
||||||
|
export function getCloudtentaclesSourceByKey(sourceKey) {
|
||||||
|
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) return null
|
||||||
|
return config.sources.find(s => s.key === key) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the entire normalized config: { enabled, sources }.
|
||||||
|
*/
|
||||||
|
export function listCloudtentaclesSources() {
|
||||||
|
return loadCloudtentaclesSourcesConfigFromFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible save: accepts both old single-object format
|
||||||
|
* and new list format, normalizes, and persists.
|
||||||
|
*/
|
||||||
export function saveCloudtentaclesSourceConfig(rawValue) {
|
export function saveCloudtentaclesSourceConfig(rawValue) {
|
||||||
const normalized = normalizeCloudtentaclesSourceConfig(rawValue)
|
const normalized = normalizeCloudtentaclesSourcesConfig(rawValue)
|
||||||
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||||
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCloudtentaclesSourceConfigFromFile() {
|
/**
|
||||||
|
* Save the entire list-format config object: { enabled, sources: [...] }.
|
||||||
|
*/
|
||||||
|
export function saveCloudtentaclesSourcesList(rawValue) {
|
||||||
|
return saveCloudtentaclesSourceConfig(rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update a single source item identified by sourceKey.
|
||||||
|
* If a source with the same key exists, it is replaced; otherwise it is appended.
|
||||||
|
*/
|
||||||
|
export function saveCloudtentaclesSourceByKey(sourceKey, data) {
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('saveCloudtentaclesSourceByKey: sourceKey is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||||
|
const normalizedItem = normalizeCloudtentaclesSourceItem({ ...data, key })
|
||||||
|
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
config.sources[existingIndex] = normalizedItem
|
||||||
|
} else {
|
||||||
|
config.sources.push(normalizedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||||
|
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single source by key. Throws if key is 'default' (cannot delete default source).
|
||||||
|
*/
|
||||||
|
export function deleteCloudtentaclesSourceByKey(sourceKey) {
|
||||||
|
const key = String(sourceKey || '').trim()
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('deleteCloudtentaclesSourceByKey: sourceKey is required')
|
||||||
|
}
|
||||||
|
if (key === 'default') {
|
||||||
|
throw new Error('deleteCloudtentaclesSourceByKey: cannot delete the default source')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = loadCloudtentaclesSourcesConfigFromFile()
|
||||||
|
const existingIndex = config.sources.findIndex(s => s.key === key)
|
||||||
|
|
||||||
|
if (existingIndex < 0) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
config.sources.splice(existingIndex, 1)
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true })
|
||||||
|
fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function loadCloudtentaclesSourcesConfigFromFile() {
|
||||||
if (!fs.existsSync(CLOUDTENTACLES_SOURCES_FILE_PATH)) {
|
if (!fs.existsSync(CLOUDTENTACLES_SOURCES_FILE_PATH)) {
|
||||||
return createDefaultCloudtentaclesSourceConfig()
|
return createDefaultCloudtentaclesSourcesConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawText = fs.readFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, 'utf8')
|
const rawText = fs.readFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, 'utf8')
|
||||||
return normalizeCloudtentaclesSourceConfig(JSON.parse(rawText))
|
return normalizeCloudtentaclesSourcesConfig(JSON.parse(rawText))
|
||||||
} catch {
|
} catch {
|
||||||
return createDefaultCloudtentaclesSourceConfig()
|
return createDefaultCloudtentaclesSourcesConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCloudtentaclesSourceConfig(rawValue) {
|
/**
|
||||||
|
* Normalize a single source item. Adds key (required) and label (optional).
|
||||||
|
*/
|
||||||
|
function normalizeCloudtentaclesSourceItem(rawValue) {
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
key: String(source.key || 'default').trim() || 'default',
|
||||||
|
label: String(source.label || '').trim(),
|
||||||
baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176',
|
baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176',
|
||||||
username: String(source.username || '').trim(),
|
username: String(source.username || '').trim(),
|
||||||
password: String(source.password || '').trim(),
|
password: String(source.password || '').trim(),
|
||||||
@@ -49,15 +142,40 @@ function normalizeCloudtentaclesSourceConfig(rawValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultCloudtentaclesSourceConfig() {
|
/**
|
||||||
|
* Normalize the overall config. Handles both old single-object format
|
||||||
|
* (auto-migrates to new list format) and new { enabled, sources } format.
|
||||||
|
*/
|
||||||
|
function normalizeCloudtentaclesSourcesConfig(rawValue) {
|
||||||
|
// Old format: { enabled: true, username: 'xxx', ... } (single object, no sources array)
|
||||||
|
if (isPlainObject(rawValue) && !Array.isArray(rawValue.sources)) {
|
||||||
|
return {
|
||||||
|
enabled: rawValue.enabled !== false,
|
||||||
|
sources: [
|
||||||
|
normalizeCloudtentaclesSourceItem({
|
||||||
|
key: 'default',
|
||||||
|
label: '默认账号',
|
||||||
|
...rawValue, // old fields auto-map to default source
|
||||||
|
}),
|
||||||
|
].filter(Boolean),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New format: { enabled, sources: [...] }
|
||||||
|
return {
|
||||||
|
enabled: isPlainObject(rawValue) ? rawValue.enabled !== false : true,
|
||||||
|
sources: isPlainObject(rawValue) && Array.isArray(rawValue.sources)
|
||||||
|
? rawValue.sources.map(s => normalizeCloudtentaclesSourceItem(s)).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultCloudtentaclesSourcesConfig() {
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
baseUrl: 'https://123.207.217.176',
|
sources: [
|
||||||
username: '',
|
normalizeCloudtentaclesSourceItem({ key: 'default', label: '默认账号' }),
|
||||||
password: '',
|
],
|
||||||
phone: '',
|
|
||||||
deviceId: '-',
|
|
||||||
deviceType: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user