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,896 @@
1
+ import EventEmitter from 'events';
2
+ import { WebSocket } from 'ws';
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { getDeviceConfig, reqBootstrapToken, saveDeviceConfig, signConnectPayload } from '../client/crypto.js';
7
+ /**
8
+ * OpenClaw Gateway WebSocket 客户端
9
+ * 用于 duolian 插件内部调用 OpenClaw API
10
+ */
11
+ export class WSClient extends EventEmitter {
12
+ ws = null;
13
+ gatewayUrl;
14
+ token;
15
+ callbacks = new Map();
16
+ messageId = 0;
17
+ isConnected = false;
18
+ isHandshaked = false;
19
+ deviceConfig;
20
+ constructor(options) {
21
+ super();
22
+ this.gatewayUrl = options.gatewayUrl;
23
+ this.token = options.token;
24
+ const conf = getDeviceConfig();
25
+ this.deviceConfig = { ...conf };
26
+ }
27
+ log(msg, type) {
28
+ switch (type) {
29
+ case 'info':
30
+ console.log(msg);
31
+ break;
32
+ case 'debug':
33
+ if (process.env.NODE_ENV === 'dev') {
34
+ console.debug(msg);
35
+ }
36
+ break;
37
+ case 'warn':
38
+ console.warn(msg);
39
+ break;
40
+ case 'error':
41
+ console.error(msg);
42
+ break;
43
+ }
44
+ }
45
+ // ==================== 连接管理 ====================
46
+ /**
47
+ * 连接到 OpenClaw Gateway
48
+ */
49
+ async connect() {
50
+ return new Promise((resolve, reject) => {
51
+ try {
52
+ this.ws = new WebSocket(this.gatewayUrl);
53
+ this.ws.on('open', () => {
54
+ this.isConnected = true;
55
+ this.emit('onConnected');
56
+ });
57
+ this.ws.on('message', (data) => {
58
+ try {
59
+ const message = JSON.parse(data.toString());
60
+ this.handleMessage(message);
61
+ }
62
+ catch (error) {
63
+ this.emit('onError', error);
64
+ }
65
+ });
66
+ this.ws.on('close', () => {
67
+ this.isConnected = false;
68
+ this.isHandshaked = false;
69
+ this.emit('onDisconnect');
70
+ });
71
+ this.ws.on('error', (error) => {
72
+ this.emit('onError', error);
73
+ });
74
+ resolve();
75
+ }
76
+ catch (error) {
77
+ reject(error);
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * 断开连接
83
+ */
84
+ disconnect() {
85
+ if (this.ws) {
86
+ this.ws.close();
87
+ this.ws = null;
88
+ }
89
+ // 清理所有pending的回调
90
+ for (const [, callback] of this.callbacks) {
91
+ clearTimeout(callback.timeout);
92
+ callback.reject(new Error('Client disconnected'));
93
+ }
94
+ this.callbacks.clear();
95
+ this.isConnected = false;
96
+ }
97
+ /**
98
+ * 检查是否已连接
99
+ */
100
+ isReady() {
101
+ return this.isConnected && this.isHandshaked;
102
+ }
103
+ // ==================== Agent 管理 API ====================
104
+ async askAgent(params) {
105
+ return this.sendRequest({
106
+ type: 'req',
107
+ method: 'agent',
108
+ params
109
+ });
110
+ }
111
+ /**
112
+ * 列出所有 Agents
113
+ */
114
+ async listAgents() {
115
+ return this.sendRequest({
116
+ type: 'req',
117
+ method: 'agents.list',
118
+ params: {}
119
+ });
120
+ }
121
+ /**
122
+ * 创建新的 Agent
123
+ */
124
+ async createAgent(params) {
125
+ return this.sendRequest({
126
+ type: 'req',
127
+ method: 'agents.create',
128
+ params
129
+ });
130
+ }
131
+ /**
132
+ * 更新 Agent 信息
133
+ */
134
+ async updateAgent(params) {
135
+ return this.sendRequest({
136
+ type: 'req',
137
+ method: 'agents.update',
138
+ params
139
+ });
140
+ }
141
+ /**
142
+ * 删除 Agent
143
+ */
144
+ async deleteAgent(params) {
145
+ return this.sendRequest({
146
+ type: 'req',
147
+ method: 'agents.delete',
148
+ params
149
+ });
150
+ }
151
+ // ==================== 任务管理 API ====================
152
+ /**
153
+ * 添加定时任务
154
+ */
155
+ async addCronTask(params) {
156
+ return this.sendRequest({
157
+ type: 'req',
158
+ method: 'cron.add',
159
+ params
160
+ });
161
+ }
162
+ /**
163
+ * 列出所有定时任务
164
+ */
165
+ async listCronTasks({ limit, offset }) {
166
+ return this.sendRequest({
167
+ type: 'req',
168
+ method: 'cron.list',
169
+ params: {
170
+ includeDisabled: true,
171
+ enabled: 'all',
172
+ limit,
173
+ offset
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * 更新定时任务
179
+ */
180
+ async updateCronTask(params) {
181
+ return this.sendRequest({
182
+ type: 'req',
183
+ method: 'cron.update',
184
+ params
185
+ });
186
+ }
187
+ /**
188
+ * 删除定时任务
189
+ */
190
+ async removeCronTask(params) {
191
+ return this.sendRequest({
192
+ type: 'req',
193
+ method: 'cron.remove',
194
+ params
195
+ });
196
+ }
197
+ /**
198
+ * 运行定时任务
199
+ */
200
+ async runCronTask(params) {
201
+ return this.sendRequest({
202
+ type: 'req',
203
+ method: 'cron.run',
204
+ params
205
+ });
206
+ }
207
+ // ==================== 状态监控 API ====================
208
+ /**
209
+ * 获取健康状态
210
+ */
211
+ async getHealthStatus() {
212
+ return this.sendRequest({
213
+ type: 'req',
214
+ method: 'health',
215
+ params: {}
216
+ });
217
+ }
218
+ /**
219
+ * 获取系统状态
220
+ */
221
+ async getSystemStatus() {
222
+ return this.sendRequest({
223
+ type: 'req',
224
+ method: 'status',
225
+ params: {}
226
+ });
227
+ }
228
+ /**
229
+ * 获取使用情况统计
230
+ */
231
+ async getUsageStatus() {
232
+ return this.sendRequest({
233
+ type: 'req',
234
+ method: 'usage.status',
235
+ params: {}
236
+ });
237
+ }
238
+ async listModels(agentId) {
239
+ // 2026-09-06 适配 openclaw 9.2:models.list 需显式 agentId(多 agent 配置下无 owner 会报 INVALID_REQUEST)
240
+ const params = {};
241
+ if (agentId) {
242
+ params.agentId = agentId;
243
+ }
244
+ return this.sendRequest({
245
+ type: 'req',
246
+ method: 'models.list',
247
+ params
248
+ });
249
+ }
250
+ async sessionsList(params) {
251
+ const p = {
252
+ //limit:params.limit, //number
253
+ activeMinutes: params.activeMinutes, //number
254
+ includeGlobal: params.includeGlobal, //bool
255
+ includeDerivedTitles: params.includeDerivedTitles, //bool
256
+ includeLastMessage: params.includeLastMessage, //bool,
257
+ label: params.label, //string,
258
+ spawnedBy: params.spawnedBy,
259
+ agentId: params.agentId, //必填
260
+ search: params.search, //string关键字
261
+ archived: params.archived //bool: true=归档会话, false/omit=活跃会话
262
+ };
263
+ if (params.limit) {
264
+ p.limit = params.limit;
265
+ }
266
+ return this.sendRequest({
267
+ type: 'req',
268
+ method: 'sessions.list',
269
+ params: p
270
+ });
271
+ }
272
+ async sessionsGet(params) {
273
+ const p = {
274
+ sessionKey: params.sessionKey,
275
+ key: params.key,
276
+ limit: params.limit
277
+ };
278
+ return this.sendRequest({
279
+ type: 'req',
280
+ method: 'sessions.get',
281
+ params: p
282
+ });
283
+ }
284
+ async sessionsDescribe(params) {
285
+ return this.sendRequest({
286
+ type: 'req',
287
+ method: 'sessions.describe',
288
+ params
289
+ });
290
+ }
291
+ async sessionsResolve(params) {
292
+ const p = {
293
+ key: params.sessionKey,
294
+ sessionId: params.sessionId,
295
+ agentId: params.agentId,
296
+ includeGlobal: params.includeGlobal, //Bool
297
+ includeUnknown: params.includeUnknown // Bool
298
+ };
299
+ if (params.sessionLabel) {
300
+ p.label = params.sessionLabel;
301
+ }
302
+ if (params.spawnedBy) {
303
+ p.spawnedBy = params.spawnedBy;
304
+ }
305
+ return this.sendRequest({
306
+ type: 'req',
307
+ method: 'sessions.resolve',
308
+ params: p
309
+ });
310
+ }
311
+ async sessionReset(params) {
312
+ return this.sendRequest({
313
+ type: 'req',
314
+ method: 'sessions.reset',
315
+ params
316
+ });
317
+ }
318
+ async chatHistories(params) {
319
+ return this.sendRequest({
320
+ type: 'req',
321
+ method: 'chat.history',
322
+ params
323
+ });
324
+ }
325
+ // ==================== Skills API ====================
326
+ async listSkills(agentId) {
327
+ const params = {};
328
+ if (agentId)
329
+ params.agentId = agentId;
330
+ return this.sendRequest({
331
+ type: 'req',
332
+ method: 'skills.status',
333
+ params
334
+ });
335
+ }
336
+ async searchSkills(params) {
337
+ return this.sendRequest({
338
+ type: 'req',
339
+ method: 'skills.search',
340
+ params: params.query ? { query: params.query, limit: params.limit || 20 } : { limit: params.limit || 20 }
341
+ });
342
+ }
343
+ async getSkillDetail(slug) {
344
+ return this.sendRequest({
345
+ type: 'req',
346
+ method: 'skills.detail',
347
+ params: { slug }
348
+ });
349
+ }
350
+ async installSkill(slug, version) {
351
+ return this.sendRequest({
352
+ type: 'req',
353
+ method: 'skills.install',
354
+ params: { source: 'clawhub', slug, ...(version ? { version } : {}) }
355
+ });
356
+ }
357
+ async updateSkillConfig(skillKey, enabled) {
358
+ return this.sendRequest({
359
+ type: 'req',
360
+ method: 'skills.update',
361
+ params: { skillKey, enabled }
362
+ });
363
+ }
364
+ /**
365
+ * 取消当前Session的Agent执行
366
+ * 发送 /queue interrupt 到Gateway中断活跃run
367
+ * @param channelId 频道ID(如 dlink),由services层传入,避免硬编码
368
+ * @param chatType 会话类型 direct | group,由WorkRelay客户端传入
369
+ */
370
+ // ==================== 会话管理 API(CRUD) ====================
371
+ /**
372
+ * 创建新会话
373
+ * @param params.key - 完整的 sessionKey
374
+ */
375
+ async sessionsCreate(params) {
376
+ return this.sendRequest({
377
+ type: 'req',
378
+ method: 'sessions.create',
379
+ params: { key: params.key }
380
+ });
381
+ }
382
+ /**
383
+ * 更新会话元数据(归档/恢复等)
384
+ * @param params.key - sessionKey
385
+ * @param params.patch - 要更新的字段,如 { archived: true }
386
+ */
387
+ async sessionsPatch(params) {
388
+ return this.sendRequest({
389
+ type: 'req',
390
+ method: 'sessions.patch',
391
+ params: { key: params.key, ...params.patch }
392
+ });
393
+ }
394
+ /**
395
+ * 删除会话
396
+ * @param params.key - sessionKey
397
+ * @param params.deleteFiles - 是否同时删除会话相关文件(貌似不支持该参数)
398
+ */
399
+ async sessionsDelete(params) {
400
+ return this.sendRequest({
401
+ type: 'req',
402
+ method: 'sessions.delete',
403
+ params: { key: params.key, emitLifecycleHooks: true }
404
+ });
405
+ }
406
+ /** 订阅会话变更事件(需在握手成功后调用) */
407
+ async subscribeSessions() {
408
+ return this.sendRequest({
409
+ type: 'req',
410
+ method: 'sessions.subscribe'
411
+ });
412
+ }
413
+ async cancelSession({ sessionKey }) {
414
+ //const sessionKey = `agent:${agentId}:${channelId}:${chatType}:${userId.toLowerCase()}`;
415
+ const agentId = sessionKey.split(':')[1];
416
+ const messageId = `cancel_${Date.now()}`;
417
+ return this.sendRequest({
418
+ type: 'req',
419
+ method: 'agent',
420
+ params: {
421
+ agentId,
422
+ message: '/queue interrupt',
423
+ sessionKey,
424
+ deliver: false,
425
+ idempotencyKey: messageId
426
+ }
427
+ });
428
+ }
429
+ /**
430
+ * 发送 /stop 到Gateway,强制中止活跃run(比 /queue interrupt 更快)
431
+ * 对应 cmd 通道 agent.stop 指令
432
+ */
433
+ async stopSession({ sessionKey }) {
434
+ //const sessionKey = `agent:${agentId}:${channelId}:${chatType}:${userId.toLowerCase()}`;
435
+ const agentId = sessionKey.split(':')[1];
436
+ const messageId = `stop_${Date.now()}`;
437
+ return this.sendRequest({
438
+ type: 'req',
439
+ method: 'agent',
440
+ params: {
441
+ agentId,
442
+ message: '/stop',
443
+ sessionKey,
444
+ deliver: false,
445
+ idempotencyKey: messageId
446
+ }
447
+ });
448
+ }
449
+ async chatAbort(params) {
450
+ const { sessionKey, runId } = params;
451
+ return this.sendRequest({
452
+ type: 'req',
453
+ method: 'chat.abort',
454
+ params: {
455
+ sessionKey,
456
+ runId
457
+ }
458
+ });
459
+ }
460
+ async getSkillSecurityVerdicts(agentId) {
461
+ const params = {};
462
+ if (agentId)
463
+ params.agentId = agentId;
464
+ return this.sendRequest({
465
+ type: 'req',
466
+ method: 'skills.securityVerdicts',
467
+ params
468
+ });
469
+ }
470
+ // 仪表盘:按日期范围聚合成本
471
+ async getUsageCost(params) {
472
+ return this.sendRequest({
473
+ type: 'req',
474
+ method: 'usage.cost',
475
+ params
476
+ });
477
+ }
478
+ // 仪表盘:每 session 详细使用统计
479
+ async getSessionsUsage(params) {
480
+ return this.sendRequest({
481
+ type: 'req',
482
+ method: 'sessions.usage',
483
+ params
484
+ });
485
+ }
486
+ // 仪表盘:每消息级别使用日志
487
+ async getSessionsUsageLogs(params) {
488
+ return this.sendRequest({
489
+ type: 'req',
490
+ method: 'sessions.usage.logs',
491
+ params
492
+ });
493
+ }
494
+ // ==================== 工作区 / Artifact API ====================
495
+ async artifactsList(params) {
496
+ return this.sendRequest({
497
+ type: 'req',
498
+ method: 'artifacts.list',
499
+ params
500
+ });
501
+ }
502
+ async artifactsGet(params) {
503
+ return this.sendRequest({
504
+ type: 'req',
505
+ method: 'artifacts.get',
506
+ params
507
+ });
508
+ }
509
+ async artifactsDownload(params) {
510
+ return this.sendRequest({
511
+ type: 'req',
512
+ method: 'artifacts.download',
513
+ params
514
+ });
515
+ }
516
+ /** 列出 Agent 工作区文件(agents.files.list) */
517
+ async agentFilesList(agentId) {
518
+ return this.sendRequest({
519
+ type: 'req',
520
+ method: 'agents.files.list',
521
+ params: { agentId }
522
+ });
523
+ }
524
+ /** 获取 Agent 工作区文件内容(agents.files.get) */
525
+ async agentFilesGet(agentId, filePath) {
526
+ return this.sendRequest({
527
+ type: 'req',
528
+ method: 'agents.files.get',
529
+ params: { agentId, name: filePath }
530
+ });
531
+ }
532
+ // ===== 2026-07-31 新增:本地文件系统直接读写(绕过 Gateway RPC 的文件白名单限制) =====
533
+ /** 跳过 Gateway RPC,直接用 Node.js fs 读取 agent 工作区文件内容
534
+ * 2026-07-31 base64 fix:文本文件 utf-8,二进制文件 base64 编码避免损坏 */
535
+ async agentFilesGetLocal(agentId, filePath) {
536
+ const workspaceDir = this.resolveWorkspaceDir(agentId);
537
+ const fullPath = path.join(workspaceDir, filePath);
538
+ const stat = await fs.stat(fullPath);
539
+ const isText = this.isTextFile(filePath);
540
+ const encoding = isText ? 'utf-8' : 'base64';
541
+ let content;
542
+ if (isText) {
543
+ content = await fs.readFile(fullPath, 'utf-8');
544
+ }
545
+ else {
546
+ const buffer = await fs.readFile(fullPath);
547
+ content = buffer.toString('base64');
548
+ }
549
+ return {
550
+ agentId,
551
+ workspace: workspaceDir,
552
+ file: {
553
+ name: filePath,
554
+ path: fullPath,
555
+ missing: false,
556
+ size: stat.size,
557
+ updatedAtMs: Math.floor(stat.mtimeMs),
558
+ content,
559
+ encoding,
560
+ },
561
+ };
562
+ }
563
+ /** 根据扩展名判断是否文本文件 */
564
+ isTextFile(filename) {
565
+ const ext = path.extname(filename).slice(1).toLowerCase();
566
+ const textExts = new Set([
567
+ 'md', 'mdx', 'txt', 'log', 'json', 'xml', 'yaml', 'yml', 'toml',
568
+ 'ts', 'tsx', 'js', 'jsx', 'jsm', 'py', 'java', 'go', 'rs', 'c', 'cpp', 'h', 'hpp',
569
+ 'css', 'scss', 'less', 'html', 'htm', 'svg', 'csv', 'env', 'cfg', 'ini',
570
+ 'sh', 'bash', 'zsh', 'sql', 'rb', 'php', 'swift', 'kt', 'r', 'lua',
571
+ 'conf', 'properties', 'gitignore', 'editorconfig',
572
+ ]);
573
+ return textExts.has(ext);
574
+ }
575
+ /** 跳过 Gateway RPC,直接用 Node.js fs 递归扫描 agent 工作区目录 */
576
+ async agentFilesListLocal(agentId) {
577
+ const workspaceDir = this.resolveWorkspaceDir(agentId);
578
+ const files = await this.scanWorkspaceDir(workspaceDir);
579
+ return {
580
+ agentId,
581
+ workspace: workspaceDir,
582
+ files,
583
+ };
584
+ }
585
+ /** 根据 agentId 推断 workspace 目录路径 */
586
+ resolveWorkspaceDir(agentId) {
587
+ const home = os.homedir();
588
+ if (agentId === 'main') {
589
+ return path.join(home, '.openclaw', 'workspace');
590
+ }
591
+ // 其他 agent 尝试 workspace-{agentId} 命名
592
+ return path.join(home, '.openclaw', `workspace-${agentId}`);
593
+ }
594
+ /**
595
+ * 2026-09-01 扫描工作区时跳过的重型目录黑名单。
596
+ * agent 在工作区创建软件项目时会产生 node_modules/dist/build 等海量文件(几万~几十万),
597
+ * 全量递归扫描会导致 MQTT 返回超大 JSON,客户端工作区列表 30s 超时。
598
+ */
599
+ static SKIP_WORKSPACE_DIRS = new Set([
600
+ 'node_modules', 'dist', 'build', '.next', 'target',
601
+ '.git', '.clawhub',
602
+ ]);
603
+ /** 递归扫描工作区目录,跳过隐藏目录(.git, .clawhub, .openclaw 等) */
604
+ async scanWorkspaceDir(dirPath, relativePrefix = '') {
605
+ const results = [];
606
+ let entries;
607
+ try {
608
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
609
+ }
610
+ catch {
611
+ // 目录不存在或无权限 → 返回空
612
+ return results;
613
+ }
614
+ for (const entry of entries) {
615
+ // 跳过隐藏目录
616
+ if (entry.isDirectory() && entry.name.startsWith('.'))
617
+ continue;
618
+ // 2026-09-01 跳过软件项目重型目录(node_modules/dist/build 等),避免全量扫描卡死客户端
619
+ if (entry.isDirectory() && WSClient.SKIP_WORKSPACE_DIRS.has(entry.name)) {
620
+ this.log(`[workspace] 跳过重型目录: ${relativePrefix ? relativePrefix + '/' : ''}${entry.name}`, 'debug');
621
+ continue;
622
+ }
623
+ const fullPath = path.join(dirPath, entry.name);
624
+ const relativePath = relativePrefix
625
+ ? path.join(relativePrefix, entry.name)
626
+ : entry.name;
627
+ let stat;
628
+ try {
629
+ stat = await fs.stat(fullPath);
630
+ }
631
+ catch {
632
+ continue; // 无法 stat 的文件跳过
633
+ }
634
+ if (entry.isDirectory()) {
635
+ // 先注册目录自身(方便客户端导航)
636
+ results.push({
637
+ name: entry.name,
638
+ path: fullPath,
639
+ relativePath,
640
+ type: 'directory',
641
+ size: 0,
642
+ modifiedAt: stat.mtimeMs,
643
+ });
644
+ // 递归子目录
645
+ const subFiles = await this.scanWorkspaceDir(fullPath, relativePath);
646
+ results.push(...subFiles);
647
+ }
648
+ else {
649
+ results.push({
650
+ name: entry.name,
651
+ path: fullPath,
652
+ relativePath,
653
+ type: 'file',
654
+ size: stat.size,
655
+ modifiedAt: stat.mtimeMs,
656
+ });
657
+ }
658
+ }
659
+ return results;
660
+ }
661
+ // ==================== 私有方法 ====================
662
+ send(message) {
663
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
664
+ throw new Error('WebSocket is not connected');
665
+ }
666
+ this.ws.send(JSON.stringify(message));
667
+ }
668
+ //握手
669
+ async sendHandshake(nonce) {
670
+ const { deviceToken, deviceId, publicKey, privateKeyPem } = this.deviceConfig;
671
+ const opts = {
672
+ type: 'req',
673
+ id: this.generateId(),
674
+ method: 'connect',
675
+ params: {
676
+ minProtocol: 3,
677
+ maxProtocol: 4,
678
+ client: {
679
+ id: 'gateway-client', //cli->mode=operator,gateway-client->node,
680
+ instanceId: "dlink-cmd-channel",
681
+ version: '1.0.0',
682
+ platform: 'web',
683
+ mode: 'backend' //'node'
684
+ },
685
+ role: 'operator',
686
+ scopes: ['operator.read', 'operator.write'],
687
+ auth: {},
688
+ device: {}
689
+ }
690
+ };
691
+ const extScopes = ['operator.approvals', 'operator.talk.secrets', 'operator.admin'];
692
+ if (this.token && this.token.length > 0) {
693
+ //console.log("使用共享Token握手 >>> ")
694
+ const { signature, signedAtMs, } = signConnectPayload({
695
+ deviceId,
696
+ privateKeyPem,
697
+ token: this.token,
698
+ clientId: opts.params.client.id,
699
+ clientMode: opts.params.client.mode,
700
+ role: opts.params.role,
701
+ scopes: [...opts.params.scopes, ...extScopes],
702
+ platform: opts.params.client.platform,
703
+ nonce: nonce
704
+ });
705
+ opts.params = {
706
+ ...opts.params,
707
+ scopes: [...opts.params.scopes, ...extScopes],
708
+ auth: { token: this.token },
709
+ device: {
710
+ id: deviceId,
711
+ publicKey,
712
+ signature,
713
+ signedAt: signedAtMs,
714
+ nonce
715
+ }
716
+ };
717
+ }
718
+ else if (!deviceToken || deviceToken.length === 0) {
719
+ const bootstrapToken = await reqBootstrapToken(this.gatewayUrl);
720
+ this.log("使用bootstrapToken握手", 'debug');
721
+ const { signature, signedAtMs, } = signConnectPayload({
722
+ deviceId,
723
+ privateKeyPem,
724
+ token: bootstrapToken,
725
+ clientId: opts.params.client.id,
726
+ clientMode: opts.params.client.mode,
727
+ role: opts.params.role,
728
+ scopes: [...opts.params.scopes],
729
+ platform: opts.params.client.platform,
730
+ nonce: nonce
731
+ });
732
+ opts.params = {
733
+ ...opts.params,
734
+ auth: { bootstrapToken: bootstrapToken },
735
+ device: {
736
+ id: deviceId,
737
+ publicKey,
738
+ signature,
739
+ signedAt: signedAtMs,
740
+ nonce
741
+ }
742
+ };
743
+ }
744
+ else {
745
+ this.log('使用deviceToken握手..', 'debug');
746
+ const { signature, signedAtMs, } = signConnectPayload({
747
+ deviceId,
748
+ privateKeyPem,
749
+ token: deviceToken,
750
+ clientId: opts.params.client.id,
751
+ clientMode: opts.params.client.mode,
752
+ role: opts.params.role,
753
+ scopes: [...opts.params.scopes, ...extScopes],
754
+ platform: opts.params.client.platform,
755
+ nonce: nonce
756
+ });
757
+ opts.params = {
758
+ ...opts.params,
759
+ scopes: [...opts.params.scopes, ...extScopes],
760
+ auth: {
761
+ deviceToken: deviceToken
762
+ },
763
+ device: {
764
+ id: deviceId,
765
+ publicKey,
766
+ signature,
767
+ signedAt: signedAtMs,
768
+ nonce
769
+ }
770
+ };
771
+ }
772
+ this.send(opts);
773
+ }
774
+ /**
775
+ * 2026-09-06 适配 openclaw 2026.9.1:通用 Gateway 方法调用(配对审批等宿主 RPC)
776
+ * 9.1 起部分能力(如 channels.pairing.approve)只以宿主 RPC 形式提供,
777
+ * 插件通过已认证的 Gateway WebSocket 调用,参数透传宿主 handler。
778
+ */
779
+ async callGatewayMethod(method, params) {
780
+ if (!this.isReady()) {
781
+ throw new Error(`Gateway WebSocket 未连接,无法调用 ${method}`);
782
+ }
783
+ return this.sendRequest({ type: 'req', method, params });
784
+ }
785
+ sendRequest(message) {
786
+ const id = this.generateId();
787
+ const fullMessage = { ...message, id };
788
+ return new Promise((resolve, reject) => {
789
+ const timeout = setTimeout(() => {
790
+ this.callbacks.delete(id);
791
+ reject(new Error('Request timeout after 30 seconds'));
792
+ }, 30000);
793
+ this.callbacks.set(id, { resolve, reject, timeout });
794
+ this.send(fullMessage);
795
+ });
796
+ }
797
+ async handleMessage(message) {
798
+ if (message.type === 'event') {
799
+ const event = message.event;
800
+ switch (event) {
801
+ case 'tick':
802
+ //心跳消息
803
+ this.emit('onTick', message.payload);
804
+ break;
805
+ case 'health':
806
+ //健康装态消息
807
+ this.emit('onHealth', message.payload);
808
+ break;
809
+ case 'connect.challenge':
810
+ //WS连接成功,开始握手
811
+ //console.log('gateway token >>> ', this.token)
812
+ this.log('WS连接成功,开始握手...', 'info');
813
+ this.sendHandshake(message.payload.nonce);
814
+ break;
815
+ case 'agent':
816
+ //agent回复
817
+ this.log(`onAgentReply >>> ${message.payload}`, 'warn');
818
+ this.emit('onAgentReply', message.payload);
819
+ break;
820
+ case 'chat':
821
+ this.log(`onAgentChat >>> ${message.payload}`, 'warn');
822
+ this.emit('onAgentChat', message.payload);
823
+ break;
824
+ case 'cron':
825
+ // 定时任务生命周期事件(创建/更新/删除/执行完成等)
826
+ this.log(`CRON事件 >>> ${message.payload}`, 'warn');
827
+ this.emit('onCron', message.payload);
828
+ break;
829
+ case 'shutdown':
830
+ // 网关关闭通知
831
+ this.log('收到网关关闭通知', 'warn'); //好像没有效果
832
+ this.emit('onShutdown', message.payload);
833
+ break;
834
+ case 'sessions.changed':
835
+ // 会话变更通知
836
+ this.log(`会话变更通知 >>> ${message.payload}`, 'warn');
837
+ this.emit('onSessionsChanged', message.payload);
838
+ break;
839
+ case 'session.message':
840
+ this.log(`会话消息,含中间回复 >>> ${message.payload}`, 'warn');
841
+ this.emit('onSessionMessage', message.payload);
842
+ break;
843
+ case 'presence':
844
+ //该事件触发频率密度高,一些平台及环境信息,可以忽略
845
+ break;
846
+ case 'heartbeat':
847
+ this.emit('onHeartbeat', message.payload);
848
+ break;
849
+ default:
850
+ this.log(`Ws监听未知/未处理的事件 >>> ${event}, payload=${JSON.stringify(message.payload)}`, 'warn');
851
+ }
852
+ return;
853
+ }
854
+ if (message.type === 'res') {
855
+ const callback = this.callbacks.get(message.id);
856
+ if (message.ok) {
857
+ const payload = message.payload;
858
+ if (payload) {
859
+ const type = payload.type;
860
+ switch (type) {
861
+ case 'hello-ok':
862
+ this.log('WS握手成功...', 'info');
863
+ const dToken = payload.auth.deviceToken;
864
+ if (dToken && dToken.length > 0) {
865
+ if (!this.deviceConfig.deviceToken || this.deviceConfig.deviceToken.length === 0) {
866
+ saveDeviceConfig({
867
+ ...this.deviceConfig,
868
+ deviceToken: dToken
869
+ });
870
+ }
871
+ }
872
+ this.isHandshaked = true;
873
+ this.isConnected = true;
874
+ this.emit('onHandshaked', payload);
875
+ break;
876
+ }
877
+ }
878
+ callback?.resolve(payload);
879
+ }
880
+ else {
881
+ const error = message.error;
882
+ if (error) {
883
+ callback?.reject(new Error(`${error.code}, ${error.message}`));
884
+ }
885
+ else {
886
+ callback?.reject(new Error('Request failed'));
887
+ }
888
+ }
889
+ this.callbacks.delete(message.id);
890
+ }
891
+ }
892
+ generateId() {
893
+ return `msg_${this.messageId++}_${Date.now()}`;
894
+ }
895
+ }
896
+ export default WSClient;