@dcrays/dcgchat 0.2.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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();
@@ -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: { text: string | any[]; }) => {
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
- const mediaList =
226
- payload.mediaUrls && payload.mediaUrls.length > 0
227
- ? payload.mediaUrls
228
- : payload.mediaUrl
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
- 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
- })
297
+ await sendDcgchatMedia({
298
+ cfg,
299
+ accountId,
300
+ log,
301
+ mediaUrl: mediaList[i],
302
+ text: "",
303
+ });
241
304
  }
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
- });
305
+ log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
259
306
  }
260
307
  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
- });
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 = 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",
222
240
  messageId: `dcg-${Date.now()}`,
package/src/request.ts CHANGED
@@ -22,7 +22,7 @@ export const signKey = {
22
22
  develop: "FE93D3322CB94E978CE95BD4AA2A37D7",
23
23
  };
24
24
 
25
- const env = "test";
25
+ const env = "production";
26
26
  export const version = "1.0.0";
27
27
 
28
28
  /**