@dcrays/dcgchat-test 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
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,131 @@ 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 [];
191
-
229
+ function extractMobookFiles(text = "") {
230
+ if (typeof text !== "string" || !text.trim()) return [];
192
231
  const result = new Set();
193
-
194
232
  // ✅ 扩展名
195
- const EXT = `(${EXT_LIST.join('|')})`;
196
-
233
+ const EXT = `(${EXT_LIST.join("|")})`;
197
234
  // ✅ 文件名字符(增强:支持中文、符号)
198
235
  const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`;
199
-
200
236
  try {
201
237
  // 1️⃣ `xxx.xxx`
202
- const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi');
203
- (text.match(backtickReg) || []).forEach(item => {
204
- const name = item.replace(/`/g, '').trim();
238
+ const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, "gi");
239
+ (text.match(backtickReg) || []).forEach((item) => {
240
+ const name = item.replace(/`/g, "").trim();
205
241
  if (isValidFileName(name)) {
206
242
  result.add(`/mobook/${name}`);
207
243
  }
208
244
  });
209
-
210
245
  // 2️⃣ /mobook/xxx.xxx
211
- const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi');
212
- (text.match(fullPathReg) || []).forEach(p => {
246
+ const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, "gi");
247
+ (text.match(fullPathReg) || []).forEach((p) => {
213
248
  result.add(normalizePath(p));
214
249
  });
215
-
216
250
  // 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'));
251
+ const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, "gi");
252
+ (text.match(inlineReg) || []).forEach((item) => {
253
+ const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, "i"));
220
254
  if (match && isValidFileName(match[0])) {
221
255
  result.add(`/mobook/${match[0].trim()}`);
222
256
  }
223
257
  });
224
-
225
258
  // 🆕 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();
259
+ const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, "gi");
260
+ (text.match(boldReg) || []).forEach((item) => {
261
+ const name = item.replace(/\*\*/g, "").trim();
229
262
  if (isValidFileName(name)) {
230
263
  result.add(`/mobook/${name}`);
231
264
  }
232
265
  });
233
-
234
266
  // 🆕 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();
267
+ const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, "gi");
268
+ (text.match(looseReg) || []).forEach((item) => {
269
+ const name = item.replace(/\s*\(.+$/, "").trim();
238
270
  if (isValidFileName(name)) {
239
271
  result.add(`/mobook/${name}`);
240
272
  }
241
273
  });
242
-
243
274
  } catch (e) {
244
- console.warn('extractMobookFiles error:', e);
275
+ console.warn("extractMobookFiles error:", e);
245
276
  }
246
-
247
277
  return [...result];
248
278
  }
249
279
 
@@ -252,13 +282,10 @@ function extractMobookFiles(text = '') {
252
282
  */
253
283
  function isValidFileName(name: string) {
254
284
  if (!name) return false;
255
-
256
285
  // 过滤异常字符
257
286
  if (/[\/\\<>:"|?*]/.test(name)) return false;
258
-
259
287
  // 长度限制(防止异常长字符串)
260
288
  if (name.length > 200) return false;
261
-
262
289
  return true;
263
290
  }
264
291
 
@@ -267,8 +294,8 @@ function isValidFileName(name: string) {
267
294
  */
268
295
  function normalizePath(path: string) {
269
296
  return path
270
- .replace(/\/+/g, '/') // 多斜杠 → 单斜杠
271
- .replace(/\/$/, ''); // 去掉结尾 /
297
+ .replace(/\/+/g, "/") // 多斜杠 → 单斜杠
298
+ .replace(/\/$/, ""); // 去掉结尾 /
272
299
  }
273
300
 
274
301
  /**
@@ -285,7 +312,7 @@ export async function handleDcgchatMessage(params: {
285
312
  const log = runtime?.log ?? console.log;
286
313
  const error = runtime?.error ?? console.error;
287
314
  // 完整的文本
288
- let completeText = ''
315
+ let completeText = "";
289
316
 
290
317
  const account = resolveAccount(cfg, accountId);
291
318
  const userId = msg._userId.toString();
@@ -314,23 +341,58 @@ export async function handleDcgchatMessage(params: {
314
341
  try {
315
342
  const core = getDcgchatRuntime();
316
343
 
344
+ const conversationId = msg.content.session_id?.trim();
345
+
317
346
  const route = core.channel.routing.resolveAgentRoute({
318
347
  cfg,
319
- channel: "dcgchat",
348
+ channel: "dcgchat-test",
320
349
  accountId: account.accountId,
321
- peer: { kind: "direct", id: userId },
350
+ peer: { kind: "direct", id: conversationId },
322
351
  });
323
352
 
353
+ // If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
354
+ const embeddedAgentId = extractAgentIdFromConversationId(conversationId);
355
+ const effectiveAgentId = embeddedAgentId ?? route.agentId;
356
+ const effectiveSessionKey = embeddedAgentId
357
+ ? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
358
+ : route.sessionKey;
359
+
360
+ const agentEntry =
361
+ effectiveAgentId && effectiveAgentId !== "main"
362
+ ? cfg.agents?.list?.find((a) => a.id === effectiveAgentId)
363
+ : undefined;
364
+ const agentDisplayName =
365
+ agentEntry?.name ??
366
+ (effectiveAgentId && effectiveAgentId !== "main"
367
+ ? effectiveAgentId
368
+ : undefined);
369
+
370
+ // Abort any existing generation for this conversation, then start a new one
371
+ const existingCtrl = activeGenerations.get(conversationId);
372
+ console.log("🚀 ~ handleDcgchatMessage ~ conversationId:", existingCtrl)
373
+ if (existingCtrl) existingCtrl.abort();
374
+ const genCtrl = new AbortController();
375
+ const genSignal = genCtrl.signal;
376
+ console.log("🚀 ~ handleDcgchatMessage ~ conversationId:", conversationId)
377
+ activeGenerations.set(conversationId, genCtrl);
378
+
324
379
  // 处理用户上传的文件
325
380
  const files = msg.content.files ?? [];
326
381
  let mediaPayload: Record<string, unknown> = {};
327
382
  if (files.length > 0) {
328
- const mediaList = await resolveMediaFromUrls(files, msg.content.bot_token, log)
383
+ const mediaList = await resolveMediaFromUrls(
384
+ files,
385
+ msg.content.bot_token,
386
+ log,
387
+ );
329
388
  mediaPayload = buildMediaPayload(mediaList);
330
- log(`dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`);
389
+ log(
390
+ `dcgchat[${accountId}]: media resolved ${mediaList.length}/${files.length} file(s), payload=${JSON.stringify(mediaList)}`,
391
+ );
331
392
  }
332
393
 
333
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
394
+ const envelopeOptions =
395
+ core.channel.reply.resolveEnvelopeFormatOptions(cfg);
334
396
  // const messageBody = `${userId}: ${text}`;
335
397
  // 补充消息
336
398
  const messageBody = text;
@@ -347,192 +409,173 @@ export async function handleDcgchatMessage(params: {
347
409
  RawBody: text,
348
410
  CommandBody: text,
349
411
  From: userId,
350
- To: userId,
351
- SessionKey: route.sessionKey,
352
- AccountId: msg.content.session_id,
412
+ To: conversationId,
413
+ SessionKey: effectiveSessionKey,
414
+ AccountId: route.accountId,
353
415
  ChatType: "direct",
354
- SenderName: userId,
416
+ SenderName: agentDisplayName,
355
417
  SenderId: userId,
356
- Provider: "dcgchat" as const,
357
- Surface: "dcgchat" as const,
418
+ Provider: "dcgchat-test" as const,
419
+ Surface: "dcgchat-test" as const,
358
420
  MessageSid: msg.content.message_id,
359
421
  Timestamp: Date.now(),
360
422
  WasMentioned: true,
361
423
  CommandAuthorized: true,
362
- OriginatingChannel: "dcgchat" as const,
424
+ OriginatingChannel: "dcgchat-test" as const,
363
425
  OriginatingTo: `user:${userId}`,
364
426
  ...mediaPayload,
365
427
  });
366
428
 
367
429
  log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
368
430
 
369
- const sentMediaKeys = new Set<string>()
370
- const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
371
- let textChunk = ''
431
+ const sentMediaKeys = new Set<string>();
432
+ const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url;
433
+ let textChunk = "";
372
434
 
373
- const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
435
+ const prefixContext = createReplyPrefixContext({
436
+ cfg,
437
+ agentId: effectiveAgentId ?? "",
438
+ channel: "dcgchat-test",
439
+ accountId: account.accountId,
440
+ });
374
441
 
375
442
  const { dispatcher, replyOptions, markDispatchIdle } =
376
443
  core.channel.reply.createReplyDispatcherWithTyping({
377
444
  responsePrefix: prefixContext.responsePrefix,
378
- responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
379
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
445
+ responsePrefixContextProvider:
446
+ prefixContext.responsePrefixContextProvider,
447
+ humanDelay: core.channel.reply.resolveHumanDelayConfig(
448
+ cfg,
449
+ route.agentId,
450
+ ),
380
451
  onReplyStart: async () => {},
381
452
  deliver: async (payload: ReplyPayload) => {
382
- log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
453
+ log(
454
+ `dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`,
455
+ );
383
456
  },
384
- onError: (err: any, info: { kind: any; }) => {
385
- error(`dcgchat[${accountId}] ${info.kind} reply failed: ${String(err)}`);
457
+ onError: (err: any, info: { kind: any }) => {
458
+ error(
459
+ `dcgchat[${accountId}] ${info.kind} reply failed: ${String(err)}`,
460
+ );
386
461
  },
387
462
  onIdle: () => {},
388
463
  });
389
464
 
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
- sentMediaKeys.add(key);
454
- await sendDcgchatMedia({
455
- cfg,
456
- accountId,
457
- log,
458
- mediaUrl,
459
- text: "",
460
- });
465
+ let wasAborted = false;
466
+ try {
467
+ log(
468
+ `dcgchat[${accountId}]: dispatching to agent (session=${route.sessionKey})`,
469
+ );
470
+ await core.channel.reply.dispatchReplyFromConfig({
471
+ ctx: ctxPayload,
472
+ cfg,
473
+ dispatcher,
474
+ replyOptions: {
475
+ ...replyOptions,
476
+ abortSignal: genSignal,
477
+ onModelSelected: prefixContext.onModelSelected,
478
+ onPartialReply: async (payload: ReplyPayload) => {
479
+ log(
480
+ `dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`,
481
+ );
482
+ if (payload.text) {
483
+ completeText = payload.text;
484
+ }
485
+ const mediaList = resolveReplyMediaList(payload);
486
+ if (mediaList.length > 0) {
487
+ for (let i = 0; i < mediaList.length; i++) {
488
+ const mediaUrl = mediaList[i];
489
+ const key = getMediaKey(mediaUrl);
490
+ if (sentMediaKeys.has(key)) continue;
491
+ sentMediaKeys.add(key);
492
+ await sendDcgchatMedia({
493
+ cfg,
494
+ accountId,
495
+ log,
496
+ mediaUrl,
497
+ text: "",
498
+ });
499
+ }
500
+ log(
501
+ `dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`,
502
+ );
461
503
  }
462
- log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
463
- }
464
- if (payload.text) {
465
- const nextTextChunk = payload.text.replace(textChunk, '');
466
- if (nextTextChunk.trim()) {
467
- log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`);
468
- params.onChunk({
469
- messageType: "openclaw_bot_chat",
470
- _userId: msg._userId,
471
- source: "client",
472
- content: {
473
- bot_token: msg.content.bot_token,
474
- domain_id: msg.content.domain_id,
475
- app_id: msg.content.app_id,
476
- bot_id: msg.content.bot_id,
477
- agent_id: msg.content.agent_id,
478
- session_id: msg.content.session_id,
479
- message_id: msg.content.message_id,
480
- response: nextTextChunk,
481
- state: 'chunk',
482
- },
483
- });
484
- log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
504
+ if (payload.text) {
505
+ const nextTextChunk = payload.text.replace(textChunk, "");
506
+ if (nextTextChunk.trim()) {
507
+ log(
508
+ `dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`,
509
+ );
510
+ params.onChunk({
511
+ messageType: "openclaw_bot_chat",
512
+ _userId: msg._userId,
513
+ source: "client",
514
+ content: {
515
+ bot_token: msg.content.bot_token,
516
+ domain_id: msg.content.domain_id,
517
+ app_id: msg.content.app_id,
518
+ bot_id: msg.content.bot_id,
519
+ agent_id: msg.content.agent_id,
520
+ session_id: msg.content.session_id,
521
+ message_id: msg.content.message_id,
522
+ response: nextTextChunk,
523
+ state: "chunk",
524
+ },
525
+ });
526
+ log(
527
+ `dcgchat[${accountId}][deliver]: chunk sent successfully`,
528
+ );
529
+ }
530
+ textChunk = payload.text;
531
+ } else {
532
+ log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
485
533
  }
486
- textChunk = payload.text
487
- } else {
488
- log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
489
- }
534
+ },
490
535
  },
491
- },
492
- });
493
- }
494
-
495
- const extractor = createFileExtractor()
496
- const completeFiles = extractor.getNewFiles(completeText)
497
- if (completeFiles.length > 0) {
498
- for (let i = 0; i < completeFiles.length; i++) {
499
- let url = completeFiles[i] as string
500
- if (!path.isAbsolute(url)) {
501
- url = path.join(getWorkspaceDir(), url)
536
+ });
537
+ } catch (err: unknown) {
538
+ if (genSignal.aborted) {
539
+ wasAborted = true;
540
+ log(
541
+ `dcgchat[${accountId}]: generation aborted for conversationId=${conversationId}`,
542
+ );
543
+ } else if (err instanceof Error && err.name === "AbortError") {
544
+ wasAborted = true;
545
+ log(
546
+ `dcgchat[${accountId}]: generation aborted by session for conversationId=${conversationId}`,
547
+ );
548
+ } else {
549
+ error(
550
+ `dcgchat[${accountId}]: dispatchReplyFromConfig error: ${String(err)}`,
551
+ );
502
552
  }
503
- const key = getMediaKey(url);
504
- if (sentMediaKeys.has(key)) {
505
- log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
506
- continue;
553
+ } finally {
554
+ if (activeGenerations.get(conversationId) === genCtrl) {
555
+ activeGenerations.delete(conversationId);
507
556
  }
508
- if (!fs.existsSync(url)) {
509
- log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
510
- continue;
557
+ if (wasAborted) {
558
+ //TODO:是否需要发消息给移动端,通知停止生成
511
559
  }
512
- sentMediaKeys.add(key);
513
- await sendDcgchatMedia({
514
- cfg,
515
- accountId,
516
- log,
517
- mediaUrl: url,
518
- text: "",
519
- });
520
560
  }
521
- log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
522
- }
523
- const mobookFiles = extractMobookFiles(completeText)
561
+
562
+ const mobookFiles = extractMobookFiles(completeText);
524
563
  if (mobookFiles.length > 0) {
525
564
  for (let i = 0; i < mobookFiles.length; i++) {
526
- let url = mobookFiles[i] as string
565
+ let url = mobookFiles[i] as string;
527
566
  const key = getMediaKey(url);
528
567
  if (sentMediaKeys.has(key)) {
529
- log(`dcgchat[${accountId}]: mobookFiles already sent, skipping: ${url}`);
568
+ log(
569
+ `dcgchat[${accountId}]: mobookFiles already sent, skipping: ${url}`,
570
+ );
530
571
  continue;
531
572
  }
532
573
  if (!fs.existsSync(url)) {
533
- url = path.join(getWorkspaceDir(), url)
574
+ url = path.join(getWorkspaceDir(), url);
534
575
  if (!fs.existsSync(url)) {
535
- log(`dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`);
576
+ log(
577
+ `dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`,
578
+ );
536
579
  continue;
537
580
  }
538
581
  }
@@ -545,7 +588,9 @@ export async function handleDcgchatMessage(params: {
545
588
  text: "",
546
589
  });
547
590
  }
548
- log(`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`);
591
+ log(
592
+ `dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`,
593
+ );
549
594
  }
550
595
  log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
551
596
  params.onChunk({
@@ -560,18 +605,36 @@ export async function handleDcgchatMessage(params: {
560
605
  agent_id: msg.content.agent_id,
561
606
  session_id: msg.content.session_id,
562
607
  message_id: msg.content.message_id,
563
- response: '',
564
- state: 'final',
608
+ response: "",
609
+ state: "final",
565
610
  },
566
611
  });
567
612
 
568
- setMsgStatus('finished');
569
- textChunk = ''
613
+ setMsgStatus("finished");
614
+ textChunk = "";
570
615
  log(`dcgchat[${accountId}]: final state sent`);
571
616
 
572
617
  markDispatchIdle();
573
618
  log(`dcgchat[${accountId}]: message handling complete`);
574
619
 
620
+ // Record session metadata
621
+ const storePath = core.channel.session.resolveStorePath(cfg.session?.store);
622
+ core.channel.session
623
+ .recordInboundSession({
624
+ storePath,
625
+ sessionKey: effectiveSessionKey,
626
+ ctx: ctxPayload,
627
+ onRecordError: (err) => {
628
+ log(
629
+ `easyclawapp[${account.accountId}]: session record error: ${String(err)}`,
630
+ );
631
+ },
632
+ })
633
+ .catch((err: unknown) => {
634
+ log(
635
+ `easyclawapp[${account.accountId}]: recordInboundSession failed: ${String(err)}`,
636
+ );
637
+ });
575
638
  } catch (err) {
576
639
  error(`dcgchat[${accountId}]: handle message failed: ${String(err)}`);
577
640
  params.onChunk({
@@ -587,7 +650,7 @@ export async function handleDcgchatMessage(params: {
587
650
  session_id: msg.content.session_id,
588
651
  message_id: msg.content.message_id,
589
652
  response: `[错误] ${err instanceof Error ? err.message : String(err)}`,
590
- state: 'final',
653
+ state: "final",
591
654
  },
592
655
  });
593
656
  }
package/src/channel.ts CHANGED
@@ -185,47 +185,47 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
185
185
  // textChunkLimit: 25,
186
186
  textChunkLimit: 4000,
187
187
  sendText: async (ctx) => {
188
- // const ws = getWsConnection()
188
+ const ws = getWsConnection()
189
189
  const params = getMsgParams();
190
190
  const log = console.log;
191
- // if (ws?.readyState === WebSocket.OPEN) {
192
- // const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
193
- // const content = {
194
- // messageType: "openclaw_bot_chat",
195
- // _userId: params.userId,
196
- // source: "client",
197
- // content: {
198
- // bot_token: botToken,
199
- // domain_id: params.domainId,
200
- // app_id: params.appId,
201
- // bot_id: params.botId,
202
- // agent_id: params.agentId,
203
- // response: ctx.text,
204
- // session_id: params.sessionId,
205
- // message_id: params.messageId || Date.now().toString(),
206
- // },
207
- // };
208
- // ws.send(JSON.stringify(content));
209
- // ws.send(JSON.stringify({
210
- // messageType: "openclaw_bot_chat",
211
- // _userId: params.userId,
212
- // source: "client",
213
- // content: {
214
- // bot_token: botToken,
215
- // domain_id: params.domainId,
216
- // app_id: params.appId,
217
- // bot_id: params.botId,
218
- // agent_id: params.agentId,
219
- // ssession_id: params.sessionId,
220
- // message_id: params.messageId || Date.now().toString(),
221
- // response: '',
222
- // state: 'final',
223
- // },
224
- // }));
225
- // log(`dcgchat[${ctx.accountId}]: channel sendText to ${params.userId}, ${JSON.stringify(content)}`);
226
- // } else {
191
+ if (ws?.readyState === WebSocket.OPEN) {
192
+ const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
193
+ const content = {
194
+ messageType: "openclaw_bot_chat",
195
+ _userId: params.userId,
196
+ source: "client",
197
+ content: {
198
+ bot_token: botToken,
199
+ domain_id: params.domainId,
200
+ app_id: params.appId,
201
+ bot_id: params.botId,
202
+ agent_id: params.agentId,
203
+ response: ctx.text,
204
+ session_id: params.sessionId,
205
+ message_id: params.messageId || Date.now().toString(),
206
+ },
207
+ };
208
+ ws.send(JSON.stringify(content));
209
+ ws.send(JSON.stringify({
210
+ messageType: "openclaw_bot_chat",
211
+ _userId: params.userId,
212
+ source: "client",
213
+ content: {
214
+ bot_token: botToken,
215
+ domain_id: params.domainId,
216
+ app_id: params.appId,
217
+ bot_id: params.botId,
218
+ agent_id: params.agentId,
219
+ ssession_id: params.sessionId,
220
+ message_id: params.messageId || Date.now().toString(),
221
+ response: '',
222
+ state: 'final',
223
+ },
224
+ }));
225
+ log(`dcgchat[${ctx.accountId}]: channel sendText to ${params.userId}, ${JSON.stringify(content)}`);
226
+ } else {
227
227
  log(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> : ${ctx.text}`);
228
- // }
228
+ }
229
229
  return {
230
230
  channel: "dcgchat-test",
231
231
  messageId: `dcg-${Date.now()}`,
package/src/monitor.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import WebSocket from "ws";
3
- import { handleDcgchatMessage } from "./bot.js";
3
+ import { abortMobookappGeneration, handleDcgchatMessage } from "./bot.js";
4
4
  import { resolveAccount } from "./channel.js";
5
5
  import { setWsConnection } from "./connection.js";
6
6
  import type { InboundMessage } from "./types.js";
7
- import { setMsgParams,setMsgStatus } from "./tool.js";
7
+ import { setMsgParams, setMsgStatus } from "./tool.js";
8
8
  import { installSkill, uninstallSkill } from "./skill.js";
9
9
 
10
10
  export type MonitorDcgchatOpts = {
@@ -16,7 +16,13 @@ export type MonitorDcgchatOpts = {
16
16
 
17
17
  const RECONNECT_DELAY_MS = 3000;
18
18
  const HEARTBEAT_INTERVAL_MS = 30_000;
19
- const emptyToolText = ['/new', '/search','/stop', '/abort', '/queue interrupt']
19
+ const emptyToolText = [
20
+ "/new",
21
+ "/search",
22
+ "/stop",
23
+ "/abort",
24
+ "/queue interrupt",
25
+ ];
20
26
 
21
27
  function buildConnectUrl(account: Record<string, string>): string {
22
28
  const { wsUrl, botToken, userId, domainId, appId } = account;
@@ -28,7 +34,9 @@ function buildConnectUrl(account: Record<string, string>): string {
28
34
  return url.toString();
29
35
  }
30
36
 
31
- export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
37
+ export async function monitorDcgchatProvider(
38
+ opts: MonitorDcgchatOpts,
39
+ ): Promise<void> {
32
40
  const { config, runtime, abortSignal, accountId } = opts;
33
41
  // @ts-ignore
34
42
  const cfg = config ?? (runtime?.config?.() as ClawdbotConfig | undefined);
@@ -100,7 +108,9 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
100
108
  }
101
109
 
102
110
  if (parsed.messageType === "openclaw_bot_heartbeat") {
103
- log(`dcgchat[${account.accountId}]: heartbeat ack received, ${data.toString()}`);
111
+ log(
112
+ `dcgchat[${account.accountId}]: heartbeat ack received, ${data.toString()}`,
113
+ );
104
114
  return;
105
115
  }
106
116
  try {
@@ -109,25 +119,36 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
109
119
  err(`dcgchat[${account.accountId}]: invalid JSON received`);
110
120
  return;
111
121
  }
112
-
122
+
113
123
  if (parsed.messageType == "openclaw_bot_chat") {
114
124
  const msg = parsed as unknown as InboundMessage;
115
125
  if (!emptyToolText.includes(msg.content.text?.trim())) {
116
- setMsgStatus('running');
126
+ setMsgStatus("running");
127
+ }
128
+ log(
129
+ `dcgchat[${account.accountId}]: openclaw_bot_chat received, ${JSON.stringify(msg)}`,
130
+ );
131
+ if (msg.content.text === "/stop") {
132
+ const rawConvId = msg.content.session_id as string | undefined;
133
+ const conversationId =
134
+ rawConvId || `${accountId}:${account.botToken}`;
135
+ console.log("🚀 ~ connect ~ conversationId:", conversationId)
136
+ abortMobookappGeneration(conversationId);
137
+ log(`[dcgchat][in] abort conversationId=${conversationId}`);
138
+ return;
117
139
  }
118
- log(`dcgchat[${account.accountId}]: openclaw_bot_chat received, ${JSON.stringify(msg)}`);
119
140
  setMsgParams({
120
141
  userId: msg._userId,
121
142
  token: msg.content.bot_token,
122
143
  sessionId: msg.content.session_id,
123
144
  messageId: msg.content.message_id,
124
- domainId: account.domainId || 1000,
125
- appId: account.appId || '100',
145
+ domainId: account.domainId || 1000,
146
+ appId: account.appId || "100",
126
147
  botId: msg.content.bot_id,
127
148
  agentId: msg.content.agent_id,
128
149
  });
129
- msg.content.app_id = account.appId || '100';
130
- msg.content.domain_id = account.domainId || '1000';
150
+ msg.content.app_id = account.appId || "100";
151
+ msg.content.domain_id = account.domainId || "1000";
131
152
  await handleDcgchatMessage({
132
153
  cfg,
133
154
  msg,
@@ -140,24 +161,52 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
140
161
  }
141
162
  },
142
163
  });
143
- } else if (parsed.messageType == "openclaw_bot_event") {
144
- const { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content ? parsed.content : {} as Record<string, any>;
145
- const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id };
164
+ } else if (parsed.messageType == "openclaw_bot_event") {
165
+ const {
166
+ event_type,
167
+ operation_type,
168
+ skill_url,
169
+ skill_code,
170
+ skill_id,
171
+ bot_token,
172
+ websocket_trace_id,
173
+ } = parsed.content ? parsed.content : ({} as Record<string, any>);
174
+ const content = {
175
+ event_type,
176
+ operation_type,
177
+ skill_url,
178
+ skill_code,
179
+ skill_id,
180
+ bot_token,
181
+ websocket_trace_id,
182
+ };
146
183
  if (event_type === "skill") {
147
- if (operation_type === "install" || operation_type === "enable" || operation_type === "update") {
184
+ if (
185
+ operation_type === "install" ||
186
+ operation_type === "enable" ||
187
+ operation_type === "update"
188
+ ) {
148
189
  installSkill({ path: skill_url, code: skill_code }, content);
149
- } else if (operation_type === "remove" || operation_type === "disable") {
190
+ } else if (
191
+ operation_type === "remove" ||
192
+ operation_type === "disable"
193
+ ) {
150
194
  uninstallSkill({ code: skill_code }, content);
151
195
  } else {
152
- log(`dcgchat[${account.accountId}]: openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`);
196
+ log(
197
+ `dcgchat[${account.accountId}]: openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`,
198
+ );
153
199
  }
154
200
  } else {
155
- log(`dcgchat[${account.accountId}]: openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`);
201
+ log(
202
+ `dcgchat[${account.accountId}]: openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`,
203
+ );
156
204
  }
157
205
  } else {
158
- log(`dcgchat[${account.accountId}]: ignoring unknown messageType: ${parsed.messageType}`);
206
+ log(
207
+ `dcgchat[${account.accountId}]: ignoring unknown messageType: ${parsed.messageType}`,
208
+ );
159
209
  }
160
-
161
210
  });
162
211
 
163
212
  ws.on("close", (code, reason) => {
@@ -167,7 +216,9 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
167
216
  `dcgchat[${account.accountId}]: disconnected (code=${code}, reason=${reason?.toString() || ""})`,
168
217
  );
169
218
  if (shouldReconnect) {
170
- log(`dcgchat[${account.accountId}]: reconnecting in ${RECONNECT_DELAY_MS}ms...`);
219
+ log(
220
+ `dcgchat[${account.accountId}]: reconnecting in ${RECONNECT_DELAY_MS}ms...`,
221
+ );
171
222
  setTimeout(connect, RECONNECT_DELAY_MS);
172
223
  }
173
224
  });
package/src/tool.ts CHANGED
@@ -32,19 +32,20 @@ let toolCallId = '';
32
32
  let toolName = '';
33
33
  type PluginHookName = "before_model_resolve" | "before_prompt_build" | "before_agent_start" | "llm_input" | "llm_output" | "agent_end" | "before_compaction" | "after_compaction" | "before_reset" | "message_received" | "message_sending" | "message_sent" | "before_tool_call" | "after_tool_call" | "tool_result_persist" | "before_message_write" | "session_start" | "session_end" | "subagent_spawning" | "subagent_delivery_target" | "subagent_spawned" | "subagent_ended" | "gateway_start" | "gateway_stop";
34
34
  const eventList = [
35
- {event: 'message_received', message: '书灵墨宝已就位,收到你的指令,正在解析...'},
35
+ {event: 'message_received', message: ''},
36
+ {event: 'before_model_resolve', message: ''},
36
37
  // {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
37
38
  // {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
38
- {event: 'subagent_spawning', message: '任务较复杂,正在召唤专项助手协作...'},
39
- {event: 'subagent_spawned', message: '专项助手已加入,准备并行处理...'},
40
- {event: 'subagent_delivery_target', message: '正在将子任务分发给对应的助手...'},
41
- {event: 'llm_input', message: '正在进行深度推理与思考'},
42
- {event: 'llm_output', message: '模型思考完毕,正在整合最终答案...'},
43
- {event: 'agent_end', message: '核心任务已处理完毕...'},
44
- {event: 'subagent_ended', message: '专项助手任务结束,已安全退出。'},
39
+ {event: 'subagent_spawning', message: ''},
40
+ {event: 'subagent_spawned', message: ''},
41
+ {event: 'subagent_delivery_target', message: ''},
42
+ {event: 'llm_input', message: ''},
43
+ {event: 'llm_output', message: ''},
44
+ // {event: 'agent_end', message: '核心任务已处理完毕...'},
45
+ {event: 'subagent_ended', message: ''},
45
46
  // {event: 'before_message_write', message: '正在将本次对话存入记忆库...'},
46
- {event: 'message_sending', message: ''},
47
- {event: 'message_send', message: ''},
47
+ // {event: 'message_sending', message: ''},
48
+ // {event: 'message_send', message: ''},
48
49
  {event: 'before_tool_call', message: ''},
49
50
  {event: 'after_tool_call', message: ''},
50
51
  ];