@dcrays/dcgchat 0.2.10 → 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 +1 -1
- package/src/bot.ts +286 -57
- package/src/channel.ts +89 -71
- package/src/monitor.ts +1 -1
- package/src/skill.ts +92 -3
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import os from "node:os";
|
|
3
3
|
import type { ClawdbotConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
4
|
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
5
5
|
import type { InboundMessage, OutboundReply } from "./types.js";
|
|
6
|
-
import { getDcgchatRuntime } from "./runtime.js";
|
|
7
|
-
import { resolveAccount } from "./channel.js";
|
|
6
|
+
import { getDcgchatRuntime, getWorkspaceDir } from "./runtime.js";
|
|
7
|
+
import { resolveAccount, sendDcgchatMedia } from "./channel.js";
|
|
8
8
|
import { setMsgStatus } from "./tool.js";
|
|
9
9
|
import { generateSignUrl } from "./api.js";
|
|
10
|
-
import { ossUpload } from "./oss.js";
|
|
11
10
|
|
|
12
11
|
type MediaInfo = {
|
|
13
12
|
path: string;
|
|
13
|
+
fileName: string;
|
|
14
14
|
contentType: string;
|
|
15
15
|
placeholder: string;
|
|
16
16
|
};
|
|
@@ -53,6 +53,7 @@ async function resolveMediaFromUrls(files: { name: string, url: string }[], botT
|
|
|
53
53
|
const isImage = contentType.startsWith("image/");
|
|
54
54
|
out.push({
|
|
55
55
|
path: saved.path,
|
|
56
|
+
fileName,
|
|
56
57
|
contentType: saved.contentType || "",
|
|
57
58
|
placeholder: isImage ? "<media:image>" : "<media:file>",
|
|
58
59
|
});
|
|
@@ -68,26 +69,208 @@ async function resolveMediaFromUrls(files: { name: string, url: string }[], botT
|
|
|
68
69
|
|
|
69
70
|
function buildMediaPayload(mediaList: MediaInfo[]): {
|
|
70
71
|
MediaPath?: string;
|
|
72
|
+
MediaFileName?: string;
|
|
71
73
|
MediaType?: string;
|
|
72
74
|
MediaUrl?: string;
|
|
75
|
+
MediaFileNames?: string[];
|
|
73
76
|
MediaPaths?: string[];
|
|
74
77
|
MediaUrls?: string[];
|
|
75
78
|
MediaTypes?: string[];
|
|
76
79
|
} {
|
|
77
80
|
if (mediaList.length === 0) return {};
|
|
78
81
|
const first = mediaList[0];
|
|
82
|
+
const mediaFileNames = mediaList.map((m) => m.fileName).filter(Boolean);
|
|
79
83
|
const mediaPaths = mediaList.map((m) => m.path);
|
|
80
84
|
const mediaTypes = mediaList.map((m) => m.contentType).filter(Boolean);
|
|
81
85
|
return {
|
|
82
86
|
MediaPath: first?.path,
|
|
87
|
+
MediaFileName: first?.fileName,
|
|
83
88
|
MediaType: first?.contentType,
|
|
84
89
|
MediaUrl: first?.path,
|
|
90
|
+
MediaFileNames: mediaFileNames.length > 0 ? mediaFileNames : undefined,
|
|
85
91
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
86
92
|
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
87
93
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
98
|
+
if (payload.mediaUrls?.length) return payload.mediaUrls.filter(Boolean);
|
|
99
|
+
return payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
100
|
+
}
|
|
101
|
+
|
|
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
|
+
|
|
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
|
+
|
|
91
274
|
/**
|
|
92
275
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
93
276
|
*/
|
|
@@ -101,6 +284,8 @@ export async function handleDcgchatMessage(params: {
|
|
|
101
284
|
const { cfg, msg, accountId, runtime } = params;
|
|
102
285
|
const log = runtime?.log ?? console.log;
|
|
103
286
|
const error = runtime?.error ?? console.error;
|
|
287
|
+
// 完整的文本
|
|
288
|
+
let completeText = ''
|
|
104
289
|
|
|
105
290
|
const account = resolveAccount(cfg, accountId);
|
|
106
291
|
const userId = msg._userId.toString();
|
|
@@ -181,6 +366,8 @@ export async function handleDcgchatMessage(params: {
|
|
|
181
366
|
|
|
182
367
|
log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
|
|
183
368
|
|
|
369
|
+
const sentMediaKeys = new Set<string>()
|
|
370
|
+
const getMediaKey = (url: string) => url.split(/[\\/]/).slice(-2).join('/')
|
|
184
371
|
let textChunk = ''
|
|
185
372
|
|
|
186
373
|
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
|
@@ -191,7 +378,7 @@ export async function handleDcgchatMessage(params: {
|
|
|
191
378
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
192
379
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
193
380
|
onReplyStart: async () => {},
|
|
194
|
-
deliver: async (payload:
|
|
381
|
+
deliver: async (payload: ReplyPayload) => {
|
|
195
382
|
log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
|
|
196
383
|
},
|
|
197
384
|
onError: (err: any, info: { kind: any; }) => {
|
|
@@ -222,64 +409,49 @@ export async function handleDcgchatMessage(params: {
|
|
|
222
409
|
onModelSelected: prefixContext.onModelSelected,
|
|
223
410
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
224
411
|
log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
? [payload.mediaUrl]
|
|
230
|
-
: [];
|
|
412
|
+
if (payload.text) {
|
|
413
|
+
completeText = payload.text
|
|
414
|
+
}
|
|
415
|
+
const mediaList = resolveReplyMediaList(payload);
|
|
231
416
|
if (mediaList.length > 0) {
|
|
232
|
-
const files = []
|
|
233
417
|
for (let i = 0; i < mediaList.length; i++) {
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
418
|
+
const mediaUrl = mediaList[i];
|
|
419
|
+
const key = getMediaKey(mediaUrl);
|
|
420
|
+
if (sentMediaKeys.has(key)) continue;
|
|
421
|
+
sentMediaKeys.add(key);
|
|
422
|
+
await sendDcgchatMedia({
|
|
423
|
+
cfg,
|
|
424
|
+
accountId,
|
|
425
|
+
log,
|
|
426
|
+
mediaUrl,
|
|
427
|
+
text: "",
|
|
428
|
+
});
|
|
241
429
|
}
|
|
242
|
-
|
|
243
|
-
messageType: "openclaw_bot_chat",
|
|
244
|
-
_userId: msg._userId,
|
|
245
|
-
source: "client",
|
|
246
|
-
content: {
|
|
247
|
-
bot_token: msg.content.bot_token,
|
|
248
|
-
domain_id: msg.content.domain_id,
|
|
249
|
-
app_id: msg.content.app_id,
|
|
250
|
-
bot_id: msg.content.bot_id,
|
|
251
|
-
agent_id: msg.content.agent_id,
|
|
252
|
-
session_id: msg.content.session_id,
|
|
253
|
-
message_id: msg.content.message_id,
|
|
254
|
-
response: '',
|
|
255
|
-
files: files,
|
|
256
|
-
state: 'chunk',
|
|
257
|
-
},
|
|
258
|
-
});
|
|
430
|
+
log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
|
|
259
431
|
}
|
|
260
432
|
if (payload.text) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
433
|
+
const nextTextChunk = payload.text.replace(textChunk, '');
|
|
434
|
+
if (nextTextChunk.trim()) {
|
|
435
|
+
log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`);
|
|
436
|
+
params.onChunk({
|
|
437
|
+
messageType: "openclaw_bot_chat",
|
|
438
|
+
_userId: msg._userId,
|
|
439
|
+
source: "client",
|
|
440
|
+
content: {
|
|
441
|
+
bot_token: msg.content.bot_token,
|
|
442
|
+
domain_id: msg.content.domain_id,
|
|
443
|
+
app_id: msg.content.app_id,
|
|
444
|
+
bot_id: msg.content.bot_id,
|
|
445
|
+
agent_id: msg.content.agent_id,
|
|
446
|
+
session_id: msg.content.session_id,
|
|
447
|
+
message_id: msg.content.message_id,
|
|
448
|
+
response: nextTextChunk,
|
|
449
|
+
state: 'chunk',
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
|
|
453
|
+
}
|
|
278
454
|
textChunk = payload.text
|
|
279
|
-
log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
|
|
280
|
-
} else if (payload.mediaUrl && payload.mediaUrls) {
|
|
281
|
-
|
|
282
|
-
|
|
283
455
|
} else {
|
|
284
456
|
log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
|
|
285
457
|
}
|
|
@@ -287,6 +459,62 @@ export async function handleDcgchatMessage(params: {
|
|
|
287
459
|
},
|
|
288
460
|
});
|
|
289
461
|
}
|
|
462
|
+
|
|
463
|
+
const extractor = createFileExtractor()
|
|
464
|
+
const completeFiles = extractor.getNewFiles(completeText)
|
|
465
|
+
if (completeFiles.length > 0) {
|
|
466
|
+
for (let i = 0; i < completeFiles.length; i++) {
|
|
467
|
+
let url = completeFiles[i] as string
|
|
468
|
+
if (!path.isAbsolute(url)) {
|
|
469
|
+
url = path.join(getWorkspaceDir(), url)
|
|
470
|
+
}
|
|
471
|
+
const key = getMediaKey(url);
|
|
472
|
+
if (sentMediaKeys.has(key)) {
|
|
473
|
+
log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (!fs.existsSync(url)) {
|
|
477
|
+
log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
sentMediaKeys.add(key);
|
|
481
|
+
await sendDcgchatMedia({
|
|
482
|
+
cfg,
|
|
483
|
+
accountId,
|
|
484
|
+
log,
|
|
485
|
+
mediaUrl: url,
|
|
486
|
+
text: "",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
|
|
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
|
+
}
|
|
290
518
|
log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
|
|
291
519
|
params.onChunk({
|
|
292
520
|
messageType: "openclaw_bot_chat",
|
|
@@ -304,6 +532,7 @@ export async function handleDcgchatMessage(params: {
|
|
|
304
532
|
state: 'final',
|
|
305
533
|
},
|
|
306
534
|
});
|
|
535
|
+
|
|
307
536
|
setMsgStatus('finished');
|
|
308
537
|
textChunk = ''
|
|
309
538
|
log(`dcgchat[${accountId}]: final state sent`);
|
package/src/channel.ts
CHANGED
|
@@ -5,6 +5,93 @@ import { getWsConnection } from "./connection.js";
|
|
|
5
5
|
import { ossUpload } from "./oss.js";
|
|
6
6
|
import { getMsgParams } from "./tool.js";
|
|
7
7
|
|
|
8
|
+
type DcgchatMediaSendContext = {
|
|
9
|
+
cfg: OpenClawConfig;
|
|
10
|
+
accountId?: string | null;
|
|
11
|
+
log?: (message: string) => void;
|
|
12
|
+
mediaUrl?: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function sendDcgchatMedia(ctx: DcgchatMediaSendContext): Promise<void> {
|
|
17
|
+
const ws = getWsConnection();
|
|
18
|
+
const params = getMsgParams();
|
|
19
|
+
const log = ctx.log ?? console.log;
|
|
20
|
+
|
|
21
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
22
|
+
log(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound media skipped -> ${ws?.readyState}: ${ctx.mediaUrl ?? ""}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fileName = ctx.mediaUrl?.split(/[\\/]/).pop() || "";
|
|
27
|
+
const { botToken } = resolveAccount(ctx.cfg, ctx.accountId);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const url = ctx.mediaUrl ? await ossUpload(ctx.mediaUrl, botToken) : "";
|
|
31
|
+
console.log("🚀 ~ sendDcgchatMedia ~ ctx.mediaUrl:", ctx.mediaUrl)
|
|
32
|
+
const content = {
|
|
33
|
+
messageType: "openclaw_bot_chat",
|
|
34
|
+
_userId: params.userId,
|
|
35
|
+
source: "client",
|
|
36
|
+
content: {
|
|
37
|
+
bot_token: botToken,
|
|
38
|
+
domain_id: params.domainId,
|
|
39
|
+
app_id: params.appId,
|
|
40
|
+
bot_id: params.botId,
|
|
41
|
+
agent_id: params.agentId,
|
|
42
|
+
response: ctx.text ?? "",
|
|
43
|
+
files: [{
|
|
44
|
+
url,
|
|
45
|
+
name: fileName,
|
|
46
|
+
}],
|
|
47
|
+
session_id: params.sessionId,
|
|
48
|
+
message_id: params.messageId || Date.now().toString(),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
ws.send(JSON.stringify(content));
|
|
52
|
+
log(`dcgchat[${ctx.accountId}]: sendMedia alioss to ${params.userId}, ${JSON.stringify(content)}`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const content = {
|
|
55
|
+
messageType: "openclaw_bot_chat",
|
|
56
|
+
_userId: params.userId,
|
|
57
|
+
source: "client",
|
|
58
|
+
content: {
|
|
59
|
+
bot_token: botToken,
|
|
60
|
+
domain_id: params.domainId,
|
|
61
|
+
app_id: params.appId,
|
|
62
|
+
bot_id: params.botId,
|
|
63
|
+
agent_id: params.agentId,
|
|
64
|
+
response: ctx.text ?? "",
|
|
65
|
+
files: [{
|
|
66
|
+
url: ctx.mediaUrl,
|
|
67
|
+
name: fileName,
|
|
68
|
+
}],
|
|
69
|
+
session_id: params.sessionId || Date.now().toString(),
|
|
70
|
+
message_id: Date.now().toString(),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
ws.send(JSON.stringify(content));
|
|
74
|
+
log(`dcgchat[${ctx.accountId}]: error sendMedia to ${params.userId}, ${JSON.stringify(content)}`);
|
|
75
|
+
} finally {
|
|
76
|
+
ws.send(JSON.stringify({
|
|
77
|
+
messageType: "openclaw_bot_chat",
|
|
78
|
+
_userId: params.userId,
|
|
79
|
+
source: "client",
|
|
80
|
+
content: {
|
|
81
|
+
bot_token: botToken,
|
|
82
|
+
domain_id: params.domainId,
|
|
83
|
+
app_id: params.appId,
|
|
84
|
+
bot_id: params.botId,
|
|
85
|
+
agent_id: params.agentId,
|
|
86
|
+
ssession_id: params.sessionId,
|
|
87
|
+
message_id: Date.now().toString(),
|
|
88
|
+
response: "",
|
|
89
|
+
state: "final",
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
8
95
|
|
|
9
96
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
10
97
|
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -100,7 +187,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
100
187
|
sendText: async (ctx) => {
|
|
101
188
|
const ws = getWsConnection()
|
|
102
189
|
const params = getMsgParams();
|
|
103
|
-
const log =
|
|
190
|
+
const log = console.log;
|
|
104
191
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
105
192
|
const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
|
|
106
193
|
const content = {
|
|
@@ -146,77 +233,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
146
233
|
};
|
|
147
234
|
},
|
|
148
235
|
sendMedia: async (ctx) => {
|
|
149
|
-
const ws = getWsConnection()
|
|
150
236
|
const params = getMsgParams();
|
|
151
|
-
|
|
152
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
153
|
-
const fileName = ctx.mediaUrl?.split(/[\\/]/).pop() || ''
|
|
154
|
-
const {botToken} = resolveAccount(ctx.cfg, ctx.accountId);
|
|
155
|
-
try {
|
|
156
|
-
const url = ctx.mediaUrl ? await ossUpload(ctx.mediaUrl, botToken) : '';
|
|
157
|
-
const content = {
|
|
158
|
-
messageType: "openclaw_bot_chat",
|
|
159
|
-
_userId: params.userId,
|
|
160
|
-
source: "client",
|
|
161
|
-
content: {
|
|
162
|
-
bot_token: botToken,
|
|
163
|
-
domain_id: params.domainId,
|
|
164
|
-
app_id: params.appId,
|
|
165
|
-
bot_id: params.botId,
|
|
166
|
-
agent_id: params.agentId,
|
|
167
|
-
response: ctx.text,
|
|
168
|
-
files: [{
|
|
169
|
-
url: url,
|
|
170
|
-
name: fileName,
|
|
171
|
-
}],
|
|
172
|
-
session_id: params.sessionId,
|
|
173
|
-
message_id: params.messageId ||Date.now().toString(),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
ws.send(JSON.stringify(content));
|
|
177
|
-
log(`dcgchat[${ctx.accountId}]: sendMedia alioss to ${params.userId}, ${JSON.stringify(content)}`);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
const content = {
|
|
180
|
-
messageType: "openclaw_bot_chat",
|
|
181
|
-
_userId: params.userId,
|
|
182
|
-
source: "client",
|
|
183
|
-
content: {
|
|
184
|
-
bot_token: botToken,
|
|
185
|
-
domain_id: params.domainId,
|
|
186
|
-
app_id: params.appId,
|
|
187
|
-
bot_id: params.botId,
|
|
188
|
-
agent_id: params.agentId,
|
|
189
|
-
response: ctx.text,
|
|
190
|
-
files: [{
|
|
191
|
-
url: ctx.mediaUrl,
|
|
192
|
-
name: fileName,
|
|
193
|
-
}],
|
|
194
|
-
session_id: params.sessionId || Date.now().toString(),
|
|
195
|
-
message_id: Date.now().toString(),
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
ws.send(JSON.stringify(content));
|
|
199
|
-
ws.send(JSON.stringify({
|
|
200
|
-
messageType: "openclaw_bot_chat",
|
|
201
|
-
_userId: params.userId,
|
|
202
|
-
source: "client",
|
|
203
|
-
content: {
|
|
204
|
-
bot_token: botToken,
|
|
205
|
-
domain_id: params.domainId,
|
|
206
|
-
app_id: params.appId,
|
|
207
|
-
bot_id: params.botId,
|
|
208
|
-
agent_id: params.agentId,
|
|
209
|
-
ssession_id: params.sessionId,
|
|
210
|
-
message_id: Date.now().toString(),
|
|
211
|
-
response: '',
|
|
212
|
-
state: 'final',
|
|
213
|
-
},
|
|
214
|
-
}));
|
|
215
|
-
log(`dcgchat[${ctx.accountId}]: error sendMedia to ${params.userId}, ${JSON.stringify(content)}`);
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
log(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> ${ws?.readyState}: ${ctx.text}`);
|
|
219
|
-
}
|
|
237
|
+
await sendDcgchatMedia(ctx);
|
|
220
238
|
return {
|
|
221
239
|
channel: "dcgchat",
|
|
222
240
|
messageId: `dcg-${Date.now()}`,
|
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)) {
|