mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-27 01:01:51 +08:00
206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
/**
|
|
* ChatLab API — Data source configuration management (hierarchical model)
|
|
* Persisted to userData/settings/data-sources.json
|
|
*
|
|
* DataSource (server) → ImportSession[] (subscribed conversations)
|
|
*/
|
|
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import * as crypto from 'crypto'
|
|
import { getSettingsDir, ensureDir } from '../paths'
|
|
import { apiLogger } from './logger'
|
|
|
|
const CONFIG_FILE = 'data-sources.json'
|
|
|
|
export interface ImportSession {
|
|
id: string
|
|
name: string
|
|
remoteSessionId: string
|
|
targetSessionId: string
|
|
lastPullAt: number
|
|
lastStatus: 'idle' | 'success' | 'error'
|
|
lastError: string
|
|
lastNewMessages: number
|
|
}
|
|
|
|
export interface DataSource {
|
|
id: string
|
|
name: string
|
|
baseUrl: string
|
|
token: string
|
|
intervalMinutes: number
|
|
enabled: boolean
|
|
createdAt: number
|
|
sessions: ImportSession[]
|
|
}
|
|
|
|
function getConfigPath(): string {
|
|
return path.join(getSettingsDir(), CONFIG_FILE)
|
|
}
|
|
|
|
/**
|
|
* Validate that parsed JSON conforms to the hierarchical DataSource[] schema.
|
|
* Returns false for legacy flat format (<=0.17.3) or any other unexpected shape.
|
|
*/
|
|
function isValidDataSourceArray(data: unknown): data is DataSource[] {
|
|
if (!Array.isArray(data)) return false
|
|
return data.every((item) => item && typeof item === 'object' && Array.isArray(item.sessions))
|
|
}
|
|
|
|
export function loadDataSources(): DataSource[] {
|
|
try {
|
|
const filePath = getConfigPath()
|
|
if (fs.existsSync(filePath)) {
|
|
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
const parsed = JSON.parse(raw)
|
|
|
|
if (!isValidDataSourceArray(parsed)) {
|
|
apiLogger.warn(
|
|
'[DataSource] Incompatible config format detected (likely pre-0.17.4), returning empty in memory.'
|
|
)
|
|
return []
|
|
}
|
|
|
|
return parsed
|
|
}
|
|
} catch (err) {
|
|
apiLogger.error('[DataSource] Failed to load config', err)
|
|
}
|
|
return []
|
|
}
|
|
|
|
export function saveDataSources(sources: DataSource[]): void {
|
|
try {
|
|
ensureDir(getSettingsDir())
|
|
fs.writeFileSync(getConfigPath(), JSON.stringify(sources, null, 2), 'utf-8')
|
|
} catch (err) {
|
|
apiLogger.error('[DataSource] Failed to save config', err)
|
|
}
|
|
}
|
|
|
|
export function generateId(prefix: string = 'ds'): string {
|
|
return `${prefix}_${crypto.randomBytes(6).toString('hex')}`
|
|
}
|
|
|
|
export function normalizeBaseUrl(input: string): string {
|
|
let url = input.trim().replace(/\/+$/, '')
|
|
if (url && !/^https?:\/\//i.test(url)) {
|
|
url = `http://${url}`
|
|
}
|
|
if (url && !url.endsWith('/api/v1')) {
|
|
url = url.replace(/\/api\/v1$/, '') + '/api/v1'
|
|
}
|
|
return url
|
|
}
|
|
|
|
// ==================== DataSource CRUD ====================
|
|
|
|
export function addDataSource(partial: {
|
|
name?: string
|
|
baseUrl: string
|
|
token: string
|
|
intervalMinutes: number
|
|
}): DataSource {
|
|
const sources = loadDataSources()
|
|
const ds: DataSource = {
|
|
id: generateId('src'),
|
|
name: partial.name || '',
|
|
baseUrl: normalizeBaseUrl(partial.baseUrl),
|
|
token: partial.token,
|
|
intervalMinutes: partial.intervalMinutes,
|
|
enabled: true,
|
|
createdAt: Math.floor(Date.now() / 1000),
|
|
sessions: [],
|
|
}
|
|
sources.push(ds)
|
|
saveDataSources(sources)
|
|
return ds
|
|
}
|
|
|
|
export function updateDataSource(
|
|
id: string,
|
|
updates: Partial<Pick<DataSource, 'name' | 'baseUrl' | 'token' | 'intervalMinutes' | 'enabled'>>
|
|
): DataSource | null {
|
|
const sources = loadDataSources()
|
|
const idx = sources.findIndex((s) => s.id === id)
|
|
if (idx === -1) return null
|
|
const ds = sources[idx]
|
|
if (updates.name !== undefined) ds.name = updates.name
|
|
if (updates.baseUrl !== undefined) ds.baseUrl = normalizeBaseUrl(updates.baseUrl)
|
|
if (updates.token !== undefined) ds.token = updates.token
|
|
if (updates.intervalMinutes !== undefined) ds.intervalMinutes = updates.intervalMinutes
|
|
if (updates.enabled !== undefined) ds.enabled = updates.enabled
|
|
saveDataSources(sources)
|
|
return ds
|
|
}
|
|
|
|
export function deleteDataSource(id: string): boolean {
|
|
const sources = loadDataSources()
|
|
const filtered = sources.filter((s) => s.id !== id)
|
|
if (filtered.length === sources.length) return false
|
|
saveDataSources(filtered)
|
|
return true
|
|
}
|
|
|
|
export function getDataSource(id: string): DataSource | null {
|
|
const sources = loadDataSources()
|
|
return sources.find((s) => s.id === id) || null
|
|
}
|
|
|
|
// ==================== ImportSession CRUD ====================
|
|
|
|
export function addImportSessions(
|
|
sourceId: string,
|
|
sessions: Array<{ name: string; remoteSessionId: string }>
|
|
): ImportSession[] {
|
|
const sources = loadDataSources()
|
|
const ds = sources.find((s) => s.id === sourceId)
|
|
if (!ds) return []
|
|
|
|
const added: ImportSession[] = []
|
|
for (const sess of sessions) {
|
|
if (ds.sessions.some((s) => s.remoteSessionId === sess.remoteSessionId)) continue
|
|
const imp: ImportSession = {
|
|
id: generateId('sess'),
|
|
name: sess.name,
|
|
remoteSessionId: sess.remoteSessionId,
|
|
targetSessionId: '',
|
|
lastPullAt: 0,
|
|
lastStatus: 'idle',
|
|
lastError: '',
|
|
lastNewMessages: 0,
|
|
}
|
|
ds.sessions.push(imp)
|
|
added.push(imp)
|
|
}
|
|
saveDataSources(sources)
|
|
return added
|
|
}
|
|
|
|
export function removeImportSession(sourceId: string, sessionId: string): boolean {
|
|
const sources = loadDataSources()
|
|
const ds = sources.find((s) => s.id === sourceId)
|
|
if (!ds) return false
|
|
const before = ds.sessions.length
|
|
ds.sessions = ds.sessions.filter((s) => s.id !== sessionId)
|
|
if (ds.sessions.length === before) return false
|
|
saveDataSources(sources)
|
|
return true
|
|
}
|
|
|
|
export function updateImportSession(
|
|
sourceId: string,
|
|
sessionId: string,
|
|
updates: Partial<ImportSession>
|
|
): ImportSession | null {
|
|
const sources = loadDataSources()
|
|
const ds = sources.find((s) => s.id === sourceId)
|
|
if (!ds) return null
|
|
const sess = ds.sessions.find((s) => s.id === sessionId)
|
|
if (!sess) return null
|
|
Object.assign(sess, updates, { id: sessionId })
|
|
saveDataSources(sources)
|
|
return sess
|
|
}
|