@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.
- package/index.js +306 -0
- package/openclaw.plugin.json +42 -12
- package/package.json +8 -35
- package/README.md +0 -167
- package/index.ts +0 -35
- package/src/agent.ts +0 -568
- package/src/bot.ts +0 -646
- package/src/channel/index.ts +0 -329
- package/src/channel/outboundMedia.ts +0 -144
- package/src/channel/outboundTarget.ts +0 -62
- package/src/channel/uploadMediaUrl.ts +0 -76
- package/src/cron/message.ts +0 -114
- package/src/cron/params.ts +0 -164
- package/src/cron/toolGuard.ts +0 -466
- package/src/cron/types.ts +0 -15
- package/src/gateway/index.ts +0 -430
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -256
- 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 +0 -269
- package/src/request/api.ts +0 -80
- package/src/request/oss.ts +0 -200
- package/src/request/request.ts +0 -191
- package/src/request/userInfo.ts +0 -44
- package/src/session.ts +0 -28
- package/src/skill.ts +0 -114
- package/src/tool.ts +0 -603
- package/src/tools/messageTool.ts +0 -334
- package/src/tools/toolCallGuard.ts +0 -327
- package/src/transport.ts +0 -217
- package/src/types.ts +0 -139
- package/src/utils/agentErrors.ts +0 -116
- package/src/utils/constant.ts +0 -60
- package/src/utils/env-config.ts +0 -19
- package/src/utils/formatLlmInputEvent.ts +0 -48
- package/src/utils/gatewayMsgHandler.ts +0 -147
- package/src/utils/global.ts +0 -309
- package/src/utils/inboundTurnState.ts +0 -66
- package/src/utils/log.ts +0 -77
- package/src/utils/mediaAttached.ts +0 -244
- package/src/utils/mediaEmitter.ts +0 -54
- package/src/utils/outboundAssistantText.ts +0 -117
- package/src/utils/params.ts +0 -104
- package/src/utils/passTxt.ts +0 -4
- package/src/utils/resolveRegisterConfig.ts +0 -33
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/sessionState.ts +0 -137
- package/src/utils/sessionTermination.ts +0 -228
- package/src/utils/streamMerge.ts +0 -150
- package/src/utils/subagentRunMap.ts +0 -71
- package/src/utils/undiciFetchInterceptor.ts +0 -346
- package/src/utils/workspaceFilePaths.ts +0 -18
- package/src/utils/wsMessageHandler.ts +0 -124
- package/src/utils/zipExtract.ts +0 -97
- 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
|
-
}
|