@dcrays/dcgchat 0.2.11 → 0.2.15

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",
3
- "version": "0.2.11",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -152,6 +152,125 @@ function createFileExtractor() {
152
152
  return { getNewFiles }
153
153
  }
154
154
 
155
+ /**
156
+ * 从文本中提取 /mobook 目录下的文件
157
+ * @param {string} text
158
+ * @returns {string[]}
159
+ */
160
+ const EXT_LIST = [
161
+ // 文档类
162
+ 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'rtf', 'odt',
163
+
164
+ // 数据/开发
165
+ 'json', 'xml', 'csv', 'yaml', 'yml',
166
+
167
+ // 前端/文本
168
+ 'html', 'htm', 'md', 'markdown', 'css', 'js', 'ts',
169
+
170
+ // 图片
171
+ 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff',
172
+
173
+ // 音频
174
+ 'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a',
175
+
176
+ // 视频
177
+ 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm',
178
+
179
+ // 压缩包
180
+ 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
181
+
182
+ // 可执行/程序
183
+ 'exe', 'dmg', 'pkg', 'apk', 'ipa',
184
+
185
+ // 其他常见
186
+ 'log', 'dat', 'bin'
187
+ ];
188
+
189
+ function extractMobookFiles(text = '') {
190
+ if (typeof text !== 'string' || !text.trim()) return [];
191
+
192
+ const result = new Set();
193
+
194
+ // ✅ 扩展名
195
+ const EXT = `(${EXT_LIST.join('|')})`;
196
+
197
+ // ✅ 文件名字符(增强:支持中文、符号)
198
+ const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`;
199
+
200
+ try {
201
+ // 1️⃣ `xxx.xxx`
202
+ const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi');
203
+ (text.match(backtickReg) || []).forEach(item => {
204
+ const name = item.replace(/`/g, '').trim();
205
+ if (isValidFileName(name)) {
206
+ result.add(`/mobook/${name}`);
207
+ }
208
+ });
209
+
210
+ // 2️⃣ /mobook/xxx.xxx
211
+ const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi');
212
+ (text.match(fullPathReg) || []).forEach(p => {
213
+ result.add(normalizePath(p));
214
+ });
215
+
216
+ // 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'));
220
+ if (match && isValidFileName(match[0])) {
221
+ result.add(`/mobook/${match[0].trim()}`);
222
+ }
223
+ });
224
+
225
+ // 🆕 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();
229
+ if (isValidFileName(name)) {
230
+ result.add(`/mobook/${name}`);
231
+ }
232
+ });
233
+
234
+ // 🆕 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();
238
+ if (isValidFileName(name)) {
239
+ result.add(`/mobook/${name}`);
240
+ }
241
+ });
242
+
243
+ } catch (e) {
244
+ console.warn('extractMobookFiles error:', e);
245
+ }
246
+
247
+ return [...result];
248
+ }
249
+
250
+ /**
251
+ * 校验文件名是否合法(避免脏数据)
252
+ */
253
+ function isValidFileName(name: string) {
254
+ if (!name) return false;
255
+
256
+ // 过滤异常字符
257
+ if (/[\/\\<>:"|?*]/.test(name)) return false;
258
+
259
+ // 长度限制(防止异常长字符串)
260
+ if (name.length > 200) return false;
261
+
262
+ return true;
263
+ }
264
+
265
+ /**
266
+ * 规范路径(去重用)
267
+ */
268
+ function normalizePath(path: string) {
269
+ return path
270
+ .replace(/\/+/g, '/') // 多斜杠 → 单斜杠
271
+ .replace(/\/$/, ''); // 去掉结尾 /
272
+ }
273
+
155
274
  /**
156
275
  * 处理一条用户消息,调用 Agent 并返回回复
157
276
  */
@@ -247,6 +366,8 @@ export async function handleDcgchatMessage(params: {
247
366
 
248
367
  log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
249
368
 
369
+ const sentMediaKeys = new Set<string>()
370
+ const getMediaKey = (url: string) => url.split(/[\\/]/).slice(-2).join('/')
250
371
  let textChunk = ''
251
372
 
252
373
  const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
@@ -294,11 +415,15 @@ export async function handleDcgchatMessage(params: {
294
415
  const mediaList = resolveReplyMediaList(payload);
295
416
  if (mediaList.length > 0) {
296
417
  for (let i = 0; i < mediaList.length; i++) {
418
+ const mediaUrl = mediaList[i];
419
+ const key = getMediaKey(mediaUrl);
420
+ if (sentMediaKeys.has(key)) continue;
421
+ sentMediaKeys.add(key);
297
422
  await sendDcgchatMedia({
298
423
  cfg,
299
424
  accountId,
300
425
  log,
301
- mediaUrl: mediaList[i],
426
+ mediaUrl,
302
427
  text: "",
303
428
  });
304
429
  }
@@ -334,23 +459,7 @@ export async function handleDcgchatMessage(params: {
334
459
  },
335
460
  });
336
461
  }
337
- log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
338
- params.onChunk({
339
- messageType: "openclaw_bot_chat",
340
- _userId: msg._userId,
341
- source: "client",
342
- content: {
343
- bot_token: msg.content.bot_token,
344
- domain_id: msg.content.domain_id,
345
- app_id: msg.content.app_id,
346
- bot_id: msg.content.bot_id,
347
- agent_id: msg.content.agent_id,
348
- session_id: msg.content.session_id,
349
- message_id: msg.content.message_id,
350
- response: '',
351
- state: 'final',
352
- },
353
- });
462
+
354
463
  const extractor = createFileExtractor()
355
464
  const completeFiles = extractor.getNewFiles(completeText)
356
465
  if (completeFiles.length > 0) {
@@ -359,10 +468,16 @@ export async function handleDcgchatMessage(params: {
359
468
  if (!path.isAbsolute(url)) {
360
469
  url = path.join(getWorkspaceDir(), url)
361
470
  }
471
+ const key = getMediaKey(url);
472
+ if (sentMediaKeys.has(key)) {
473
+ log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
474
+ continue;
475
+ }
362
476
  if (!fs.existsSync(url)) {
363
- log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
477
+ log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
364
478
  continue;
365
479
  }
480
+ sentMediaKeys.add(key);
366
481
  await sendDcgchatMedia({
367
482
  cfg,
368
483
  accountId,
@@ -373,6 +488,50 @@ export async function handleDcgchatMessage(params: {
373
488
  }
374
489
  log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
375
490
  }
491
+ const mobookFiles = extractMobookFiles(completeText)
492
+ if (mobookFiles.length > 0) {
493
+ for (let i = 0; i < mobookFiles.length; i++) {
494
+ let url = mobookFiles[i] as string
495
+ const key = getMediaKey(url);
496
+ if (sentMediaKeys.has(key)) {
497
+ log(`dcgchat[${accountId}]: mobookFiles already sent, skipping: ${url}`);
498
+ continue;
499
+ }
500
+ if (!fs.existsSync(url)) {
501
+ url = path.join(getWorkspaceDir(), url)
502
+ if (!fs.existsSync(url)) {
503
+ log(`dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`);
504
+ continue;
505
+ }
506
+ }
507
+ sentMediaKeys.add(key);
508
+ await sendDcgchatMedia({
509
+ cfg,
510
+ accountId,
511
+ log,
512
+ mediaUrl: url,
513
+ text: "",
514
+ });
515
+ }
516
+ log(`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`);
517
+ }
518
+ log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
519
+ params.onChunk({
520
+ messageType: "openclaw_bot_chat",
521
+ _userId: msg._userId,
522
+ source: "client",
523
+ content: {
524
+ bot_token: msg.content.bot_token,
525
+ domain_id: msg.content.domain_id,
526
+ app_id: msg.content.app_id,
527
+ bot_id: msg.content.bot_id,
528
+ agent_id: msg.content.agent_id,
529
+ session_id: msg.content.session_id,
530
+ message_id: msg.content.message_id,
531
+ response: '',
532
+ state: 'final',
533
+ },
534
+ });
376
535
 
377
536
  setMsgStatus('finished');
378
537
  textChunk = ''
package/src/monitor.ts CHANGED
@@ -140,7 +140,7 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
140
140
  const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id };
141
141
  if (event_type === "skill") {
142
142
  if (operation_type === "install" || operation_type === "enable" || operation_type === "update") {
143
- installSkill({ path: skill_url, code: skill_code }, content);
143
+ installSkill({ path: skill_url, code: skill_code }, content, { cfg, accountId: account.accountId, runtime });
144
144
  } else if (operation_type === "remove" || operation_type === "disable") {
145
145
  uninstallSkill({ code: skill_code }, content);
146
146
  } else {
package/src/skill.ts CHANGED
@@ -5,15 +5,24 @@ import unzipper from 'unzipper';
5
5
  import { pipeline } from "stream/promises";
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
8
9
  import { logDcgchat } from './log.js';
9
- import { getWorkspaceDir } from './runtime.js';
10
+ import { getDcgchatRuntime, getWorkspaceDir } from './runtime.js';
10
11
  import { getWsConnection } from './connection.js';
12
+ import { resolveAccount } from './channel.js';
13
+ import { getMsgParams } from './tool.js';
11
14
 
12
15
  type ISkillParams = {
13
16
  path: string;
14
17
  code: string;
15
18
  }
16
19
 
20
+ type SkillContext = {
21
+ cfg: ClawdbotConfig;
22
+ accountId: string;
23
+ runtime?: RuntimeEnv;
24
+ }
25
+
17
26
  function sendEvent(msgContent: Record<string, any>) {
18
27
  const ws = getWsConnection()
19
28
  if (ws?.readyState === WebSocket.OPEN) {
@@ -27,7 +36,77 @@ function sendEvent(msgContent: Record<string, any>) {
27
36
  }
28
37
  }
29
38
 
30
- export async function installSkill(params: ISkillParams, msgContent: Record<string, any>) {
39
+ async function sendNewSessionCommand(ctx: SkillContext) {
40
+ try {
41
+ const core = getDcgchatRuntime();
42
+ const log = ctx.runtime?.log ?? console.log;
43
+ const params = getMsgParams();
44
+ const account = resolveAccount(ctx.cfg, ctx.accountId);
45
+ const userId = String(params.userId);
46
+
47
+ const route = core.channel.routing.resolveAgentRoute({
48
+ cfg: ctx.cfg,
49
+ channel: "dcgchat",
50
+ accountId: account.accountId,
51
+ peer: { kind: "direct", id: userId },
52
+ });
53
+
54
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(ctx.cfg);
55
+ const bodyFormatted = core.channel.reply.formatAgentEnvelope({
56
+ channel: "书灵墨宝",
57
+ from: userId,
58
+ timestamp: new Date(),
59
+ envelope: envelopeOptions,
60
+ body: "/new",
61
+ });
62
+
63
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
64
+ Body: bodyFormatted,
65
+ RawBody: "/new",
66
+ CommandBody: "/new",
67
+ From: userId,
68
+ To: userId,
69
+ SessionKey: route.sessionKey,
70
+ AccountId: params.sessionId,
71
+ ChatType: "direct",
72
+ SenderName: userId,
73
+ SenderId: userId,
74
+ Provider: "dcgchat" as const,
75
+ Surface: "dcgchat" as const,
76
+ MessageSid: Date.now().toString(),
77
+ Timestamp: Date.now(),
78
+ WasMentioned: true,
79
+ CommandAuthorized: true,
80
+ OriginatingChannel: "dcgchat" as const,
81
+ OriginatingTo: `user:${userId}`,
82
+ });
83
+
84
+ const noopDispatcher = {
85
+ sendToolResult: () => false,
86
+ sendBlockReply: () => false,
87
+ sendFinalReply: () => false,
88
+ waitForIdle: async () => {},
89
+ getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
90
+ markComplete: () => {},
91
+ };
92
+
93
+ await core.channel.reply.withReplyDispatcher({
94
+ dispatcher: noopDispatcher,
95
+ run: () =>
96
+ core.channel.reply.dispatchReplyFromConfig({
97
+ ctx: ctxPayload,
98
+ cfg: ctx.cfg,
99
+ dispatcher: noopDispatcher,
100
+ }),
101
+ });
102
+
103
+ log(`dcgchat: /new command dispatched silently after skill install`);
104
+ } catch (err) {
105
+ logDcgchat.error(`sendNewSessionCommand failed: ${err}`);
106
+ }
107
+ }
108
+
109
+ export async function installSkill(params: ISkillParams, msgContent: Record<string, any>, ctx?: SkillContext) {
31
110
  const { path: cdnUrl, code } = params;
32
111
  const workspacePath = getWorkspaceDir();
33
112
 
@@ -67,7 +146,14 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
67
146
  }
68
147
 
69
148
  try {
70
- const entryPath = entry.path;
149
+ const flags = entry.props?.flags ?? 0;
150
+ const isUtf8 = (flags & 0x800) !== 0;
151
+ let entryPath: string;
152
+ if (!isUtf8 && entry.props?.pathBuffer) {
153
+ entryPath = new TextDecoder('gbk').decode(entry.props.pathBuffer);
154
+ } else {
155
+ entryPath = entry.path;
156
+ }
71
157
  const pathParts = entryPath.split("/");
72
158
 
73
159
  // 检测根目录
@@ -122,6 +208,9 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
122
208
  });
123
209
  });
124
210
  sendEvent({ ...msgContent, status: 'ok' })
211
+ if (ctx) {
212
+ await sendNewSessionCommand(ctx);
213
+ }
125
214
  } catch (error) {
126
215
  // 如果安装失败,清理目录
127
216
  if (fs.existsSync(skillDir)) {