chore: 忽略 electron-userdata,更新 agent-core 源码
This commit is contained in:
@@ -13,12 +13,16 @@ export { llmConfigManager } from './llm/config'
|
||||
|
||||
// ============ 启动配置 ============
|
||||
|
||||
const WS_PORT = Number(process.env.AGENT_CORE_PORT) || 8787
|
||||
// WebSocket 监听在 UDS / 命名管道上,不占用 TCP 端口
|
||||
const IS_WIN = process.platform === 'win32'
|
||||
const SOCKET_PATH =
|
||||
process.env.AGENT_CORE_SOCKET ||
|
||||
(IS_WIN ? '\\\\.\\pipe\\dndclaw-agent-core' : '/tmp/dndclaw-agent-core.sock')
|
||||
const DB_DIR = process.env.AGENT_CORE_DB_DIR || './data'
|
||||
|
||||
/**
|
||||
* agent-core 入口
|
||||
* 启动 SQLite + WebSocket 服务器
|
||||
* 启动 SQLite + WebSocket 服务器(UDS/命名管道)
|
||||
*/
|
||||
function main() {
|
||||
console.log('========================================')
|
||||
@@ -29,9 +33,9 @@ function main() {
|
||||
initDatabase(DB_DIR)
|
||||
console.log(`[core] 数据库目录: ${DB_DIR}`)
|
||||
|
||||
// 2. 启动 WebSocket
|
||||
const wss = startWebSocketServer(WS_PORT)
|
||||
console.log(`[core] WebSocket 端口: ${WS_PORT}`)
|
||||
// 2. 启动 WebSocket(监听 UDS/命名管道)
|
||||
const wss = startWebSocketServer(SOCKET_PATH)
|
||||
console.log(`[core] WebSocket 管道: ${SOCKET_PATH}`)
|
||||
|
||||
// 3. 启动云端同步 Worker(本地变更 → outbox → 云端推送;云端变更 → 拉取合并)
|
||||
syncWorker.start()
|
||||
|
||||
@@ -4,10 +4,8 @@ import type { OutboxRecord } from './outbox'
|
||||
* 云端同步客户端
|
||||
*
|
||||
* 云端地址通过环境变量配置:
|
||||
* CLOUD_SYNC_URL - 云端同步服务地址(如 http://localhost:9000)
|
||||
* CLOUD_SYNC_URL - 云端同步服务地址(如 https://service.hotblaz.com/api/data)
|
||||
* CLOUD_SYNC_TOKEN - 鉴权 Token(可选)
|
||||
*
|
||||
* 端口留空,等用户提供后通过环境变量传入即可。
|
||||
*/
|
||||
|
||||
const CLOUD_URL = process.env.CLOUD_SYNC_URL || ''
|
||||
@@ -33,52 +31,113 @@ export interface CloudPullResult {
|
||||
|
||||
/**
|
||||
* 推送一批 outbox 记录到云端
|
||||
*
|
||||
* 按表分组,使用对应表的批量同步接口:
|
||||
* - llm_configs → POST {CLOUD_URL}/llm-configs/sync(幂等 upsert + 批量)
|
||||
* - 其他表 → 暂未实现云端批量接口,标记失败
|
||||
*/
|
||||
export async function pushToCloud(records: OutboxRecord[]): Promise<Map<string, CloudPushResult>> {
|
||||
const results = new Map<string, CloudPushResult>()
|
||||
|
||||
if (!CLOUD_URL) {
|
||||
// 云端地址未配置,全部标记失败
|
||||
for (const r of records) {
|
||||
results.set(r.id, { success: false, error: 'CLOUD_SYNC_URL 未配置' })
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// 按表分组
|
||||
const grouped = new Map<string, OutboxRecord[]>()
|
||||
for (const r of records) {
|
||||
try {
|
||||
const res = await fetch(`${CLOUD_URL}/sync/push`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(CLOUD_TOKEN ? { Authorization: `Bearer ${CLOUD_TOKEN}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
table: r.table_name,
|
||||
record_id: r.record_id,
|
||||
operation: r.operation,
|
||||
payload: JSON.parse(r.payload),
|
||||
local_updated_at: r.updated_at
|
||||
})
|
||||
})
|
||||
const list = grouped.get(r.table_name) || []
|
||||
list.push(r)
|
||||
grouped.set(r.table_name, list)
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
results.set(r.id, { success: true })
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
results.set(r.id, {
|
||||
success: false,
|
||||
error: `HTTP ${res.status}: ${data?.message || res.statusText}`
|
||||
})
|
||||
for (const [tableName, tableRecords] of grouped) {
|
||||
if (tableName === 'llm_configs') {
|
||||
await pushLLMConfigs(tableRecords, results)
|
||||
} else {
|
||||
// 其他表暂无云端批量接口
|
||||
for (const r of tableRecords) {
|
||||
results.set(r.id, { success: false, error: `云端暂不支持表 ${tableName} 的同步` })
|
||||
}
|
||||
} catch (e: any) {
|
||||
results.set(r.id, { success: false, error: e?.message || String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送 llm_configs 变更到云端批量同步接口
|
||||
*
|
||||
* 云端接口:POST {CLOUD_URL}/llm-configs/sync
|
||||
* 请求体:{ items: [{ id, name, provider, config, isActive, operation }] }
|
||||
* operation: 'upsert' | 'delete'
|
||||
*/
|
||||
async function pushLLMConfigs(
|
||||
records: OutboxRecord[],
|
||||
results: Map<string, CloudPushResult>
|
||||
) {
|
||||
const items = records.map((r) => {
|
||||
const payload = JSON.parse(r.payload || '{}')
|
||||
const operation = r.operation === 'DELETE' ? 'delete' : 'upsert'
|
||||
return {
|
||||
id: r.record_id,
|
||||
name: payload.name,
|
||||
provider: payload.provider,
|
||||
config: payload.config,
|
||||
isActive: payload.is_active === 1 || payload.isActive === true,
|
||||
operation
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CLOUD_URL}/llm-configs/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(CLOUD_TOKEN ? { Authorization: `Bearer ${CLOUD_TOKEN}` } : {})
|
||||
},
|
||||
body: JSON.stringify({ items })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
const errorMsg = `HTTP ${res.status}: ${data?.error || res.statusText}`
|
||||
for (const r of records) {
|
||||
results.set(r.id, { success: false, error: errorMsg })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
// 云端返回 { ok, total, results: [{ id, status, reason }] }
|
||||
const cloudResults: Array<{ id: string; status: string; reason?: string }> = data?.results || []
|
||||
const resultMap = new Map(cloudResults.map((cr) => [cr.id, cr]))
|
||||
|
||||
for (const r of records) {
|
||||
const cr = resultMap.get(r.record_id)
|
||||
if (cr && (cr.status === 'created' || cr.status === 'updated' || cr.status === 'deleted')) {
|
||||
results.set(r.id, { success: true })
|
||||
} else if (cr && cr.status === 'skipped') {
|
||||
// 跳过(如删除不存在的记录),视为成功
|
||||
results.set(r.id, { success: true })
|
||||
} else {
|
||||
results.set(r.id, {
|
||||
success: false,
|
||||
error: cr?.reason || `云端返回状态: ${cr?.status || 'unknown'}`
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.message || String(e)
|
||||
for (const r of records) {
|
||||
results.set(r.id, { success: false, error: errorMsg })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从云端拉取变更记录(增量)
|
||||
* @param cursor 上次拉取的游标,为空则全量拉取
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createServer } from 'http'
|
||||
import { WebSocketServer, WebSocket } from 'ws'
|
||||
import { llmConfigManager } from '../llm/config'
|
||||
import type { LLMConfig } from '../types'
|
||||
@@ -17,13 +18,28 @@ interface WSMessage {
|
||||
const clients = new Set<WebSocket>()
|
||||
|
||||
/**
|
||||
* 启动 WebSocket 服务器
|
||||
* @param port 端口号
|
||||
* 启动 WebSocket 服务器(监听在 UDS / 命名管道上,不占用 TCP 端口)
|
||||
*
|
||||
* - Windows: 命名管道 \\.\pipe\dndclaw-agent-core
|
||||
* - Linux/Mac: UDS socket 文件
|
||||
*
|
||||
* @param socketPath 命名管道或 UDS socket 路径
|
||||
*/
|
||||
export function startWebSocketServer(port: number): WebSocketServer {
|
||||
const wss = new WebSocketServer({ port })
|
||||
export function startWebSocketServer(socketPath: string): WebSocketServer {
|
||||
const server = createServer()
|
||||
const wss = new WebSocketServer({ server })
|
||||
|
||||
console.log(`[ws] WebSocket 服务器启动于 ws://localhost:${port}`)
|
||||
server.listen(socketPath, () => {
|
||||
console.log(`[ws] WebSocket 服务器启动于 ${socketPath}`)
|
||||
})
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`[ws] 管道已被占用: ${socketPath}`)
|
||||
} else {
|
||||
console.error('[ws] 服务器错误:', err.message)
|
||||
}
|
||||
})
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws)
|
||||
|
||||
Reference in New Issue
Block a user