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