@dcrays/dcgchat-test 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +167 -0
  2. package/index.ts +31 -0
  3. package/openclaw.plugin.json +12 -41
  4. package/package.json +39 -12
  5. package/src/agent.ts +592 -0
  6. package/src/bot.ts +676 -0
  7. package/src/channel/index.ts +329 -0
  8. package/src/channel/outboundMedia.ts +144 -0
  9. package/src/channel/outboundTarget.ts +24 -0
  10. package/src/channel/uploadMediaUrl.ts +76 -0
  11. package/src/cron/message.ts +109 -0
  12. package/src/cron/params.ts +166 -0
  13. package/src/cron/toolGuard.ts +285 -0
  14. package/src/cron/types.ts +15 -0
  15. package/src/gateway/index.ts +430 -0
  16. package/src/gateway/security.ts +95 -0
  17. package/src/gateway/socket.ts +256 -0
  18. package/src/libs/ali-oss-6.23.0.tgz +0 -0
  19. package/src/libs/axios-1.13.6.tgz +0 -0
  20. package/src/libs/md5-2.3.0.tgz +0 -0
  21. package/src/libs/mime-types-3.0.2.tgz +0 -0
  22. package/src/libs/unzipper-0.12.3.tgz +0 -0
  23. package/src/libs/ws-8.19.0.tgz +0 -0
  24. package/src/monitor.ts +269 -0
  25. package/src/request/api.ts +80 -0
  26. package/src/request/oss.ts +200 -0
  27. package/src/request/request.ts +191 -0
  28. package/src/request/userInfo.ts +44 -0
  29. package/src/session.ts +28 -0
  30. package/src/skill.ts +114 -0
  31. package/src/tool.ts +625 -0
  32. package/src/tools/messageTool.ts +334 -0
  33. package/src/tools/toolCallGuard.ts +323 -0
  34. package/src/transport.ts +217 -0
  35. package/src/types.ts +139 -0
  36. package/src/utils/agentErrors.ts +23 -0
  37. package/src/utils/constant.ts +60 -0
  38. package/src/utils/env-config.ts +19 -0
  39. package/src/utils/formatLlmInputEvent.ts +48 -0
  40. package/src/utils/gatewayMsgHandler.ts +89 -0
  41. package/src/utils/global.ts +306 -0
  42. package/src/utils/inboundTurnState.ts +66 -0
  43. package/src/utils/log.ts +77 -0
  44. package/src/utils/mediaAttached.ts +244 -0
  45. package/src/utils/mediaEmitter.ts +54 -0
  46. package/src/utils/outboundAssistantText.ts +110 -0
  47. package/src/utils/params.ts +104 -0
  48. package/src/utils/passTxt.ts +4 -0
  49. package/src/utils/resolveRegisterConfig.ts +33 -0
  50. package/src/utils/searchFile.ts +228 -0
  51. package/src/utils/sessionState.ts +137 -0
  52. package/src/utils/sessionTermination.ts +228 -0
  53. package/src/utils/streamMerge.ts +150 -0
  54. package/src/utils/subagentRunMap.ts +71 -0
  55. package/src/utils/workspaceFilePaths.ts +18 -0
  56. package/src/utils/wsMessageHandler.ts +124 -0
  57. package/src/utils/zipExtract.ts +97 -0
  58. package/src/utils/zipPath.ts +24 -0
  59. package/index.js +0 -304
package/src/agent.ts ADDED
@@ -0,0 +1,592 @@
1
+ import axios from 'axios'
2
+ import fs from 'fs'
3
+ import fsPromise from 'fs/promises'
4
+ import path from 'path'
5
+ import type { OpenClawConfig } from 'openclaw/plugin-sdk'
6
+ import { getDcgchatRuntime, getOpenClawConfig, getWorkspaceDir, getWsConnection, setOpenClawConfig } from './utils/global.js'
7
+ import { dcgLogger } from './utils/log.js'
8
+ import { isWsOpen } from './transport.js'
9
+ import { sendGatewayRpc } from './gateway/socket.js'
10
+ import { extractZipBufferToDirectory } from './utils/zipExtract.js'
11
+ import { retryWithBackoff, ENV } from './utils/constant.js'
12
+
13
+ interface AgentEventMessage {
14
+ url?: string
15
+ clone_code?: string
16
+ clone_codes?: string[]
17
+ code_list?: string[]
18
+ remove_files?: string[]
19
+ name?: string
20
+ description?: string
21
+ status?: string
22
+ [key: string]: unknown
23
+ }
24
+
25
+ function sendEvent(msgContent: AgentEventMessage) {
26
+ const ws = getWsConnection()
27
+ if (isWsOpen()) {
28
+ ws?.send(
29
+ JSON.stringify({
30
+ messageType: 'openclaw_bot_event',
31
+ source: 'client',
32
+ content: msgContent
33
+ })
34
+ )
35
+ dcgLogger(`agent安装: ${JSON.stringify(msgContent)}`)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 异步版本,避免阻塞事件循环
41
+ * workspace-${clone_code}/agent 存在,则复制到 agents/${clone_code}/agent
42
+ * @param clone_code
43
+ * @returns
44
+ */
45
+ async function copyAgentsFilesAsync(clone_code: string) {
46
+ const workspacePath = getWorkspaceDir()
47
+
48
+ if (!workspacePath) return
49
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
50
+ const agentDir = path.join(workspacePath, '../', `agents/${clone_code}`)
51
+ const sourceAgent = path.join(workspaceDir, 'agent')
52
+
53
+ dcgLogger(`copyAgentsFilesAsync agentDir=${agentDir} sourceAgent=${sourceAgent}`)
54
+
55
+ try {
56
+ const stat = await fsPromise.stat(sourceAgent).catch(() => null)
57
+ if (!stat || !stat.isDirectory()) return
58
+
59
+ await fsPromise.mkdir(agentDir, { recursive: true })
60
+
61
+ const dest = path.join(agentDir, 'agent')
62
+
63
+ await fsPromise.rm(dest, { recursive: true, force: true }).catch((e) => dcgLogger(`agent workspace cleanup failed: ${String(e)}`))
64
+
65
+ await fsPromise.cp(sourceAgent, dest, { recursive: true })
66
+
67
+ dcgLogger(`copyAgentsFilesAsync success ${sourceAgent} ==> ${dest}`)
68
+ } catch (err: unknown) {
69
+ dcgLogger(`copyAgentsFilesAsync failed: ${String(err)}`, 'error')
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Gateway 已写盘后,把 Runtime 当前配置同步到插件全局,避免 getOpenClawConfig() 仍是 register 时的旧快照。
75
+ * 专家会话若在缓存里没有 agents.list 条目,OpenClaw 会把 workspace 解析成 defaults.workspace/<id>,
76
+ * 与插件实际的 sibling `workspace-<id>` 不一致,必须先刷新或通过 agents.update 纠正。
77
+ */
78
+ export function refreshCachedOpenClawConfigFromRuntime(): void {
79
+ try {
80
+ const cfg = getDcgchatRuntime().config.loadConfig()
81
+ setOpenClawConfig(structuredClone(cfg) as OpenClawConfig)
82
+ dcgLogger('refreshCachedOpenClawConfigFromRuntime: ok')
83
+ } catch (err: unknown) {
84
+ dcgLogger(`refreshCachedOpenClawConfigFromRuntime failed: ${String(err)}`, 'error')
85
+ }
86
+ }
87
+
88
+ function findAgentEntry(cfg: OpenClawConfig | null, agentId: string) {
89
+ const needle = agentId.trim().toLowerCase()
90
+ return cfg?.agents?.list?.find((a) => (a?.id ?? '').trim().toLowerCase() === needle)
91
+ }
92
+
93
+ function workspacePathMatches(expected: string, configured?: string): boolean {
94
+ const w = configured?.trim()
95
+ if (!w) return false
96
+ return path.resolve(w) === expected
97
+ }
98
+
99
+ /**
100
+ * 确保网关配置里该专家的 workspace 指向 sibling `workspace-<id>`(与 agents.create / 解压目录一致)。
101
+ * 在首次会话消息早于插件缓存刷新、或 workspace 字段缺失/错误时修复路径。
102
+ */
103
+ export async function ensureExpertAgentWorkspaceBinding(agentId: string): Promise<void> {
104
+ const id = typeof agentId === 'string' ? agentId.trim() : ''
105
+ if (!id || id === 'main') return
106
+
107
+ refreshCachedOpenClawConfigFromRuntime()
108
+
109
+ const wsRoot = getWorkspaceDir()
110
+ if (!wsRoot) return
111
+
112
+ const expectedWorkspace = path.resolve(path.join(wsRoot, '..', `workspace-${id}`))
113
+ try {
114
+ await fsPromise.access(expectedWorkspace)
115
+ } catch (err) {
116
+ const errMsg = err instanceof Error ? err.message : String(err)
117
+ dcgLogger(`ensureExpertAgentWorkspaceBinding workspace access failed: ${errMsg}`, 'error')
118
+ return
119
+ }
120
+
121
+ let cfg = getOpenClawConfig()
122
+ let entry = findAgentEntry(cfg, id)
123
+
124
+ if (entry && workspacePathMatches(expectedWorkspace, entry.workspace)) return
125
+
126
+ if (entry && !workspacePathMatches(expectedWorkspace, entry.workspace)) {
127
+ try {
128
+ await sendGatewayRpc({
129
+ method: 'agents.update',
130
+ params: { agentId: id, workspace: expectedWorkspace }
131
+ })
132
+ refreshCachedOpenClawConfigFromRuntime()
133
+ } catch (err: unknown) {
134
+ dcgLogger(`ensureExpertAgentWorkspaceBinding agents.update failed after retries: ${String(err)}`, 'error')
135
+ }
136
+ return
137
+ }
138
+
139
+ try {
140
+ await sendGatewayRpc({
141
+ method: 'agents.create',
142
+ params: { name: id, workspace: expectedWorkspace }
143
+ })
144
+ refreshCachedOpenClawConfigFromRuntime()
145
+ } catch (err: unknown) {
146
+ const msg = String(err)
147
+ if (/already exists/i.test(msg)) {
148
+ refreshCachedOpenClawConfigFromRuntime()
149
+ cfg = getOpenClawConfig()
150
+ entry = findAgentEntry(cfg, id)
151
+ if (entry && !workspacePathMatches(expectedWorkspace, entry.workspace)) {
152
+ try {
153
+ await sendGatewayRpc({
154
+ method: 'agents.update',
155
+ params: { agentId: id, workspace: expectedWorkspace }
156
+ })
157
+ refreshCachedOpenClawConfigFromRuntime()
158
+ } catch (e2: unknown) {
159
+ dcgLogger(`ensureExpertAgentWorkspaceBinding repair workspace: ${String(e2)}`, 'error')
160
+ }
161
+ }
162
+ } else {
163
+ dcgLogger(`ensureExpertAgentWorkspaceBinding agents.create: ${msg}`, 'error')
164
+ }
165
+ }
166
+ }
167
+
168
+ async function onCreateAgent(params: AgentEventMessage) {
169
+ const { clone_code, name } = params
170
+ if (!clone_code) {
171
+ dcgLogger('onCreateAgent: clone_code is missing', 'error')
172
+ return
173
+ }
174
+ let createOk = false
175
+ try {
176
+ const workspacePath = getWorkspaceDir()
177
+ if (!workspacePath) return
178
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
179
+
180
+ dcgLogger(`onCreateAgent workspaceDir ==> ${workspaceDir}`);
181
+ // 如果尝试创建一个已存在的 agent,会报错。 agents.create failed: Error: agent "reviser-expert-11-2-37-16" already exists
182
+ await sendGatewayRpc({ method: 'agents.create', params: { name: clone_code, workspace: workspaceDir } })
183
+
184
+ createOk = true
185
+ } catch (err: unknown) {
186
+ dcgLogger(`agents.create failed: ${String(err)}`, 'error')
187
+ }
188
+ // Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
189
+ try {
190
+ await sendGatewayRpc({ method: 'agents.update', params: { name: name, agentId: clone_code } })
191
+ } catch (err: unknown) {
192
+ dcgLogger(`agents.update failed: ${String(err)}`, 'error')
193
+ }
194
+ if (createOk) {
195
+ refreshCachedOpenClawConfigFromRuntime()
196
+ }
197
+
198
+ await copyAgentsFilesAsync(clone_code)
199
+ sendEvent({ ...params, status: 'ok' })
200
+ }
201
+
202
+ // 校验Agent id 对应的文件目录是否存在
203
+ export function isAgentInstalled(agentId: string): boolean {
204
+ const workspacePath = getWorkspaceDir()
205
+ if (!workspacePath) return false
206
+ const agentWorkspaceDir = path.join(workspacePath, '../', `workspace-${agentId}`)
207
+ return fs.existsSync(agentWorkspaceDir)
208
+ }
209
+ export async function onRemoveAgent(msgContent: AgentEventMessage) {
210
+ const { clone_code } = msgContent
211
+ if (!clone_code) {
212
+ dcgLogger('onRemoveAgent: clone_code is missing', 'error')
213
+ return
214
+ }
215
+ clearAgentDownloadUrl(clone_code)
216
+ await removeAgentDownloadUrlMeta(clone_code)
217
+ await sendGatewayRpc({ method: 'agents.delete', params: { agentId: clone_code } })
218
+ }
219
+ export async function onUpdateAgent(msgContent: AgentEventMessage) {
220
+ const { code_list, url, remove_files } = msgContent
221
+ const list = (code_list || []).filter((item): item is string => typeof item === 'string' && Boolean(item.trim()))
222
+ const removeFiles = (remove_files || []).filter((item): item is string => typeof item === 'string' && Boolean(item.trim()))
223
+ if (!list?.length) {
224
+ dcgLogger('onUpdateAgent: clone_codes is missing', 'error')
225
+ return
226
+ }
227
+ if (!url) {
228
+ dcgLogger('onUpdateAgent: url is missing', 'error')
229
+ sendEvent({ ...msgContent, status: 'fail' })
230
+ return
231
+ }
232
+ const workspacePath = getWorkspaceDir()
233
+ const tempDir = path.join(workspacePath, '../', `-temp-${Date.now()}`)
234
+ try {
235
+ const response = await axios({ method: 'get', url, responseType: 'arraybuffer' })
236
+ const zipBuffer = Buffer.from(response.data)
237
+ dcgLogger(`onUpdateAgent download ok: ${zipBuffer.length} bytes`)
238
+
239
+ if (fs.existsSync(tempDir)) {
240
+ fs.rmSync(tempDir, { recursive: true, force: true })
241
+ }
242
+ fs.mkdirSync(tempDir, { recursive: true })
243
+
244
+ await extractZipBufferToDirectory(zipBuffer, tempDir)
245
+ const results = await Promise.all(
246
+ list.map(async (clone_code) => {
247
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
248
+ let installSucceeded = false
249
+ markAgentInstallStarted(clone_code)
250
+ try {
251
+ if (!fs.existsSync(workspaceDir)) {
252
+ dcgLogger(`onUpdateAgent file not found : ${clone_code}`)
253
+ return true
254
+ }
255
+ const overwriteFilesRecursively = (src: string, dest: string) => {
256
+ const entries = fs.readdirSync(src, { withFileTypes: true })
257
+ for (const entry of entries) {
258
+ const srcPath = path.join(src, entry.name)
259
+ const destPath = path.join(dest, entry.name)
260
+ if (entry.isDirectory()) {
261
+ if (!fs.existsSync(destPath)) {
262
+ fs.mkdirSync(destPath, { recursive: true })
263
+ }
264
+ overwriteFilesRecursively(srcPath, destPath)
265
+ } else {
266
+ if (fs.existsSync(destPath)) {
267
+ fs.rmSync(destPath, { force: true })
268
+ }
269
+ fs.copyFileSync(srcPath, destPath)
270
+ }
271
+ }
272
+ }
273
+ overwriteFilesRecursively(tempDir, workspaceDir)
274
+ removeFiles.forEach((file) => {
275
+ const filePath = path.join(workspaceDir, file)
276
+ if (fs.existsSync(filePath)) {
277
+ fs.rmSync(filePath, { recursive: true, force: true })
278
+ }
279
+ })
280
+ await sendGatewayRpc({ method: 'agents.update', params: { agentId: clone_code } })
281
+ installSucceeded = true
282
+ return true
283
+ } catch (error: unknown) {
284
+ const errMsg = error instanceof Error ? error.message : String(error)
285
+ dcgLogger(`onUpdateAgent error: clone_code=${clone_code}, workspaceDir=${workspaceDir}, error=${errMsg}`, 'error')
286
+ return false
287
+ } finally {
288
+ markAgentInstallFinished(clone_code, installSucceeded)
289
+ }
290
+ })
291
+ )
292
+ sendEvent({ ...msgContent, status: results.every(Boolean) ? 'ok' : 'fail' })
293
+ } catch (err: unknown) {
294
+ dcgLogger(`onUpdateAgent download failed: ${String(err)}`, 'error')
295
+ sendEvent({ ...msgContent, status: 'fail' })
296
+ } finally {
297
+ fs.rmSync(tempDir, { recursive: true, force: true })
298
+ }
299
+ }
300
+ export async function createAgent(msgContent: AgentEventMessage) {
301
+ const { url, clone_code, name, description } = msgContent
302
+
303
+ dcgLogger(`createAgent start: msgContent==> ${JSON.stringify(msgContent)}`)
304
+
305
+ if (!url || !clone_code) {
306
+ dcgLogger(`createAgent failed empty url&clone_code: ${JSON.stringify(msgContent)}`, 'error')
307
+ sendEvent({ ...msgContent, status: 'fail' })
308
+ return
309
+ }
310
+ // 缓存下载 URL,供消息处理流程中检测到 agent 缺失时重试下载
311
+ cacheAgentDownloadUrl(clone_code, url, name, description)
312
+
313
+ const workspacePath = getWorkspaceDir()
314
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
315
+
316
+ dcgLogger(`createAgent workspaceDir ==> ${workspaceDir}`)
317
+
318
+ // 如果目标目录已存在,先删除(避免残留文件干扰新安装)
319
+ if (fs.existsSync(workspaceDir)) {
320
+ fs.rmSync(workspaceDir, { recursive: true, force: true })
321
+ dcgLogger(`createAgent removing existing dir: ${workspaceDir}`)
322
+ }
323
+
324
+ markAgentInstallStarted(clone_code)
325
+ let installSucceeded = false
326
+ try {
327
+ // 通过 retryWithBackoff 最多尝试 3 次下载+安装
328
+ const bytes = await retryWithBackoff(
329
+ () => downloadAndSetupAgent(url, clone_code, name, description),
330
+ 3
331
+ )
332
+ dcgLogger(`createAgent download ok: ${bytes} bytes`)
333
+ dcgLogger(`createAgent ok: ${clone_code}`)
334
+ installSucceeded = true
335
+ } catch (error) {
336
+ dcgLogger(`createAgent error: ${error}`, 'error')
337
+ sendEvent({ ...msgContent, status: 'fail' })
338
+ } finally {
339
+ markAgentInstallFinished(clone_code, installSucceeded)
340
+ }
341
+ }
342
+
343
+ /** 正以 clone_code 安装中的专家 workspace(解压 + 网关注册整段流程) */
344
+ const installingAgentCloneCodes = new Set<string>()
345
+ const installCompleteWaitersByCloneCode = new Map<string, Array<(success: boolean) => void>>()
346
+
347
+ function markAgentInstallStarted(cloneCode: string): void {
348
+ const id = typeof cloneCode === 'string' ? cloneCode.trim() : ''
349
+ if (!id) return
350
+ installingAgentCloneCodes.add(id)
351
+ }
352
+
353
+ function notifyInstallWaiters(cloneCode: string, success: boolean): void {
354
+ const waiters = installCompleteWaitersByCloneCode.get(cloneCode)
355
+ if (waiters) {
356
+ for (const r of waiters) {
357
+ r(success)
358
+ }
359
+ installCompleteWaitersByCloneCode.delete(cloneCode)
360
+ }
361
+ }
362
+
363
+ function markAgentInstallFinished(cloneCode: string, success: boolean): void {
364
+ const id = typeof cloneCode === 'string' ? cloneCode.trim() : ''
365
+ if (!id) return
366
+ installingAgentCloneCodes.delete(id)
367
+ notifyInstallWaiters(id, success)
368
+ }
369
+
370
+ export function isAgentInstalling(cloneCode: string): boolean {
371
+ const id = typeof cloneCode === 'string' ? cloneCode.trim() : ''
372
+ if (!id) return false
373
+ return installingAgentCloneCodes.has(id)
374
+ }
375
+
376
+ /** 等待当前安装流程结束;若未在安装中则立即返回是否已安装成功 */
377
+ export function waitForAgentInstall(cloneCode: string): Promise<boolean> {
378
+ const id = typeof cloneCode === 'string' ? cloneCode.trim() : ''
379
+ if (!id) return Promise.resolve(false)
380
+ if (!installingAgentCloneCodes.has(id)) {
381
+ return Promise.resolve(isAgentInstalled(id))
382
+ }
383
+ return new Promise((resolve) => {
384
+ let list = installCompleteWaitersByCloneCode.get(id)
385
+ if (!list) {
386
+ list = []
387
+ installCompleteWaitersByCloneCode.set(id, list)
388
+ }
389
+ list.push(resolve)
390
+ })
391
+ }
392
+
393
+ /* ── Agent 下载 URL 缓存与消息触发重试 ── */
394
+ // createAgent 收到 WebSocket 事件时缓存 url/cloneCode,消息处理流程检测到
395
+ // agent 文件缺失时通过 retryDownloadAgent 利用缓存的 URL 重试一次下载。
396
+ // agentRetryFailed 保证每个 cloneCode 在消息流程中只重试一次,避免无限重试。
397
+
398
+ const agentDownloadUrls = new Map<string, { url: string; name?: string; description?: string }>()
399
+ const agentRetryFailed = new Set<string>()
400
+
401
+ function cacheAgentDownloadUrl(
402
+ cloneCode: string,
403
+ url: string,
404
+ name?: string,
405
+ description?: string
406
+ ): void {
407
+ agentDownloadUrls.set(cloneCode, { url, name, description })
408
+ }
409
+
410
+ function clearAgentDownloadUrl(cloneCode: string): void {
411
+ agentDownloadUrls.delete(cloneCode)
412
+ agentRetryFailed.delete(cloneCode)
413
+ }
414
+
415
+ /* ── 在 .mobook-agents-donwload-url-cache.json 文件中缓存 Agents 下载的 url,用于重新加载 */
416
+ const AGENTS_DONWNLOAD_URL_CACHE_FILE = '.mobook-agents-donwload-url-cache.json'
417
+
418
+ interface AgentMeta {
419
+ url: string
420
+ name?: string
421
+ description?: string
422
+ }
423
+
424
+ interface AgentsCache {
425
+ version: number
426
+ agents: Record<string, Record<string, AgentMeta>>
427
+ }
428
+
429
+ let _agentsCache: AgentsCache | null = null
430
+
431
+ function getAgentsDownloadUrlCachePath(): string | null {
432
+ const workspacePath = getWorkspaceDir()
433
+ if (!workspacePath) return null
434
+ return path.join(workspacePath, AGENTS_DONWNLOAD_URL_CACHE_FILE)
435
+ }
436
+
437
+ async function loadAgentsDownloadUrlCache(): Promise<AgentsCache> {
438
+ if (_agentsCache) return _agentsCache
439
+
440
+ const cachePath = getAgentsDownloadUrlCachePath()
441
+ if (!cachePath) return { version: 1, agents: {} }
442
+
443
+ try {
444
+ const raw = await fsPromise.readFile(cachePath, 'utf-8')
445
+ const parsed = JSON.parse(raw) as AgentsCache
446
+ if (typeof parsed.version === 'number' && parsed.agents && typeof parsed.agents === 'object') {
447
+ _agentsCache = parsed
448
+ return parsed
449
+ }
450
+ } catch(error) {
451
+ dcgLogger(`loadAgentsCache error: ${JSON.stringify(error)}`);
452
+ }
453
+
454
+ /* 文件不存在或解析失败,返回空缓存 */
455
+ return { version: 1, agents: {} }
456
+ }
457
+
458
+ async function saveAgentsDownloadUrl(cache: AgentsCache): Promise<void> {
459
+ const cachePath = getAgentsDownloadUrlCachePath()
460
+
461
+ dcgLogger(`saveAgentsCache cachePath: ${cachePath}`)
462
+ if (!cachePath) return
463
+
464
+ try {
465
+ // 先写 .tmp 再 rename,避免写入中断导致文件损坏
466
+ const tempPath = `${cachePath}.tmp`
467
+ await fsPromise.writeFile(tempPath, JSON.stringify(cache, null, 2), 'utf-8')
468
+ await fsPromise.rename(tempPath, cachePath)
469
+ _agentsCache = cache
470
+ } catch (err) {
471
+ dcgLogger(`saveAgentsCache failed: ${String(err)}`, 'error')
472
+ }
473
+ }
474
+
475
+ async function writeAgentsDownloadUrlMeta(cloneCode: string, meta: AgentMeta): Promise<void> {
476
+ const cache = await loadAgentsDownloadUrlCache()
477
+ if (!cache.agents[ENV]) {
478
+ cache.agents[ENV] = {}
479
+ }
480
+ cache.agents[ENV][cloneCode] = meta
481
+ await saveAgentsDownloadUrl(cache)
482
+ }
483
+
484
+ async function readAgentDownloadUrlMeta(cloneCode: string): Promise<AgentMeta | null> {
485
+ const cache = await loadAgentsDownloadUrlCache()
486
+ return cache.agents[ENV]?.[cloneCode] ?? null
487
+ }
488
+
489
+ async function removeAgentDownloadUrlMeta(cloneCode: string): Promise<void> {
490
+ const cache = await loadAgentsDownloadUrlCache()
491
+ const envAgents = cache.agents[ENV]
492
+ if (!envAgents || !envAgents[cloneCode]) return
493
+ delete envAgents[cloneCode]
494
+ if (Object.keys(envAgents).length === 0) {
495
+ delete cache.agents[ENV]
496
+ }
497
+ await saveAgentsDownloadUrl(cache)
498
+ }
499
+
500
+ /**
501
+ * 核心下载+安装逻辑:单次 axios 请求,下载 zip、解压、调用 onCreateAgent 注册。
502
+ * 失败时自动清理目录并抛出异常,供上层 retryWithBackoff 捕获重试。
503
+ * @returns 下载的字节数
504
+ */
505
+ async function downloadAndSetupAgent(
506
+ url: string,
507
+ cloneCode: string,
508
+ name?: string,
509
+ description?: string
510
+ ): Promise<number> {
511
+ const workspacePath = getWorkspaceDir()
512
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${cloneCode}`)
513
+
514
+ // 每次调用时先清理残留目录,保证重试时环境干净
515
+ if (fs.existsSync(workspaceDir)) {
516
+ fs.rmSync(workspaceDir, { recursive: true, force: true })
517
+ }
518
+
519
+ try {
520
+ // 下载 agent file stream
521
+ const response = await axios({ method: 'get', url, responseType: 'arraybuffer' })
522
+
523
+ fs.mkdirSync(workspaceDir, { recursive: true })
524
+ await extractZipBufferToDirectory(Buffer.from(response.data), workspaceDir)
525
+
526
+ await onCreateAgent({ url, clone_code: cloneCode, name, description })
527
+ await writeAgentsDownloadUrlMeta(cloneCode, { url, name, description })
528
+ return response.data?.length ?? 0
529
+ } catch (error) {
530
+ dcgLogger(`downloadAndSetupAgent error: ${error}`);
531
+ const resolvedCleanDir = path.resolve(workspaceDir)
532
+ if (fs.existsSync(resolvedCleanDir) && resolvedCleanDir !== '/') {
533
+ fs.rmSync(resolvedCleanDir, { recursive: true, force: true })
534
+ }
535
+ throw error
536
+ }
537
+ }
538
+
539
+ /**
540
+ * 在消息处理流程中,对之前下载失败的 agent 尝试一次重新下载。
541
+ * 依赖 createAgent 缓存的 URL,每个 cloneCode 在生命周期内只尝试一次。
542
+ * @returns 下载并注册成功返回 true;失败或已重试过返回 false
543
+ */
544
+ export async function retryDownloadAgent(cloneCode: string): Promise<boolean> {
545
+
546
+ // console.log("retryDownloadAgent cloneCode ==>",cloneCode );
547
+
548
+ const id = typeof cloneCode === 'string' ? cloneCode.trim() : ''
549
+ if (!id) return false
550
+
551
+ let cached = agentDownloadUrls.get(id)
552
+
553
+ // 内存未命中 → 尝试从 workspace 缓存文件中恢复
554
+ if (!cached) {
555
+ const meta = await readAgentDownloadUrlMeta(id)
556
+ if (meta) {
557
+ cached = meta
558
+ agentDownloadUrls.set(id, cached)
559
+ dcgLogger(`retryDownloadAgent: recovered url from cache meta file for ${id}`)
560
+ }
561
+ }
562
+
563
+ if (!cached) {
564
+ dcgLogger(`retryDownloadAgent: no cached url for ${id}`)
565
+ return false
566
+ }
567
+
568
+ if (agentRetryFailed.has(id)) {
569
+ dcgLogger(`retryDownloadAgent: already retried and failed for ${id}`)
570
+ return false
571
+ }
572
+
573
+ dcgLogger(`retryDownloadAgent: retrying ${id}`)
574
+
575
+ try {
576
+
577
+ // 重新下载之前,先发送 "agents.delete" rpc, 避免下载成功之后, 发送 'agents.create' rpc 报错
578
+ await sendGatewayRpc({ method: 'agents.delete', params: { agentId: cloneCode } })
579
+
580
+ // 重新下载文件
581
+ await downloadAndSetupAgent(cached.url, id, cached.name, cached.description)
582
+
583
+ dcgLogger(`retryDownloadAgent ok: ${id}`)
584
+ agentRetryFailed.delete(id)
585
+ return true
586
+ } catch (error) {
587
+ const errMsg = error instanceof Error ? error.message : String(error)
588
+ dcgLogger(`retryDownloadAgent error: ${id}, error=${errMsg}`, 'error')
589
+ agentRetryFailed.add(id)
590
+ return false
591
+ }
592
+ }