@dcrays/dcgchat 0.3.33 → 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 +10 -19
- package/src/gateway/index.ts +2 -23
- package/src/gateway/socket.ts +5 -3
- package/src/monitor.ts +3 -54
- package/src/session.ts +19 -0
- 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
|
@@ -0,0 +1,215 @@
|
|
|
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 === '/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
|
+
let content = args.content ?? ''
|
|
187
|
+
for (const filepath of sentFiles) {
|
|
188
|
+
const posix = toPosixPath(filepath)
|
|
189
|
+
const variants = posix === filepath ? [filepath] : [filepath, posix]
|
|
190
|
+
const seen = new Set<string>()
|
|
191
|
+
for (const v of variants) {
|
|
192
|
+
if (!v || seen.has(v)) continue
|
|
193
|
+
seen.add(v)
|
|
194
|
+
content = content.split(v).join('')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
content = content.trim()
|
|
198
|
+
|
|
199
|
+
if (content.length > 0) {
|
|
200
|
+
const msgCtx = getOutboundMsgParams(sessionKey)
|
|
201
|
+
sendText(content, msgCtx)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return jsonResult({
|
|
205
|
+
success: true,
|
|
206
|
+
sentMediaCount: sentFiles.size
|
|
207
|
+
})
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return jsonResult({
|
|
210
|
+
error: err instanceof Error ? err.message : String(err)
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -152,11 +152,14 @@ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boole
|
|
|
152
152
|
*/
|
|
153
153
|
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
|
|
154
154
|
const ws = getWsConnection()
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
156
|
+
dcgLogger(`server socket not ready ${ws?.readyState}`, 'error')
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
const envelope = buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })
|
|
160
|
+
ws.send(JSON.stringify(envelope))
|
|
161
|
+
if (isLog) {
|
|
162
|
+
dcgLogger('已发送:' + JSON.stringify(envelope))
|
|
160
163
|
}
|
|
161
164
|
return true
|
|
162
165
|
}
|
|
@@ -166,7 +169,7 @@ export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): bool
|
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
export function sendFinal(ctx: IMsgParams, tag: string): boolean {
|
|
169
|
-
dcgLogger(` message handling complete state: final tag:${tag}`)
|
|
172
|
+
dcgLogger(` message handling complete state: to=${ctx.sessionId} final tag:${tag}`)
|
|
170
173
|
return wsSend(ctx, { response: '', state: 'final' })
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -178,7 +181,7 @@ export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
|
|
|
178
181
|
return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
export function sendEventMessage(
|
|
184
|
+
export function sendEventMessage(params: Record<string, string> = {}) {
|
|
182
185
|
const ctx = getParamsDefaults()
|
|
183
186
|
const ws = getWsConnection()
|
|
184
187
|
if (isWsOpen()) {
|
|
@@ -190,7 +193,6 @@ export function sendEventMessage(url: string, params: Record<string, string> = {
|
|
|
190
193
|
bot_token: ctx.botToken,
|
|
191
194
|
domain_id: ctx.domainId,
|
|
192
195
|
app_id: ctx.appId,
|
|
193
|
-
oss_url: url,
|
|
194
196
|
bot_id: ctx.botId,
|
|
195
197
|
...params
|
|
196
198
|
}
|
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,7 +43,9 @@ 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
|
|
48
|
+
agent_clone_code?: string
|
|
42
49
|
domain_id?: string
|
|
43
50
|
app_id?: string
|
|
44
51
|
bot_id?: string
|
package/src/utils/constant.ts
CHANGED
|
@@ -2,6 +2,6 @@ export const ENV: 'production' | 'test' | 'develop' = 'production'
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const systemCommand = ['/new', '/status']
|
|
5
|
-
export const interruptCommand = ['
|
|
5
|
+
export const interruptCommand = ['/stop']
|
|
6
6
|
|
|
7
|
-
export const ignoreToolCommand = ['/search', '/abort', '/
|
|
7
|
+
export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GatewayEvent } from '../gateway/index.js'
|
|
2
|
+
import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
|
|
3
|
+
import { dcgLogger } from './log.js'
|
|
4
|
+
import { getEffectiveMsgParams, getSessionKeyBySubAgentRunId } from './params.js'
|
|
5
|
+
import { sendChunk } from '../transport.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 处理网关 event 帧的副作用(agent 流式输出、cron 同步),并构造供上层分发的 GatewayEvent。
|
|
9
|
+
*/
|
|
10
|
+
export function handleGatewayEventMessage(msg: { event?: string; payload?: Record<string, unknown>; seq?: number }): GatewayEvent {
|
|
11
|
+
try {
|
|
12
|
+
if (msg.event === 'agent') {
|
|
13
|
+
const pl = msg.payload as { runId: string; data?: { delta?: unknown } }
|
|
14
|
+
const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
|
|
15
|
+
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
16
|
+
if (pl.data?.delta) {
|
|
17
|
+
if (outboundCtx.sessionId) {
|
|
18
|
+
dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
|
|
19
|
+
sendChunk(pl.data.delta as string, outboundCtx, 0)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (msg.event === 'cron') {
|
|
24
|
+
const p = msg.payload
|
|
25
|
+
dcgLogger(`[Gateway] 收到定时任务事件: ${JSON.stringify(p)}`)
|
|
26
|
+
if (p?.action === 'added') {
|
|
27
|
+
sendDcgchatCron(p?.jobId as string)
|
|
28
|
+
}
|
|
29
|
+
if (p?.action === 'updated') {
|
|
30
|
+
sendDcgchatCron(p?.jobId as string)
|
|
31
|
+
}
|
|
32
|
+
if (p?.action === 'removed') {
|
|
33
|
+
sendDcgchatCron(p?.jobId as string)
|
|
34
|
+
}
|
|
35
|
+
if (p?.action === 'finished') {
|
|
36
|
+
finishedDcgchatCron(p?.jobId as string)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
type: msg.event as string,
|
|
44
|
+
payload: msg.payload,
|
|
45
|
+
seq: msg.seq as number | undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/utils/global.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
/** socket connection */
|
|
2
1
|
import type WebSocket from 'ws'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createPluginRuntimeStore, type OpenClawConfig, type PluginRuntime } from 'openclaw/plugin-sdk'
|
|
6
|
+
import { channelInfo, ENV } from './constant.js'
|
|
7
|
+
import { dcgLogger } from './log.js'
|
|
3
8
|
|
|
9
|
+
/** socket connection */
|
|
4
10
|
let ws: WebSocket | null = null
|
|
5
11
|
|
|
6
12
|
export function setWsConnection(next: WebSocket | null) {
|
|
@@ -11,7 +17,6 @@ export function getWsConnection(): WebSocket | null {
|
|
|
11
17
|
return ws
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
// OpenClawConfig
|
|
15
20
|
let config: OpenClawConfig | null = null
|
|
16
21
|
|
|
17
22
|
export function setOpenClawConfig(next: OpenClawConfig | null) {
|
|
@@ -22,15 +27,7 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
22
27
|
return config
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
import { dcgLogger } from './log.js'
|
|
27
|
-
import { channelInfo, ENV } from './constant.js'
|
|
28
|
-
|
|
29
|
-
const path = require('path')
|
|
30
|
-
const fs = require('fs')
|
|
31
|
-
const os = require('os')
|
|
32
|
-
|
|
33
|
-
function getWorkspacePath() {
|
|
30
|
+
function getWorkspacePath(): string | null {
|
|
34
31
|
const workspacePath = path.join(
|
|
35
32
|
os.homedir(),
|
|
36
33
|
config?.channels?.["dcgchat"]?.appId == 110 ? '.mobook' : '.openclaw',
|
|
@@ -42,14 +39,14 @@ function getWorkspacePath() {
|
|
|
42
39
|
return null
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
let
|
|
46
|
-
let workspaceDir: string = getWorkspacePath()
|
|
42
|
+
let workspaceDir: string = getWorkspacePath() ?? ''
|
|
47
43
|
|
|
48
44
|
export function setWorkspaceDir(dir?: string) {
|
|
49
45
|
if (dir) {
|
|
50
46
|
workspaceDir = dir
|
|
51
47
|
}
|
|
52
48
|
}
|
|
49
|
+
|
|
53
50
|
export function getWorkspaceDir(): string {
|
|
54
51
|
if (!workspaceDir) {
|
|
55
52
|
dcgLogger?.('Workspace directory not initialized', 'error')
|
|
@@ -57,16 +54,10 @@ export function getWorkspaceDir(): string {
|
|
|
57
54
|
return workspaceDir
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
runtime
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export function getDcgchatRuntime(): PluginRuntime {
|
|
65
|
-
if (!runtime) {
|
|
66
|
-
dcgLogger?.('runtime not initialized', 'error')
|
|
67
|
-
}
|
|
68
|
-
return runtime as PluginRuntime
|
|
69
|
-
}
|
|
57
|
+
const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
|
|
58
|
+
`${"dcgchat"} runtime not initialized`
|
|
59
|
+
)
|
|
60
|
+
export { setDcgchatRuntime, getDcgchatRuntime }
|
|
70
61
|
|
|
71
62
|
export type MsgSessionStatus = 'running' | 'finished' | ''
|
|
72
63
|
|
|
@@ -117,19 +108,6 @@ export function clearSentMediaKeys(messageId?: string) {
|
|
|
117
108
|
}
|
|
118
109
|
}
|
|
119
110
|
|
|
120
|
-
export const getSessionKey = (content: any, accountId: string) => {
|
|
121
|
-
const { real_mobook, agent_id, conversation_id, session_id } = content
|
|
122
|
-
const core = getDcgchatRuntime()
|
|
123
|
-
|
|
124
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
125
|
-
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
126
|
-
channel: "dcgchat",
|
|
127
|
-
accountId: accountId || 'default',
|
|
128
|
-
peer: { kind: 'direct', id: session_id }
|
|
129
|
-
})
|
|
130
|
-
return real_mobook == '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
111
|
const cronMessageIdMap = new Map<string, string>()
|
|
134
112
|
|
|
135
113
|
export function setCronMessageId(sk: string, messageId: string) {
|
|
@@ -144,6 +122,21 @@ export function removeCronMessageId(sk: string) {
|
|
|
144
122
|
cronMessageIdMap.delete(sk)
|
|
145
123
|
}
|
|
146
124
|
|
|
125
|
+
export const getSessionKey = (content: any, accountId: string) => {
|
|
126
|
+
const { real_mobook, agent_id, agent_clone_code, session_id } = content
|
|
127
|
+
const core = getDcgchatRuntime()
|
|
128
|
+
|
|
129
|
+
const agentCode = agent_clone_code || 'main'
|
|
130
|
+
|
|
131
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
132
|
+
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
133
|
+
channel: "dcgchat",
|
|
134
|
+
accountId: accountId || 'default',
|
|
135
|
+
peer: { kind: 'direct', id: session_id }
|
|
136
|
+
})
|
|
137
|
+
return real_mobook == '1' ? route.sessionKey : `agent:${agentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
|
|
138
|
+
}
|
|
139
|
+
|
|
147
140
|
export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
|
|
148
141
|
const sessionInfo = sk.split(':')
|
|
149
142
|
return { sessionId: sessionInfo.at(-1) ?? '', agentId: sessionInfo.at(-2) ?? '' }
|
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()
|
|
@@ -69,3 +68,21 @@ export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>
|
|
|
69
68
|
export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
|
|
70
69
|
return paramsMessageMap.get(sessionKey)
|
|
71
70
|
}
|
|
71
|
+
|
|
72
|
+
export function clearParamsMessage(sessionKey: string): void {
|
|
73
|
+
const k = sessionKey?.trim()
|
|
74
|
+
if (!k) return
|
|
75
|
+
paramsMessageMap.delete(k)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// sessionKey 对应的 子agent的runId
|
|
79
|
+
const subagentRunIdMap = new Map<string, string>()
|
|
80
|
+
export function getSessionKeyBySubAgentRunId(runId: string): string | undefined {
|
|
81
|
+
return subagentRunIdMap.get(runId)
|
|
82
|
+
}
|
|
83
|
+
export function setSessionKeyBySubAgentRunId(runId: string, sessionKey: string) {
|
|
84
|
+
subagentRunIdMap.set(runId, sessionKey)
|
|
85
|
+
}
|
|
86
|
+
export function deleteSessionKeyBySubAgentRunId(runId: string) {
|
|
87
|
+
subagentRunIdMap.delete(runId)
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { handleDcgchatMessage } from '../bot.js'
|
|
2
|
+
import { setMsgStatus, getSessionKey } from './global.js'
|
|
3
|
+
import type { InboundMessage } from '../types.js'
|
|
4
|
+
import { installSkill, uninstallSkill } from '../skill.js'
|
|
5
|
+
import { dcgLogger } from './log.js'
|
|
6
|
+
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from '../cron.js'
|
|
7
|
+
import { ignoreToolCommand } from './constant.js'
|
|
8
|
+
import { createAgent } from '../agent.js'
|
|
9
|
+
|
|
10
|
+
export type ParsedWsPayload = {
|
|
11
|
+
messageType?: string
|
|
12
|
+
content: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 处理 WebSocket 已解析 JSON 且 content 已二次 parse 后的业务消息(openclaw_bot_chat / openclaw_bot_event)。
|
|
17
|
+
*/
|
|
18
|
+
export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload: string, accountId: string): Promise<void> {
|
|
19
|
+
if (parsed.messageType == 'openclaw_bot_chat') {
|
|
20
|
+
const msg = parsed as unknown as InboundMessage
|
|
21
|
+
// 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
|
|
22
|
+
const effectiveSessionKey = getSessionKey(msg.content, accountId)
|
|
23
|
+
if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
|
|
24
|
+
setMsgStatus(effectiveSessionKey, 'running')
|
|
25
|
+
} else {
|
|
26
|
+
setMsgStatus(effectiveSessionKey, 'finished')
|
|
27
|
+
}
|
|
28
|
+
await handleDcgchatMessage(msg, accountId)
|
|
29
|
+
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
30
|
+
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
31
|
+
if (event_type === 'skill') {
|
|
32
|
+
const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
33
|
+
const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
|
|
34
|
+
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
35
|
+
installSkill({ path: skill_url, code: skill_code }, content)
|
|
36
|
+
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
37
|
+
uninstallSkill({ code: skill_code }, content)
|
|
38
|
+
} else {
|
|
39
|
+
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
40
|
+
}
|
|
41
|
+
} else if (event_type === 'agent') {
|
|
42
|
+
if (operation_type === 'create') {
|
|
43
|
+
await createAgent(parsed.content)
|
|
44
|
+
} else {
|
|
45
|
+
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
|
|
46
|
+
}
|
|
47
|
+
} else if (event_type === 'cron') {
|
|
48
|
+
const { job_id, message_id } = parsed.content
|
|
49
|
+
if (operation_type === 'remove') {
|
|
50
|
+
await onRemoveCronJob(job_id)
|
|
51
|
+
} else if (operation_type === 'enable') {
|
|
52
|
+
await onEnabledCronJob(job_id)
|
|
53
|
+
} else if (operation_type === 'disable') {
|
|
54
|
+
await onDisabledCronJob(job_id)
|
|
55
|
+
} else if (operation_type === 'run') {
|
|
56
|
+
await onRunCronJob(job_id, message_id)
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${rawPayload}`)
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
/** @ts-ignore */
|
|
4
|
+
import unzipper from 'unzipper'
|
|
5
|
+
import { pipeline } from 'stream/promises'
|
|
6
|
+
import { decodeZipEntryPath } from './zipPath.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 若且唯若所有条目都在同一顶层目录下(如 GitHub 下载的 repo-name/...),返回该目录名;否则返回 null。
|
|
10
|
+
* 不能再用「第一个多段路径的第一段」推断,否则 ZIP 条目顺序变化时会误判(例如先出现 .github/ 或 src/)。
|
|
11
|
+
*/
|
|
12
|
+
export function computeSharedZipRootPrefix(decodedPaths: string[]): string | null {
|
|
13
|
+
const normalized = decodedPaths
|
|
14
|
+
.map((p) => p.replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
15
|
+
.filter((p) => p.length > 0)
|
|
16
|
+
|
|
17
|
+
if (normalized.length === 0) return null
|
|
18
|
+
|
|
19
|
+
const firstSegs = new Set<string>()
|
|
20
|
+
for (const p of normalized) {
|
|
21
|
+
const seg = p.split('/').filter(Boolean)[0]
|
|
22
|
+
if (seg) firstSegs.add(seg)
|
|
23
|
+
}
|
|
24
|
+
if (firstSegs.size !== 1) return null
|
|
25
|
+
|
|
26
|
+
const root = [...firstSegs][0]!
|
|
27
|
+
const prefix = `${root}/`
|
|
28
|
+
for (const p of normalized) {
|
|
29
|
+
if (p !== root && !p.startsWith(prefix)) return null
|
|
30
|
+
}
|
|
31
|
+
return root
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertSafeZipTarget(destDir: string, relativePath: string): string {
|
|
35
|
+
const resolvedPath = path.resolve(destDir, relativePath)
|
|
36
|
+
const resolvedDest = path.resolve(destDir)
|
|
37
|
+
const rel = path.relative(resolvedDest, resolvedPath)
|
|
38
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
39
|
+
throw new Error(`zip 路径越界: ${relativePath}`)
|
|
40
|
+
}
|
|
41
|
+
return resolvedPath
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ZipEntry = {
|
|
45
|
+
path: string
|
|
46
|
+
pathBuffer: Buffer
|
|
47
|
+
flags: number
|
|
48
|
+
type: string
|
|
49
|
+
stream: (password?: string) => NodeJS.ReadableStream
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 将已下载的 zip 解压到 destDir;顶层单根目录(若存在)会被剥掉,与原先流式 Parse 行为一致,但根目录由全量路径计算,与条目顺序无关。
|
|
54
|
+
*/
|
|
55
|
+
export async function extractZipBufferToDirectory(buf: Buffer, destDir: string): Promise<void> {
|
|
56
|
+
const directory = await unzipper.Open.buffer(buf)
|
|
57
|
+
const files = (await directory.files) as ZipEntry[]
|
|
58
|
+
|
|
59
|
+
const decodedPaths = files.map((entry) =>
|
|
60
|
+
decodeZipEntryPath(entry.pathBuffer, entry.flags ?? 0, entry.path)
|
|
61
|
+
)
|
|
62
|
+
const rootDir = computeSharedZipRootPrefix(decodedPaths)
|
|
63
|
+
|
|
64
|
+
// 与 unzipper 默认 extract 一致:串行读各 entry,避免同一 buffer 上多路解压竞争
|
|
65
|
+
for (let i = 0; i < files.length; i++) {
|
|
66
|
+
const entry = files[i]!
|
|
67
|
+
const entryPath = decodedPaths[i]!
|
|
68
|
+
let newPath = entryPath.replace(/\\/g, '/')
|
|
69
|
+
if (rootDir) {
|
|
70
|
+
if (newPath === rootDir || newPath === `${rootDir}/`) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (newPath.startsWith(`${rootDir}/`)) {
|
|
74
|
+
newPath = newPath.slice(rootDir.length + 1)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
newPath = newPath.replace(/\/+$/, '')
|
|
78
|
+
if (!newPath) continue
|
|
79
|
+
|
|
80
|
+
const targetPath = assertSafeZipTarget(destDir, newPath)
|
|
81
|
+
|
|
82
|
+
if (entry.type === 'Directory') {
|
|
83
|
+
fs.mkdirSync(targetPath, { recursive: true })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const parentDir = path.dirname(targetPath)
|
|
88
|
+
fs.mkdirSync(parentDir, { recursive: true })
|
|
89
|
+
const writeStream = fs.createWriteStream(targetPath)
|
|
90
|
+
try {
|
|
91
|
+
await pipeline(entry.stream(), writeStream)
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
94
|
+
throw new Error(`解压文件失败 ${entryPath}: ${message}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZIP 文件名编码:规范要求 UTF-8 时设置 0x800;很多工具未设标志但仍写 UTF-8 字节。
|
|
3
|
+
* 无标志时若一律按 GBK 解码,会得到「鍥句功…」类乱码。先严格 UTF-8,失败再 GBK(兼容 Windows 中文 ZIP)。
|
|
4
|
+
*/
|
|
5
|
+
export function decodeZipEntryPath(
|
|
6
|
+
pathBuffer: Buffer | Uint8Array | undefined,
|
|
7
|
+
flags: number,
|
|
8
|
+
fallbackPath: string
|
|
9
|
+
): string {
|
|
10
|
+
if ((flags & 0x800) !== 0) {
|
|
11
|
+
if (pathBuffer) {
|
|
12
|
+
return new TextDecoder('utf-8').decode(pathBuffer)
|
|
13
|
+
}
|
|
14
|
+
return fallbackPath
|
|
15
|
+
}
|
|
16
|
+
if (pathBuffer && pathBuffer.length > 0) {
|
|
17
|
+
try {
|
|
18
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(pathBuffer)
|
|
19
|
+
} catch {
|
|
20
|
+
return new TextDecoder('gbk').decode(pathBuffer)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return fallbackPath
|
|
24
|
+
}
|