@dcrays/dcgchat 0.3.35 → 0.4.4
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.ts +3 -4
- package/package.json +1 -1
- package/src/agent.ts +125 -0
- package/src/bot.ts +161 -182
- package/src/channel.ts +91 -19
- package/src/cron.ts +8 -9
- package/src/gateway/index.ts +2 -26
- package/src/gateway/socket.ts +5 -3
- package/src/monitor.ts +3 -52
- package/src/session.ts +2 -2
- package/src/tool.ts +236 -3
- package/src/tools/messageTool.ts +215 -0
- package/src/transport.ts +10 -8
- package/src/types.ts +7 -0
- package/src/utils/constant.ts +2 -2
- package/src/utils/gatewayMsgHanlder.ts +47 -0
- package/src/utils/global.ts +29 -36
- package/src/utils/params.ts +20 -3
- package/src/utils/wsMessageHandler.ts +64 -0
- package/src/utils/zipExtract.ts +97 -0
- package/src/utils/zipPath.ts +24 -0
package/src/channel.ts
CHANGED
|
@@ -1,13 +1,61 @@
|
|
|
1
|
-
import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
1
|
+
import type { ChannelPlugin, OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { createPluginRuntimeStore, DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
3
3
|
import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
|
|
4
4
|
import { ossUpload } from './request/oss.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
addSentMediaKey,
|
|
7
|
+
getCronMessageId,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getInfoBySessionKey,
|
|
10
|
+
getOpenClawConfig,
|
|
11
|
+
hasSentMediaKey
|
|
12
|
+
} from './utils/global.js'
|
|
6
13
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
7
14
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
9
16
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
10
17
|
|
|
18
|
+
function dcgchatChannelCfg(): DcgchatConfig {
|
|
19
|
+
return (getOpenClawConfig()?.channels?.["dcgchat"] as DcgchatConfig | undefined) ?? {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
|
|
23
|
+
function isMobookDirectSessionKey(s: string): boolean {
|
|
24
|
+
const parts = s.split(':').filter((p) => p.length > 0)
|
|
25
|
+
const low = parts.map((p) => p.toLowerCase())
|
|
26
|
+
return parts.length >= 6 && low[0] === 'agent' && low[2] === 'mobook' && low[3] === 'direct'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** real_mobook 等线路下 Core 分配的 `agent:<agentId>:…` sessionKey */
|
|
30
|
+
function isAgentPrefixedSessionKey(s: string): boolean {
|
|
31
|
+
const parts = s.split(':').filter((p) => p.length > 0)
|
|
32
|
+
return parts.length >= 3 && parts[0].toLowerCase() === 'agent'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 供 `messaging.targetResolver.looksLikeId` 使用:与 OpenClaw `resolveMessagingTarget` 对齐,
|
|
37
|
+
* 仅当 target「像合法会话路由键」时才走 id 类解析;纯数字不会命中,从而在系统层拒绝误填 userId。
|
|
38
|
+
*/
|
|
39
|
+
function looksLikeDcgchatMessageToolTarget(raw: string): boolean {
|
|
40
|
+
let s = raw.trim()
|
|
41
|
+
if (!s) return false
|
|
42
|
+
const prefix = 'dcg-cron:'
|
|
43
|
+
if (s.startsWith(prefix)) {
|
|
44
|
+
s = s.slice(prefix.length).trim()
|
|
45
|
+
if (!s) return false
|
|
46
|
+
}
|
|
47
|
+
if (isMobookDirectSessionKey(s)) return true
|
|
48
|
+
if (isAgentPrefixedSessionKey(s)) return true
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dcgchatMessageTargetLooksLikeId(raw: string, _normalized?: string): boolean {
|
|
53
|
+
if (dcgchatChannelCfg().strictMessageToolTarget === false) {
|
|
54
|
+
return Boolean(raw?.trim())
|
|
55
|
+
}
|
|
56
|
+
return looksLikeDcgchatMessageToolTarget(raw)
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
export type DcgchatMediaSendOptions = {
|
|
12
60
|
/** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
|
|
13
61
|
sessionKey: string
|
|
@@ -98,7 +146,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
98
146
|
effects: true
|
|
99
147
|
// blockStreaming: true,
|
|
100
148
|
},
|
|
101
|
-
|
|
149
|
+
/** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
|
|
150
|
+
reload: { configPrefixes: [`channels.${"dcgchat"}`, 'channels.dcgchat'] },
|
|
102
151
|
configSchema: {
|
|
103
152
|
schema: {
|
|
104
153
|
type: 'object',
|
|
@@ -107,16 +156,25 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
107
156
|
enabled: { type: 'boolean' },
|
|
108
157
|
wsUrl: { type: 'string' },
|
|
109
158
|
botToken: { type: 'string' },
|
|
110
|
-
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与
|
|
159
|
+
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 dcgchat_message 工具的 target(dcgSessionKey)无关' },
|
|
111
160
|
appId: { type: 'string' },
|
|
112
161
|
domainId: { type: 'string' },
|
|
113
|
-
capabilities: { type: 'array', items: { type: 'string' } }
|
|
162
|
+
capabilities: { type: 'array', items: { type: 'string' } },
|
|
163
|
+
strictMessageToolTarget: {
|
|
164
|
+
type: 'boolean',
|
|
165
|
+
description:
|
|
166
|
+
'默认 true:内置 message 工具的 target 须为 sessionKey 形态(如 agent:…:mobook:direct:… 或 agent: 前缀多段),禁止纯数字 WS userId。设为 false 关闭此校验。'
|
|
167
|
+
}
|
|
114
168
|
}
|
|
115
169
|
},
|
|
116
170
|
uiHints: {
|
|
117
171
|
userId: {
|
|
118
172
|
label: 'WS 连接 _userId',
|
|
119
|
-
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用
|
|
173
|
+
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 dcgSessionKey(与入站上下文 SessionKey 相同)。'
|
|
174
|
+
},
|
|
175
|
+
strictMessageToolTarget: {
|
|
176
|
+
label: '严格 message.target',
|
|
177
|
+
help: '开启后由通道目标解析层拒绝纯数字等非 sessionKey 的 target(推荐开启);关闭则与旧版行为一致。'
|
|
120
178
|
}
|
|
121
179
|
}
|
|
122
180
|
},
|
|
@@ -124,16 +182,17 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
124
182
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
125
183
|
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
126
184
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
127
|
-
setAccountEnabled: ({ cfg, enabled }) =>
|
|
128
|
-
|
|
129
|
-
channels
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
185
|
+
setAccountEnabled: ({ cfg, enabled }) => {
|
|
186
|
+
const channelKey = "dcgchat"
|
|
187
|
+
const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
188
|
+
return {
|
|
189
|
+
...cfg,
|
|
190
|
+
channels: {
|
|
191
|
+
...cfg.channels,
|
|
192
|
+
[channelKey]: { ...prev, enabled }
|
|
134
193
|
}
|
|
135
194
|
}
|
|
136
|
-
}
|
|
195
|
+
},
|
|
137
196
|
isConfigured: (account) => account.configured,
|
|
138
197
|
describeAccount: (account) => ({
|
|
139
198
|
accountId: account.accountId,
|
|
@@ -149,14 +208,26 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
149
208
|
messaging: {
|
|
150
209
|
normalizeTarget: (raw) => raw || undefined,
|
|
151
210
|
targetResolver: {
|
|
152
|
-
looksLikeId:
|
|
153
|
-
hint: '
|
|
211
|
+
looksLikeId: dcgchatMessageTargetLooksLikeId,
|
|
212
|
+
hint: '须为完整 dcgSessionKey(与 SessionKey 一致,形如 agent:…:mobook:direct:… 或 agent: 前缀路由键);禁止填 WS userId 等纯数字。可在通道配置 strictMessageToolTarget=false 关闭校验。'
|
|
154
213
|
}
|
|
155
214
|
},
|
|
215
|
+
/**
|
|
216
|
+
* 与 Telegram 等通道一致:用入站路由键 `To`(即 SessionKey / OriginatingTo)作为 message 工具默认 `currentChannelId`。
|
|
217
|
+
* 显式 target 由 `messaging.targetResolver.looksLikeId` + OpenClaw `resolveMessagingTarget` 校验(见 strictMessageToolTarget)。
|
|
218
|
+
*/
|
|
219
|
+
threading: {
|
|
220
|
+
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
221
|
+
currentChannelId: context.To?.trim() || undefined,
|
|
222
|
+
hasRepliedRef
|
|
223
|
+
})
|
|
224
|
+
},
|
|
156
225
|
agentPrompt: {
|
|
157
226
|
messageToolHints: () => [
|
|
158
|
-
'
|
|
159
|
-
'
|
|
227
|
+
'生成文件后,**尽可能不要**把文件路径、地址直接告诉用户。',
|
|
228
|
+
'生成文件后,把文件名告诉用户。',
|
|
229
|
+
'使用内置 `message` 或 `dcgchat_message` 发消息时,target 必须是完整 dcgSessionKey(与上下文 SessionKey 相同),禁止填 From、SenderId、WS userId 等纯数字。',
|
|
230
|
+
'生成文件后,须通过工具发送文件,勿在文本里直接输出路径或地址。'
|
|
160
231
|
]
|
|
161
232
|
},
|
|
162
233
|
outbound: {
|
|
@@ -167,6 +238,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
167
238
|
}
|
|
168
239
|
return { ok: true, to: to }
|
|
169
240
|
},
|
|
241
|
+
chunker: (text, limit) => getDcgchatRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
170
242
|
textChunkLimit: 4000,
|
|
171
243
|
sendText: async (ctx) => {
|
|
172
244
|
dcgLogger(`channel sendText to ${ctx.to} `)
|
package/src/cron.ts
CHANGED
|
@@ -40,11 +40,6 @@ export function readCronJob(jobPath: string, jobId: string): Record<string, any>
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
|
|
44
|
-
if (!p?.botToken) return null
|
|
45
|
-
return p
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
const CRON_UPLOAD_DEBOUNCE_MS = 2400
|
|
49
44
|
|
|
50
45
|
let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -63,9 +58,10 @@ async function runCronJobsUpload(sessionKey: string): Promise<void> {
|
|
|
63
58
|
event_type: 'cron',
|
|
64
59
|
operation_type: 'install',
|
|
65
60
|
session_id: sessionId,
|
|
66
|
-
agent_id: agentId
|
|
61
|
+
agent_id: agentId,
|
|
62
|
+
oss_url: url
|
|
67
63
|
}
|
|
68
|
-
sendEventMessage(
|
|
64
|
+
sendEventMessage(params)
|
|
69
65
|
} catch (error) {
|
|
70
66
|
dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
|
|
71
67
|
}
|
|
@@ -170,6 +166,7 @@ export const finishedDcgchatCron = async (jobId: string) => {
|
|
|
170
166
|
sendFinal(merged, 'cron send')
|
|
171
167
|
}
|
|
172
168
|
const ws = getWsConnection()
|
|
169
|
+
const baseContent = getParamsDefaults()
|
|
173
170
|
if (isWsOpen()) {
|
|
174
171
|
ws?.send(
|
|
175
172
|
JSON.stringify({
|
|
@@ -178,15 +175,17 @@ export const finishedDcgchatCron = async (jobId: string) => {
|
|
|
178
175
|
content: {
|
|
179
176
|
event_type: 'notify',
|
|
180
177
|
operation_type: 'cron',
|
|
178
|
+
bot_token: baseContent.botToken,
|
|
179
|
+
app_id: baseContent.appId,
|
|
181
180
|
session_id: sessionId,
|
|
182
|
-
|
|
181
|
+
agent_id: agentId,
|
|
183
182
|
real_mobook: !sessionId ? 1 : '',
|
|
184
183
|
title: name
|
|
185
184
|
}
|
|
186
185
|
})
|
|
187
186
|
)
|
|
188
|
-
dcgLogger(`定时任务执行成功: ${id}`)
|
|
189
187
|
}
|
|
188
|
+
dcgLogger(`定时任务执行成功: ${id}`)
|
|
190
189
|
removeCronMessageId(sessionKey)
|
|
191
190
|
dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
|
|
192
191
|
}
|
package/src/gateway/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { WebSocket } from 'ws'
|
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
|
|
5
5
|
import { dcgLogger } from '../utils/log.js'
|
|
6
|
-
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
7
6
|
import { getWorkspaceDir } from '../utils/global.js'
|
|
7
|
+
import { handleGatewayEventMessage } from '../utils/gatewayMsgHanlder.js'
|
|
8
8
|
|
|
9
9
|
export interface GatewayEvent {
|
|
10
10
|
type: string
|
|
@@ -358,31 +358,7 @@ export class GatewayConnection {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
if (msg.type === 'event') {
|
|
361
|
-
|
|
362
|
-
// 定时任务相关事件
|
|
363
|
-
if (msg.event === 'cron') {
|
|
364
|
-
dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
|
|
365
|
-
if (msg.payload?.action === 'added') {
|
|
366
|
-
sendDcgchatCron(msg.payload?.jobId)
|
|
367
|
-
}
|
|
368
|
-
if (msg.payload?.action === 'updated') {
|
|
369
|
-
sendDcgchatCron(msg.payload?.jobId as string)
|
|
370
|
-
}
|
|
371
|
-
if (msg.payload?.action === 'removed') {
|
|
372
|
-
sendDcgchatCron(msg.payload?.jobId as string)
|
|
373
|
-
}
|
|
374
|
-
if (msg.payload?.action === 'finished') {
|
|
375
|
-
finishedDcgchatCron(msg.payload?.jobId as string)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
|
380
|
-
}
|
|
381
|
-
const event: GatewayEvent = {
|
|
382
|
-
type: msg.event as string,
|
|
383
|
-
payload: msg.payload as Record<string, unknown> | undefined,
|
|
384
|
-
seq: msg.seq as number | undefined
|
|
385
|
-
}
|
|
361
|
+
const event = handleGatewayEventMessage(msg)
|
|
386
362
|
this.eventHandlers.forEach((h) => h(event))
|
|
387
363
|
}
|
|
388
364
|
}
|
package/src/gateway/socket.ts
CHANGED
|
@@ -212,7 +212,7 @@ async function connectPersistentGateway(): Promise<void> {
|
|
|
212
212
|
startPingTimer(gw)
|
|
213
213
|
dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
|
|
214
214
|
} catch (e) {
|
|
215
|
-
dcgLogger(`Gateway
|
|
215
|
+
dcgLogger(`Gateway 连接失败: ${e}`, 'error')
|
|
216
216
|
persistentConn = null
|
|
217
217
|
clearPingTimer()
|
|
218
218
|
if (!socketStopped) {
|
|
@@ -230,8 +230,10 @@ export function startDcgchatGatewaySocket(): void {
|
|
|
230
230
|
socketStopped = false
|
|
231
231
|
clearReconnectTimer()
|
|
232
232
|
if (startupConnectTimer != null) return
|
|
233
|
-
startupConnectTimer =
|
|
234
|
-
|
|
233
|
+
startupConnectTimer = setTimeout(() => {
|
|
234
|
+
startupConnectTimer = null
|
|
235
|
+
void connectPersistentGateway()
|
|
236
|
+
}, 0)
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
/**
|
package/src/monitor.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
|
-
import { handleDcgchatMessage } from './bot.js'
|
|
4
3
|
import { resolveAccount } from './channel.js'
|
|
5
|
-
import { setWsConnection, getOpenClawConfig
|
|
6
|
-
import type { InboundMessage } from './types.js'
|
|
7
|
-
import { installSkill, uninstallSkill } from './skill.js'
|
|
4
|
+
import { setWsConnection, getOpenClawConfig } from './utils/global.js'
|
|
8
5
|
import { dcgLogger } from './utils/log.js'
|
|
9
|
-
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
|
|
10
|
-
import { ignoreToolCommand } from './utils/constant.js'
|
|
11
6
|
import { isWsOpen } from './transport.js'
|
|
12
|
-
import {
|
|
7
|
+
import { handleParsedWsMessage } from './utils/wsMessageHandler.js'
|
|
13
8
|
|
|
14
9
|
export type MonitorDcgchatOpts = {
|
|
15
10
|
config?: ClawdbotConfig
|
|
@@ -128,51 +123,7 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
128
123
|
dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
|
|
129
124
|
return
|
|
130
125
|
}
|
|
131
|
-
|
|
132
|
-
if (parsed.messageType == 'openclaw_bot_chat') {
|
|
133
|
-
const msg = parsed as unknown as InboundMessage
|
|
134
|
-
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
135
|
-
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
136
|
-
if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
|
|
137
|
-
setMsgStatus(effectiveSessionKey, 'running')
|
|
138
|
-
} else {
|
|
139
|
-
setMsgStatus(effectiveSessionKey, 'finished')
|
|
140
|
-
}
|
|
141
|
-
await handleDcgchatMessage(msg, account.accountId)
|
|
142
|
-
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
143
|
-
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
144
|
-
if (event_type === 'skill') {
|
|
145
|
-
const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
146
|
-
const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
|
|
147
|
-
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
148
|
-
installSkill({ path: skill_url, code: skill_code }, content)
|
|
149
|
-
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
150
|
-
uninstallSkill({ code: skill_code }, content)
|
|
151
|
-
} else {
|
|
152
|
-
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
|
|
153
|
-
}
|
|
154
|
-
} else if (event_type === 'cron') {
|
|
155
|
-
const { job_id, message_id } = parsed.content
|
|
156
|
-
if (operation_type === 'remove') {
|
|
157
|
-
await onRemoveCronJob(job_id)
|
|
158
|
-
} else if (operation_type === 'enable') {
|
|
159
|
-
await onEnabledCronJob(job_id)
|
|
160
|
-
} else if (operation_type === 'disable') {
|
|
161
|
-
await onDisabledCronJob(job_id)
|
|
162
|
-
} else if (operation_type === 'run') {
|
|
163
|
-
await onRunCronJob(job_id, message_id)
|
|
164
|
-
}
|
|
165
|
-
} else if (event_type === 'session') {
|
|
166
|
-
const { agent_id, session_id, agent_clone_code } = parsed.content
|
|
167
|
-
if (operation_type === 'remove') {
|
|
168
|
-
await onRemoveSession({ agent_id, session_id, agent_clone_code, account_id: accountId })
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
175
|
-
}
|
|
126
|
+
await handleParsedWsMessage(parsed, payloadStr, account.accountId)
|
|
176
127
|
})
|
|
177
128
|
|
|
178
129
|
ws.on('close', (code, reason) => {
|
package/src/session.ts
CHANGED
|
@@ -6,11 +6,11 @@ interface TSession {
|
|
|
6
6
|
agent_id: string
|
|
7
7
|
session_id: string
|
|
8
8
|
agent_clone_code?: string
|
|
9
|
-
account_id
|
|
9
|
+
account_id: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const onRemoveSession = async ({ agent_id, session_id, agent_clone_code, account_id }: TSession) => {
|
|
13
|
-
const sessionKey = getSessionKey({ agent_id, session_id }, account_id)
|
|
13
|
+
const sessionKey = getSessionKey({ agent_id, session_id, agent_clone_code }, account_id)
|
|
14
14
|
if (!session_id) {
|
|
15
15
|
dcgLogger('onRemoveSession: empty session_id', 'error')
|
|
16
16
|
return
|
package/src/tool.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import { getMsgStatus } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
4
|
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
|
-
import { getEffectiveMsgParams } from './utils/params.js'
|
|
5
|
+
import { getEffectiveMsgParams, deleteSessionKeyBySubAgentRunId, setSessionKeyBySubAgentRunId } from './utils/params.js'
|
|
6
6
|
import { cronToolCall } from './cronToolCall.js'
|
|
7
7
|
|
|
8
8
|
type PluginHookName =
|
|
@@ -68,10 +68,243 @@ interface CronDelivery {
|
|
|
68
68
|
[key: string]: unknown
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// --- Subagent 活跃跟踪(按主会话 requesterSessionKey)---
|
|
72
|
+
|
|
73
|
+
/** 主会话 sessionKey -> 仍活跃的子 agent runId */
|
|
74
|
+
const activeSubagentRunIdsByRequester = new Map<string, Set<string>>()
|
|
75
|
+
/** 子会话 childSessionKey -> 主会话 requesterSessionKey */
|
|
76
|
+
const requesterByChildSessionKey = new Map<string, string>()
|
|
77
|
+
/** 子会话 childSessionKey -> spawn 时的 runId(ended 事件可能不带 runId) */
|
|
78
|
+
const runIdByChildSessionKey = new Map<string, string>()
|
|
79
|
+
/** 主会话 -> 等待「子 agent 全部结束」的回调 */
|
|
80
|
+
const subagentIdleWaiters = new Map<string, Set<() => void>>()
|
|
81
|
+
|
|
82
|
+
function getOrCreateRunIdSet(requesterSessionKey: string): Set<string> {
|
|
83
|
+
let set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
84
|
+
if (!set) {
|
|
85
|
+
set = new Set()
|
|
86
|
+
activeSubagentRunIdsByRequester.set(requesterSessionKey, set)
|
|
87
|
+
}
|
|
88
|
+
return set
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function flushSubagentIdleWaiters(requesterSessionKey: string): void {
|
|
92
|
+
const set = activeSubagentRunIdsByRequester.get(requesterSessionKey)
|
|
93
|
+
if (set && set.size > 0) return
|
|
94
|
+
activeSubagentRunIdsByRequester.delete(requesterSessionKey)
|
|
95
|
+
const waiters = subagentIdleWaiters.get(requesterSessionKey)
|
|
96
|
+
if (!waiters?.size) return
|
|
97
|
+
subagentIdleWaiters.delete(requesterSessionKey)
|
|
98
|
+
for (const w of waiters) {
|
|
99
|
+
try {
|
|
100
|
+
w()
|
|
101
|
+
} catch (e) {
|
|
102
|
+
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function registerSubagentSpawn(requesterSessionKey: string, runId: string, childSessionKey: string): void {
|
|
108
|
+
const req = requesterSessionKey.trim()
|
|
109
|
+
const rid = runId.trim()
|
|
110
|
+
const child = childSessionKey.trim()
|
|
111
|
+
if (!req || !rid || !child) {
|
|
112
|
+
dcgLogger(`subagent track spawn skipped: missing key req=${req} runId=${rid} child=${child}`)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
getOrCreateRunIdSet(req).add(rid)
|
|
116
|
+
requesterByChildSessionKey.set(child, req)
|
|
117
|
+
runIdByChildSessionKey.set(child, rid)
|
|
118
|
+
dcgLogger(`subagent track spawn: requester=${req} runId=${rid} child=${child} active=${getOrCreateRunIdSet(req).size}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function registerSubagentEnd(
|
|
122
|
+
ctx: { requesterSessionKey?: string; sessionKey?: string },
|
|
123
|
+
targetSessionKey: string,
|
|
124
|
+
runId?: string
|
|
125
|
+
): void {
|
|
126
|
+
const child = targetSessionKey.trim()
|
|
127
|
+
if (!child) return
|
|
128
|
+
const req = ctx.requesterSessionKey?.trim() || requesterByChildSessionKey.get(child) || ctx.sessionKey?.trim() || ''
|
|
129
|
+
const resolvedRunId = (runId?.trim() || runIdByChildSessionKey.get(child) || '').trim()
|
|
130
|
+
deleteSessionKeyBySubAgentRunId(resolvedRunId)
|
|
131
|
+
if (!req) {
|
|
132
|
+
dcgLogger(`subagent track end: no requester for child=${child} runId=${resolvedRunId}`)
|
|
133
|
+
requesterByChildSessionKey.delete(child)
|
|
134
|
+
runIdByChildSessionKey.delete(child)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const set = activeSubagentRunIdsByRequester.get(req)
|
|
138
|
+
if (set && resolvedRunId) {
|
|
139
|
+
set.delete(resolvedRunId)
|
|
140
|
+
}
|
|
141
|
+
requesterByChildSessionKey.delete(child)
|
|
142
|
+
runIdByChildSessionKey.delete(child)
|
|
143
|
+
dcgLogger(`subagent track end: requester=${req} runId=${resolvedRunId || 'n/a'} remaining=${set?.size ?? 0}`)
|
|
144
|
+
if (set && set.size === 0) {
|
|
145
|
+
activeSubagentRunIdsByRequester.delete(req)
|
|
146
|
+
}
|
|
147
|
+
flushSubagentIdleWaiters(req)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 当前跟踪到的、挂在该主会话下的子会话 sessionKey(供 /stop 时逐个 chat.abort) */
|
|
151
|
+
export function getChildSessionKeysTrackedForRequester(requesterSessionKey: string): string[] {
|
|
152
|
+
const req = requesterSessionKey.trim()
|
|
153
|
+
if (!req) return []
|
|
154
|
+
const out: string[] = []
|
|
155
|
+
for (const [child, parent] of requesterByChildSessionKey.entries()) {
|
|
156
|
+
if (parent === req) out.push(child)
|
|
157
|
+
}
|
|
158
|
+
return out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 打断后清空本地子 agent 跟踪(runId、父子映射、子 runId→sessionKey),并唤醒 waitUntilSubagentsIdle,避免永久挂起。
|
|
163
|
+
*/
|
|
164
|
+
export function resetSubagentStateForRequesterSession(requesterSessionKey: string): void {
|
|
165
|
+
const req = requesterSessionKey.trim()
|
|
166
|
+
if (!req) return
|
|
167
|
+
|
|
168
|
+
const runIdSet = activeSubagentRunIdsByRequester.get(req)
|
|
169
|
+
if (runIdSet) {
|
|
170
|
+
for (const rid of runIdSet) {
|
|
171
|
+
deleteSessionKeyBySubAgentRunId(rid)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [child, parent] of [...requesterByChildSessionKey.entries()]) {
|
|
176
|
+
if (parent === req) {
|
|
177
|
+
requesterByChildSessionKey.delete(child)
|
|
178
|
+
runIdByChildSessionKey.delete(child)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
activeSubagentRunIdsByRequester.delete(req)
|
|
183
|
+
|
|
184
|
+
const waiters = subagentIdleWaiters.get(req)
|
|
185
|
+
if (!waiters?.size) return
|
|
186
|
+
subagentIdleWaiters.delete(req)
|
|
187
|
+
for (const w of waiters) {
|
|
188
|
+
try {
|
|
189
|
+
w()
|
|
190
|
+
} catch (e) {
|
|
191
|
+
dcgLogger(`subagent idle waiter error: ${String(e)}`, 'error')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 当前主会话下仍在跑的子 agent 数量(按 spawn 时 runId 去重) */
|
|
197
|
+
export function getActiveSubagentCount(sessionKey: string): number {
|
|
198
|
+
const sk = sessionKey?.trim()
|
|
199
|
+
if (!sk) return 0
|
|
200
|
+
return activeSubagentRunIdsByRequester.get(sk)?.size ?? 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 等到指定主会话下已跟踪的子 agent 全部结束(spawn 失败等路径也会触发 subagent_ended,一般会配对清理)。
|
|
205
|
+
* 注意:须在收到 `subagent_spawned` 之后才会计入;仅 spawning 未 spawned 的不会阻塞。
|
|
206
|
+
*/
|
|
207
|
+
export function waitUntilSubagentsIdle(sessionKey: string, opts?: { timeoutMs?: number; signal?: AbortSignal }): Promise<void> {
|
|
208
|
+
const sk = sessionKey?.trim()
|
|
209
|
+
if (!sk) return Promise.resolve()
|
|
210
|
+
|
|
211
|
+
if (getActiveSubagentCount(sk) === 0) return Promise.resolve()
|
|
212
|
+
|
|
213
|
+
return new Promise<void>((resolve, reject) => {
|
|
214
|
+
let settled = false
|
|
215
|
+
const finish = (fn: () => void) => {
|
|
216
|
+
if (settled) return
|
|
217
|
+
settled = true
|
|
218
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
219
|
+
opts?.signal?.removeEventListener('abort', onAbort)
|
|
220
|
+
removeWaiter()
|
|
221
|
+
fn()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const removeWaiter = () => {
|
|
225
|
+
const bucket = subagentIdleWaiters.get(sk)
|
|
226
|
+
if (!bucket) return
|
|
227
|
+
bucket.delete(onIdle)
|
|
228
|
+
if (bucket.size === 0) subagentIdleWaiters.delete(sk)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const onIdle = () => finish(() => resolve())
|
|
232
|
+
|
|
233
|
+
const onAbort = () => {
|
|
234
|
+
const reason = opts?.signal?.reason
|
|
235
|
+
finish(() => reject(reason instanceof Error ? reason : new Error(String(reason ?? 'Aborted'))))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
239
|
+
if (opts?.timeoutMs != null && opts.timeoutMs > 0) {
|
|
240
|
+
timeoutId = setTimeout(
|
|
241
|
+
() => finish(() => reject(new Error(`waitUntilSubagentsIdle timeout ${opts.timeoutMs}ms`))),
|
|
242
|
+
opts.timeoutMs
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (opts?.signal) {
|
|
247
|
+
if (opts.signal.aborted) {
|
|
248
|
+
onAbort()
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
opts.signal.addEventListener('abort', onAbort, { once: true })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let set = subagentIdleWaiters.get(sk)
|
|
255
|
+
if (!set) {
|
|
256
|
+
set = new Set()
|
|
257
|
+
subagentIdleWaiters.set(sk, set)
|
|
258
|
+
}
|
|
259
|
+
set.add(onIdle)
|
|
260
|
+
|
|
261
|
+
if (getActiveSubagentCount(sk) === 0) {
|
|
262
|
+
onIdle()
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveHookSessionKey(
|
|
268
|
+
eventName: string,
|
|
269
|
+
args: { sessionKey?: string; requesterSessionKey?: string; runId?: string }
|
|
270
|
+
): string {
|
|
271
|
+
if (
|
|
272
|
+
eventName === 'subagent_spawned' ||
|
|
273
|
+
eventName === 'subagent_ended' ||
|
|
274
|
+
eventName === 'subagent_spawning' ||
|
|
275
|
+
eventName === 'subagent_delivery_target'
|
|
276
|
+
) {
|
|
277
|
+
if (args?.runId && args?.requesterSessionKey) setSessionKeyBySubAgentRunId(args?.runId, args?.requesterSessionKey)
|
|
278
|
+
return (args?.requesterSessionKey || args?.sessionKey || '').trim()
|
|
279
|
+
}
|
|
280
|
+
return (args?.sessionKey || '').trim()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function trackSubagentLifecycle(eventName: string, event: any, args: any): void {
|
|
284
|
+
if (eventName === 'subagent_spawned') {
|
|
285
|
+
const runId = typeof event?.runId === 'string' ? event.runId : ''
|
|
286
|
+
const childSessionKey = typeof event?.childSessionKey === 'string' ? event.childSessionKey : ''
|
|
287
|
+
const requester =
|
|
288
|
+
typeof args?.requesterSessionKey === 'string'
|
|
289
|
+
? args.requesterSessionKey
|
|
290
|
+
: typeof args?.sessionKey === 'string'
|
|
291
|
+
? args.sessionKey
|
|
292
|
+
: ''
|
|
293
|
+
registerSubagentSpawn(requester, runId, childSessionKey)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
if (eventName === 'subagent_ended') {
|
|
297
|
+
const targetSessionKey = typeof event?.targetSessionKey === 'string' ? event.targetSessionKey : ''
|
|
298
|
+
const runId = typeof event?.runId === 'string' ? event.runId : undefined
|
|
299
|
+
registerSubagentEnd(args ?? {}, targetSessionKey, runId)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
71
303
|
export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
72
304
|
for (const item of eventList) {
|
|
73
305
|
api.on(item.event as PluginHookName, (event: any, args: any) => {
|
|
74
|
-
|
|
306
|
+
trackSubagentLifecycle(item.event, event, args)
|
|
307
|
+
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
75
308
|
if (sk) {
|
|
76
309
|
const status = getMsgStatus(sk)
|
|
77
310
|
if (status === 'running') {
|
|
@@ -116,7 +349,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
116
349
|
dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
|
|
117
350
|
}
|
|
118
351
|
}
|
|
119
|
-
} else {
|
|
352
|
+
} else if (item.event !== 'before_tool_call') {
|
|
120
353
|
dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
|
|
121
354
|
}
|
|
122
355
|
})
|