@dcrays/dcgchat 0.2.10 → 0.2.11
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 +127 -57
- package/src/channel.ts +89 -71
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,89 @@ 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
|
+
|
|
91
155
|
/**
|
|
92
156
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
93
157
|
*/
|
|
@@ -101,6 +165,8 @@ export async function handleDcgchatMessage(params: {
|
|
|
101
165
|
const { cfg, msg, accountId, runtime } = params;
|
|
102
166
|
const log = runtime?.log ?? console.log;
|
|
103
167
|
const error = runtime?.error ?? console.error;
|
|
168
|
+
// 完整的文本
|
|
169
|
+
let completeText = ''
|
|
104
170
|
|
|
105
171
|
const account = resolveAccount(cfg, accountId);
|
|
106
172
|
const userId = msg._userId.toString();
|
|
@@ -191,7 +257,7 @@ export async function handleDcgchatMessage(params: {
|
|
|
191
257
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
192
258
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
193
259
|
onReplyStart: async () => {},
|
|
194
|
-
deliver: async (payload:
|
|
260
|
+
deliver: async (payload: ReplyPayload) => {
|
|
195
261
|
log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
|
|
196
262
|
},
|
|
197
263
|
onError: (err: any, info: { kind: any; }) => {
|
|
@@ -222,64 +288,45 @@ export async function handleDcgchatMessage(params: {
|
|
|
222
288
|
onModelSelected: prefixContext.onModelSelected,
|
|
223
289
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
224
290
|
log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
? [payload.mediaUrl]
|
|
230
|
-
: [];
|
|
291
|
+
if (payload.text) {
|
|
292
|
+
completeText = payload.text
|
|
293
|
+
}
|
|
294
|
+
const mediaList = resolveReplyMediaList(payload);
|
|
231
295
|
if (mediaList.length > 0) {
|
|
232
|
-
const files = []
|
|
233
296
|
for (let i = 0; i < mediaList.length; i++) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
})
|
|
297
|
+
await sendDcgchatMedia({
|
|
298
|
+
cfg,
|
|
299
|
+
accountId,
|
|
300
|
+
log,
|
|
301
|
+
mediaUrl: mediaList[i],
|
|
302
|
+
text: "",
|
|
303
|
+
});
|
|
241
304
|
}
|
|
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
|
-
});
|
|
305
|
+
log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
|
|
259
306
|
}
|
|
260
307
|
if (payload.text) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
const nextTextChunk = payload.text.replace(textChunk, '');
|
|
309
|
+
if (nextTextChunk.trim()) {
|
|
310
|
+
log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`);
|
|
311
|
+
params.onChunk({
|
|
312
|
+
messageType: "openclaw_bot_chat",
|
|
313
|
+
_userId: msg._userId,
|
|
314
|
+
source: "client",
|
|
315
|
+
content: {
|
|
316
|
+
bot_token: msg.content.bot_token,
|
|
317
|
+
domain_id: msg.content.domain_id,
|
|
318
|
+
app_id: msg.content.app_id,
|
|
319
|
+
bot_id: msg.content.bot_id,
|
|
320
|
+
agent_id: msg.content.agent_id,
|
|
321
|
+
session_id: msg.content.session_id,
|
|
322
|
+
message_id: msg.content.message_id,
|
|
323
|
+
response: nextTextChunk,
|
|
324
|
+
state: 'chunk',
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
|
|
328
|
+
}
|
|
278
329
|
textChunk = payload.text
|
|
279
|
-
log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
|
|
280
|
-
} else if (payload.mediaUrl && payload.mediaUrls) {
|
|
281
|
-
|
|
282
|
-
|
|
283
330
|
} else {
|
|
284
331
|
log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
|
|
285
332
|
}
|
|
@@ -304,6 +351,29 @@ export async function handleDcgchatMessage(params: {
|
|
|
304
351
|
state: 'final',
|
|
305
352
|
},
|
|
306
353
|
});
|
|
354
|
+
const extractor = createFileExtractor()
|
|
355
|
+
const completeFiles = extractor.getNewFiles(completeText)
|
|
356
|
+
if (completeFiles.length > 0) {
|
|
357
|
+
for (let i = 0; i < completeFiles.length; i++) {
|
|
358
|
+
let url = completeFiles[i] as string
|
|
359
|
+
if (!path.isAbsolute(url)) {
|
|
360
|
+
url = path.join(getWorkspaceDir(), url)
|
|
361
|
+
}
|
|
362
|
+
if (!fs.existsSync(url)) {
|
|
363
|
+
log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
await sendDcgchatMedia({
|
|
367
|
+
cfg,
|
|
368
|
+
accountId,
|
|
369
|
+
log,
|
|
370
|
+
mediaUrl: url,
|
|
371
|
+
text: "",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
|
|
375
|
+
}
|
|
376
|
+
|
|
307
377
|
setMsgStatus('finished');
|
|
308
378
|
textChunk = ''
|
|
309
379
|
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()}`,
|