dlinkclaw-openclaw 1.0.0

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 (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. package/package.json +95 -0
@@ -0,0 +1,588 @@
1
+ import EventEmitter from 'events';
2
+ import { buildDLReplyMessage, messagePreviewHandler } from './message.js';
3
+ import mqtt from 'mqtt';
4
+ import { DLHttp } from './dl-http.js';
5
+ import { decryptAesEcbPkcs5 } from './crypto.js';
6
+ import { AUTO_RELAY_API, AUTO_RELAY_UPLOAD } from './const.js';
7
+ import { createCmdHandler } from '../services/index.js';
8
+ import { uuidStr } from '../utils.js';
9
+ export class DLClient extends EventEmitter {
10
+ ctx;
11
+ mqttConfig = null;
12
+ mqttClient = null;
13
+ cmdHandler = null;
14
+ http;
15
+ logger;
16
+ accountLog;
17
+ lastInboundAt = null;
18
+ //private runtime: PluginRuntime | null = null;
19
+ constructor(ctx, logger) {
20
+ super();
21
+ this.ctx = ctx;
22
+ this.logger = logger;
23
+ this.accountLog = `account[${ctx.account.accountId}] [DL-Client.ts]`;
24
+ const { baseUrl, apiKey, secret } = ctx.account.config;
25
+ this.http = new DLHttp({ baseUrl, apiKey, secret }, logger);
26
+ this.initCmdHandler();
27
+ }
28
+ /**
29
+ * 2026-09-16 修复(石头哥报:加同事后 model.list / session.list 全空):
30
+ * cmdHandler 原来只在构造函数里创建一次,任何把它置空的路径(stop / 刷新配置)
31
+ * 都会导致「此后永远为 null」→ 所有 cmd 请求失败且不可自愈(只能重启 openclaw 网关)。
32
+ * 抽成幂等方法后,start() 会兜底重建。
33
+ */
34
+ initCmdHandler() {
35
+ if (this.cmdHandler) {
36
+ return;
37
+ }
38
+ const cfg = this.ctx.cfg;
39
+ if (cfg.gateway?.auth?.token) {
40
+ const token = cfg.gateway.auth.token.toString();
41
+ const gatewayUrl = 'ws://127.0.0.1:18789';
42
+ this.cmdHandler = createCmdHandler({
43
+ cfg,
44
+ channelId: this.ctx.channelId,
45
+ gatewayUrl,
46
+ token,
47
+ baseUrl: this.ctx.account.config.baseUrl
48
+ }, this, this.logger);
49
+ }
50
+ }
51
+ async fetchConfig() {
52
+ const url = `/guest/mqtt/getConfig/${this.ctx.account.config.apiKey}`;
53
+ const res = await this.http.get(url);
54
+ if (res) {
55
+ const secret = this.ctx.account.config.secret;
56
+ const mqttConfig = decryptAesEcbPkcs5(res, secret);
57
+ if (mqttConfig) {
58
+ this.logger.info(`mqttconfig:${JSON.stringify(mqttConfig)}`);
59
+ const topics = mqttConfig.serverTopics;
60
+ const messageTopic = topics.find(item => item.endsWith('/message'));
61
+ const cmdTopic = topics.find(item => item.endsWith('/cmd'));
62
+ this.mqttConfig = { ...mqttConfig, serverTopics: [messageTopic, cmdTopic] };
63
+ return;
64
+ }
65
+ }
66
+ throw new Error(`Failed to get config!`);
67
+ }
68
+ async changeConnectStatus(status) {
69
+ const url = '/guest/relayBot/changeConnectStatus/' + this.ctx.account.config.apiKey;
70
+ const p = {
71
+ connect: status === 'start' ? 'true' : 'false'
72
+ };
73
+ return await this.http.post(url, p);
74
+ }
75
+ async connect() {
76
+ if (!this.mqttConfig) {
77
+ await this.fetchConfig();
78
+ }
79
+ const config = this.mqttConfig;
80
+ const connectionUrl = `mqtt://${config.url}:${config.port}`;
81
+ const rbotId = config.clientTopics[0].split('/')[1];
82
+ const msgTopic = config.clientTopics.filter((item) => item.endsWith('/message'))[0];
83
+ const cmdTopic = config.clientTopics.filter((item) => item.endsWith('/cmd'))[0];
84
+ const account = this.ctx.account;
85
+ return new Promise((resolve, reject) => {
86
+ this.mqttClient = mqtt.connect(connectionUrl, {
87
+ clientId: `openclaw_${account.accountId}_${rbotId}_${Date.now()}`,
88
+ username: config.userName,
89
+ password: config.password,
90
+ qos: config.qos,
91
+ clean: true,
92
+ reconnectPeriod: 5000,
93
+ connectTimeout: 30000
94
+ });
95
+ this.mqttClient.on("connect", () => {
96
+ this.logger.info(`${this.accountLog} Connected to broker`);
97
+ this.mqttClient?.subscribe([msgTopic, cmdTopic], { qos: config.qos }, (err) => {
98
+ if (err) {
99
+ this.logger.error(`${this.accountLog} Subscribe error:${err}`);
100
+ }
101
+ else {
102
+ this.logger.info(`${this.accountLog} Subscribed to topic:[${msgTopic}, ${cmdTopic}]`);
103
+ this.ctx.rbotId = rbotId;
104
+ }
105
+ resolve();
106
+ });
107
+ });
108
+ this.mqttClient.on("message", (topic, message) => {
109
+ try {
110
+ this.logger.debug ?? (`${this.accountLog} Topic: ${topic}, Received message:${message}`);
111
+ const payload = JSON.parse(message.toString());
112
+ this.receiveEventHandle(topic, payload);
113
+ }
114
+ catch (err) {
115
+ this.logger.error(`${this.accountLog} Failed to parse message:${String(err)}`);
116
+ }
117
+ });
118
+ this.mqttClient.on("error", (err) => {
119
+ this.logger.error(`${this.accountLog} Connection error:${err}`);
120
+ reject(err);
121
+ });
122
+ this.mqttClient.on("close", () => {
123
+ this.logger.warn(`${this.accountLog} Connection closed`);
124
+ if (this.closeHandler != null) {
125
+ this.closeHandler();
126
+ }
127
+ });
128
+ this.mqttClient.on("reconnect", () => {
129
+ this.logger.info(`${this.accountLog} Reconnecting...`);
130
+ });
131
+ // try {
132
+ // this.runtime = getDLRuntime();
133
+ // if (this.runtime) {
134
+ // this.runtime.events.onAgentEvent(event => {
135
+ // this.logger.warn(`中间文本 >>> ${JSON.stringify(event)}`)
136
+ // if (event?.stream === "assistant" && event.data?.phase === "commentary" && event.data?.text) {
137
+ // // this.logger.warn(`中间文本 >>> ${event.data?.text}`)
138
+ // }
139
+ // })
140
+ // this.logger.info(`runtime.events.onAgentEvent事件注册成功...`)
141
+ // }
142
+ // } catch (e) {
143
+ // this.logger.error(`${this.accountLog} ${new String(e)}`);
144
+ // }
145
+ });
146
+ }
147
+ async receiveEventHandle(topic, msg) {
148
+ if (topic.endsWith('/cmd')) {
149
+ // 2026-09-15 方案A(石头哥确认):服务端在新增/移除授权成员后,会往 bot 的 cmd topic
150
+ // 发一个轻量信号 {"type":"event","event":"openUserChanged"}。
151
+ // 插件收到后重新拉取 mqtt 配置并重连,让 openUserTopics 立刻是最新的
152
+ // (否则新用户收不到 onTick / onHealth / onSessionsChanged 等事件推送)。
153
+ // 只重连插件自身的 MQTT,不重启 openclaw 网关。
154
+ if (msg?.event === 'openUserChanged') {
155
+ void this.refreshConfigAndReconnect();
156
+ return;
157
+ }
158
+ const cmdMessage = msg;
159
+ let result = {
160
+ requestId: cmdMessage.requestId,
161
+ success: false,
162
+ error: 'CMD 处理器未初始化',
163
+ timestamp: Date.now()
164
+ };
165
+ if (this.cmdHandler?.isReady()) {
166
+ const res = await this.cmdHandler.handleCmdMessage(cmdMessage);
167
+ result = {
168
+ ...res,
169
+ requestId: result.requestId
170
+ };
171
+ }
172
+ if (cmdMessage.from) {
173
+ await this.publishCmdResultToDLClient(cmdMessage.from, result, "1");
174
+ }
175
+ else {
176
+ await this.publishCmdResultToServer(result, '1');
177
+ }
178
+ return;
179
+ }
180
+ this.lastInboundAt = Date.now();
181
+ const accStatus = messagePreviewHandler(msg, this.ctx.account.accountId, this.logger);
182
+ const t = this.emit('message', {
183
+ "ctx": this.ctx,
184
+ "msg": msg,
185
+ "accStatus": accStatus
186
+ }, this);
187
+ if (!t) {
188
+ this.logger.error('DLClient Event[message] emit fail!');
189
+ }
190
+ }
191
+ closeHandler() {
192
+ const t = this.emit('close');
193
+ if (!t) {
194
+ this.logger.warn(`DLClient account[${this.ctx.account.accountId}] Event[close] emit fail!`);
195
+ }
196
+ }
197
+ publish(topic, payload, sqos) {
198
+ const qos = (sqos ? parseInt(sqos) : (this.mqttConfig?.qos ?? 0));
199
+ return new Promise((reslove, reject) => {
200
+ this.mqttClient.publish(topic, payload, { qos }, (error) => {
201
+ if (error) {
202
+ const errorStr = String(error);
203
+ this.logger.error(`${this.accountLog} Publish error:${errorStr}`);
204
+ reject(errorStr);
205
+ }
206
+ else {
207
+ //this.logger.info(`${this.accountLog} Published to ${topic}`);
208
+ reslove();
209
+ }
210
+ });
211
+ });
212
+ }
213
+ getLastInboundAt() {
214
+ return this.lastInboundAt;
215
+ }
216
+ isConnected() {
217
+ return this.mqttClient?.connected ?? false;
218
+ }
219
+ async start() {
220
+ // 2026-09-16 修复:cmdHandler 缺失就补建(幂等),保证 start() 可重入、不再出现
221
+ // “start 之后 RPC 永久失效”的情况。
222
+ if (!this.cmdHandler) {
223
+ this.initCmdHandler();
224
+ }
225
+ if (!this.isConnected()) {
226
+ await this.connect();
227
+ }
228
+ if (this.cmdHandler) {
229
+ await this.cmdHandler.initialize();
230
+ }
231
+ await this.changeConnectStatus('start');
232
+ }
233
+ async stop() {
234
+ if (this.mqttClient) {
235
+ // 2026-09-16 修复:原实现不等 end 回调(清理动作都在回调里),与紧随其后的 start()
236
+ // 形成竞态:isConnected() 仍为 true → 跳过 connect(),随后回调才把 mqttClient /
237
+ // cmdHandler / mqttConfig 清空 → 连接彻底废掉。这里等回调执行完再返回。
238
+ await new Promise((resolve) => {
239
+ this.mqttClient.end(false, () => {
240
+ this.mqttClient = null;
241
+ this.logger.warn(`DLClient account[${this.ctx.account.accountId}] Disconnected`);
242
+ if (this.cmdHandler) {
243
+ this.cmdHandler.cleanup();
244
+ }
245
+ this.cmdHandler = null;
246
+ this.mqttConfig = null;
247
+ resolve();
248
+ });
249
+ });
250
+ }
251
+ await this.changeConnectStatus('stop');
252
+ }
253
+ /**
254
+ * 2026-09-15 方案A(石头哥确认):重新拉取 mqtt 配置(含 openUserTopics)。
255
+ * 触发时机:服务端新增/移除授权成员后,会往 bot 的 cmd topic 发 openUserChanged 信号。
256
+ *
257
+ * 2026-09-16 修复(石头哥报:加同事后 model.list / session.list 全部为空):
258
+ * 原实现是「断连 + 置空 cmdHandler + 重连」,有致命副作用——cmdHandler 只在构造函数
259
+ * 里创建一次,置空后 start() 不会重建 → 所有 cmd RPC 永久失效,只能重启 openclaw 网关恢复。
260
+ * 而本方法的目的只是刷新 openUserTopics——它是 publishEventToDLClient(L348)实时读取的,
261
+ * 与 MQTT 订阅目标(clientTopics)、Gateway WS 通道都无关 → 只刷新配置即可,不断连、不重连。
262
+ */
263
+ async refreshConfigAndReconnect() {
264
+ try {
265
+ await this.fetchConfig();
266
+ this.logger.info(`${this.accountLog} mqtt 配置已刷新(openUserTopics 已更新,未重连)`);
267
+ }
268
+ catch (e) {
269
+ this.logger.error(`${this.accountLog} 刷新 mqtt 配置失败(保持旧配置): ${String(e)}`);
270
+ }
271
+ }
272
+ async exeCmd(cmdMessage) {
273
+ if (this.cmdHandler?.isReady()) {
274
+ return await this.cmdHandler.handleCmdMessage(cmdMessage);
275
+ }
276
+ }
277
+ async publishEventToDLClient(payload, event, sqos, to) {
278
+ const timestamp = Date.now();
279
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
280
+ const api_key = this.ctx.account.config.apiKey;
281
+ const message = {
282
+ apiKey: api_key,
283
+ type: 'event',
284
+ event: event,
285
+ payload: payload,
286
+ agentBrand: 'openClaw',
287
+ timestamp,
288
+ timezone
289
+ };
290
+ if (to && to.length > 0) {
291
+ this.logger.info(`推送客户端事件消息, topic= ${to}`);
292
+ try {
293
+ await this.publish(to, JSON.stringify(message), sqos);
294
+ return;
295
+ }
296
+ catch (e) {
297
+ this.logger.error(String(e));
298
+ }
299
+ return;
300
+ }
301
+ const openUserTopics = this.mqttConfig?.openUserTopics ?? [];
302
+ //this.logger.info(`推送客户端事件消息, topics= [${openUserTopics}]`,)
303
+ for (const topic of openUserTopics) {
304
+ try {
305
+ await this.publish(topic, JSON.stringify(message), sqos);
306
+ }
307
+ catch (e) {
308
+ this.logger.error(String(e));
309
+ }
310
+ }
311
+ }
312
+ async publishCmdResultToDLClient(topic, playload, sqos) {
313
+ const timestamp = Date.now();
314
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
315
+ const api_key = this.ctx.account.config.apiKey;
316
+ const message = {
317
+ apiKey: api_key,
318
+ type: 'cmd',
319
+ payload: playload,
320
+ agentBrand: 'openClaw',
321
+ timestamp,
322
+ timezone
323
+ };
324
+ return await this.publish(topic, JSON.stringify(message), sqos);
325
+ }
326
+ async publishEventToServer(playload, event, sqos) {
327
+ const topic = this.mqttConfig?.serverTopics[1];
328
+ if (!topic) {
329
+ return;
330
+ }
331
+ const timestamp = Date.now();
332
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
333
+ const api_key = this.ctx.account.config.apiKey;
334
+ const message = {
335
+ apiKey: api_key,
336
+ type: 'event',
337
+ event: event,
338
+ payload: playload,
339
+ agentBrand: 'openClaw',
340
+ timestamp,
341
+ timezone
342
+ };
343
+ return await this.publish(topic, JSON.stringify(message), sqos);
344
+ }
345
+ async publishCmdResultToServer(playload, sqos) {
346
+ const topic = this.mqttConfig?.serverTopics[1];
347
+ if (!topic) {
348
+ return;
349
+ }
350
+ const timestamp = Date.now();
351
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
352
+ const api_key = this.ctx.account.config.apiKey;
353
+ const message = {
354
+ apiKey: api_key,
355
+ type: 'cmd',
356
+ payload: playload,
357
+ agentBrand: 'openClaw',
358
+ timestamp,
359
+ timezone
360
+ };
361
+ return await this.publish(topic, JSON.stringify(message), sqos);
362
+ }
363
+ async publishToServer(params, autoParams) {
364
+ const { streamId, content, finish, mediaFiles, session, kind } = params;
365
+ if (!this.mqttClient || !this.mqttConfig) {
366
+ throw new Error(`${this.accountLog} client not connected`);
367
+ }
368
+ const message = buildDLReplyMessage({
369
+ streamId,
370
+ content,
371
+ finish,
372
+ kind
373
+ });
374
+ if (!message) {
375
+ if (autoParams) {
376
+ const { toAgent, contactId, relayId, chatId, notifyUrl, isGroup, botType } = autoParams;
377
+ this.logger.info(`${this.accountLog} streamId:[${streamId}] has been cleaned up due to overdue. current use autoSend...`);
378
+ const data = {
379
+ "chatId": chatId,
380
+ "botType": botType,
381
+ "group": isGroup ? "true" : "false",
382
+ "content": content ?? "",
383
+ "notifyUrl": notifyUrl ?? "",
384
+ "toAgent.id": toAgent.id,
385
+ "toAgent.sessionKey": toAgent.sessionKey,
386
+ "contactId": contactId ?? '',
387
+ "relayId": relayId,
388
+ "relayType": "openClaw"
389
+ };
390
+ if (session) {
391
+ data["archiveSession.archived"] = session.archived ? 'true' : 'false';
392
+ data["archiveSession.sessionKey"] = session.sessionKey;
393
+ if (session.newSessionKey) {
394
+ data["archiveSession.newSessionKey"] = session.newSessionKey;
395
+ }
396
+ data["archiveSession.runId"] = session.runId;
397
+ }
398
+ let res;
399
+ if (mediaFiles && mediaFiles.length > 0) {
400
+ res = await this.http.upload(AUTO_RELAY_API, mediaFiles, data);
401
+ }
402
+ else if (data.content.length > 0) {
403
+ res = await this.http.post(AUTO_RELAY_API, data);
404
+ }
405
+ return {
406
+ autoRelay: true,
407
+ reqId: res
408
+ };
409
+ }
410
+ this.logger.error(`${this.accountLog} streamId:[${streamId}] has been cleaned up due to overdue.`);
411
+ return {
412
+ autoRelay: true
413
+ };
414
+ }
415
+ const now = Date.now();
416
+ if (message.relayTimeRhreshold && now > message.relayTimeRhreshold) {
417
+ //超时回复使用主动回复
418
+ this.logger.info(`${this.accountLog} Over the reply time, take the initiative to reply!`);
419
+ message.notify = true;
420
+ }
421
+ if (mediaFiles && mediaFiles.length > 0) {
422
+ this.logger.info(`${this.accountLog} upload media files..`);
423
+ const data = {
424
+ "streamId": streamId
425
+ };
426
+ const res = await this.http.upload(AUTO_RELAY_UPLOAD, mediaFiles, data);
427
+ if (res && res.length > 0) {
428
+ this.logger.info(`${this.accountLog} upload media files success!`);
429
+ }
430
+ }
431
+ if (session) {
432
+ message.archiveSession = { ...session };
433
+ }
434
+ const topic = this.mqttConfig.serverTopics[0];
435
+ try {
436
+ await this.publish(topic, JSON.stringify(message));
437
+ return {
438
+ autoRelay: false,
439
+ reqId: message.streamId
440
+ };
441
+ }
442
+ catch (e) {
443
+ throw e;
444
+ }
445
+ }
446
+ /**
447
+ * 减轻服务端负担,将处理状态直接推送到客户端(处理结果走服务端)
448
+ * @param params
449
+ */
450
+ async publishProcessingState(params) {
451
+ const { streamId, content, kind, status, runId } = params;
452
+ if (!this.mqttClient || !this.mqttConfig) {
453
+ throw new Error(`${this.accountLog} client not connected`);
454
+ }
455
+ const message = buildDLReplyMessage({
456
+ streamId,
457
+ content,
458
+ finish: false,
459
+ kind,
460
+ runId
461
+ });
462
+ if (!message) {
463
+ this.logger.error(`[publishProcessingState]${this.accountLog} streamId:[${streamId}] has been cleaned up due to overdue.`);
464
+ return;
465
+ }
466
+ const now = Date.now();
467
+ if (message.relayTimeRhreshold && now > message.relayTimeRhreshold) {
468
+ this.logger.error(`[publishProcessingState]${this.accountLog} Over the reply time, take the initiative to reply!`);
469
+ return;
470
+ }
471
+ const fromUsers = message.fromUsers ?? [];
472
+ if (fromUsers.length === 0) {
473
+ this.logger.warn(`[publishProcessingState]${this.accountLog} from users is empty, can not send message status!`);
474
+ return;
475
+ }
476
+ //if (kind === 'plan' && message.contactType === 'group') {
477
+ //群组时中间态消息就不回传了,显得消息太杂
478
+ //return;
479
+ //}
480
+ let result_content = message.payload;
481
+ if (kind !== 'patch' && kind !== 'plan' && kind !== 'block') {
482
+ if (result_content.length > 200) {
483
+ result_content = result_content.substring(0, 200);
484
+ }
485
+ }
486
+ const toAgent = message.toAgent;
487
+ const sessionKey = toAgent.sessionKey;
488
+ for (const user of fromUsers) {
489
+ const msg = {
490
+ msgId: uuidStr(36),
491
+ streamId,
492
+ chatId: message.chatId,
493
+ sender: {
494
+ id: toAgent.id,
495
+ nickname: toAgent.nickname,
496
+ avatar: toAgent.avatarUrl,
497
+ jobPost: toAgent.jobPost
498
+ },
499
+ receiver: {
500
+ id: user.id,
501
+ nickname: user.nickname
502
+ },
503
+ contactType: message.contactType,
504
+ contactId: message.contactId,
505
+ botType: message.botType,
506
+ payload: result_content,
507
+ createdAt: Date.now(),
508
+ timezone: message.timezone,
509
+ status,
510
+ finish: message.finish,
511
+ kind,
512
+ sessionKey,
513
+ runId,
514
+ relayType: 'openClaw'
515
+ };
516
+ try {
517
+ await this.publish(user.chatTopic, JSON.stringify(msg));
518
+ }
519
+ catch (e) {
520
+ this.logger.error(String(e));
521
+ }
522
+ }
523
+ }
524
+ async downloadFileRaw(url, botType) {
525
+ return await this.http.downloadFileRaw(url, { botType });
526
+ }
527
+ /**
528
+ * 对码成功回调通知
529
+ */
530
+ async approved(ok) {
531
+ const url = '/guest/relayBot/approved/' + this.ctx.rbotId;
532
+ const p = {
533
+ "approved": ok ? "true" : "false"
534
+ };
535
+ await this.http.post(url, p);
536
+ }
537
+ async autoReply(params) {
538
+ const { to: rawTo, text, mediaFiles } = params;
539
+ const { channelId, account } = this.ctx;
540
+ const config = account.config;
541
+ if (!config.configured) {
542
+ throw new Error(`${channelId} account[${account.accountId}] not configured`);
543
+ }
544
+ if ((!text || text.length === 0) && (!mediaFiles || mediaFiles.length === 0)) {
545
+ throw new Error(`${channelId} account[${account.accountId}] content is empty!`);
546
+ }
547
+ // 从 to 中解析 sessionKey(格式: dlink:user:xxx-xxx-dLink#agent:main:dlink-00001:direct:xxx)
548
+ const hashIdx = rawTo.lastIndexOf('#');
549
+ const to = hashIdx > 0 ? rawTo.substring(0, hashIdx) : rawTo;
550
+ const sessionKey = hashIdx > 0 ? rawTo.substring(hashIdx + 1) : undefined;
551
+ const channelPrefix = new RegExp(`^${channelId}:`, "i");
552
+ const chat_id = to.replace(channelPrefix, "");
553
+ const isGroup = chat_id.includes("group:");
554
+ const strArr = isGroup ? chat_id.replace(new RegExp('^group:', 'i'), "").split("-") : chat_id.replace(new RegExp('^user:', 'i'), "").split("-");
555
+ const chatId = strArr[0];
556
+ const botId = strArr[1];
557
+ const botType = strArr[2];
558
+ const relayId = strArr[3];
559
+ const data = {
560
+ "chatId": chatId,
561
+ "toAgent.id": botId,
562
+ "botType": botType,
563
+ "group": isGroup ? "true" : "false",
564
+ "content": text ?? '',
565
+ "relayId": relayId,
566
+ "relayType": "openClaw"
567
+ };
568
+ // 携带 sessionKey 到服务端,用于下游 Agent 路由
569
+ if (sessionKey) {
570
+ data["toAgent.sessionKey"] = sessionKey;
571
+ }
572
+ let res;
573
+ if (mediaFiles && mediaFiles.length > 0) {
574
+ res = await this.http.upload(AUTO_RELAY_API, mediaFiles, data);
575
+ }
576
+ else {
577
+ res = await this.http.post(AUTO_RELAY_API, data);
578
+ }
579
+ if (res) {
580
+ return {
581
+ channel: channelId,
582
+ messageId: res,
583
+ chatId: chatId
584
+ };
585
+ }
586
+ throw new Error(`${channelId} account[${account.accountId}] Sending message failed.`);
587
+ }
588
+ }
@@ -0,0 +1,27 @@
1
+ import { PluginLogger } from 'openclaw/plugin-sdk/core';
2
+ import { BotType } from './types.js';
3
+ export type HttpOpts = {
4
+ baseUrl: string;
5
+ apiKey: string;
6
+ secret: string;
7
+ };
8
+ export declare class DLHttp {
9
+ private readonly opts;
10
+ private readonly deviceId;
11
+ private readonly client;
12
+ private readonly logger;
13
+ constructor(opts: HttpOpts, logger: PluginLogger, timeout?: number);
14
+ private randomString;
15
+ private genSign;
16
+ private response;
17
+ get<T>(url: string, data?: Record<string, string>): Promise<T | undefined>;
18
+ post<T>(url: string, data?: Record<string, string>): Promise<T | undefined>;
19
+ upload<T>(url: string, files: Array<string>, data?: Record<string, string>): Promise<T | undefined>;
20
+ downloadFileRaw(url: string, { timeout, botType }: {
21
+ timeout?: number;
22
+ botType: BotType;
23
+ }): Promise<{
24
+ buffer: Buffer;
25
+ filename?: string;
26
+ }>;
27
+ }