@dcrays/dcgchat 0.4.29 → 0.5.1
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 +292 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +18 -13
- package/schemas/gateway-cron-finished.payload.json +39 -0
- package/index.ts +0 -26
- package/src/agent.ts +0 -128
- package/src/bot.ts +0 -500
- package/src/channel.ts +0 -470
- package/src/cron.ts +0 -194
- package/src/cronToolCall.ts +0 -202
- package/src/gateway/index.ts +0 -447
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -285
- 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 -165
- package/src/request/api.ts +0 -70
- package/src/request/oss.ts +0 -212
- package/src/request/request.ts +0 -192
- package/src/request/userInfo.ts +0 -99
- package/src/session.ts +0 -19
- package/src/sessionTermination.ts +0 -154
- package/src/skill.ts +0 -151
- package/src/tool.ts +0 -422
- package/src/tools/messageTool.ts +0 -224
- package/src/transport.ts +0 -203
- package/src/types.ts +0 -139
- package/src/utils/constant.ts +0 -7
- package/src/utils/gatewayMsgHanlder.ts +0 -55
- package/src/utils/global.ts +0 -160
- package/src/utils/log.ts +0 -15
- package/src/utils/params.ts +0 -88
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/wsMessageHandler.ts +0 -64
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/skill.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
/** @ts-ignore */
|
|
3
|
-
import unzipper from 'unzipper'
|
|
4
|
-
import { pipeline } from 'stream/promises'
|
|
5
|
-
import fs from 'fs'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import { getWorkspaceDir } from './utils/global.js'
|
|
8
|
-
import { getWsConnection } from './utils/global.js'
|
|
9
|
-
import { dcgLogger } from './utils/log.js'
|
|
10
|
-
import { isWsOpen } from './transport.js'
|
|
11
|
-
import { sendMessageToGateway } from './gateway/socket.js'
|
|
12
|
-
import { decodeZipEntryPath } from './utils/zipPath.js'
|
|
13
|
-
|
|
14
|
-
type ISkillParams = {
|
|
15
|
-
path: string
|
|
16
|
-
code: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function sendEvent(msgContent: Record<string, any>) {
|
|
20
|
-
const ws = getWsConnection()
|
|
21
|
-
if (isWsOpen()) {
|
|
22
|
-
ws?.send(
|
|
23
|
-
JSON.stringify({
|
|
24
|
-
messageType: 'openclaw_bot_event',
|
|
25
|
-
source: 'client',
|
|
26
|
-
content: msgContent
|
|
27
|
-
})
|
|
28
|
-
)
|
|
29
|
-
dcgLogger(`技能安装: ${JSON.stringify(msgContent)}`)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function installSkill(params: ISkillParams, msgContent: Record<string, any>) {
|
|
34
|
-
const { path: cdnUrl, code } = params
|
|
35
|
-
const workspacePath = getWorkspaceDir()
|
|
36
|
-
|
|
37
|
-
// 确保 skills 目录存在
|
|
38
|
-
const skillsDir = path.join(workspacePath, 'skills')
|
|
39
|
-
if (!fs.existsSync(skillsDir)) {
|
|
40
|
-
fs.mkdirSync(skillsDir, { recursive: true })
|
|
41
|
-
}
|
|
42
|
-
// 如果目标目录已存在,先删除
|
|
43
|
-
const skillDir = path.join(workspacePath, 'skills', code)
|
|
44
|
-
if (fs.existsSync(skillDir)) {
|
|
45
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
// 下载 zip 文件
|
|
50
|
-
const response = await axios({
|
|
51
|
-
method: 'get',
|
|
52
|
-
url: cdnUrl,
|
|
53
|
-
responseType: 'stream'
|
|
54
|
-
})
|
|
55
|
-
// 创建目标目录
|
|
56
|
-
fs.mkdirSync(skillDir, { recursive: true })
|
|
57
|
-
// 解压文件到目标目录,跳过顶层文件夹
|
|
58
|
-
await new Promise((resolve, reject) => {
|
|
59
|
-
const tasks: Promise<void>[] = []
|
|
60
|
-
let rootDir: string | null = null
|
|
61
|
-
let hasError = false
|
|
62
|
-
|
|
63
|
-
response.data
|
|
64
|
-
.pipe(unzipper.Parse())
|
|
65
|
-
.on('entry', (entry: any) => {
|
|
66
|
-
if (hasError) {
|
|
67
|
-
entry.autodrain()
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
const flags = entry.props?.flags ?? 0
|
|
72
|
-
const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
|
|
73
|
-
const pathParts = entryPath.split('/')
|
|
74
|
-
|
|
75
|
-
// 检测根目录
|
|
76
|
-
if (!rootDir && pathParts.length > 1) {
|
|
77
|
-
rootDir = pathParts[0]
|
|
78
|
-
}
|
|
79
|
-
let newPath = entryPath
|
|
80
|
-
// 移除顶层文件夹
|
|
81
|
-
if (rootDir && entryPath.startsWith(rootDir + '/')) {
|
|
82
|
-
newPath = entryPath.slice(rootDir.length + 1)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!newPath) {
|
|
86
|
-
entry.autodrain()
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const targetPath = path.join(skillDir, newPath)
|
|
91
|
-
|
|
92
|
-
if (entry.type === 'Directory') {
|
|
93
|
-
fs.mkdirSync(targetPath, { recursive: true })
|
|
94
|
-
entry.autodrain()
|
|
95
|
-
} else {
|
|
96
|
-
const parentDir = path.dirname(targetPath)
|
|
97
|
-
fs.mkdirSync(parentDir, { recursive: true })
|
|
98
|
-
const writeStream = fs.createWriteStream(targetPath)
|
|
99
|
-
const task = pipeline(entry, writeStream).catch((err) => {
|
|
100
|
-
hasError = true
|
|
101
|
-
throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
|
|
102
|
-
})
|
|
103
|
-
tasks.push(task)
|
|
104
|
-
}
|
|
105
|
-
} catch (err) {
|
|
106
|
-
hasError = true
|
|
107
|
-
entry.autodrain()
|
|
108
|
-
reject(new Error(`处理entry失败: ${err}`))
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
.on('close', async () => {
|
|
112
|
-
try {
|
|
113
|
-
await Promise.all(tasks)
|
|
114
|
-
resolve(null)
|
|
115
|
-
} catch (err) {
|
|
116
|
-
reject(err)
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
.on('error', (err: { message: any }) => {
|
|
120
|
-
hasError = true
|
|
121
|
-
reject(new Error(`解压流错误: ${err.message}`))
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
sendEvent({ ...msgContent, status: 'ok' })
|
|
125
|
-
sendMessageToGateway(JSON.stringify({ method: 'skills.status', params: {} }))
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// 如果安装失败,清理目录
|
|
128
|
-
if (fs.existsSync(skillDir)) {
|
|
129
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
130
|
-
}
|
|
131
|
-
sendEvent({ ...msgContent, status: 'fail' })
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: Record<string, any>) {
|
|
136
|
-
const { code } = params
|
|
137
|
-
|
|
138
|
-
const workspacePath = getWorkspaceDir()
|
|
139
|
-
if (!workspacePath) {
|
|
140
|
-
sendEvent({ ...msgContent, status: 'ok' })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const skillDir = path.join(workspacePath, 'skills', code)
|
|
144
|
-
|
|
145
|
-
if (fs.existsSync(skillDir)) {
|
|
146
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
147
|
-
sendEvent({ ...msgContent, status: 'ok' })
|
|
148
|
-
} else {
|
|
149
|
-
sendEvent({ ...msgContent, status: 'ok' })
|
|
150
|
-
}
|
|
151
|
-
}
|
package/src/tool.ts
DELETED
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { getMsgStatus } from './utils/global.js'
|
|
3
|
-
import { dcgLogger } from './utils/log.js'
|
|
4
|
-
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
|
-
import { getEffectiveMsgParams, deleteSessionKeyBySubAgentRunId, setSessionKeyBySubAgentRunId } from './utils/params.js'
|
|
6
|
-
import { cronToolCall } from './cronToolCall.js'
|
|
7
|
-
|
|
8
|
-
type PluginHookName =
|
|
9
|
-
| 'before_model_resolve'
|
|
10
|
-
| 'before_prompt_build'
|
|
11
|
-
| 'before_agent_start'
|
|
12
|
-
| 'llm_input'
|
|
13
|
-
| 'llm_output'
|
|
14
|
-
| 'agent_end'
|
|
15
|
-
| 'before_compaction'
|
|
16
|
-
| 'after_compaction'
|
|
17
|
-
| 'before_reset'
|
|
18
|
-
| 'message_received'
|
|
19
|
-
| 'message_sending'
|
|
20
|
-
| 'message_sent'
|
|
21
|
-
| 'before_tool_call'
|
|
22
|
-
| 'after_tool_call'
|
|
23
|
-
| 'tool_result_persist'
|
|
24
|
-
| 'before_message_write'
|
|
25
|
-
| 'session_start'
|
|
26
|
-
| 'session_end'
|
|
27
|
-
| 'subagent_spawning'
|
|
28
|
-
| 'subagent_delivery_target'
|
|
29
|
-
| 'subagent_spawned'
|
|
30
|
-
| 'subagent_ended'
|
|
31
|
-
| 'gateway_start'
|
|
32
|
-
| 'gateway_stop'
|
|
33
|
-
|
|
34
|
-
// message_received 没有 sessionKey 前置到bot中执行
|
|
35
|
-
const eventList = [
|
|
36
|
-
// { event: 'message_received', message: '' },
|
|
37
|
-
// {event: 'before_model_resolve', message: ''},
|
|
38
|
-
// {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
|
|
39
|
-
// {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
|
|
40
|
-
{ event: 'subagent_spawning', message: '' },
|
|
41
|
-
{ event: 'subagent_spawned', message: '' },
|
|
42
|
-
{ event: 'subagent_delivery_target', message: '' },
|
|
43
|
-
// {event: 'llm_input', message: ''},
|
|
44
|
-
{ event: 'llm_output', message: '' },
|
|
45
|
-
// {event: 'agent_end', message: '核心任务已处理完毕...'},
|
|
46
|
-
{ event: 'subagent_ended', message: '' },
|
|
47
|
-
// {event: 'before_message_write', message: '正在将本次对话存入记忆库...'},
|
|
48
|
-
// {event: 'message_sending', message: ''},
|
|
49
|
-
// {event: 'message_send', message: ''},
|
|
50
|
-
{ event: 'before_tool_call', message: '' },
|
|
51
|
-
{ event: 'after_tool_call', message: '' }
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
/** 子 agent 的 sessionKey 往往未写入 params map,回落到主会话 outbound 参数避免 messageId 缺失 */
|
|
55
|
-
function resolveOutboundParamsForSession(sk: string) {
|
|
56
|
-
const k = sk.trim()
|
|
57
|
-
let params = getEffectiveMsgParams(k)
|
|
58
|
-
if (params.messageId?.trim() || params.sessionId?.trim()) return params
|
|
59
|
-
const parent = requesterByChildSessionKey.get(k)
|
|
60
|
-
if (parent) {
|
|
61
|
-
const parentParams = getEffectiveMsgParams(parent)
|
|
62
|
-
if (parentParams.messageId?.trim() || parentParams.sessionId?.trim()) return parentParams
|
|
63
|
-
}
|
|
64
|
-
return params
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 主会话已 running 时,子会话上的工具/事件也应下发(否则子 key 无 running 状态会整段丢消息) */
|
|
68
|
-
export function isSessionActiveForTool(sk: string): boolean {
|
|
69
|
-
const k = sk.trim()
|
|
70
|
-
if (!k) return false
|
|
71
|
-
if (getMsgStatus(k) === 'running') return true
|
|
72
|
-
const parent = requesterByChildSessionKey.get(k)
|
|
73
|
-
return parent ? getMsgStatus(parent) === 'running' : false
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
|
|
77
|
-
const params = resolveOutboundParamsForSession(sk)
|
|
78
|
-
const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
|
|
79
|
-
wsSendRaw(params, content, false)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 深拷贝 params 并注入 bestEffort: true
|
|
84
|
-
*/
|
|
85
|
-
interface CronDelivery {
|
|
86
|
-
mode?: string
|
|
87
|
-
channel?: string
|
|
88
|
-
to?: string
|
|
89
|
-
bestEffort?: boolean
|
|
90
|
-
[key: string]: unknown
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// --- Subagent 活跃跟踪(按主会话 requesterSessionKey)---
|
|
94
|
-
|
|
95
|
-
/** 主会话 sessionKey -> 仍活跃的子 agent runId */
|
|
96
|
-
const activeSubagentRunIdsByRequester = new Map<string, Set<string>>()
|
|
97
|
-
/** 子会话 childSessionKey -> 主会话 requesterSessionKey */
|
|
98
|
-
const requesterByChildSessionKey = new Map<string, string>()
|
|
99
|
-
/** 子会话 childSessionKey -> spawn 时的 runId(ended 事件可能不带 runId) */
|
|
100
|
-
const runIdByChildSessionKey = new Map<string, string>()
|
|
101
|
-
/** 主会话 -> 等待「子 agent 全部结束」的回调 */
|
|
102
|
-
const subagentIdleWaiters = new Map<string, Set<() => void>>()
|
|
103
|
-
|
|
104
|
-
function getOrCreateRunIdSet(requesterSessionKey: string): Set<string> {
|
|
105
|
-
let set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
106
|
-
if (!set) {
|
|
107
|
-
set = new Set()
|
|
108
|
-
activeSubagentRunIdsByRequester.set(requesterSessionKey, set)
|
|
109
|
-
}
|
|
110
|
-
return set
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function flushSubagentIdleWaiters(requesterSessionKey: string): void {
|
|
114
|
-
const set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
115
|
-
if (set && set.size > 0) return
|
|
116
|
-
activeSubagentRunIdsByRequester.delete(requesterSessionKey)
|
|
117
|
-
const waiters = subagentIdleWaiters.get(requesterSessionKey)
|
|
118
|
-
if (!waiters?.size) return
|
|
119
|
-
subagentIdleWaiters.delete(requesterSessionKey)
|
|
120
|
-
for (const w of waiters) {
|
|
121
|
-
try {
|
|
122
|
-
w()
|
|
123
|
-
} catch (e) {
|
|
124
|
-
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function registerSubagentSpawn(requesterSessionKey: string, runId: string, childSessionKey: string): void {
|
|
130
|
-
const req = requesterSessionKey.trim()
|
|
131
|
-
const rid = runId.trim()
|
|
132
|
-
const child = childSessionKey.trim()
|
|
133
|
-
if (!req || !rid || !child) {
|
|
134
|
-
dcgLogger(`subagent track spawn skipped: missing key req=${req} runId=${rid} child=${child}`)
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
getOrCreateRunIdSet(req).add(rid)
|
|
138
|
-
requesterByChildSessionKey.set(child, req)
|
|
139
|
-
runIdByChildSessionKey.set(child, rid)
|
|
140
|
-
dcgLogger(`subagent track spawn: requester=${req} runId=${rid} child=${child} active=${getOrCreateRunIdSet(req).size}`)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function registerSubagentEnd(
|
|
144
|
-
ctx: { requesterSessionKey?: string; sessionKey?: string },
|
|
145
|
-
targetSessionKey: string,
|
|
146
|
-
runId?: string
|
|
147
|
-
): void {
|
|
148
|
-
const child = targetSessionKey.trim()
|
|
149
|
-
if (!child) return
|
|
150
|
-
const req = ctx.requesterSessionKey?.trim() || requesterByChildSessionKey.get(child) || ctx.sessionKey?.trim() || ''
|
|
151
|
-
const resolvedRunId = (runId?.trim() || runIdByChildSessionKey.get(child) || '').trim()
|
|
152
|
-
deleteSessionKeyBySubAgentRunId(resolvedRunId)
|
|
153
|
-
if (!req) {
|
|
154
|
-
dcgLogger(`subagent track end: no requester for child=${child} runId=${resolvedRunId}`)
|
|
155
|
-
requesterByChildSessionKey.delete(child)
|
|
156
|
-
runIdByChildSessionKey.delete(child)
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
const set = activeSubagentRunIdsByRequester.get(req)
|
|
160
|
-
if (set && resolvedRunId) {
|
|
161
|
-
set.delete(resolvedRunId)
|
|
162
|
-
}
|
|
163
|
-
requesterByChildSessionKey.delete(child)
|
|
164
|
-
runIdByChildSessionKey.delete(child)
|
|
165
|
-
dcgLogger(`subagent track end: requester=${req} runId=${resolvedRunId || 'n/a'} remaining=${set?.size ?? 0}`)
|
|
166
|
-
if (set && set.size === 0) {
|
|
167
|
-
activeSubagentRunIdsByRequester.delete(req)
|
|
168
|
-
}
|
|
169
|
-
flushSubagentIdleWaiters(req)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** 当前跟踪到的、挂在该主会话下的子会话 sessionKey(供 /stop 时逐个 chat.abort) */
|
|
173
|
-
export function getChildSessionKeysTrackedForRequester(requesterSessionKey: string): string[] {
|
|
174
|
-
const req = requesterSessionKey.trim()
|
|
175
|
-
if (!req) return []
|
|
176
|
-
const out: string[] = []
|
|
177
|
-
for (const [child, parent] of requesterByChildSessionKey.entries()) {
|
|
178
|
-
if (parent === req) out.push(child)
|
|
179
|
-
}
|
|
180
|
-
return out
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 自根 requester 起 BFS 收集所有已跟踪后代子会话(含嵌套)。网关 abort 时宜自深到浅,调用方对结果 `.reverse()` 后再逐个 chat.abort。
|
|
185
|
-
*/
|
|
186
|
-
export function getDescendantSessionKeysForRequester(rootRequesterSessionKey: string): string[] {
|
|
187
|
-
const root = rootRequesterSessionKey.trim()
|
|
188
|
-
if (!root) return []
|
|
189
|
-
const ordered: string[] = []
|
|
190
|
-
const seen = new Set<string>()
|
|
191
|
-
let frontier = getChildSessionKeysTrackedForRequester(root)
|
|
192
|
-
while (frontier.length > 0) {
|
|
193
|
-
const next: string[] = []
|
|
194
|
-
for (const sk of frontier) {
|
|
195
|
-
const k = sk.trim()
|
|
196
|
-
if (!k || seen.has(k)) continue
|
|
197
|
-
seen.add(k)
|
|
198
|
-
ordered.push(k)
|
|
199
|
-
next.push(...getChildSessionKeysTrackedForRequester(k))
|
|
200
|
-
}
|
|
201
|
-
frontier = next
|
|
202
|
-
}
|
|
203
|
-
return ordered
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
|
|
208
|
-
*/
|
|
209
|
-
export function resetSubagentStateForRequesterSession(requesterSessionKey: string): void {
|
|
210
|
-
const req = requesterSessionKey.trim()
|
|
211
|
-
if (!req) return
|
|
212
|
-
|
|
213
|
-
const runIdSet = activeSubagentRunIdsByRequester.get(req)
|
|
214
|
-
if (runIdSet) {
|
|
215
|
-
for (const rid of runIdSet) {
|
|
216
|
-
deleteSessionKeyBySubAgentRunId(rid)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for (const [child, parent] of [...requesterByChildSessionKey.entries()]) {
|
|
221
|
-
if (parent === req) {
|
|
222
|
-
requesterByChildSessionKey.delete(child)
|
|
223
|
-
runIdByChildSessionKey.delete(child)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
activeSubagentRunIdsByRequester.delete(req)
|
|
228
|
-
|
|
229
|
-
const waiters = subagentIdleWaiters.get(req)
|
|
230
|
-
if (!waiters?.size) return
|
|
231
|
-
subagentIdleWaiters.delete(req)
|
|
232
|
-
for (const w of waiters) {
|
|
233
|
-
try {
|
|
234
|
-
w()
|
|
235
|
-
} catch (e) {
|
|
236
|
-
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** 当前主会话下仍在跑的子 agent 数量(按 spawn 时 runId 去重) */
|
|
242
|
-
export function getActiveSubagentCount(sessionKey: string): number {
|
|
243
|
-
const sk = sessionKey?.trim()
|
|
244
|
-
if (!sk) return 0
|
|
245
|
-
return activeSubagentRunIdsByRequester.get(sk)?.size ?? 0
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 等到指定主会话下已跟踪的子 agent 全部结束(spawn 失败等路径也会触发 subagent_ended,一般会配对清理)。
|
|
250
|
-
* 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
|
|
251
|
-
*/
|
|
252
|
-
export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
|
|
253
|
-
const sk = sessionKey?.trim()
|
|
254
|
-
if (!sk) return Promise.resolve()
|
|
255
|
-
|
|
256
|
-
if (getActiveSubagentCount(sk) === 0) return Promise.resolve()
|
|
257
|
-
|
|
258
|
-
return new Promise<void>((resolve, reject) => {
|
|
259
|
-
let settled = false
|
|
260
|
-
const finish = (fn: () => void) => {
|
|
261
|
-
if (settled) return
|
|
262
|
-
settled = true
|
|
263
|
-
if (timeoutId) clearTimeout(timeoutId)
|
|
264
|
-
opts?.signal?.removeEventListener('abort', onAbort)
|
|
265
|
-
removeWaiter()
|
|
266
|
-
fn()
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const removeWaiter = () => {
|
|
270
|
-
const bucket = subagentIdleWaiters.get(sk)
|
|
271
|
-
if (!bucket) return
|
|
272
|
-
bucket.delete(onIdle)
|
|
273
|
-
if (bucket.size === 0) subagentIdleWaiters.delete(sk)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const onIdle = () => finish(() => resolve())
|
|
277
|
-
|
|
278
|
-
const onAbort = () => {
|
|
279
|
-
const reason = opts?.signal?.reason
|
|
280
|
-
finish(() => reject(reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted'))))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
284
|
-
if (opts?.timeoutMs != null && opts.timeoutMs > 0) {
|
|
285
|
-
timeoutId = setTimeout(
|
|
286
|
-
() => finish(() => reject(new Error(`waitUntilSubagentsIdle timeout ${opts.timeoutMs}ms`))),
|
|
287
|
-
opts.timeoutMs
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (opts?.signal) {
|
|
292
|
-
if (opts.signal.aborted) {
|
|
293
|
-
onAbort()
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let set = subagentIdleWaiters.get(sk)
|
|
300
|
-
if (!set) {
|
|
301
|
-
set = new Set()
|
|
302
|
-
subagentIdleWaiters.set(sk, set)
|
|
303
|
-
}
|
|
304
|
-
set.add(onIdle)
|
|
305
|
-
|
|
306
|
-
if (getActiveSubagentCount(sk) === 0) {
|
|
307
|
-
onIdle()
|
|
308
|
-
}
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function resolveHookSessionKey(
|
|
313
|
-
eventName: string,
|
|
314
|
-
args: { sessionKey?: string; requesterSessionKey?: string; runId?: string }
|
|
315
|
-
): string {
|
|
316
|
-
if (
|
|
317
|
-
eventName === 'subagent_spawned' ||
|
|
318
|
-
eventName === 'subagent_ended' ||
|
|
319
|
-
eventName === 'subagent_spawning' ||
|
|
320
|
-
eventName === 'subagent_delivery_target'
|
|
321
|
-
) {
|
|
322
|
-
if (args?.runId && args?.requesterSessionKey) setSessionKeyBySubAgentRunId(args?.runId, args?.requesterSessionKey)
|
|
323
|
-
return (args?.requesterSessionKey || args?.sessionKey || '').trim()
|
|
324
|
-
}
|
|
325
|
-
return (args?.sessionKey || '').trim()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** 定时触发时会话往往非 running,但仍需跑 before_tool_call 以注入 sessionKey / delivery(见 cronToolCall) */
|
|
329
|
-
function shouldRunBeforeToolCallWithoutRunningSession(event: { toolName?: string; params?: { command?: string } }): boolean {
|
|
330
|
-
if (event?.toolName === 'cron') return true
|
|
331
|
-
const cmd = event?.params?.command
|
|
332
|
-
if (event?.toolName === 'exec' && typeof cmd === 'string') {
|
|
333
|
-
return cmd.includes('cron create') || cmd.includes('cron add')
|
|
334
|
-
}
|
|
335
|
-
return false
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
|
|
339
|
-
if (eventName === 'subagent_spawned') {
|
|
340
|
-
const runId = typeof event?.runId === 'string' ? event.runId : ''
|
|
341
|
-
const childSessionKey = typeof event?.childSessionKey === 'string' ? event.childSessionKey : ''
|
|
342
|
-
const requester =
|
|
343
|
-
typeof args?.requesterSessionKey === 'string'
|
|
344
|
-
? args.requesterSessionKey
|
|
345
|
-
: typeof args?.sessionKey === 'string'
|
|
346
|
-
? args.sessionKey
|
|
347
|
-
: ''
|
|
348
|
-
registerSubagentSpawn(requester, runId, childSessionKey)
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
if (eventName === 'subagent_ended') {
|
|
352
|
-
const targetSessionKey = typeof event?.targetSessionKey === 'string' ? event.targetSessionKey : ''
|
|
353
|
-
const runId = typeof event?.runId === 'string' ? event.runId : undefined
|
|
354
|
-
registerSubagentEnd(args ?? {}, targetSessionKey, runId)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
359
|
-
for (const item of eventList) {
|
|
360
|
-
api.on(item.event as PluginHookName, (event: any, args: any) => {
|
|
361
|
-
trackSubagentLifecycle(item.event, event, args)
|
|
362
|
-
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
363
|
-
if (sk) {
|
|
364
|
-
const toolHooksOk =
|
|
365
|
-
isSessionActiveForTool(sk) || (item.event === 'before_tool_call' && shouldRunBeforeToolCallWithoutRunningSession(event))
|
|
366
|
-
if (toolHooksOk) {
|
|
367
|
-
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
368
|
-
const { result: _result, ...rest } = event
|
|
369
|
-
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
|
370
|
-
|
|
371
|
-
if (item.event === 'before_tool_call') {
|
|
372
|
-
const hookResult = cronToolCall(rest, sk)
|
|
373
|
-
const text = JSON.stringify({
|
|
374
|
-
type: item.event,
|
|
375
|
-
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
376
|
-
callId: event.toolCallId || event.runId || Date.now().toString(),
|
|
377
|
-
...rest,
|
|
378
|
-
status: 'running'
|
|
379
|
-
})
|
|
380
|
-
sendToolCallMessage(sk, text, event.toolCallId || event.runId || Date.now().toString(), 0)
|
|
381
|
-
return hookResult
|
|
382
|
-
}
|
|
383
|
-
const text = JSON.stringify({
|
|
384
|
-
type: item.event,
|
|
385
|
-
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
386
|
-
callId: event.toolCallId || event.runId || Date.now().toString(),
|
|
387
|
-
...rest,
|
|
388
|
-
status: item.event === 'after_tool_call' ? 'finished' : 'running'
|
|
389
|
-
})
|
|
390
|
-
sendToolCallMessage(
|
|
391
|
-
sk,
|
|
392
|
-
text,
|
|
393
|
-
event.toolCallId || event.runId || Date.now().toString(),
|
|
394
|
-
item.event === 'after_tool_call' ? 1 : 0
|
|
395
|
-
)
|
|
396
|
-
} else if (item.event) {
|
|
397
|
-
const msgCtx = resolveOutboundParamsForSession(sk)
|
|
398
|
-
if (item.event === 'llm_output') {
|
|
399
|
-
if (event.lastAssistant?.errorMessage === '1003-额度不足请充值') {
|
|
400
|
-
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
|
401
|
-
sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
|
|
402
|
-
sendFinal(msgCtx, '积分不足')
|
|
403
|
-
return
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const text = JSON.stringify({
|
|
407
|
-
type: item.event,
|
|
408
|
-
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
409
|
-
toolName: '',
|
|
410
|
-
callId: event.runId || Date.now().toString(),
|
|
411
|
-
params: item.message
|
|
412
|
-
})
|
|
413
|
-
sendToolCallMessage(sk, text, event.runId || Date.now().toString(), 0)
|
|
414
|
-
dcgLogger(`工具调用结果: ~ event:${item.event}`)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
} else if (item.event !== 'before_tool_call') {
|
|
418
|
-
dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
|
|
419
|
-
}
|
|
420
|
-
})
|
|
421
|
-
}
|
|
422
|
-
}
|