@dcrays/dcgchat 0.2.25 → 0.2.32
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 +20 -17
- package/package.json +3 -8
- package/src/bot.ts +230 -593
- package/src/channel.ts +110 -178
- package/src/monitor.ts +124 -180
- package/src/{api.ts → request/api.ts} +34 -35
- package/src/request/oss.ts +58 -0
- package/src/request/request.ts +198 -0
- package/src/{userInfo.ts → request/userInfo.ts} +36 -34
- package/src/skill.ts +110 -194
- package/src/tool.ts +102 -113
- package/src/transport.ts +108 -0
- package/src/types.ts +75 -64
- package/src/utils/constant.ts +7 -0
- package/src/utils/global.ts +117 -0
- package/src/utils/log.ts +15 -0
- package/src/utils/searchFile.ts +212 -0
- package/src/connection.ts +0 -11
- package/src/log.ts +0 -46
- package/src/oss.ts +0 -72
- package/src/request.ts +0 -201
- package/src/runtime.ts +0 -40
package/src/bot.ts
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
|
+
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
5
|
+
import type { InboundMessage } from './types.js'
|
|
6
|
+
import {
|
|
7
|
+
clearSentMediaKeys,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getOpenClawConfig,
|
|
10
|
+
getWorkspaceDir,
|
|
11
|
+
getWsConnection,
|
|
12
|
+
setMsgStatus
|
|
13
|
+
} from './utils/global.js'
|
|
14
|
+
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
|
+
import { generateSignUrl } from './request/api.js'
|
|
16
|
+
import { extractMobookFiles } from './utils/searchFile.js'
|
|
17
|
+
import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
|
|
18
|
+
import { dcgLogger } from './utils/log.js'
|
|
19
|
+
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
14
20
|
|
|
15
21
|
type MediaInfo = {
|
|
16
|
-
path: string
|
|
17
|
-
fileName: string
|
|
18
|
-
contentType: string
|
|
19
|
-
placeholder: string
|
|
20
|
-
}
|
|
22
|
+
path: string
|
|
23
|
+
fileName: string
|
|
24
|
+
contentType: string
|
|
25
|
+
placeholder: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type TFileInfo = { name: string; url: string }
|
|
21
29
|
|
|
22
|
-
const mediaMaxBytes = 300 * 1024 * 1024
|
|
30
|
+
const mediaMaxBytes = 300 * 1024 * 1024
|
|
23
31
|
|
|
24
32
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
25
|
-
const activeGenerations = new Map<string, AbortController>()
|
|
33
|
+
const activeGenerations = new Map<string, AbortController>()
|
|
26
34
|
|
|
27
35
|
/** Abort an in-progress LLM generation for a given conversationId */
|
|
28
36
|
export function abortMobookappGeneration(conversationId: string): void {
|
|
29
|
-
const ctrl = activeGenerations.get(conversationId)
|
|
30
|
-
console.log("🚀 ~ abortMobookappGeneration ~ ctrl:", ctrl)
|
|
37
|
+
const ctrl = activeGenerations.get(conversationId)
|
|
31
38
|
if (ctrl) {
|
|
32
|
-
ctrl.abort()
|
|
33
|
-
activeGenerations.delete(conversationId)
|
|
39
|
+
ctrl.abort()
|
|
40
|
+
activeGenerations.delete(conversationId)
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -38,92 +45,71 @@ export function abortMobookappGeneration(conversationId: string): void {
|
|
|
38
45
|
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
39
46
|
* Returns null if the conversation_id does not contain the "::" separator.
|
|
40
47
|
*/
|
|
41
|
-
export function extractAgentIdFromConversationId(
|
|
42
|
-
conversationId
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if (idx <= 0) return null;
|
|
46
|
-
return conversationId.slice(0, idx);
|
|
48
|
+
export function extractAgentIdFromConversationId(conversationId: string): string | null {
|
|
49
|
+
const idx = conversationId.indexOf('::')
|
|
50
|
+
if (idx <= 0) return null
|
|
51
|
+
return conversationId.slice(0, idx)
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
async function resolveMediaFromUrls(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
): Promise<MediaInfo[]> {
|
|
54
|
-
const core = getDcgchatRuntime();
|
|
55
|
-
const out: MediaInfo[] = [];
|
|
56
|
-
log(
|
|
57
|
-
`dcgchat media: starting resolve for ${files.length} file(s): ${JSON.stringify(files)}`,
|
|
58
|
-
);
|
|
54
|
+
async function resolveMediaFromUrls(files: TFileInfo[], botToken: string): Promise<MediaInfo[]> {
|
|
55
|
+
const core = getDcgchatRuntime()
|
|
56
|
+
const out: MediaInfo[] = []
|
|
57
|
+
dcgLogger(`media: user upload files: ${JSON.stringify(files)}`)
|
|
59
58
|
|
|
60
59
|
for (let i = 0; i < files.length; i++) {
|
|
61
|
-
const file = files[i]
|
|
60
|
+
const file = files[i]
|
|
62
61
|
try {
|
|
63
|
-
let data =
|
|
62
|
+
let data = ''
|
|
64
63
|
if (/^https?:\/\//i.test(file.url)) {
|
|
65
|
-
data = file.url
|
|
64
|
+
data = file.url
|
|
66
65
|
} else {
|
|
67
|
-
data = await generateSignUrl(file.url, botToken)
|
|
66
|
+
data = await generateSignUrl(file.url, botToken)
|
|
68
67
|
}
|
|
69
|
-
|
|
70
|
-
const response = await fetch(data)
|
|
68
|
+
dcgLogger(`media: generateSignUrl: ${data}`)
|
|
69
|
+
const response = await fetch(data)
|
|
71
70
|
if (!response.ok) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
continue;
|
|
71
|
+
dcgLogger?.(`media: ${file.url} fetch failed with HTTP ${response.status}`, 'error')
|
|
72
|
+
continue
|
|
76
73
|
}
|
|
77
|
-
const buffer = Buffer.from(await response.arrayBuffer())
|
|
74
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
78
75
|
|
|
79
|
-
let contentType = response.headers.get(
|
|
76
|
+
let contentType = response.headers.get('content-type') || ''
|
|
80
77
|
if (!contentType) {
|
|
81
|
-
contentType = (await core.media.detectMime({ buffer })) ||
|
|
78
|
+
contentType = (await core.media.detectMime({ buffer })) || ''
|
|
82
79
|
}
|
|
83
|
-
const fileName =
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
buffer,
|
|
87
|
-
contentType,
|
|
88
|
-
"inbound",
|
|
89
|
-
mediaMaxBytes,
|
|
90
|
-
fileName,
|
|
91
|
-
);
|
|
92
|
-
const isImage = contentType.startsWith("image/");
|
|
80
|
+
const fileName = file.name || path.basename(new URL(file.url).pathname) || 'file'
|
|
81
|
+
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, 'inbound', mediaMaxBytes, fileName)
|
|
82
|
+
const isImage = contentType.startsWith('image/')
|
|
93
83
|
out.push({
|
|
94
84
|
path: saved.path,
|
|
95
85
|
fileName,
|
|
96
|
-
contentType: saved.contentType ||
|
|
97
|
-
placeholder: isImage ?
|
|
98
|
-
})
|
|
86
|
+
contentType: saved.contentType || '',
|
|
87
|
+
placeholder: isImage ? '<media:image>' : '<media:file>'
|
|
88
|
+
})
|
|
99
89
|
} catch (err) {
|
|
100
|
-
|
|
101
|
-
`dcgchat media: [${i + 1}/${files.length}] FAILED to process ${file.url}: ${String(err)}`,
|
|
102
|
-
);
|
|
90
|
+
dcgLogger(`media: ${file.url} FAILED to process: ${String(err)}`, 'error')
|
|
103
91
|
}
|
|
104
92
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
return out;
|
|
93
|
+
dcgLogger(`media: resolve complete, ${out.length}/${files.length} file(s) succeeded`)
|
|
94
|
+
return out
|
|
110
95
|
}
|
|
111
96
|
|
|
112
|
-
|
|
113
|
-
MediaPath?: string
|
|
114
|
-
MediaFileName?: string
|
|
115
|
-
MediaType?: string
|
|
116
|
-
MediaUrl?: string
|
|
117
|
-
MediaFileNames?: string[]
|
|
118
|
-
MediaPaths?: string[]
|
|
119
|
-
MediaUrls?: string[]
|
|
120
|
-
MediaTypes?: string[]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
97
|
+
type MediaPayload = {
|
|
98
|
+
MediaPath?: string
|
|
99
|
+
MediaFileName?: string
|
|
100
|
+
MediaType?: string
|
|
101
|
+
MediaUrl?: string
|
|
102
|
+
MediaFileNames?: string[]
|
|
103
|
+
MediaPaths?: string[]
|
|
104
|
+
MediaUrls?: string[]
|
|
105
|
+
MediaTypes?: string[]
|
|
106
|
+
}
|
|
107
|
+
function buildMediaPayload(mediaList: MediaInfo[]): MediaPayload {
|
|
108
|
+
if (mediaList.length === 0) return {}
|
|
109
|
+
const first = mediaList[0]
|
|
110
|
+
const mediaFileNames = mediaList.map((m) => m.fileName).filter(Boolean)
|
|
111
|
+
const mediaPaths = mediaList.map((m) => m.path)
|
|
112
|
+
const mediaTypes = mediaList.map((m) => m.contentType).filter(Boolean)
|
|
127
113
|
return {
|
|
128
114
|
MediaPath: first?.path,
|
|
129
115
|
MediaFileName: first?.fileName,
|
|
@@ -132,333 +118,84 @@ function buildMediaPayload(mediaList: MediaInfo[]): {
|
|
|
132
118
|
MediaFileNames: mediaFileNames.length > 0 ? mediaFileNames : undefined,
|
|
133
119
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
134
120
|
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
135
|
-
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
140
|
-
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean);
|
|
141
|
-
return payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* 从文本中提取 /mobook 目录下的文件
|
|
147
|
-
* @param {string} text
|
|
148
|
-
* @returns {string[]}
|
|
149
|
-
*/
|
|
150
|
-
const EXT_LIST = [
|
|
151
|
-
// 文档类
|
|
152
|
-
"doc",
|
|
153
|
-
"docx",
|
|
154
|
-
"xls",
|
|
155
|
-
"xlsx",
|
|
156
|
-
"ppt",
|
|
157
|
-
"pptx",
|
|
158
|
-
"pdf",
|
|
159
|
-
"txt",
|
|
160
|
-
"rtf",
|
|
161
|
-
"odt",
|
|
162
|
-
|
|
163
|
-
// 数据/开发
|
|
164
|
-
"json",
|
|
165
|
-
"xml",
|
|
166
|
-
"csv",
|
|
167
|
-
"yaml",
|
|
168
|
-
"yml",
|
|
169
|
-
|
|
170
|
-
// 前端/文本
|
|
171
|
-
"html",
|
|
172
|
-
"htm",
|
|
173
|
-
"md",
|
|
174
|
-
"markdown",
|
|
175
|
-
"css",
|
|
176
|
-
"js",
|
|
177
|
-
"ts",
|
|
178
|
-
|
|
179
|
-
// 图片
|
|
180
|
-
"png",
|
|
181
|
-
"jpg",
|
|
182
|
-
"jpeg",
|
|
183
|
-
"gif",
|
|
184
|
-
"bmp",
|
|
185
|
-
"webp",
|
|
186
|
-
"svg",
|
|
187
|
-
"ico",
|
|
188
|
-
"tiff",
|
|
189
|
-
|
|
190
|
-
// 音频
|
|
191
|
-
"mp3",
|
|
192
|
-
"wav",
|
|
193
|
-
"ogg",
|
|
194
|
-
"aac",
|
|
195
|
-
"flac",
|
|
196
|
-
"m4a",
|
|
197
|
-
|
|
198
|
-
// 视频
|
|
199
|
-
"mp4",
|
|
200
|
-
"avi",
|
|
201
|
-
"mov",
|
|
202
|
-
"wmv",
|
|
203
|
-
"flv",
|
|
204
|
-
"mkv",
|
|
205
|
-
"webm",
|
|
206
|
-
|
|
207
|
-
// 压缩包
|
|
208
|
-
"zip",
|
|
209
|
-
"rar",
|
|
210
|
-
"7z",
|
|
211
|
-
"tar",
|
|
212
|
-
"gz",
|
|
213
|
-
"bz2",
|
|
214
|
-
"xz",
|
|
215
|
-
|
|
216
|
-
// 可执行/程序
|
|
217
|
-
"exe",
|
|
218
|
-
"dmg",
|
|
219
|
-
"pkg",
|
|
220
|
-
"apk",
|
|
221
|
-
"ipa",
|
|
222
|
-
|
|
223
|
-
// 其他常见
|
|
224
|
-
"log",
|
|
225
|
-
"dat",
|
|
226
|
-
"bin",
|
|
227
|
-
];
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* 扩展名按长度降序,用于正则交替,避免 xls 抢先匹配 xlsx、htm 抢先匹配 html 等
|
|
231
|
-
*/
|
|
232
|
-
const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length);
|
|
233
|
-
|
|
234
|
-
/** 去除控制符、零宽字符等常见脏值 */
|
|
235
|
-
function stripMobookNoise(s: string) {
|
|
236
|
-
return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, "");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 从文本中扫描 `/mobook/` 片段,按最长后缀匹配合法扩展名(兜底,不依赖 FILE_NAME 字符集)
|
|
241
|
-
*/
|
|
242
|
-
function collectMobookPathsByScan(text: string, result: Set<string>): void {
|
|
243
|
-
const lower = text.toLowerCase();
|
|
244
|
-
const needle = "/mobook/";
|
|
245
|
-
let from = 0;
|
|
246
|
-
while (from < text.length) {
|
|
247
|
-
const i = lower.indexOf(needle, from);
|
|
248
|
-
if (i < 0) break;
|
|
249
|
-
const start = i + needle.length;
|
|
250
|
-
const tail = text.slice(start);
|
|
251
|
-
const seg = tail.match(/^([^\s\]\)'"}\u3002,,]+)/);
|
|
252
|
-
if (!seg) {
|
|
253
|
-
from = start + 1;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
let raw = stripMobookNoise(seg[1]).trim();
|
|
257
|
-
if (!raw || raw.includes("\uFFFD")) {
|
|
258
|
-
from = start + 1;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
const low = raw.toLowerCase();
|
|
262
|
-
let matchedExt: string | undefined;
|
|
263
|
-
for (const ext of EXT_SORTED_FOR_REGEX) {
|
|
264
|
-
if (low.endsWith(`.${ext}`)) {
|
|
265
|
-
matchedExt = ext;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (!matchedExt) {
|
|
270
|
-
from = start + 1;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const base = raw.slice(0, -(matchedExt.length + 1));
|
|
274
|
-
const fileName = `${base}.${matchedExt}`;
|
|
275
|
-
if (isValidFileName(fileName)) {
|
|
276
|
-
result.add(normalizePath(`/mobook/${fileName}`));
|
|
277
|
-
}
|
|
278
|
-
from = start + 1;
|
|
121
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined
|
|
279
122
|
}
|
|
280
123
|
}
|
|
281
124
|
|
|
282
|
-
function
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
// ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
|
|
286
|
-
const EXT = `(${EXT_SORTED_FOR_REGEX.join("|")})`;
|
|
287
|
-
// ✅ 文件名字符(增强:支持中文、符号)
|
|
288
|
-
const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`;
|
|
289
|
-
try {
|
|
290
|
-
// 1️⃣ `xxx.xxx`
|
|
291
|
-
const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, "gi");
|
|
292
|
-
(text.match(backtickReg) || []).forEach((item) => {
|
|
293
|
-
const name = item.replace(/`/g, "").trim();
|
|
294
|
-
if (isValidFileName(name)) {
|
|
295
|
-
result.add(`/mobook/${name}`);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
// 2️⃣ /mobook/xxx.xxx
|
|
299
|
-
const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, "gi");
|
|
300
|
-
(text.match(fullPathReg) || []).forEach((p) => {
|
|
301
|
-
result.add(normalizePath(p));
|
|
302
|
-
});
|
|
303
|
-
// 3️⃣ mobook下的 xxx.xxx
|
|
304
|
-
const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, "gi");
|
|
305
|
-
(text.match(inlineReg) || []).forEach((item) => {
|
|
306
|
-
const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, "i"));
|
|
307
|
-
if (match && isValidFileName(match[0])) {
|
|
308
|
-
result.add(`/mobook/${match[0].trim()}`);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
// 🆕 4️⃣ **xxx.xxx**
|
|
312
|
-
const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, "gi");
|
|
313
|
-
(text.match(boldReg) || []).forEach((item) => {
|
|
314
|
-
const name = item.replace(/\*\*/g, "").trim();
|
|
315
|
-
if (isValidFileName(name)) {
|
|
316
|
-
result.add(`/mobook/${name}`);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
// 🆕 5️⃣ xxx.xxx (123字节)
|
|
320
|
-
const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, "gi");
|
|
321
|
-
(text.match(looseReg) || []).forEach((item) => {
|
|
322
|
-
const name = item.replace(/\s*\(.+$/, "").trim();
|
|
323
|
-
if (isValidFileName(name)) {
|
|
324
|
-
result.add(`/mobook/${name}`);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
328
|
-
collectMobookPathsByScan(text, result);
|
|
329
|
-
} catch (e) {
|
|
330
|
-
console.warn("extractMobookFiles error:", e);
|
|
331
|
-
}
|
|
332
|
-
return [...result];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* 校验文件名是否合法(避免脏数据)
|
|
337
|
-
*/
|
|
338
|
-
function isValidFileName(name: string) {
|
|
339
|
-
if (!name) return false;
|
|
340
|
-
const cleaned = stripMobookNoise(name).trim();
|
|
341
|
-
if (!cleaned) return false;
|
|
342
|
-
if (cleaned.includes("\uFFFD")) return false;
|
|
343
|
-
// 过滤异常字符
|
|
344
|
-
if (/[\/\\<>:"|?*]/.test(cleaned)) return false;
|
|
345
|
-
// 长度限制(防止异常长字符串)
|
|
346
|
-
if (cleaned.length > 200) return false;
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* 规范路径(去重用)
|
|
352
|
-
*/
|
|
353
|
-
function normalizePath(path: string) {
|
|
354
|
-
return path
|
|
355
|
-
.replace(/\/+/g, "/") // 多斜杠 → 单斜杠
|
|
356
|
-
.replace(/\/$/, ""); // 去掉结尾 /
|
|
125
|
+
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
126
|
+
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean)
|
|
127
|
+
return payload.mediaUrl ? [payload.mediaUrl] : []
|
|
357
128
|
}
|
|
358
129
|
|
|
359
130
|
/**
|
|
360
131
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
361
132
|
*/
|
|
362
|
-
export async function handleDcgchatMessage(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const account = resolveAccount(cfg, accountId);
|
|
376
|
-
const userId = msg._userId.toString();
|
|
377
|
-
const text = msg.content.text?.trim();
|
|
133
|
+
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
134
|
+
const msgCtx = createMsgContext(msg)
|
|
135
|
+
|
|
136
|
+
let completeText = ''
|
|
137
|
+
const config = getOpenClawConfig()
|
|
138
|
+
if (!config) {
|
|
139
|
+
dcgLogger('no config available', 'error')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const account = resolveAccount(config, accountId)
|
|
143
|
+
const userId = msg._userId.toString()
|
|
144
|
+
const text = msg.content.text?.trim()
|
|
378
145
|
|
|
379
146
|
if (!text) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
source: "client",
|
|
384
|
-
// @ts-ignore
|
|
385
|
-
content: {
|
|
386
|
-
bot_token: msg.content.bot_token,
|
|
387
|
-
domain_id: msg.content.domain_id,
|
|
388
|
-
app_id: msg.content.app_id,
|
|
389
|
-
bot_id: msg.content.bot_id,
|
|
390
|
-
agent_id: msg.content.agent_id,
|
|
391
|
-
session_id: msg.content.session_id,
|
|
392
|
-
message_id: msg.content.message_id,
|
|
393
|
-
response: "你需要我帮你做什么呢?",
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
return;
|
|
147
|
+
sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
|
|
148
|
+
sendFinal(msgCtx)
|
|
149
|
+
return
|
|
397
150
|
}
|
|
398
151
|
|
|
399
152
|
try {
|
|
400
|
-
const core = getDcgchatRuntime()
|
|
153
|
+
const core = getDcgchatRuntime()
|
|
401
154
|
|
|
402
|
-
const conversationId = msg.content.session_id?.trim()
|
|
155
|
+
const conversationId = msg.content.session_id?.trim()
|
|
403
156
|
|
|
404
157
|
const route = core.channel.routing.resolveAgentRoute({
|
|
405
|
-
cfg,
|
|
158
|
+
cfg: config,
|
|
406
159
|
channel: "dcgchat",
|
|
407
160
|
accountId: account.accountId,
|
|
408
|
-
peer: { kind:
|
|
409
|
-
})
|
|
161
|
+
peer: { kind: 'direct', id: conversationId }
|
|
162
|
+
})
|
|
410
163
|
|
|
411
164
|
// If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
|
|
412
|
-
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
413
|
-
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
165
|
+
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
166
|
+
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
414
167
|
const effectiveSessionKey = embeddedAgentId
|
|
415
168
|
? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
|
|
416
|
-
: route.sessionKey
|
|
169
|
+
: route.sessionKey
|
|
417
170
|
|
|
418
171
|
const agentEntry =
|
|
419
|
-
effectiveAgentId && effectiveAgentId !==
|
|
420
|
-
|
|
421
|
-
: undefined;
|
|
422
|
-
const agentDisplayName =
|
|
423
|
-
agentEntry?.name ??
|
|
424
|
-
(effectiveAgentId && effectiveAgentId !== "main"
|
|
425
|
-
? effectiveAgentId
|
|
426
|
-
: undefined);
|
|
172
|
+
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
173
|
+
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
427
174
|
|
|
428
175
|
// Abort any existing generation for this conversation, then start a new one
|
|
429
|
-
const existingCtrl = activeGenerations.get(conversationId)
|
|
430
|
-
if (existingCtrl) existingCtrl.abort()
|
|
431
|
-
const genCtrl = new AbortController()
|
|
432
|
-
const genSignal = genCtrl.signal
|
|
433
|
-
activeGenerations.set(conversationId, genCtrl)
|
|
176
|
+
const existingCtrl = activeGenerations.get(conversationId)
|
|
177
|
+
if (existingCtrl) existingCtrl.abort()
|
|
178
|
+
const genCtrl = new AbortController()
|
|
179
|
+
const genSignal = genCtrl.signal
|
|
180
|
+
activeGenerations.set(conversationId, genCtrl)
|
|
434
181
|
|
|
435
182
|
// 处理用户上传的文件
|
|
436
|
-
const files = msg.content.files ?? []
|
|
437
|
-
let mediaPayload: Record<string, unknown> = {}
|
|
183
|
+
const files = msg.content.files ?? []
|
|
184
|
+
let mediaPayload: Record<string, unknown> = {}
|
|
438
185
|
if (files.length > 0) {
|
|
439
|
-
const mediaList = await resolveMediaFromUrls(
|
|
440
|
-
|
|
441
|
-
msg.content.bot_token,
|
|
442
|
-
log,
|
|
443
|
-
);
|
|
444
|
-
mediaPayload = buildMediaPayload(mediaList);
|
|
445
|
-
log(
|
|
446
|
-
`dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`,
|
|
447
|
-
);
|
|
186
|
+
const mediaList = await resolveMediaFromUrls(files, msg.content.bot_token)
|
|
187
|
+
mediaPayload = buildMediaPayload(mediaList)
|
|
448
188
|
}
|
|
449
189
|
|
|
450
|
-
const envelopeOptions =
|
|
451
|
-
|
|
452
|
-
// const messageBody = `${userId}: ${text}`;
|
|
453
|
-
// 补充消息
|
|
454
|
-
const messageBody = text;
|
|
190
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config)
|
|
191
|
+
const messageBody = text
|
|
455
192
|
const bodyFormatted = core.channel.reply.formatAgentEnvelope({
|
|
456
|
-
channel:
|
|
193
|
+
channel: '书灵墨宝',
|
|
457
194
|
from: userId,
|
|
458
195
|
timestamp: new Date(),
|
|
459
196
|
envelope: envelopeOptions,
|
|
460
|
-
body: messageBody
|
|
461
|
-
})
|
|
197
|
+
body: messageBody
|
|
198
|
+
})
|
|
462
199
|
|
|
463
200
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
464
201
|
Body: bodyFormatted,
|
|
@@ -468,260 +205,160 @@ export async function handleDcgchatMessage(params: {
|
|
|
468
205
|
To: conversationId,
|
|
469
206
|
SessionKey: effectiveSessionKey,
|
|
470
207
|
AccountId: route.accountId,
|
|
471
|
-
ChatType:
|
|
208
|
+
ChatType: 'direct',
|
|
472
209
|
SenderName: agentDisplayName,
|
|
473
210
|
SenderId: userId,
|
|
474
|
-
Provider: "dcgchat"
|
|
475
|
-
Surface: "dcgchat"
|
|
211
|
+
Provider: "dcgchat",
|
|
212
|
+
Surface: "dcgchat",
|
|
476
213
|
MessageSid: msg.content.message_id,
|
|
477
214
|
Timestamp: Date.now(),
|
|
478
215
|
WasMentioned: true,
|
|
479
216
|
CommandAuthorized: true,
|
|
480
|
-
OriginatingChannel: "dcgchat"
|
|
217
|
+
OriginatingChannel: "dcgchat",
|
|
481
218
|
OriginatingTo: `user:${userId}`,
|
|
482
|
-
...mediaPayload
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
|
|
219
|
+
...mediaPayload
|
|
220
|
+
})
|
|
486
221
|
|
|
487
222
|
const sentMediaKeys = new Set<string>()
|
|
488
223
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
489
|
-
let
|
|
224
|
+
let streamedTextLen = 0
|
|
490
225
|
|
|
491
226
|
const prefixContext = createReplyPrefixContext({
|
|
492
|
-
cfg,
|
|
493
|
-
agentId: effectiveAgentId ??
|
|
227
|
+
cfg: config,
|
|
228
|
+
agentId: effectiveAgentId ?? '',
|
|
494
229
|
channel: "dcgchat",
|
|
495
|
-
accountId: account.accountId
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
});
|
|
230
|
+
accountId: account.accountId
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
234
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
235
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
236
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
237
|
+
onReplyStart: async () => {},
|
|
238
|
+
deliver: async (payload: ReplyPayload, info) => {
|
|
239
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
240
|
+
for (const mediaUrl of mediaList) {
|
|
241
|
+
const key = getMediaKey(mediaUrl)
|
|
242
|
+
if (sentMediaKeys.has(key)) continue
|
|
243
|
+
sentMediaKeys.add(key)
|
|
244
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
onError: (err: unknown, info: { kind: string }) => {
|
|
248
|
+
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
|
249
|
+
},
|
|
250
|
+
onIdle: () => {
|
|
251
|
+
sendFinal(msgCtx)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
520
254
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
)
|
|
255
|
+
let wasAborted = false
|
|
256
|
+
try {
|
|
257
|
+
if (systemCommand.includes(text?.trim())) {
|
|
258
|
+
dcgLogger(`dispatching /new`)
|
|
259
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
260
|
+
ctx: ctxPayload,
|
|
261
|
+
cfg: config,
|
|
262
|
+
dispatcher,
|
|
263
|
+
replyOptions: {
|
|
264
|
+
...replyOptions,
|
|
265
|
+
onModelSelected: prefixContext.onModelSelected
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
} else if (interruptCommand.includes(text?.trim())) {
|
|
269
|
+
dcgLogger(`interrupt command: ${text}`)
|
|
270
|
+
abortMobookappGeneration(conversationId)
|
|
271
|
+
sendFinal(msgCtx)
|
|
272
|
+
return
|
|
273
|
+
} else {
|
|
274
|
+
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
538
275
|
await core.channel.reply.dispatchReplyFromConfig({
|
|
539
276
|
ctx: ctxPayload,
|
|
540
|
-
cfg,
|
|
277
|
+
cfg: config,
|
|
541
278
|
dispatcher,
|
|
542
279
|
replyOptions: {
|
|
543
280
|
...replyOptions,
|
|
544
281
|
abortSignal: genSignal,
|
|
545
282
|
onModelSelected: prefixContext.onModelSelected,
|
|
546
283
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
547
|
-
|
|
548
|
-
`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`,
|
|
549
|
-
);
|
|
284
|
+
// Accumulate full text
|
|
550
285
|
if (payload.text) {
|
|
551
|
-
completeText = payload.text
|
|
552
|
-
}
|
|
553
|
-
const mediaList = resolveReplyMediaList(payload);
|
|
554
|
-
if (mediaList.length > 0) {
|
|
555
|
-
for (let i = 0; i < mediaList.length; i++) {
|
|
556
|
-
const mediaUrl = mediaList[i];
|
|
557
|
-
const key = getMediaKey(mediaUrl);
|
|
558
|
-
if (sentMediaKeys.has(key)) continue;
|
|
559
|
-
sentMediaKeys.add(key);
|
|
560
|
-
await sendDcgchatMedia({
|
|
561
|
-
cfg,
|
|
562
|
-
accountId,
|
|
563
|
-
log,
|
|
564
|
-
mediaUrl,
|
|
565
|
-
text: "",
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
log(
|
|
569
|
-
`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`,
|
|
570
|
-
);
|
|
286
|
+
completeText = payload.text
|
|
571
287
|
}
|
|
288
|
+
// --- Streaming text chunks ---
|
|
572
289
|
if (payload.text) {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
messageType: "openclaw_bot_chat",
|
|
580
|
-
_userId: msg._userId,
|
|
581
|
-
source: "client",
|
|
582
|
-
content: {
|
|
583
|
-
bot_token: msg.content.bot_token,
|
|
584
|
-
domain_id: msg.content.domain_id,
|
|
585
|
-
app_id: msg.content.app_id,
|
|
586
|
-
bot_id: msg.content.bot_id,
|
|
587
|
-
agent_id: msg.content.agent_id,
|
|
588
|
-
session_id: msg.content.session_id,
|
|
589
|
-
message_id: msg.content.message_id,
|
|
590
|
-
response: nextTextChunk,
|
|
591
|
-
state: "chunk",
|
|
592
|
-
},
|
|
593
|
-
});
|
|
594
|
-
log(
|
|
595
|
-
`dcgchat[${accountId}][deliver]: chunk sent successfully`,
|
|
596
|
-
);
|
|
290
|
+
const delta = payload.text.startsWith(completeText.slice(0, streamedTextLen))
|
|
291
|
+
? payload.text.slice(streamedTextLen)
|
|
292
|
+
: payload.text
|
|
293
|
+
if (delta.trim()) {
|
|
294
|
+
sendChunk(msgCtx, delta)
|
|
295
|
+
dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
|
|
597
296
|
}
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
|
|
297
|
+
streamedTextLen = payload.text.length
|
|
298
|
+
}
|
|
299
|
+
// --- Media from payload ---
|
|
300
|
+
const mediaList = resolveReplyMediaList(payload)
|
|
301
|
+
for (const mediaUrl of mediaList) {
|
|
302
|
+
const key = getMediaKey(mediaUrl)
|
|
303
|
+
if (sentMediaKeys.has(key)) continue
|
|
304
|
+
sentMediaKeys.add(key)
|
|
305
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
|
|
601
306
|
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
})
|
|
605
310
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
`dcgchat[${accountId}]: generation aborted by session for conversationId=${conversationId}`,
|
|
616
|
-
);
|
|
617
|
-
} else {
|
|
618
|
-
error(
|
|
619
|
-
`dcgchat[${accountId}]: dispatchReplyFromConfig error: ${String(err)}`,
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
} finally {
|
|
623
|
-
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
624
|
-
activeGenerations.delete(conversationId);
|
|
625
|
-
}
|
|
626
|
-
if (wasAborted) {
|
|
627
|
-
//TODO:是否需要发消息给移动端,通知停止生成
|
|
628
|
-
}
|
|
311
|
+
} catch (err: unknown) {
|
|
312
|
+
if (genSignal.aborted) {
|
|
313
|
+
wasAborted = true
|
|
314
|
+
dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
315
|
+
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
316
|
+
wasAborted = true
|
|
317
|
+
dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
318
|
+
} else {
|
|
319
|
+
dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
629
320
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
);
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
321
|
+
} finally {
|
|
322
|
+
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
323
|
+
activeGenerations.delete(conversationId)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
markRunComplete()
|
|
328
|
+
} catch (err) {
|
|
329
|
+
dcgLogger(` markRunComplete error: ${String(err)}`, 'error')
|
|
330
|
+
}
|
|
331
|
+
markDispatchIdle()
|
|
332
|
+
if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
|
|
333
|
+
for (const file of extractMobookFiles(completeText)) {
|
|
334
|
+
let resolved = file
|
|
335
|
+
if (!fs.existsSync(resolved)) {
|
|
336
|
+
resolved = path.join(getWorkspaceDir(), file)
|
|
337
|
+
if (!fs.existsSync(resolved)) return
|
|
650
338
|
}
|
|
651
|
-
|
|
652
|
-
await sendDcgchatMedia({
|
|
653
|
-
cfg,
|
|
654
|
-
accountId,
|
|
655
|
-
log,
|
|
656
|
-
mediaUrl: url,
|
|
657
|
-
text: "",
|
|
658
|
-
});
|
|
339
|
+
await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
|
|
659
340
|
}
|
|
660
|
-
log(
|
|
661
|
-
`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`,
|
|
662
|
-
);
|
|
663
341
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
_userId: msg._userId,
|
|
668
|
-
source: "client",
|
|
669
|
-
content: {
|
|
670
|
-
bot_token: msg.content.bot_token,
|
|
671
|
-
domain_id: msg.content.domain_id,
|
|
672
|
-
app_id: msg.content.app_id,
|
|
673
|
-
bot_id: msg.content.bot_id,
|
|
674
|
-
agent_id: msg.content.agent_id,
|
|
675
|
-
session_id: msg.content.session_id,
|
|
676
|
-
message_id: msg.content.message_id,
|
|
677
|
-
response: "",
|
|
678
|
-
state: "final",
|
|
679
|
-
},
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
setMsgStatus("finished");
|
|
683
|
-
textChunk = "";
|
|
684
|
-
log(`dcgchat[${accountId}]: final state sent`);
|
|
685
|
-
|
|
686
|
-
markDispatchIdle();
|
|
687
|
-
log(`dcgchat[${accountId}]: message handling complete`);
|
|
342
|
+
sendFinal(msgCtx)
|
|
343
|
+
clearSentMediaKeys(msg.content.message_id)
|
|
344
|
+
setMsgStatus('finished')
|
|
688
345
|
|
|
689
346
|
// Record session metadata
|
|
690
|
-
const storePath = core.channel.session.resolveStorePath(
|
|
347
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
691
348
|
core.channel.session
|
|
692
349
|
.recordInboundSession({
|
|
693
350
|
storePath,
|
|
694
351
|
sessionKey: effectiveSessionKey,
|
|
695
352
|
ctx: ctxPayload,
|
|
696
353
|
onRecordError: (err) => {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
700
|
-
},
|
|
354
|
+
dcgLogger(` session record error: ${String(err)}`, 'error')
|
|
355
|
+
}
|
|
701
356
|
})
|
|
702
357
|
.catch((err: unknown) => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
);
|
|
706
|
-
});
|
|
358
|
+
dcgLogger(` recordInboundSession failed: ${String(err)}`, 'error')
|
|
359
|
+
})
|
|
707
360
|
} catch (err) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
messageType: "openclaw_bot_chat",
|
|
711
|
-
_userId: msg._userId,
|
|
712
|
-
source: "client",
|
|
713
|
-
content: {
|
|
714
|
-
bot_token: msg.content.bot_token,
|
|
715
|
-
domain_id: msg.content.domain_id,
|
|
716
|
-
app_id: msg.content.app_id,
|
|
717
|
-
bot_id: msg.content.bot_id,
|
|
718
|
-
agent_id: msg.content.agent_id,
|
|
719
|
-
session_id: msg.content.session_id,
|
|
720
|
-
message_id: msg.content.message_id,
|
|
721
|
-
response: `[错误] ${err instanceof Error ? err.message : String(err)}`,
|
|
722
|
-
state: "final",
|
|
723
|
-
},
|
|
724
|
-
});
|
|
361
|
+
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
362
|
+
sendError(msgCtx, err instanceof Error ? err.message : String(err))
|
|
725
363
|
}
|
|
726
364
|
}
|
|
727
|
-
|