@dcrays/dcgchat-test 0.2.8 → 0.2.12

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/bot.ts +140 -57
  3. package/src/channel.ts +89 -71
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.2.8",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
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();
@@ -181,6 +247,8 @@ export async function handleDcgchatMessage(params: {
181
247
 
182
248
  log(`dcgchat[${accountId}]: ctxPayload=${JSON.stringify(ctxPayload)}`);
183
249
 
250
+ const sentMediaKeys = new Set<string>()
251
+ const getMediaKey = (url: string) => url.split(/[\\/]/).slice(-2).join('/')
184
252
  let textChunk = ''
185
253
 
186
254
  const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
@@ -191,7 +259,7 @@ export async function handleDcgchatMessage(params: {
191
259
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
192
260
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
193
261
  onReplyStart: async () => {},
194
- deliver: async (payload: { text: string | any[]; }) => {
262
+ deliver: async (payload: ReplyPayload) => {
195
263
  log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
196
264
  },
197
265
  onError: (err: any, info: { kind: any; }) => {
@@ -222,64 +290,49 @@ export async function handleDcgchatMessage(params: {
222
290
  onModelSelected: prefixContext.onModelSelected,
223
291
  onPartialReply: async (payload: ReplyPayload) => {
224
292
  log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
225
- const mediaList =
226
- payload.mediaUrls && payload.mediaUrls.length > 0
227
- ? payload.mediaUrls
228
- : payload.mediaUrl
229
- ? [payload.mediaUrl]
230
- : [];
293
+ if (payload.text) {
294
+ completeText = payload.text
295
+ }
296
+ const mediaList = resolveReplyMediaList(payload);
231
297
  if (mediaList.length > 0) {
232
- const files = []
233
298
  for (let i = 0; i < mediaList.length; i++) {
234
- const file = mediaList[i]
235
- const fileName = file.split(/[\\/]/).pop() || ''
236
- const url = await ossUpload(file, msg.content.bot_token)
237
- files.push({
238
- url: url,
239
- name: fileName,
240
- })
299
+ const mediaUrl = mediaList[i];
300
+ const key = getMediaKey(mediaUrl);
301
+ if (sentMediaKeys.has(key)) continue;
302
+ sentMediaKeys.add(key);
303
+ await sendDcgchatMedia({
304
+ cfg,
305
+ accountId,
306
+ log,
307
+ mediaUrl,
308
+ text: "",
309
+ });
241
310
  }
242
- params.onChunk({
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
- });
311
+ log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
259
312
  }
260
313
  if (payload.text) {
261
- log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${payload.text.slice(0, 50)}..."`);
262
- params.onChunk({
263
- messageType: "openclaw_bot_chat",
264
- _userId: msg._userId,
265
- source: "client",
266
- content: {
267
- bot_token: msg.content.bot_token,
268
- domain_id: msg.content.domain_id,
269
- app_id: msg.content.app_id,
270
- bot_id: msg.content.bot_id,
271
- agent_id: msg.content.agent_id,
272
- session_id: msg.content.session_id,
273
- message_id: msg.content.message_id,
274
- response: payload.text.replace(textChunk, ''),
275
- state: 'chunk',
276
- },
277
- });
314
+ const nextTextChunk = payload.text.replace(textChunk, '');
315
+ if (nextTextChunk.trim()) {
316
+ log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${nextTextChunk.slice(0, 50)}..."`);
317
+ params.onChunk({
318
+ messageType: "openclaw_bot_chat",
319
+ _userId: msg._userId,
320
+ source: "client",
321
+ content: {
322
+ bot_token: msg.content.bot_token,
323
+ domain_id: msg.content.domain_id,
324
+ app_id: msg.content.app_id,
325
+ bot_id: msg.content.bot_id,
326
+ agent_id: msg.content.agent_id,
327
+ session_id: msg.content.session_id,
328
+ message_id: msg.content.message_id,
329
+ response: nextTextChunk,
330
+ state: 'chunk',
331
+ },
332
+ });
333
+ log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
334
+ }
278
335
  textChunk = payload.text
279
- log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
280
- } else if (payload.mediaUrl && payload.mediaUrls) {
281
-
282
-
283
336
  } else {
284
337
  log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
285
338
  }
@@ -287,6 +340,35 @@ export async function handleDcgchatMessage(params: {
287
340
  },
288
341
  });
289
342
  }
343
+
344
+ const extractor = createFileExtractor()
345
+ const completeFiles = extractor.getNewFiles(completeText)
346
+ if (completeFiles.length > 0) {
347
+ for (let i = 0; i < completeFiles.length; i++) {
348
+ let url = completeFiles[i] as string
349
+ if (!path.isAbsolute(url)) {
350
+ url = path.join(getWorkspaceDir(), url)
351
+ }
352
+ const key = getMediaKey(url);
353
+ if (sentMediaKeys.has(key)) {
354
+ log(`dcgchat[${accountId}]: completeFiles already sent, skipping: ${url}`);
355
+ continue;
356
+ }
357
+ if (!fs.existsSync(url)) {
358
+ log(`dcgchat[${accountId}]: completeFiles file not found, skipping: ${url}`);
359
+ continue;
360
+ }
361
+ sentMediaKeys.add(key);
362
+ await sendDcgchatMedia({
363
+ cfg,
364
+ accountId,
365
+ log,
366
+ mediaUrl: url,
367
+ text: "",
368
+ });
369
+ }
370
+ log(`dcgchat[${accountId}][deliver]: sent ${completeFiles.length} media file(s) through channel adapter`);
371
+ }
290
372
  log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
291
373
  params.onChunk({
292
374
  messageType: "openclaw_bot_chat",
@@ -304,6 +386,7 @@ export async function handleDcgchatMessage(params: {
304
386
  state: 'final',
305
387
  },
306
388
  });
389
+
307
390
  setMsgStatus('finished');
308
391
  textChunk = ''
309
392
  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 = ctx.runtime?.log ?? console.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
- const log = ctx.runtime?.log ?? console.log;
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-test",
222
240
  messageId: `dcg-${Date.now()}`,