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