@dcrays/dcgchat-test 0.2.7 → 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/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
- import { monitoringToolMessage } from "./src/tool.js";
5
+ // import { monitoringToolMessage } from "./src/tool.js";
6
6
 
7
7
  const plugin = {
8
8
  id: "dcgchat-test",
@@ -11,7 +11,7 @@ const plugin = {
11
11
  configSchema: emptyPluginConfigSchema(),
12
12
  register(api: OpenClawPluginApi) {
13
13
  setDcgchatRuntime(api.runtime);
14
- monitoringToolMessage(api);
14
+ // monitoringToolMessage(api);
15
15
  api.registerChannel({ plugin: dcgchatPlugin });
16
16
  api.registerTool((ctx) => {
17
17
  const workspaceDir = ctx.workspaceDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.2.7",
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,12 +165,14 @@ 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();
107
173
  const text = msg.content.text?.trim();
108
174
 
109
- if (!text ) {
175
+ if (!text) {
110
176
  params.onChunk({
111
177
  messageType: "openclaw_bot_chat",
112
178
  _userId: msg._userId,
@@ -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; }) => {
@@ -200,82 +266,74 @@ export async function handleDcgchatMessage(params: {
200
266
  onIdle: () => {},
201
267
  });
202
268
 
203
- log(`dcgchat[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
204
-
205
- await core.channel.reply.dispatchReplyFromConfig({
206
- ctx: ctxPayload,
207
- cfg,
208
- dispatcher,
209
- replyOptions: {
210
- ...replyOptions,
211
- onModelSelected: prefixContext.onModelSelected,
212
- onPartialReply: async (payload: ReplyPayload) => {
213
- log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
214
- const mediaList =
215
- payload.mediaUrls && payload.mediaUrls.length > 0
216
- ? payload.mediaUrls
217
- : payload.mediaUrl
218
- ? [payload.mediaUrl]
219
- : [];
220
- if (mediaList.length > 0) {
221
- const files = []
222
- for (let i = 0; i < mediaList.length; i++) {
223
- const file = mediaList[i]
224
- const fileName = file.split(/[\\/]/).pop() || ''
225
- const url = await ossUpload(file, msg.content.bot_token)
226
- files.push({
227
- url: url,
228
- name: fileName,
229
- })
269
+ if (text === '/new') {
270
+ log(`dcgchat[${accountId}]: skipping agent dispatch for /new`);
271
+ await core.channel.reply.dispatchReplyFromConfig({
272
+ ctx: ctxPayload,
273
+ cfg,
274
+ dispatcher,
275
+ replyOptions: {
276
+ ...replyOptions,
277
+ onModelSelected: prefixContext.onModelSelected
278
+ },
279
+ });
280
+ } else {
281
+ log(`dcgchat[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
282
+ await core.channel.reply.dispatchReplyFromConfig({
283
+ ctx: ctxPayload,
284
+ cfg,
285
+ dispatcher,
286
+ replyOptions: {
287
+ ...replyOptions,
288
+ onModelSelected: prefixContext.onModelSelected,
289
+ onPartialReply: async (payload: ReplyPayload) => {
290
+ log(`dcgchat[${accountId}][deliver]: received chunk, text length=${payload.text?.length || 0}`);
291
+ if (payload.text) {
292
+ completeText = payload.text
293
+ }
294
+ const mediaList = resolveReplyMediaList(payload);
295
+ if (mediaList.length > 0) {
296
+ for (let i = 0; i < mediaList.length; i++) {
297
+ await sendDcgchatMedia({
298
+ cfg,
299
+ accountId,
300
+ log,
301
+ mediaUrl: mediaList[i],
302
+ text: "",
303
+ });
304
+ }
305
+ log(`dcgchat[${accountId}][deliver]: sent ${mediaList.length} media file(s) through channel adapter`);
306
+ }
307
+ if (payload.text) {
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
+ }
329
+ textChunk = payload.text
330
+ } else {
331
+ log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
230
332
  }
231
- params.onChunk({
232
- messageType: "openclaw_bot_chat",
233
- _userId: msg._userId,
234
- source: "client",
235
- content: {
236
- bot_token: msg.content.bot_token,
237
- domain_id: msg.content.domain_id,
238
- app_id: msg.content.app_id,
239
- bot_id: msg.content.bot_id,
240
- agent_id: msg.content.agent_id,
241
- session_id: msg.content.session_id,
242
- message_id: msg.content.message_id,
243
- response: '',
244
- files: files,
245
- state: 'chunk',
246
- },
247
- });
248
- }
249
- if (payload.text) {
250
- log(`dcgchat[${accountId}][deliver]: sending chunk to user ${msg._userId}, text="${payload.text.slice(0, 50)}..."`);
251
- params.onChunk({
252
- messageType: "openclaw_bot_chat",
253
- _userId: msg._userId,
254
- source: "client",
255
- content: {
256
- bot_token: msg.content.bot_token,
257
- domain_id: msg.content.domain_id,
258
- app_id: msg.content.app_id,
259
- bot_id: msg.content.bot_id,
260
- agent_id: msg.content.agent_id,
261
- session_id: msg.content.session_id,
262
- message_id: msg.content.message_id,
263
- response: payload.text.replace(textChunk, ''),
264
- state: 'chunk',
265
- },
266
- });
267
- textChunk = payload.text
268
- log(`dcgchat[${accountId}][deliver]: chunk sent successfully`);
269
- } else if (payload.mediaUrl && payload.mediaUrls) {
270
-
271
-
272
- } else {
273
- log(`dcgchat[${accountId}][deliver]: skipping empty chunk`);
274
- }
333
+ },
275
334
  },
276
- },
277
- });
278
-
335
+ });
336
+ }
279
337
  log(`dcgchat[${accountId}]: dispatch complete, sending final state`);
280
338
  params.onChunk({
281
339
  messageType: "openclaw_bot_chat",
@@ -293,6 +351,29 @@ export async function handleDcgchatMessage(params: {
293
351
  state: 'final',
294
352
  },
295
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
+
296
377
  setMsgStatus('finished');
297
378
  textChunk = ''
298
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 = {
@@ -135,7 +222,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
135
222
  state: 'final',
136
223
  },
137
224
  }));
138
- log(`dcgchat[${ctx.accountId}]: sendText to ${params.userId}, ${JSON.stringify(content)}`);
225
+ log(`dcgchat[${ctx.accountId}]: channel sendText to ${params.userId}, ${JSON.stringify(content)}`);
139
226
  } else {
140
227
  log(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> ${ws?.readyState}: ${ctx.text}`);
141
228
  }
@@ -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: params.messageId ||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: params.messageId || 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()}`,