@dcrays/dcgchat-test 0.6.12 → 0.7.0

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 (60) hide show
  1. package/index.js +306 -0
  2. package/openclaw.plugin.json +42 -12
  3. package/package.json +8 -35
  4. package/README.md +0 -167
  5. package/index.ts +0 -35
  6. package/src/agent.ts +0 -568
  7. package/src/bot.ts +0 -646
  8. package/src/channel/index.ts +0 -329
  9. package/src/channel/outboundMedia.ts +0 -144
  10. package/src/channel/outboundTarget.ts +0 -62
  11. package/src/channel/uploadMediaUrl.ts +0 -76
  12. package/src/cron/message.ts +0 -114
  13. package/src/cron/params.ts +0 -164
  14. package/src/cron/toolGuard.ts +0 -466
  15. package/src/cron/types.ts +0 -15
  16. package/src/gateway/index.ts +0 -430
  17. package/src/gateway/security.ts +0 -95
  18. package/src/gateway/socket.ts +0 -256
  19. package/src/libs/ali-oss-6.23.0.tgz +0 -0
  20. package/src/libs/axios-1.13.6.tgz +0 -0
  21. package/src/libs/md5-2.3.0.tgz +0 -0
  22. package/src/libs/mime-types-3.0.2.tgz +0 -0
  23. package/src/libs/unzipper-0.12.3.tgz +0 -0
  24. package/src/libs/ws-8.19.0.tgz +0 -0
  25. package/src/monitor.ts +0 -269
  26. package/src/request/api.ts +0 -80
  27. package/src/request/oss.ts +0 -200
  28. package/src/request/request.ts +0 -191
  29. package/src/request/userInfo.ts +0 -44
  30. package/src/session.ts +0 -28
  31. package/src/skill.ts +0 -114
  32. package/src/tool.ts +0 -603
  33. package/src/tools/messageTool.ts +0 -334
  34. package/src/tools/toolCallGuard.ts +0 -327
  35. package/src/transport.ts +0 -217
  36. package/src/types.ts +0 -139
  37. package/src/utils/agentErrors.ts +0 -116
  38. package/src/utils/constant.ts +0 -60
  39. package/src/utils/env-config.ts +0 -19
  40. package/src/utils/formatLlmInputEvent.ts +0 -48
  41. package/src/utils/gatewayMsgHandler.ts +0 -147
  42. package/src/utils/global.ts +0 -309
  43. package/src/utils/inboundTurnState.ts +0 -66
  44. package/src/utils/log.ts +0 -77
  45. package/src/utils/mediaAttached.ts +0 -244
  46. package/src/utils/mediaEmitter.ts +0 -54
  47. package/src/utils/outboundAssistantText.ts +0 -117
  48. package/src/utils/params.ts +0 -104
  49. package/src/utils/passTxt.ts +0 -4
  50. package/src/utils/resolveRegisterConfig.ts +0 -33
  51. package/src/utils/searchFile.ts +0 -228
  52. package/src/utils/sessionState.ts +0 -137
  53. package/src/utils/sessionTermination.ts +0 -228
  54. package/src/utils/streamMerge.ts +0 -150
  55. package/src/utils/subagentRunMap.ts +0 -71
  56. package/src/utils/undiciFetchInterceptor.ts +0 -346
  57. package/src/utils/workspaceFilePaths.ts +0 -18
  58. package/src/utils/wsMessageHandler.ts +0 -124
  59. package/src/utils/zipExtract.ts +0 -97
  60. package/src/utils/zipPath.ts +0 -24
package/src/tool.ts DELETED
@@ -1,603 +0,0 @@
1
- import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
- import { formatText, getMsgStatus } from './utils/global.js'
3
- import { dcgLogger } from './utils/log.js'
4
- import { sendEventMessage, sendFinal, sendText, wsSendRaw } from './transport.js'
5
- import { getEffectiveMsgParams } from './utils/params.js'
6
- import {
7
- cleanupSubagentRunMappingsForSessionKeys,
8
- clearSubagentRunMappings,
9
- deleteSubagentRunMappings,
10
- resolveChildSessionKeyBySubagentRunId,
11
- resolveRequesterSessionKeyBySubagentRunId,
12
- restoreSubagentRunMappings,
13
- setSubagentRunChild,
14
- setSubagentRunRequester,
15
- snapshotSubagentRunMappings
16
- } from './utils/subagentRunMap.js'
17
- import { randomUUID } from 'node:crypto'
18
- import { sendGatewayRpc, waitForDcgchatGatewaySocketConnected } from './gateway/socket.js'
19
- import {
20
- writeSessionStateFile,
21
- readSessionStateFile,
22
- restoreParamsAndGlobal,
23
- removeSessionStateFile,
24
- collectRootSessionKeysFromSnapshot,
25
- type SubagentTrackingSnapshot
26
- } from './utils/sessionState.js'
27
- import { cronToolCall } from './cron/toolGuard.js'
28
- import { cronAfterToolCallHandler, isCreateCronTool as isCronToolCall } from './cron/params.js'
29
- import { formatLlmInputEvent } from './utils/formatLlmInputEvent.js'
30
- import { hasCronSessionKeyMap } from './channel/outboundTarget.js'
31
- import {
32
- mergeDcgchatBeforeToolCallHookResults,
33
- shouldRunBeforeToolCallGuardWhenSessionInactive,
34
- toolCallGuardAfter,
35
- toolCallGuardBefore
36
- } from './tools/toolCallGuard.js'
37
- import { setPendingLlmBodyExt } from './utils/undiciFetchInterceptor.js'
38
-
39
- /** 与 `OpenClawPluginApi['on']` 对齐,随宿主 SDK 扩展钩子名时自动一致 */
40
- type PluginHookName = Parameters<OpenClawPluginApi['on']>[0]
41
-
42
- // message_received 没有 sessionKey 前置到bot中执行
43
- const eventList: ReadonlyArray<PluginHookName> = [
44
- 'subagent_spawning',
45
- 'subagent_spawned',
46
- 'subagent_delivery_target',
47
- 'llm_output',
48
- 'subagent_ended',
49
- 'before_tool_call',
50
- 'after_tool_call'
51
- ]
52
-
53
- /** 子 agent 的 sessionKey 往往未写入 params map,回落到主会话 outbound 参数避免 messageId 缺失 */
54
- function resolveOutboundParamsForSession(sk: string) {
55
- const k = sk.trim()
56
- let params = getEffectiveMsgParams(k)
57
- if (params.messageId?.trim() || params.sessionId?.trim()) return params
58
- const parent = requesterByChildSessionKey.get(k)
59
- if (parent) {
60
- const parentParams = getEffectiveMsgParams(parent)
61
- if (parentParams.messageId?.trim() || parentParams.sessionId?.trim()) return parentParams
62
- }
63
- return params
64
- }
65
-
66
- /** 主会话已 running 时,子会话上的工具/事件也应下发(否则子 key 无 running 状态会整段丢消息) */
67
- export function isSessionActiveForTool(sk: string): boolean {
68
- const k = sk.trim()
69
- if (!k) return false
70
- if (getMsgStatus(k) === 'running') return true
71
- const parent = requesterByChildSessionKey.get(k)
72
- return parent ? getMsgStatus(parent) === 'running' : false
73
- }
74
-
75
- export function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
76
- const params = resolveOutboundParamsForSession(sk)
77
- const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
78
- wsSendRaw(params, content, false)
79
- }
80
-
81
- function canEmitHookMessage(sessionKey: string): boolean {
82
- return hasCronSessionKeyMap(sessionKey) || isSessionActiveForTool(sessionKey)
83
- }
84
-
85
- function shouldDropHookMessage(sessionKey: string, hookName: PluginHookName): boolean {
86
- if (canEmitHookMessage(sessionKey)) return false
87
- dcgLogger(`工具调用结果: ~ event:${hookName} ~ session not active, dropped: ${sessionKey}`)
88
- return true
89
- }
90
-
91
- function runBeforeToolCallGuards(event: any, sessionKey: string) {
92
- const sessionActive = canEmitHookMessage(sessionKey)
93
- const guardResult =
94
- sessionActive || shouldRunBeforeToolCallGuardWhenSessionInactive(event) ? toolCallGuardBefore(event, { sessionKey, sessionActive }) : undefined
95
- const cronResult = cronToolCall(event, sessionKey)
96
- return mergeDcgchatBeforeToolCallHookResults(guardResult, cronResult)
97
- }
98
-
99
- export function stringifyToolHookPayload(
100
- eventName: PluginHookName,
101
- rest: Record<string, unknown>,
102
- event: { toolCallId?: string; runId?: string },
103
- status: 'running' | 'finished' | 'lifecycle'
104
- ): { text: string; callId: string } {
105
- const callId = event.toolCallId || event.runId || Date.now().toString()
106
- const specialIdentification = 'dcgchat_tool_call_special_identification'
107
- if (status === 'lifecycle') {
108
- return {
109
- callId,
110
- text: JSON.stringify({ type: eventName, specialIdentification, toolName: '', params: '', callId })
111
- }
112
- }
113
- return {
114
- callId,
115
- text: JSON.stringify({ type: eventName, specialIdentification, callId, ...rest, status })
116
- }
117
- }
118
-
119
- // --- Subagent 活跃跟踪(按主会话 requesterSessionKey)---
120
-
121
- /** 主会话 sessionKey -> 仍活跃的子 agent runId */
122
- const activeSubagentRunIdsByRequester = new Map<string, Set<string>>()
123
- /** 子会话 childSessionKey -> 主会话 requesterSessionKey */
124
- const requesterByChildSessionKey = new Map<string, string>()
125
- /** 子会话 childSessionKey -> spawn 时的 runId(ended 事件可能不带 runId) */
126
- const runIdByChildSessionKey = new Map<string, string>()
127
- /** 主会话 -> 等待「子 agent 全部结束」的回调 */
128
- const subagentIdleWaiters = new Map<string, Set<() => void>>()
129
-
130
- /**
131
- * 在会话终止时清理 subagent 相关的 Map 数据,防止内存泄漏。
132
- * 与 resetSubagentStateForRequesterSession 不同,本函数仅清理 Map 条目,
133
- * 适用于会话正常结束时的轻量级资源回收。
134
- */
135
- export function cleanupSubagentStateForSession(sessionKey: string): void {
136
- const sk = sessionKey?.trim()
137
- if (!sk) return
138
- const affected = collectSubagentTreeSessionKeys(sk)
139
-
140
- // 清理 params.ts 中的 subagentRunIdMap
141
- for (const key of affected) {
142
- const runIdSet = activeSubagentRunIdsByRequester.get(key)
143
- if (!runIdSet) continue
144
- for (const rid of runIdSet) {
145
- deleteSubagentRunMappings(rid)
146
- }
147
- }
148
-
149
- // 清理正反向映射(主/子会话活跃 runId、子会话→主会话、子会话→runId)
150
- for (const key of affected) {
151
- activeSubagentRunIdsByRequester.delete(key)
152
- requesterByChildSessionKey.delete(key)
153
- runIdByChildSessionKey.delete(key)
154
- }
155
- cleanupSubagentRunMappingsForSessionKeys(affected)
156
-
157
- // 触发 idle waiters
158
- for (const key of affected) {
159
- flushSubagentIdleWaiters(key, true)
160
- }
161
- }
162
-
163
- function getOrCreateRunIdSet(requesterSessionKey: string): Set<string> {
164
- let set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
165
- if (!set) {
166
- set = new Set()
167
- activeSubagentRunIdsByRequester.set(requesterSessionKey, set)
168
- }
169
- return set
170
- }
171
-
172
- function flushSubagentIdleWaiters(requesterSessionKey: string, force = false): void {
173
- const set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
174
- if (set && set.size > 0) return
175
- if (!force && getActiveSubagentCountDeep(requesterSessionKey) > 0) return
176
- activeSubagentRunIdsByRequester.delete(requesterSessionKey)
177
- const waiters = subagentIdleWaiters.get(requesterSessionKey)
178
- if (!waiters?.size) return
179
- subagentIdleWaiters.delete(requesterSessionKey)
180
- for (const w of waiters) {
181
- try {
182
- w()
183
- } catch (e) {
184
- dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
185
- }
186
- }
187
- }
188
-
189
- function getAncestorRequesterSessionKeys(sessionKey: string): string[] {
190
- const out: string[] = []
191
- const seen = new Set<string>()
192
- let current = sessionKey.trim()
193
- while (current) {
194
- const parent = requesterByChildSessionKey.get(current)?.trim()
195
- if (!parent || seen.has(parent)) break
196
- seen.add(parent)
197
- out.push(parent)
198
- current = parent
199
- }
200
- return out
201
- }
202
-
203
- function collectSubagentTreeSessionKeys(rootSessionKey: string): Set<string> {
204
- const root = rootSessionKey.trim()
205
- const out = new Set<string>()
206
- if (!root) return out
207
- out.add(root)
208
- for (const child of getDescendantSessionKeysForRequester(root)) {
209
- out.add(child)
210
- }
211
- return out
212
- }
213
-
214
- function registerSubagentSpawn(requesterSessionKey: string, runId: string, childSessionKey: string): void {
215
- const req = requesterSessionKey.trim()
216
- const rid = runId.trim()
217
- const child = childSessionKey.trim()
218
- if (!req || !rid || !child) {
219
- dcgLogger(`subagent track spawn skipped: missing key req=${req} runId=${rid} child=${child}`)
220
- return
221
- }
222
- getOrCreateRunIdSet(req).add(rid)
223
- requesterByChildSessionKey.set(child, req)
224
- runIdByChildSessionKey.set(child, rid)
225
- setSubagentRunRequester(rid, req)
226
- setSubagentRunChild(rid, child)
227
- dcgLogger(`subagent track spawn: requester=${req} runId=${rid} child=${child} active=${getOrCreateRunIdSet(req).size}`)
228
- }
229
-
230
- function registerSubagentEnd(ctx: { requesterSessionKey?: string; sessionKey?: string }, targetSessionKey: string, runId?: string): void {
231
- const resolvedRunId = runId?.trim()
232
- const child = targetSessionKey.trim() || resolveChildSessionKeyBySubagentRunId(resolvedRunId) || ''
233
- const req =
234
- ctx.requesterSessionKey?.trim() ||
235
- (child ? requesterByChildSessionKey.get(child) : undefined) ||
236
- resolveRequesterSessionKeyBySubagentRunId(resolvedRunId) ||
237
- ctx.sessionKey?.trim() ||
238
- ''
239
- const affectedWaiterKeys = new Set([req, ...getAncestorRequesterSessionKeys(req)].filter(Boolean))
240
- const rid = (resolvedRunId || (child ? runIdByChildSessionKey.get(child) : undefined) || '').trim()
241
- deleteSubagentRunMappings(rid)
242
- if (!req) {
243
- dcgLogger(`subagent track end: no requester for child=${child || 'n/a'} runId=${rid || 'n/a'}`)
244
- if (child) {
245
- requesterByChildSessionKey.delete(child)
246
- runIdByChildSessionKey.delete(child)
247
- }
248
- return
249
- }
250
- const set = activeSubagentRunIdsByRequester.get(req)
251
- if (set && rid) {
252
- set.delete(rid)
253
- }
254
- if (child && getActiveSubagentCountDeep(child) === 0) {
255
- requesterByChildSessionKey.delete(child)
256
- }
257
- if (child) runIdByChildSessionKey.delete(child)
258
- dcgLogger(`subagent track end: requester=${req} child=${child || 'n/a'} runId=${rid || 'n/a'} remaining=${set?.size ?? 0}`)
259
- if (set && set.size === 0) {
260
- activeSubagentRunIdsByRequester.delete(req)
261
- }
262
- for (const key of affectedWaiterKeys) {
263
- flushSubagentIdleWaiters(key)
264
- }
265
- }
266
-
267
- /** 当前跟踪到的、挂在该主会话下的子会话 sessionKey(供 /stop 时逐个 chat.abort) */
268
- export function getChildSessionKeysTrackedForRequester(requesterSessionKey: string): string[] {
269
- const req = requesterSessionKey.trim()
270
- if (!req) return []
271
- const out: string[] = []
272
- for (const [child, parent] of requesterByChildSessionKey.entries()) {
273
- if (parent === req) out.push(child)
274
- }
275
- return out
276
- }
277
-
278
- /**
279
- * 自根 requester 起 BFS 收集所有已跟踪后代子会话(含嵌套)。网关 abort 时宜自深到浅,调用方对结果 `.reverse()` 后再逐个 chat.abort。
280
- */
281
- export function getDescendantSessionKeysForRequester(rootRequesterSessionKey: string): string[] {
282
- const root = rootRequesterSessionKey.trim()
283
- if (!root) return []
284
- const ordered: string[] = []
285
- const seen = new Set<string>()
286
- let frontier = getChildSessionKeysTrackedForRequester(root)
287
- while (frontier.length > 0) {
288
- const next: string[] = []
289
- for (const sk of frontier) {
290
- const k = sk.trim()
291
- if (!k || seen.has(k)) continue
292
- seen.add(k)
293
- ordered.push(k)
294
- next.push(...getChildSessionKeysTrackedForRequester(k))
295
- }
296
- frontier = next
297
- }
298
- return ordered
299
- }
300
-
301
- /**
302
- * 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
303
- */
304
- export function resetSubagentStateForRequesterSession(requesterSessionKey: string): void {
305
- const req = requesterSessionKey.trim()
306
- if (!req) return
307
- const affected = collectSubagentTreeSessionKeys(req)
308
-
309
- for (const key of affected) {
310
- const runIdSet = activeSubagentRunIdsByRequester.get(key)
311
- if (!runIdSet) continue
312
- for (const rid of runIdSet) {
313
- deleteSubagentRunMappings(rid)
314
- }
315
- }
316
-
317
- for (const key of affected) {
318
- activeSubagentRunIdsByRequester.delete(key)
319
- requesterByChildSessionKey.delete(key)
320
- runIdByChildSessionKey.delete(key)
321
- }
322
- cleanupSubagentRunMappingsForSessionKeys(affected)
323
-
324
- for (const key of affected) {
325
- flushSubagentIdleWaiters(key, true)
326
- }
327
- }
328
-
329
- /** 当前主会话下仍在跑的子 agent 数量(按 spawn 时 runId 去重) */
330
- export function getActiveSubagentCount(sessionKey: string): number {
331
- const sk = sessionKey?.trim()
332
- if (!sk) return 0
333
- return activeSubagentRunIdsByRequester.get(sk)?.size ?? 0
334
- }
335
-
336
- function getActiveSubagentCountDeep(sessionKey: string): number {
337
- const sk = sessionKey?.trim()
338
- if (!sk) return 0
339
- let total = getActiveSubagentCount(sk)
340
- for (const child of getDescendantSessionKeysForRequester(sk)) {
341
- total += getActiveSubagentCount(child)
342
- }
343
- return total
344
- }
345
-
346
- /**
347
- * 等到指定主会话下已跟踪的子 agent 全部结束(spawn 失败等路径也会触发 subagent_ended,一般会配对清理)。
348
- * 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
349
- */
350
- export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
351
- const sk = sessionKey?.trim()
352
- if (!sk) return Promise.resolve()
353
-
354
- if (getActiveSubagentCountDeep(sk) === 0) return Promise.resolve()
355
-
356
- return new Promise<void>((resolve, reject) => {
357
- let settled = false
358
- const finish = (fn: () => void) => {
359
- if (settled) return
360
- settled = true
361
- if (timeoutId) clearTimeout(timeoutId)
362
- opts?.signal?.removeEventListener('abort', onAbort)
363
- removeWaiter()
364
- fn()
365
- }
366
-
367
- const removeWaiter = () => {
368
- const bucket = subagentIdleWaiters.get(sk)
369
- if (!bucket) return
370
- bucket.delete(onIdle)
371
- if (bucket.size === 0) subagentIdleWaiters.delete(sk)
372
- }
373
-
374
- const onIdle = () => finish(() => resolve())
375
-
376
- const onAbort = () => {
377
- const reason = opts?.signal?.reason
378
- finish(() => reject(reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted'))))
379
- }
380
-
381
- let timeoutId: ReturnType<typeof setTimeout> | undefined
382
- if (opts?.timeoutMs != null && opts.timeoutMs > 0) {
383
- timeoutId = setTimeout(() => finish(() => reject(new Error(`waitUntilSubagentsIdle timeout ${opts.timeoutMs}ms`))), opts.timeoutMs)
384
- }
385
-
386
- if (opts?.signal) {
387
- if (opts.signal.aborted) {
388
- onAbort()
389
- return
390
- }
391
- opts.signal.addEventListener('abort', onAbort, { once: true })
392
- }
393
-
394
- let set = subagentIdleWaiters.get(sk)
395
- if (!set) {
396
- set = new Set()
397
- subagentIdleWaiters.set(sk, set)
398
- }
399
- set.add(onIdle)
400
-
401
- if (getActiveSubagentCountDeep(sk) === 0) {
402
- onIdle()
403
- }
404
- })
405
- }
406
-
407
- interface IArgs {
408
- sessionKey?: string
409
- requesterSessionKey?: string
410
- runId?: string
411
- }
412
- const eventNameList = ['subagent_spawned', 'subagent_ended', 'subagent_spawning', 'subagent_delivery_target']
413
- function resolveHookSessionKey(eventName: string, args: IArgs): string {
414
- if (eventNameList.includes(eventName)) {
415
- setSubagentRunRequester(args?.runId, args?.requesterSessionKey)
416
- return (args?.requesterSessionKey || args?.sessionKey || '').trim()
417
- }
418
- return (args?.sessionKey || '').trim()
419
- }
420
-
421
- /** 将本模块维护的子 agent 跟踪状态导出为可序列化快照(idle waiters 为函数,不参与持久化) */
422
- function snapshotSubagentTracking(): SubagentTrackingSnapshot {
423
- return {
424
- activeSubagentRunIdsByRequester: [...activeSubagentRunIdsByRequester.entries()].map(([k, s]) => [k, [...s]]),
425
- requesterByChildSessionKey: [...requesterByChildSessionKey.entries()],
426
- runIdByChildSessionKey: [...runIdByChildSessionKey.entries()],
427
- ...snapshotSubagentRunMappings()
428
- }
429
- }
430
-
431
- function clearSubagentTrackingMaps(): void {
432
- activeSubagentRunIdsByRequester.clear()
433
- requesterByChildSessionKey.clear()
434
- runIdByChildSessionKey.clear()
435
- clearSubagentRunMappings()
436
- }
437
-
438
- /** 丢弃子 agent 跟踪时顺带清空 idle waiter,避免网关注册恢复后永久挂起 */
439
- function flushAllSubagentIdleWaiters(): void {
440
- for (const waiters of subagentIdleWaiters.values()) {
441
- for (const w of waiters) {
442
- try {
443
- w()
444
- } catch (e) {
445
- dcgLogger(`flushAllSubagentIdleWaiters error: ${String(e)}`, 'error')
446
- }
447
- }
448
- }
449
- subagentIdleWaiters.clear()
450
- }
451
-
452
- function restoreSubagentTracking(snap?: SubagentTrackingSnapshot | null): void {
453
- clearSubagentTrackingMaps()
454
- if (!snap) {
455
- flushAllSubagentIdleWaiters()
456
- return
457
- }
458
-
459
- for (const [k, arr] of snap.activeSubagentRunIdsByRequester ?? []) {
460
- if (typeof k !== 'string' || !k || !Array.isArray(arr)) continue
461
- const set = new Set(arr.filter((rid): rid is string => typeof rid === 'string' && rid.length > 0))
462
- if (set.size > 0) activeSubagentRunIdsByRequester.set(k, set)
463
- }
464
- for (const [k, v] of snap.requesterByChildSessionKey ?? []) {
465
- if (typeof k === 'string' && typeof v === 'string' && k && v) requesterByChildSessionKey.set(k, v)
466
- }
467
- for (const [k, v] of snap.runIdByChildSessionKey ?? []) {
468
- if (typeof k === 'string' && typeof v === 'string' && k && v) runIdByChildSessionKey.set(k, v)
469
- }
470
- restoreSubagentRunMappings(snap)
471
- }
472
-
473
- /** 网关注销后恢复:对每个相关 session 发一次用户侧 `chat.send`,与 sessionTermination 中 RPC 同路径 */
474
- async function sendResumeAfterGatewayRestoreForSessionKeys(sessionKeys: string[]): Promise<void> {
475
- const ready = await waitForDcgchatGatewaySocketConnected({ timeoutMs: 120_000, intervalMs: 150 })
476
- if (!ready) {
477
- dcgLogger('gateway_start resume: Gateway 在超时内未就绪,已跳过「继续执行」 chat.send;请重连后手动发消息或再次触发恢复', 'error')
478
- return
479
- }
480
- for (const sk of sessionKeys) {
481
- try {
482
- await sendGatewayRpc({
483
- method: 'chat.send',
484
- params: { sessionKey: sk, message: '继续执行', deliver: false, idempotencyKey: randomUUID() }
485
- })
486
- dcgLogger(`gateway_start resume: chat.send ok sessionKey=${sk}`)
487
- } catch (e) {
488
- dcgLogger(`gateway_start resume: chat.send failed sessionKey=${sk}: ${String(e)}`, 'error')
489
- }
490
- }
491
- }
492
-
493
- /**
494
- * `registerFull` 可能对同一 `api` 引用多次调用;仅按对象引用去重,避免重复 `api.on`。
495
- * 不能用 id/source/version 字符串去重:宿主若每次构造新 `api` 但字段相同,第二次若跳过则监听会留在旧实例上,事件不再触发。
496
- */
497
- const monitoringToolMessageInstalledForApi = new WeakSet<OpenClawPluginApi>()
498
-
499
- function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
500
- if (eventName === 'subagent_spawned') {
501
- const runId = typeof event?.runId === 'string' ? event.runId : ''
502
- const childSessionKey = typeof event?.childSessionKey === 'string' ? event.childSessionKey : ''
503
- const requester =
504
- typeof args?.requesterSessionKey === 'string' ? args.requesterSessionKey : typeof args?.sessionKey === 'string' ? args.sessionKey : ''
505
- registerSubagentSpawn(requester, runId, childSessionKey)
506
- return
507
- }
508
- if (eventName === 'subagent_ended') {
509
- const targetSessionKey = typeof event?.targetSessionKey === 'string' ? event.targetSessionKey : ''
510
- const runId = typeof event?.runId === 'string' ? event.runId : undefined
511
- registerSubagentEnd(args ?? {}, targetSessionKey, runId)
512
- }
513
- }
514
-
515
- export function monitoringToolMessage(api: OpenClawPluginApi) {
516
- if (monitoringToolMessageInstalledForApi.has(api)) {
517
- return
518
- }
519
- monitoringToolMessageInstalledForApi.add(api)
520
-
521
- api.on('llm_input', (_event: any, ctx: { sessionKey?: string; agentId?: string }) => {
522
- const sessionKey = ctx?.sessionKey?.trim() || ''
523
- if (sessionKey) {
524
- setPendingLlmBodyExt({ sessionKey })
525
- }
526
- // llm_input 缩略文字信息
527
- dcgLogger(formatLlmInputEvent(_event, ctx))
528
- })
529
-
530
- api.on('gateway_stop', (_event: any, _args: any) => {
531
- const params = {
532
- event_type: 'close',
533
- operation_type: ''
534
- }
535
- sendEventMessage(params)
536
- try {
537
- writeSessionStateFile(snapshotSubagentTracking())
538
- } catch (e) {
539
- dcgLogger(`gateway_stopped persist session state failed: ${String(e)}`, 'error')
540
- }
541
- })
542
- api.on('gateway_start', (_event: any, _args: any) => {
543
- try {
544
- const snap = readSessionStateFile()
545
- if (!snap) return
546
- const sessionKeys = collectRootSessionKeysFromSnapshot(snap)
547
- restoreParamsAndGlobal(snap)
548
- // 子 agent 跟踪不跨进程恢复:快照内 runId / 映射在 SIGINT、崩溃或未收到 subagent_ended 后已失效, 恢复会留下「幽灵」活跃计数,waitUntilSubagentsIdle 卡住,永远不 sendFinal(end)
549
- restoreSubagentTracking()
550
- removeSessionStateFile()
551
- if (!sessionKeys.length) return
552
- dcgLogger(`gateway_started: 已从快照恢复 session state(savedAt=${snap.savedAt}, 恢复会话数量:${sessionKeys.toString()})`)
553
- void sendResumeAfterGatewayRestoreForSessionKeys(sessionKeys)
554
- } catch (e) {
555
- dcgLogger(`gateway_started restore session state failed: ${String(e)}`, 'error')
556
- }
557
- })
558
- for (const hookName of eventList) {
559
- api.on(hookName, (event: any, args: any) => {
560
- // ACP 等非 subagent 的 ended 事件不应驱动书灵子会话状态机(见 SDK PluginHookSubagentEndedEvent.targetKind)
561
- if (hookName === 'subagent_ended' && event?.targetKind === 'acp') {
562
- return
563
- }
564
- trackSubagentLifecycle(hookName, event, args)
565
- const sk = resolveHookSessionKey(hookName, args ?? {})
566
- if (!sk) {
567
- dcgLogger(`工具调用结果: ~ event:${hookName} ~ 没有sessionKey 为执行`, 'error')
568
- return
569
- }
570
- if (['before_tool_call', 'after_tool_call'].includes(hookName)) {
571
- const { result: _result, ...rest } = event
572
- if (hookName === 'before_tool_call') {
573
- const beforeResult = runBeforeToolCallGuards(event, sk)
574
- if (beforeResult) return beforeResult
575
- if (shouldDropHookMessage(sk, hookName)) return
576
- }
577
- if (hookName === 'after_tool_call') {
578
- toolCallGuardAfter(event)
579
- if (isCronToolCall(event)) {
580
- void cronAfterToolCallHandler(event, sk)
581
- }
582
- if (shouldDropHookMessage(sk, hookName)) return
583
- }
584
- const { text, callId } = stringifyToolHookPayload(hookName, rest, event, 'finished')
585
- sendToolCallMessage(sk, text, callId, hookName === 'after_tool_call' ? 1 : 0)
586
- return
587
- }
588
- dcgLogger(`工具调用结果: ~ event:${hookName}`)
589
- if (shouldDropHookMessage(sk, hookName)) return
590
- if (hookName === 'llm_output') {
591
- if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
592
- const msgCtx = resolveOutboundParamsForSession(sk)
593
- const message = '您的墨滴已消耗完,您可以通过充值墨滴来继续使用'
594
- sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
595
- sendFinal(msgCtx, '墨滴不足')
596
- return
597
- }
598
- }
599
- const { text, callId } = stringifyToolHookPayload(hookName, {}, event, 'lifecycle')
600
- sendToolCallMessage(sk, text, callId, 0)
601
- })
602
- }
603
- }