@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/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 { ClawdbotConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk";
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
- async function resolveMediaFromUrls(files: { name: string, url: string }[], botToken: string, log: (message: string) => void): Promise<MediaInfo[]> {
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(`dcgchat media: starting resolve for ${files.length} file(s): ${JSON.stringify(files)}`);
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
- if (/^https?:\/\//i.test(file.url)) {
29
- data = file.url
30
- } else {
31
- data = await generateSignUrl(file.url, botToken);
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?.(`dcgchat media: [${i + 1}/${files.length}] fetch failed with HTTP ${response.status}, skipping`);
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 = file.name || path.basename(new URL(file.url).pathname) || "file";
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(`dcgchat media: [${i + 1}/${files.length}] FAILED to process ${file.url}: ${String(err)}`);
100
+ log(
101
+ `dcgchat media: [${i + 1}/${files.length}] FAILED to process ${file.url}: ${String(err)}`,
102
+ );
63
103
  }
64
104
  }
65
- log(`dcgchat media: resolve complete, ${out.length}/${files.length} file(s) succeeded`);
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
- 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'rtf', 'odt',
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
- 'json', 'xml', 'csv', 'yaml', 'yml',
164
+ "json",
165
+ "xml",
166
+ "csv",
167
+ "yaml",
168
+ "yml",
166
169
 
167
170
  // 前端/文本
168
- 'html', 'htm', 'md', 'markdown', 'css', 'js', 'ts',
171
+ "html",
172
+ "htm",
173
+ "md",
174
+ "markdown",
175
+ "css",
176
+ "js",
177
+ "ts",
169
178
 
170
179
  // 图片
171
- 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff',
180
+ "png",
181
+ "jpg",
182
+ "jpeg",
183
+ "gif",
184
+ "bmp",
185
+ "webp",
186
+ "svg",
187
+ "ico",
188
+ "tiff",
172
189
 
173
190
  // 音频
174
- 'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a',
191
+ "mp3",
192
+ "wav",
193
+ "ogg",
194
+ "aac",
195
+ "flac",
196
+ "m4a",
175
197
 
176
198
  // 视频
177
- 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
199
+ "mp4",
200
+ "avi",
201
+ "mov",
202
+ "wmv",
203
+ "flv",
204
+ "mkv",
205
+ "webm",
178
206
 
179
207
  // 压缩包
180
- 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
208
+ "zip",
209
+ "rar",
210
+ "7z",
211
+ "tar",
212
+ "gz",
213
+ "bz2",
214
+ "xz",
181
215
 
182
216
  // 可执行/程序
183
- 'exe', 'dmg', 'pkg', 'apk', 'ipa',
217
+ "exe",
218
+ "dmg",
219
+ "pkg",
220
+ "apk",
221
+ "ipa",
184
222
 
185
223
  // 其他常见
186
- 'log', 'dat', 'bin'
224
+ "log",
225
+ "dat",
226
+ "bin",
187
227
  ];
188
228
 
189
- function extractMobookFiles(text = '') {
190
- if (typeof text !== 'string' || !text.trim()) return [];
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
- const result = new Set();
234
+ /** 去除控制符、零宽字符等常见脏值 */
235
+ function stripMobookNoise(s: string) {
236
+ return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, "");
237
+ }
193
238
 
194
- // ✅ 扩展名
195
- const EXT = `(${EXT_LIST.join('|')})`;
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})\``, 'gi');
203
- (text.match(backtickReg) || []).forEach(item => {
204
- const name = item.replace(/`/g, '').trim();
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}`, 'gi');
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})`, 'gi');
218
- (text.match(inlineReg) || []).forEach(item => {
219
- const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'));
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})\\*\\*`, 'gi');
227
- (text.match(boldReg) || []).forEach(item => {
228
- const name = item.replace(/\*\*/g, '').trim();
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*\\(`, 'gi');
236
- (text.match(looseReg) || []).forEach(item => {
237
- const name = item.replace(/\s*\(.+$/, '').trim();
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('extractMobookFiles error:', e);
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(name)) return false;
258
-
344
+ if (/[\/\\<>:"|?*]/.test(cleaned)) return false;
259
345
  // 长度限制(防止异常长字符串)
260
- if (name.length > 200) return false;
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: userId },
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(files, msg.content.bot_token, log)
439
+ const mediaList = await resolveMediaFromUrls(
440
+ files,
441
+ msg.content.bot_token,
442
+ log,
443
+ );
329
444
  mediaPayload = buildMediaPayload(mediaList);
330
- log(`dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`);
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 = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
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: userId,
351
- SessionKey: route.sessionKey,
352
- AccountId: msg.content.session_id,
468
+ To: conversationId,
469
+ SessionKey: effectiveSessionKey,
470
+ AccountId: route.accountId,
353
471
  ChatType: "direct",
354
- SenderName: userId,
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({ cfg, agentId: route.agentId });
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: prefixContext.responsePrefixContextProvider,
379
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
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(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
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(`dcgchat[${accountId}] ${info.kind} reply failed: ${String(err)}`);
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
- if (text === '/new') {
391
- await core.channel.reply.dispatchReplyFromConfig({
392
- ctx: ctxPayload,
393
- cfg,
394
- dispatcher,
395
- replyOptions: {
396
- ...replyOptions,
397
- onModelSelected: prefixContext.onModelSelected
398
- },
399
- });
400
- log(`dcgchat[${accountId}]: skipping agent dispatch for /new`);
401
- params.onChunk({
402
- messageType: "openclaw_bot_chat",
403
- _userId: msg._userId,
404
- source: "client",
405
- content: {
406
- bot_token: msg.content.bot_token,
407
- domain_id: msg.content.domain_id,
408
- app_id: msg.content.app_id,
409
- bot_id: msg.content.bot_id,
410
- agent_id: msg.content.agent_id,
411
- session_id: msg.content.session_id,
412
- message_id: msg.content.message_id,
413
- response: '好呀~我不会忘记我们之前的聊天,只是暂时翻篇,让我们更轻松地开启新话题。',
414
- state: 'chunk',
415
- },
416
- });
417
- params.onChunk({
418
- messageType: "openclaw_bot_chat",
419
- _userId: msg._userId,
420
- source: "client",
421
- content: {
422
- bot_token: msg.content.bot_token,
423
- domain_id: msg.content.domain_id,
424
- app_id: msg.content.app_id,
425
- bot_id: msg.content.bot_id,
426
- agent_id: msg.content.agent_id,
427
- session_id: msg.content.session_id,
428
- message_id: msg.content.message_id,
429
- response: '',
430
- state: 'final',
431
- },
432
- });
433
- } else {
434
- log(`dcgchat[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
435
- await core.channel.reply.dispatchReplyFromConfig({
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
- sentMediaKeys.add(key);
458
- await sendDcgchatMedia({
459
- cfg,
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
- log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
467
- }
468
- if (payload.text) {
469
- const nextTextChunk = payload.text.replace(textChunk, '');
470
- if (nextTextChunk.trim()) {
471
- log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`);
472
- params.onChunk({
473
- messageType: "openclaw_bot_chat",
474
- _userId: msg._userId,
475
- source: "client",
476
- content: {
477
- bot_token: msg.content.bot_token,
478
- domain_id: msg.content.domain_id,
479
- app_id: msg.content.app_id,
480
- bot_id: msg.content.bot_id,
481
- agent_id: msg.content.agent_id,
482
- session_id: msg.content.session_id,
483
- message_id: msg.content.message_id,
484
- response: nextTextChunk,
485
- state: 'chunk',
486
- },
487
- });
488
- log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
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
- textChunk = payload.text
491
- } else {
492
- log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
493
- }
602
+ },
494
603
  },
495
- },
496
- });
497
- }
498
-
499
- const extractor = createFileExtractor()
500
- const completeFiles = extractor.getNewFiles(completeText)
501
- if (completeFiles.length > 0) {
502
- for (let i = 0; i < completeFiles.length; i++) {
503
- let url = completeFiles[i] as string
504
- if (!path.isAbsolute(url)) {
505
- url = path.join(getWorkspaceDir(), url)
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
- const key = getMediaKey(url);
508
- if (sentMediaKeys.has(key)) {
509
- log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
510
- continue;
622
+ } finally {
623
+ if (activeGenerations.get(conversationId) === genCtrl) {
624
+ activeGenerations.delete(conversationId);
511
625
  }
512
- if (!fs.existsSync(url)) {
513
- log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
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
- log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
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(`dcgchat[${accountId}]: mobookFiles already sent, skipping: ${url}`);
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(`dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`);
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(`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`);
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: 'final',
677
+ response: "",
678
+ state: "final",
569
679
  },
570
680
  });
571
681
 
572
- setMsgStatus('finished');
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: 'final',
722
+ state: "final",
595
723
  },
596
724
  });
597
725
  }
598
726
  }
727
+