@dcrays/dcgchat 0.4.27 → 0.5.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 +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 -61
- 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/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
|
-
}
|
package/src/tools/messageTool.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import type { AnyAgentTool } from 'openclaw/plugin-sdk'
|
|
5
|
-
import { jsonResult } from 'openclaw/plugin-sdk'
|
|
6
|
-
import { sendDcgchatMedia } from '../channel.js'
|
|
7
|
-
import { getOutboundMsgParams } from '../utils/params.js'
|
|
8
|
-
import { sendText } from '../transport.js'
|
|
9
|
-
|
|
10
|
-
/** 与 `registerTool` 工厂入参一致(主包未导出 `OpenClawPluginToolContext` 时仅用所需字段)。 */
|
|
11
|
-
export type DcgchatMessageToolContext = {
|
|
12
|
-
sessionKey?: string
|
|
13
|
-
workspaceDir?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** 统一为 POSIX 风格斜杠,便于跨平台判断(不改变语义,仅用于匹配)。 */
|
|
17
|
-
function toPosixPath(p: string): string {
|
|
18
|
-
return path.normalize(p.trim()).replace(/\\/g, '/')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** `filepath` 解析后在 `rootDir` 内或等于 `rootDir`(防 `..` 逃逸)。 */
|
|
22
|
-
function isPathInsideDir(filepath: string, rootDir: string): boolean {
|
|
23
|
-
const root = path.resolve(rootDir)
|
|
24
|
-
const resolved = path.resolve(filepath)
|
|
25
|
-
const rel = path.relative(root, resolved)
|
|
26
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) return false
|
|
27
|
-
return true
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 允许发送的路径:
|
|
32
|
-
* - 当前 Agent 工作区根及其子路径(`workspaceDir`,如 ~/.openclaw/workspace-xxx/output/...);
|
|
33
|
-
* - 兼容旧挂载:Unix `/workspace`、`/mobook`;Windows 盘符下 `workspace`、`mobook`。
|
|
34
|
-
*/
|
|
35
|
-
function isSafePath(filepath: string, workspaceDir?: string): boolean {
|
|
36
|
-
const ws = workspaceDir?.trim()
|
|
37
|
-
if (ws && isPathInsideDir(filepath, ws)) return true
|
|
38
|
-
const p = toPosixPath(filepath)
|
|
39
|
-
if (p.startsWith('/workspace/') || p === '/workspace') return true
|
|
40
|
-
if (p.startsWith('/mobook/') || p === '/mobook') return true
|
|
41
|
-
return /^[A-Za-z]:\/(workspace|mobook)(\/|$)/.test(p)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 同一路径在 Windows 上可能大小写不同,用于 Set 去重。 */
|
|
45
|
-
function pathKey(filepath: string): string {
|
|
46
|
-
const n = path.normalize(filepath.trim())
|
|
47
|
-
return os.platform() === 'win32' ? n.toLowerCase() : n
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const fileType1 = ['.webp', '.gif', '.bmp', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.rtf', '.odt', '.json']
|
|
51
|
-
const fileType2 = ['.xml', '.csv', '.yaml', '.yml', '.html', '.htm', '.md', '.markdown', '.css', '.js', '.ts', '.png', '.jpg', '.jpeg']
|
|
52
|
-
const fileType3 = ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.exe', '.dmg', '.pkg', '.apk', '.ipa', '.log', '.dat', '.bin']
|
|
53
|
-
const fileType4 = ['.svg', '.ico', '.mp3', '.wav', '.ogg', '.aac', '.m4a', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm']
|
|
54
|
-
const SAFE_EXTENSIONS = new Set([...fileType1, ...fileType2, ...fileType3, ...fileType4])
|
|
55
|
-
|
|
56
|
-
const messageToolParameters = {
|
|
57
|
-
type: 'object',
|
|
58
|
-
additionalProperties: false,
|
|
59
|
-
properties: {
|
|
60
|
-
target: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
description: '目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
|
|
63
|
-
},
|
|
64
|
-
content: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
description: '发送文本内容'
|
|
67
|
-
},
|
|
68
|
-
media: {
|
|
69
|
-
type: 'array',
|
|
70
|
-
description: '发送附件',
|
|
71
|
-
items: {
|
|
72
|
-
type: 'object',
|
|
73
|
-
additionalProperties: false,
|
|
74
|
-
properties: {
|
|
75
|
-
file: {
|
|
76
|
-
type: 'string',
|
|
77
|
-
description:
|
|
78
|
-
'文件绝对路径:须在「当前 Agent 工作区」目录下(如 /root/.openclaw/workspace-xxx/output/28337/slices_result.json),或为兼容环境的 /workspace/、/mobook/(Windows 盘符下 workspace、mobook)'
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
required: ['file']
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
oneOf: [{ required: ['content'] }, { required: ['media'] }]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** 从正文提取可发送的文件路径(固定挂载 + 当前工作区前缀)。 */
|
|
89
|
-
function extractPaths(text: string | undefined, workspaceDir?: string): string[] {
|
|
90
|
-
if (!text) return []
|
|
91
|
-
const unix = text.match(/\/workspace\/[^\s]+|\/mobook\/[^\s]+/g) ?? []
|
|
92
|
-
const win = text.match(/[A-Za-z]:[/\\](?:workspace|mobook)[/\\][^\s]+/g) ?? []
|
|
93
|
-
const underWs: string[] = []
|
|
94
|
-
const ws = workspaceDir?.trim()
|
|
95
|
-
if (ws) {
|
|
96
|
-
const variants = new Set<string>()
|
|
97
|
-
variants.add(ws)
|
|
98
|
-
variants.add(toPosixPath(ws))
|
|
99
|
-
if (path.sep === '\\') variants.add(ws.replace(/\//g, '\\'))
|
|
100
|
-
for (const prefix of variants) {
|
|
101
|
-
if (!prefix) continue
|
|
102
|
-
let from = 0
|
|
103
|
-
while (from < text.length) {
|
|
104
|
-
const i = text.indexOf(prefix, from)
|
|
105
|
-
if (i === -1) break
|
|
106
|
-
let end = i + prefix.length
|
|
107
|
-
while (end < text.length && !/\s/.test(text[end])) end++
|
|
108
|
-
underWs.push(text.slice(i, end))
|
|
109
|
-
from = i + 1
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return [...new Set([...unix, ...win, ...underWs])]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isSafeFile(filepath: string) {
|
|
117
|
-
if (!fs.existsSync(filepath)) return false
|
|
118
|
-
const stat = fs.statSync(filepath)
|
|
119
|
-
if (!stat.isFile()) return false
|
|
120
|
-
if (stat.size === 0) return false
|
|
121
|
-
const ext = path.extname(filepath).toLowerCase()
|
|
122
|
-
return SAFE_EXTENSIONS.has(ext)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 书灵墨宝出站消息工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
|
|
127
|
-
* 工具名使用 `dcgchat_message`,避免与核心内置 `message` 冲突。
|
|
128
|
-
* 通过注册时的 `OpenClawPluginToolContext.sessionKey` 出站,不再使用非标准的 `execute(args, ctx)`。
|
|
129
|
-
*/
|
|
130
|
-
export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext): AnyAgentTool {
|
|
131
|
-
return {
|
|
132
|
-
name: 'dcgchat_message',
|
|
133
|
-
label: 'dcgchat_message',
|
|
134
|
-
description: `
|
|
135
|
-
向用户发送消息。
|
|
136
|
-
若传 target,target 必须是 sessionKey,不能是 userId。
|
|
137
|
-
如果发送附件:必须使用 media 字段
|
|
138
|
-
文件路径须在当前 Agent 工作区目录下(随部署变化,如 ~/.openclaw/workspace-xxx/...),或为兼容环境的 /workspace/、/mobook/(Windows 盘符下 workspace、mobook)。
|
|
139
|
-
禁止在正文中直接输出可访问路径(应通过 media 发送)
|
|
140
|
-
`,
|
|
141
|
-
parameters: messageToolParameters,
|
|
142
|
-
execute: async (_toolCallId, args, signal) => {
|
|
143
|
-
if (signal?.aborted) {
|
|
144
|
-
const err = new Error('Message send aborted')
|
|
145
|
-
err.name = 'AbortError'
|
|
146
|
-
throw err
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const sessionKey = pluginCtx.sessionKey?.trim()
|
|
150
|
-
if (!sessionKey) {
|
|
151
|
-
return jsonResult({ error: '缺少 sessionKey,无法向当前会话发送消息' })
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const sentFiles = new Set<string>()
|
|
156
|
-
const sentKeys = new Set<string>()
|
|
157
|
-
const workspaceDir = pluginCtx.workspaceDir
|
|
158
|
-
|
|
159
|
-
if (args.media?.length) {
|
|
160
|
-
for (const media of args.media) {
|
|
161
|
-
const filepath = media.file
|
|
162
|
-
if (!filepath) continue
|
|
163
|
-
if (!isSafePath(filepath, workspaceDir)) continue
|
|
164
|
-
if (!isSafeFile(filepath)) continue
|
|
165
|
-
const key = pathKey(filepath)
|
|
166
|
-
if (sentKeys.has(key)) continue
|
|
167
|
-
|
|
168
|
-
await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
|
|
169
|
-
sentFiles.add(filepath)
|
|
170
|
-
sentKeys.add(key)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const fallbackPaths = extractPaths(args.content, workspaceDir)
|
|
175
|
-
for (const filepath of fallbackPaths) {
|
|
176
|
-
if (!isSafePath(filepath, workspaceDir)) continue
|
|
177
|
-
if (!isSafeFile(filepath)) continue
|
|
178
|
-
const key = pathKey(filepath)
|
|
179
|
-
if (sentKeys.has(key)) continue
|
|
180
|
-
|
|
181
|
-
await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
|
|
182
|
-
sentFiles.add(filepath)
|
|
183
|
-
sentKeys.add(key)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (args.media?.length && sentFiles.size === 0) {
|
|
187
|
-
return jsonResult({
|
|
188
|
-
success: false,
|
|
189
|
-
error:
|
|
190
|
-
'未能发送任何附件:路径须位于当前 Agent 工作区,或为 /workspace/、/mobook/ 下的真实文件(非空、扩展名在白名单内)。',
|
|
191
|
-
sentMediaCount: 0
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
let content = args.content ?? ''
|
|
196
|
-
for (const filepath of sentFiles) {
|
|
197
|
-
const posix = toPosixPath(filepath)
|
|
198
|
-
const variants = posix === filepath ? [filepath] : [filepath, posix]
|
|
199
|
-
const seen = new Set<string>()
|
|
200
|
-
for (const v of variants) {
|
|
201
|
-
if (!v || seen.has(v)) continue
|
|
202
|
-
seen.add(v)
|
|
203
|
-
content = content.split(v).join('')
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
content = content.trim()
|
|
207
|
-
|
|
208
|
-
if (content.length > 0) {
|
|
209
|
-
const msgCtx = getOutboundMsgParams(sessionKey)
|
|
210
|
-
sendText(content, msgCtx)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return jsonResult({
|
|
214
|
-
success: true,
|
|
215
|
-
sentMediaCount: sentFiles.size
|
|
216
|
-
})
|
|
217
|
-
} catch (err) {
|
|
218
|
-
return jsonResult({
|
|
219
|
-
error: err instanceof Error ? err.message : String(err)
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|