@dcrays/dcgchat 0.2.11 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +2 -2
- package/package.json +1 -1
- package/src/bot.ts +182 -19
- package/src/monitor.ts +1 -1
- package/src/skill.ts +92 -3
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
3
|
import { dcgchatPlugin } from "./src/channel.js";
|
|
4
4
|
import { setDcgchatRuntime, setWorkspaceDir } from "./src/runtime.js";
|
|
5
|
-
|
|
5
|
+
import { monitoringToolMessage } from "./src/tool.js";
|
|
6
6
|
|
|
7
7
|
const plugin = {
|
|
8
8
|
id: "dcgchat",
|
|
@@ -11,7 +11,7 @@ const plugin = {
|
|
|
11
11
|
configSchema: emptyPluginConfigSchema(),
|
|
12
12
|
register(api: OpenClawPluginApi) {
|
|
13
13
|
setDcgchatRuntime(api.runtime);
|
|
14
|
-
|
|
14
|
+
monitoringToolMessage(api);
|
|
15
15
|
api.registerChannel({ plugin: dcgchatPlugin });
|
|
16
16
|
api.registerTool((ctx) => {
|
|
17
17
|
const workspaceDir = ctx.workspaceDir;
|
package/package.json
CHANGED
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,19 @@ 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
|
+
if (!/^https?:\/\//i.test(mediaUrl) && !fs.existsSync(mediaUrl)) {
|
|
422
|
+
log(`dcgchat[${accountId}][deliver]: media file not found, skipping: ${mediaUrl}`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
sentMediaKeys.add(key);
|
|
297
426
|
await sendDcgchatMedia({
|
|
298
427
|
cfg,
|
|
299
428
|
accountId,
|
|
300
429
|
log,
|
|
301
|
-
mediaUrl
|
|
430
|
+
mediaUrl,
|
|
302
431
|
text: "",
|
|
303
432
|
});
|
|
304
433
|
}
|
|
@@ -334,23 +463,7 @@ export async function handleDcgchatMessage(params: {
|
|
|
334
463
|
},
|
|
335
464
|
});
|
|
336
465
|
}
|
|
337
|
-
|
|
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
|
-
});
|
|
466
|
+
|
|
354
467
|
const extractor = createFileExtractor()
|
|
355
468
|
const completeFiles = extractor.getNewFiles(completeText)
|
|
356
469
|
if (completeFiles.length > 0) {
|
|
@@ -359,10 +472,16 @@ export async function handleDcgchatMessage(params: {
|
|
|
359
472
|
if (!path.isAbsolute(url)) {
|
|
360
473
|
url = path.join(getWorkspaceDir(), url)
|
|
361
474
|
}
|
|
475
|
+
const key = getMediaKey(url);
|
|
476
|
+
if (sentMediaKeys.has(key)) {
|
|
477
|
+
log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
362
480
|
if (!fs.existsSync(url)) {
|
|
363
|
-
log(`dcgchat[${accountId}]:
|
|
481
|
+
log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
|
|
364
482
|
continue;
|
|
365
483
|
}
|
|
484
|
+
sentMediaKeys.add(key);
|
|
366
485
|
await sendDcgchatMedia({
|
|
367
486
|
cfg,
|
|
368
487
|
accountId,
|
|
@@ -373,6 +492,50 @@ export async function handleDcgchatMessage(params: {
|
|
|
373
492
|
}
|
|
374
493
|
log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
|
|
375
494
|
}
|
|
495
|
+
const mobookFiles = extractMobookFiles(completeText)
|
|
496
|
+
if (mobookFiles.length > 0) {
|
|
497
|
+
for (let i = 0; i < mobookFiles.length; i++) {
|
|
498
|
+
let url = mobookFiles[i] as string
|
|
499
|
+
const key = getMediaKey(url);
|
|
500
|
+
if (sentMediaKeys.has(key)) {
|
|
501
|
+
log(`dcgchat[${accountId}]: mobookFiles already sent, skipping: ${url}`);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!fs.existsSync(url)) {
|
|
505
|
+
url = path.join(getWorkspaceDir(), url)
|
|
506
|
+
if (!fs.existsSync(url)) {
|
|
507
|
+
log(`dcgchat[${accountId}]: mobookFiles file not found, skipping: ${url}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
sentMediaKeys.add(key);
|
|
512
|
+
await sendDcgchatMedia({
|
|
513
|
+
cfg,
|
|
514
|
+
accountId,
|
|
515
|
+
log,
|
|
516
|
+
mediaUrl: url,
|
|
517
|
+
text: "",
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
log(`dcgchat[${accountId}][deliver]: sent ${mobookFiles.length} media file(s) through channel adapter`);
|
|
521
|
+
}
|
|
522
|
+
log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
|
|
523
|
+
params.onChunk({
|
|
524
|
+
messageType: "openclaw_bot_chat",
|
|
525
|
+
_userId: msg._userId,
|
|
526
|
+
source: "client",
|
|
527
|
+
content: {
|
|
528
|
+
bot_token: msg.content.bot_token,
|
|
529
|
+
domain_id: msg.content.domain_id,
|
|
530
|
+
app_id: msg.content.app_id,
|
|
531
|
+
bot_id: msg.content.bot_id,
|
|
532
|
+
agent_id: msg.content.agent_id,
|
|
533
|
+
session_id: msg.content.session_id,
|
|
534
|
+
message_id: msg.content.message_id,
|
|
535
|
+
response: '',
|
|
536
|
+
state: 'final',
|
|
537
|
+
},
|
|
538
|
+
});
|
|
376
539
|
|
|
377
540
|
setMsgStatus('finished');
|
|
378
541
|
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
|
-
|
|
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
|
|
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)) {
|