@dcrays/dcgchat-test 0.3.40 → 0.4.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/package.json +1 -1
- package/src/agent.ts +15 -23
- package/src/bot.ts +10 -20
- package/src/channel.ts +67 -8
- package/src/monitor.ts +0 -1
- package/src/session.ts +2 -2
- package/src/tool.ts +1 -1
- package/src/tools/messageTool.ts +52 -21
- package/src/types.ts +6 -0
- package/src/utils/gatewayMsgHanlder.ts +4 -2
- package/src/utils/params.ts +2 -3
- package/src/utils/wsMessageHandler.ts +2 -2
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -32,20 +32,12 @@ function sendEvent(msgContent: Record<string, any>) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
workspace: string
|
|
38
|
-
name?: string
|
|
39
|
-
description?: string
|
|
40
|
-
msgContent?: Record<string, any>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 若 workspace-${code}/agent 存在,则复制到 agents/${code}/agent */
|
|
44
|
-
function copyAgentsFiles(code: string) {
|
|
35
|
+
/** 若 workspace-${clone_code}/agent 存在,则复制到 agents/${clone_code}/agent */
|
|
36
|
+
function copyAgentsFiles(clone_code: string) {
|
|
45
37
|
const workspacePath = getWorkspaceDir()
|
|
46
38
|
if (!workspacePath) return
|
|
47
|
-
const workspaceDir = path.join(workspacePath, '../', `workspace-${
|
|
48
|
-
const agentDir = path.join(workspacePath, '../', `agents/${
|
|
39
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
|
|
40
|
+
const agentDir = path.join(workspacePath, '../', `agents/${clone_code}`)
|
|
49
41
|
const sourceAgent = path.join(workspaceDir, 'agent')
|
|
50
42
|
try {
|
|
51
43
|
if (!fs.existsSync(sourceAgent)) return
|
|
@@ -62,15 +54,15 @@ function copyAgentsFiles(code: string) {
|
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
export async function onCreateAgent(params: Record<string, any>) {
|
|
65
|
-
const {
|
|
57
|
+
const { clone_code, name, description } = params
|
|
66
58
|
try {
|
|
67
|
-
await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name:
|
|
59
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: clone_code, workspace: clone_code } }))
|
|
68
60
|
} catch (err: unknown) {
|
|
69
61
|
dcgLogger(`agents.create failed: ${String(err)}`, 'error')
|
|
70
62
|
}
|
|
71
63
|
// Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
|
|
72
64
|
try {
|
|
73
|
-
await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId:
|
|
65
|
+
await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: clone_code } }))
|
|
74
66
|
} catch (err: unknown) {
|
|
75
67
|
dcgLogger(`agents.update failed: ${String(err)}`, 'error')
|
|
76
68
|
}
|
|
@@ -79,7 +71,7 @@ export async function onCreateAgent(params: Record<string, any>) {
|
|
|
79
71
|
await sendMessageToGateway(
|
|
80
72
|
JSON.stringify({
|
|
81
73
|
method: 'agents.files.set',
|
|
82
|
-
params: { agentId:
|
|
74
|
+
params: { agentId: clone_code, name: 'IDENTITY.md', content: description.trim() }
|
|
83
75
|
})
|
|
84
76
|
)
|
|
85
77
|
} catch {
|
|
@@ -91,26 +83,26 @@ export async function onCreateAgent(params: Record<string, any>) {
|
|
|
91
83
|
await sendMessageToGateway(
|
|
92
84
|
JSON.stringify({
|
|
93
85
|
method: 'agents.files.set',
|
|
94
|
-
params: { agentId:
|
|
86
|
+
params: { agentId: clone_code, name: 'USER.md', content: name.trim() }
|
|
95
87
|
})
|
|
96
88
|
)
|
|
97
89
|
} catch {
|
|
98
90
|
// Non-fatal
|
|
99
91
|
}
|
|
100
92
|
}
|
|
101
|
-
copyAgentsFiles(
|
|
93
|
+
copyAgentsFiles(clone_code)
|
|
102
94
|
sendEvent({ ...params, status: 'ok' })
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
export async function createAgent(msgContent: Record<string, any>) {
|
|
106
|
-
const { url,
|
|
107
|
-
if (!url || !
|
|
108
|
-
dcgLogger(`createAgent failed empty url&
|
|
98
|
+
const { url, clone_code } = msgContent
|
|
99
|
+
if (!url || !clone_code) {
|
|
100
|
+
dcgLogger(`createAgent failed empty url&clone_code: ${JSON.stringify(msgContent)}`, 'error')
|
|
109
101
|
sendEvent({ ...msgContent, status: 'fail' })
|
|
110
102
|
return
|
|
111
103
|
}
|
|
112
104
|
const workspacePath = getWorkspaceDir()
|
|
113
|
-
const workspaceDir = path.join(workspacePath, '../', `workspace-${
|
|
105
|
+
const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
|
|
114
106
|
|
|
115
107
|
// 如果目标目录已存在,先删除
|
|
116
108
|
if (fs.existsSync(workspaceDir)) {
|
|
@@ -159,7 +151,7 @@ export async function createAgent(msgContent: Record<string, any>) {
|
|
|
159
151
|
return
|
|
160
152
|
}
|
|
161
153
|
|
|
162
|
-
const targetPath = path.join(
|
|
154
|
+
const targetPath = path.join(workspaceDir, newPath)
|
|
163
155
|
|
|
164
156
|
if (entry.type === 'Directory') {
|
|
165
157
|
fs.mkdirSync(targetPath, { recursive: true })
|
package/src/bot.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from './utils/global.js'
|
|
13
13
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
14
14
|
import { generateSignUrl } from './request/api.js'
|
|
15
|
-
import { extractMobookFiles } from './utils/searchFile.js'
|
|
16
15
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
17
16
|
import { dcgLogger } from './utils/log.js'
|
|
18
17
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
@@ -181,7 +180,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
181
180
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
182
181
|
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
183
182
|
|
|
184
|
-
|
|
183
|
+
let text = msg.content.text?.trim()
|
|
185
184
|
|
|
186
185
|
if (!text) {
|
|
187
186
|
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
@@ -236,6 +235,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
236
235
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
237
236
|
let streamedTextLen = 0
|
|
238
237
|
|
|
238
|
+
if (msg.content.skills_scope.length > 0) {
|
|
239
|
+
const workspaceDir = getWorkspaceDir()
|
|
240
|
+
const skillCode = msg.content.skills_scope.map((skill) => `${workspaceDir}/skills/${skill.skill_code}`).join('\n')
|
|
241
|
+
const skillText = `技能${skillCode} 在目录${skillCode}下,在目录${skillCode}下查找技能 \n`
|
|
242
|
+
text = skillText ? `${skillText} \n ${text}` : text
|
|
243
|
+
}
|
|
239
244
|
const prefixContext = createReplyPrefixContext({
|
|
240
245
|
cfg: config,
|
|
241
246
|
agentId: effectiveAgentId ?? '',
|
|
@@ -298,10 +303,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
298
303
|
})
|
|
299
304
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
300
305
|
dcgLogger(`interrupt command: ${text}`)
|
|
301
|
-
sendFinal(outboundCtx, 'abort')
|
|
306
|
+
sendFinal({ ...outboundCtx, messageId: `${Date.now()}` }, 'abort')
|
|
302
307
|
sendText('会话已终止', outboundCtx)
|
|
303
308
|
sessionStreamSuppressed.add(dcgSessionKey)
|
|
304
|
-
|
|
305
309
|
const abortOneSession = async (sessionKey: string) => {
|
|
306
310
|
try {
|
|
307
311
|
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey } })
|
|
@@ -309,7 +313,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
309
313
|
dcgLogger(`chat.abort ${sessionKey}: ${String(e)}`, 'error')
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
|
-
|
|
313
316
|
const keysToAbort = new Set<string>(getChildSessionKeysTrackedForRequester(dcgSessionKey))
|
|
314
317
|
try {
|
|
315
318
|
const listed = await sendGatewayRpc<{ sessions?: Array<{ key?: string }> }>({
|
|
@@ -327,17 +330,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
327
330
|
await abortOneSession(sk)
|
|
328
331
|
}
|
|
329
332
|
await abortOneSession(dcgSessionKey)
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
await sendGatewayRpc({
|
|
333
|
-
method: 'sessions.reset',
|
|
334
|
-
params: { key: dcgSessionKey, reason: 'reset' }
|
|
335
|
-
})
|
|
336
|
-
} catch (e) {
|
|
337
|
-
dcgLogger(`sessions.reset: ${String(e)}`, 'error')
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
activeRunIdBySessionKey.delete(dcgSessionKey)
|
|
341
333
|
streamChunkIdxBySessionKey.delete(dcgSessionKey)
|
|
342
334
|
resetSubagentStateForRequesterSession(dcgSessionKey)
|
|
343
335
|
setMsgStatus(dcgSessionKey, 'finished')
|
|
@@ -393,7 +385,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
393
385
|
const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
|
|
394
386
|
streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
|
|
395
387
|
sendChunk(delta, outboundCtx, prev)
|
|
396
|
-
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${
|
|
388
|
+
dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${outboundCtx.sessionId} ${delta.slice(0, 100)}`)
|
|
397
389
|
}
|
|
398
390
|
streamedTextLen = payload.text.length
|
|
399
391
|
} else {
|
|
@@ -425,9 +417,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
425
417
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
426
418
|
await waitUntilSubagentsIdle(dcgSessionKey, { timeoutMs: 600_000 })
|
|
427
419
|
sendFinal(outboundCtx, 'end')
|
|
428
|
-
dcgLogger(
|
|
429
|
-
`record session route: rawTarget=${userId}, normalizedTarget=${dcgSessionKey}, updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`
|
|
430
|
-
)
|
|
420
|
+
dcgLogger(`record session route: updateLastRoute.to=${dcgSessionKey}, accountId=${route.accountId}`)
|
|
431
421
|
core.channel.session
|
|
432
422
|
.recordInboundSession({
|
|
433
423
|
storePath,
|
package/src/channel.ts
CHANGED
|
@@ -15,6 +15,47 @@ import { dcgLogger, setLogger } from './utils/log.js'
|
|
|
15
15
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
16
16
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
17
17
|
|
|
18
|
+
function dcgchatChannelCfg(): DcgchatConfig {
|
|
19
|
+
return (getOpenClawConfig()?.channels?.["dcgchat-test"] 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
|
+
|
|
18
59
|
export type DcgchatMediaSendOptions = {
|
|
19
60
|
/** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
|
|
20
61
|
sessionKey: string
|
|
@@ -115,16 +156,25 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
115
156
|
enabled: { type: 'boolean' },
|
|
116
157
|
wsUrl: { type: 'string' },
|
|
117
158
|
botToken: { type: 'string' },
|
|
118
|
-
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与
|
|
159
|
+
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 dcgchat_message 工具的 target(dcgSessionKey)无关' },
|
|
119
160
|
appId: { type: 'string' },
|
|
120
161
|
domainId: { type: 'string' },
|
|
121
|
-
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
|
+
}
|
|
122
168
|
}
|
|
123
169
|
},
|
|
124
170
|
uiHints: {
|
|
125
171
|
userId: {
|
|
126
172
|
label: 'WS 连接 _userId',
|
|
127
173
|
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 dcgSessionKey(与入站上下文 SessionKey 相同)。'
|
|
174
|
+
},
|
|
175
|
+
strictMessageToolTarget: {
|
|
176
|
+
label: '严格 message.target',
|
|
177
|
+
help: '开启后由通道目标解析层拒绝纯数字等非 sessionKey 的 target(推荐开启);关闭则与旧版行为一致。'
|
|
128
178
|
}
|
|
129
179
|
}
|
|
130
180
|
},
|
|
@@ -134,8 +184,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
134
184
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
135
185
|
setAccountEnabled: ({ cfg, enabled }) => {
|
|
136
186
|
const channelKey = "dcgchat-test"
|
|
137
|
-
const prev =
|
|
138
|
-
(cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
187
|
+
const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
139
188
|
return {
|
|
140
189
|
...cfg,
|
|
141
190
|
channels: {
|
|
@@ -159,16 +208,26 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
159
208
|
messaging: {
|
|
160
209
|
normalizeTarget: (raw) => raw || undefined,
|
|
161
210
|
targetResolver: {
|
|
162
|
-
looksLikeId:
|
|
163
|
-
hint: 'dcgSessionKey(与 SessionKey
|
|
211
|
+
looksLikeId: dcgchatMessageTargetLooksLikeId,
|
|
212
|
+
hint: '须为完整 dcgSessionKey(与 SessionKey 一致,形如 agent:…:mobook:direct:… 或 agent: 前缀路由键);禁止填 WS userId 等纯数字。可在通道配置 strictMessageToolTarget=false 关闭校验。'
|
|
164
213
|
}
|
|
165
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
|
+
},
|
|
166
225
|
agentPrompt: {
|
|
167
226
|
messageToolHints: () => [
|
|
168
227
|
'生成文件后,**尽可能不要**把文件路径、地址直接告诉用户。',
|
|
169
228
|
'生成文件后,把文件名告诉用户。',
|
|
170
|
-
'
|
|
171
|
-
'
|
|
229
|
+
'使用内置 `message` 或 `dcgchat_message` 发消息时,target 必须是完整 dcgSessionKey(与上下文 SessionKey 相同),禁止填 From、SenderId、WS userId 等纯数字。',
|
|
230
|
+
'生成文件后,须通过工具发送文件,勿在文本里直接输出路径或地址。'
|
|
172
231
|
]
|
|
173
232
|
},
|
|
174
233
|
outbound: {
|
package/src/monitor.ts
CHANGED
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
|
@@ -349,7 +349,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
349
349
|
dcgLogger(`工具调用结果: ~ event:${item.event} ${status}`)
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
|
-
} else {
|
|
352
|
+
} else if (item.event !== 'before_tool_call') {
|
|
353
353
|
dcgLogger(`工具调用结果: ~ event:${item.event} ~ 没有sessionKey 为执行`)
|
|
354
354
|
}
|
|
355
355
|
})
|
package/src/tools/messageTool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import type { AnyAgentTool } from 'openclaw/plugin-sdk'
|
|
4
5
|
import { jsonResult } from 'openclaw/plugin-sdk'
|
|
@@ -12,7 +13,28 @@ export type DcgchatMessageToolContext = {
|
|
|
12
13
|
workspaceDir?: string
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
/** 统一为 POSIX 风格斜杠,便于跨平台判断(不改变语义,仅用于匹配)。 */
|
|
17
|
+
function toPosixPath(p: string): string {
|
|
18
|
+
return path.normalize(p.trim()).replace(/\\/g, '/')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 允许发送的挂载根:Unix/macOS/Linux 为 /workspace、/mobook;
|
|
23
|
+
* Windows 为盘符路径如 C:/workspace、D:\\mobook\\...(规范化后比较)。
|
|
24
|
+
*/
|
|
25
|
+
function isSafePath(filepath: string): boolean {
|
|
26
|
+
const p = toPosixPath(filepath)
|
|
27
|
+
if (p.startsWith('/workspace/') || p === '/workspace') return true
|
|
28
|
+
if (p.startsWith('/mobook/') || p === '/mobook') return true
|
|
29
|
+
// Windows: C:/workspace/...、c:/mobook/...
|
|
30
|
+
return /^[A-Za-z]:\/(workspace|mobook)(\/|$)/.test(p)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 同一路径在 Windows 上可能大小写不同,用于 Set 去重。 */
|
|
34
|
+
function pathKey(filepath: string): string {
|
|
35
|
+
const n = path.normalize(filepath.trim())
|
|
36
|
+
return os.platform() === 'win32' ? n.toLowerCase() : n
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
const fileType1 = ['.webp', '.gif', '.bmp', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.rtf', '.odt', '.json']
|
|
18
40
|
const fileType2 = ['.xml', '.csv', '.yaml', '.yml', '.html', '.htm', '.md', '.markdown', '.css', '.js', '.ts', '.png', '.jpg', '.jpeg']
|
|
@@ -26,8 +48,7 @@ const messageToolParameters = {
|
|
|
26
48
|
properties: {
|
|
27
49
|
target: {
|
|
28
50
|
type: 'string',
|
|
29
|
-
description:
|
|
30
|
-
'目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
|
|
51
|
+
description: '目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
|
|
31
52
|
},
|
|
32
53
|
content: {
|
|
33
54
|
type: 'string',
|
|
@@ -42,7 +63,8 @@ const messageToolParameters = {
|
|
|
42
63
|
properties: {
|
|
43
64
|
file: {
|
|
44
65
|
type: 'string',
|
|
45
|
-
description:
|
|
66
|
+
description:
|
|
67
|
+
'文件路径。Unix/macOS/Linux 如 /workspace/output/report.pdf;Windows 如 C:/workspace/output/report.pdf 或 C:\\workspace\\output\\report.pdf'
|
|
46
68
|
}
|
|
47
69
|
},
|
|
48
70
|
required: ['file']
|
|
@@ -52,14 +74,12 @@ const messageToolParameters = {
|
|
|
52
74
|
oneOf: [{ required: ['content'] }, { required: ['media'] }]
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
function extractPaths(text?: string) {
|
|
77
|
+
function extractPaths(text?: string): string[] {
|
|
56
78
|
if (!text) return []
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
function isSafePath(filepath: string) {
|
|
62
|
-
return SAFE_PREFIXES.some((prefix) => filepath.startsWith(prefix))
|
|
79
|
+
const unix = text.match(/\/workspace\/[^\s]+|\/mobook\/[^\s]+/g) ?? []
|
|
80
|
+
// Windows: C:\workspace\...、C:/mobook/...(不含空白)
|
|
81
|
+
const win = text.match(/[A-Za-z]:[/\\](?:workspace|mobook)[/\\][^\s]+/g) ?? []
|
|
82
|
+
return [...new Set([...unix, ...win])]
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
function isSafeFile(filepath: string) {
|
|
@@ -67,25 +87,24 @@ function isSafeFile(filepath: string) {
|
|
|
67
87
|
const stat = fs.statSync(filepath)
|
|
68
88
|
if (!stat.isFile()) return false
|
|
69
89
|
if (stat.size === 0) return false
|
|
70
|
-
const ext = path.extname(filepath)
|
|
90
|
+
const ext = path.extname(filepath).toLowerCase()
|
|
71
91
|
return SAFE_EXTENSIONS.has(ext)
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
/**
|
|
75
|
-
*
|
|
95
|
+
* 书灵墨宝出站消息工具:须符合 OpenClaw `AgentTool`(execute 返回 `AgentToolResult`)。
|
|
96
|
+
* 工具名使用 `dcgchat_message`,避免与核心内置 `message` 冲突。
|
|
76
97
|
* 通过注册时的 `OpenClawPluginToolContext.sessionKey` 出站,不再使用非标准的 `execute(args, ctx)`。
|
|
77
98
|
*/
|
|
78
99
|
export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext): AnyAgentTool {
|
|
79
100
|
return {
|
|
80
|
-
name: '
|
|
81
|
-
label: '
|
|
101
|
+
name: 'dcgchat_message',
|
|
102
|
+
label: 'dcgchat_message',
|
|
82
103
|
description: `
|
|
83
104
|
向用户发送消息。
|
|
84
105
|
若传 target,target 必须是 sessionKey,不能是 userId。
|
|
85
106
|
如果发送附件:必须使用 media 字段
|
|
86
|
-
|
|
87
|
-
/workspace/
|
|
88
|
-
/mobook/
|
|
107
|
+
支持路径目录(Unix/macOS/Linux:/workspace/、/mobook/;Windows:盘符下 workspace、mobook,如 C:/workspace/):
|
|
89
108
|
禁止直接输出路径文本
|
|
90
109
|
`,
|
|
91
110
|
parameters: messageToolParameters,
|
|
@@ -103,6 +122,7 @@ export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext):
|
|
|
103
122
|
|
|
104
123
|
try {
|
|
105
124
|
const sentFiles = new Set<string>()
|
|
125
|
+
const sentKeys = new Set<string>()
|
|
106
126
|
|
|
107
127
|
if (args.media?.length) {
|
|
108
128
|
for (const media of args.media) {
|
|
@@ -110,10 +130,12 @@ export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext):
|
|
|
110
130
|
if (!filepath) continue
|
|
111
131
|
if (!isSafePath(filepath)) continue
|
|
112
132
|
if (!isSafeFile(filepath)) continue
|
|
113
|
-
|
|
133
|
+
const key = pathKey(filepath)
|
|
134
|
+
if (sentKeys.has(key)) continue
|
|
114
135
|
|
|
115
136
|
await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
|
|
116
137
|
sentFiles.add(filepath)
|
|
138
|
+
sentKeys.add(key)
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
141
|
|
|
@@ -121,15 +143,24 @@ export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext):
|
|
|
121
143
|
for (const filepath of fallbackPaths) {
|
|
122
144
|
if (!isSafePath(filepath)) continue
|
|
123
145
|
if (!isSafeFile(filepath)) continue
|
|
124
|
-
|
|
146
|
+
const key = pathKey(filepath)
|
|
147
|
+
if (sentKeys.has(key)) continue
|
|
125
148
|
|
|
126
149
|
await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
|
|
127
150
|
sentFiles.add(filepath)
|
|
151
|
+
sentKeys.add(key)
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
let content = args.content ?? ''
|
|
131
155
|
for (const filepath of sentFiles) {
|
|
132
|
-
|
|
156
|
+
const posix = toPosixPath(filepath)
|
|
157
|
+
const variants = posix === filepath ? [filepath] : [filepath, posix]
|
|
158
|
+
const seen = new Set<string>()
|
|
159
|
+
for (const v of variants) {
|
|
160
|
+
if (!v || seen.has(v)) continue
|
|
161
|
+
seen.add(v)
|
|
162
|
+
content = content.split(v).join('')
|
|
163
|
+
}
|
|
133
164
|
}
|
|
134
165
|
content = content.trim()
|
|
135
166
|
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export type DcgchatConfig = {
|
|
|
11
11
|
userId?: string
|
|
12
12
|
domainId?: string
|
|
13
13
|
appId?: string
|
|
14
|
+
/**
|
|
15
|
+
* 内置 `message` 工具走 OpenClaw 目标解析:`true`(默认)时仅将符合 sessionKey 形态的字符串视为合法 target,
|
|
16
|
+
* 纯数字(WS userId 等)会解析失败;设为 `false` 恢复旧版宽松行为(不推荐)。
|
|
17
|
+
*/
|
|
18
|
+
strictMessageToolTarget?: boolean
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export type ResolvedDcgchatAccount = {
|
|
@@ -38,6 +43,7 @@ export type InboundMessage = {
|
|
|
38
43
|
source: string // 'server',
|
|
39
44
|
// content: string;
|
|
40
45
|
content: {
|
|
46
|
+
skills_scope: Record<string, any>[]
|
|
41
47
|
bot_token: string
|
|
42
48
|
agent_clone_code?: string
|
|
43
49
|
domain_id?: string
|
|
@@ -10,11 +10,13 @@ import { sendChunk } from '../transport.js'
|
|
|
10
10
|
export function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): GatewayEvent {
|
|
11
11
|
try {
|
|
12
12
|
if (msg.event === 'agent') {
|
|
13
|
-
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
14
13
|
const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
|
|
15
14
|
const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
|
|
16
15
|
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
17
|
-
if (
|
|
16
|
+
if (pl.data?.delta) {
|
|
17
|
+
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
18
|
+
if (outboundCtx.sessionId) sendChunk(pl.data.delta as string, outboundCtx, 0)
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
if (msg.event === 'cron') {
|
|
20
22
|
const p = msg.payload
|
package/src/utils/params.ts
CHANGED
|
@@ -47,9 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Agent `
|
|
51
|
-
* `setParamsMessage`
|
|
52
|
-
* 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
|
|
50
|
+
* Agent `dcgchat_message` / 出站 `target` 应为 `dcgSessionKey`(如 `agent:main:mobook:direct:...`)。
|
|
51
|
+
* `setParamsMessage` 的 key 与此一致;查不到 map 时回落到配置缺省(无会话级 messageId/sessionId)。
|
|
53
52
|
*/
|
|
54
53
|
export function getOutboundMsgParams(preferredKey: string): IMsgParams {
|
|
55
54
|
const k = preferredKey?.trim()
|
|
@@ -39,8 +39,8 @@ export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload:
|
|
|
39
39
|
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
40
40
|
}
|
|
41
41
|
} else if (event_type === 'agent') {
|
|
42
|
-
if (operation_type === '
|
|
43
|
-
createAgent(parsed.content)
|
|
42
|
+
if (operation_type === 'create') {
|
|
43
|
+
await createAgent(parsed.content)
|
|
44
44
|
} else {
|
|
45
45
|
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
46
46
|
}
|