dlinkclaw-openclaw 2026.8.24-alpha.1

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 (77) hide show
  1. package/README.en.md +46 -0
  2. package/README.md +25 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.js +1 -0
  5. package/dist/package.json +95 -0
  6. package/dist/src/account.d.ts +53 -0
  7. package/dist/src/account.js +394 -0
  8. package/dist/src/channel.d.ts +17 -0
  9. package/dist/src/channel.js +153 -0
  10. package/dist/src/client/client-manager.d.ts +4 -0
  11. package/dist/src/client/client-manager.js +10 -0
  12. package/dist/src/client/const.d.ts +37 -0
  13. package/dist/src/client/const.js +155 -0
  14. package/dist/src/client/crypto.d.ts +47 -0
  15. package/dist/src/client/crypto.js +260 -0
  16. package/dist/src/client/dl-client.d.ts +99 -0
  17. package/dist/src/client/dl-client.js +535 -0
  18. package/dist/src/client/dl-http.d.ts +27 -0
  19. package/dist/src/client/dl-http.js +223 -0
  20. package/dist/src/client/media-handler.d.ts +21 -0
  21. package/dist/src/client/media-handler.js +196 -0
  22. package/dist/src/client/message.d.ts +96 -0
  23. package/dist/src/client/message.js +1204 -0
  24. package/dist/src/client/timeout.d.ts +20 -0
  25. package/dist/src/client/timeout.js +36 -0
  26. package/dist/src/client/types.d.ts +224 -0
  27. package/dist/src/client/types.js +1 -0
  28. package/dist/src/monitor.d.ts +17 -0
  29. package/dist/src/monitor.js +320 -0
  30. package/dist/src/outbound.d.ts +4 -0
  31. package/dist/src/outbound.js +83 -0
  32. package/dist/src/policy.d.ts +42 -0
  33. package/dist/src/policy.js +205 -0
  34. package/dist/src/runtime.d.ts +9 -0
  35. package/dist/src/runtime.js +65 -0
  36. package/dist/src/services/index.d.ts +41 -0
  37. package/dist/src/services/index.js +225 -0
  38. package/dist/src/services/services.d.ts +152 -0
  39. package/dist/src/services/services.js +1066 -0
  40. package/dist/src/services/types.d.ts +391 -0
  41. package/dist/src/services/types.js +5 -0
  42. package/dist/src/services/ws-client.d.ts +174 -0
  43. package/dist/src/services/ws-client.js +866 -0
  44. package/dist/src/utils.d.ts +20 -0
  45. package/dist/src/utils.js +128 -0
  46. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  47. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  48. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  49. package/ext_capabilities/workrelay-speak/README.md +79 -0
  50. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  51. package/ext_capabilities/workrelay-speak/install.sh +280 -0
  52. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  53. package/ext_capabilities/workrelay-speak/manifest.json +49 -0
  54. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  55. package/ext_capabilities/workrelay-speak/status.sh +90 -0
  56. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  57. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  58. package/ext_capabilities/workrelay-speak-luxtts/install.sh +282 -0
  59. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +49 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/status.sh +94 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  67. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  68. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  69. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  70. package/ext_capabilities/workrelay-transcribe/install.sh +207 -0
  71. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  72. package/ext_capabilities/workrelay-transcribe/status.sh +62 -0
  73. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  74. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  75. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  76. package/openclaw.plugin.json +142 -0
  77. package/package.json +95 -0
@@ -0,0 +1,866 @@
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() {
239
+ return this.sendRequest({
240
+ type: 'req',
241
+ method: 'models.list',
242
+ params: {}
243
+ });
244
+ }
245
+ async sessionsList(params) {
246
+ const p = {
247
+ //limit:params.limit, //number
248
+ activeMinutes: params.activeMinutes, //number
249
+ includeGlobal: params.includeGlobal, //bool
250
+ includeDerivedTitles: params.includeDerivedTitles, //bool
251
+ includeLastMessage: params.includeLastMessage, //bool,
252
+ label: params.label, //string,
253
+ spawnedBy: params.spawnedBy,
254
+ agentId: params.agentId, //必填
255
+ search: params.search, //string关键字
256
+ archived: params.archived //bool: true=归档会话, false/omit=活跃会话
257
+ };
258
+ if (params.limit) {
259
+ p.limit = params.limit;
260
+ }
261
+ return this.sendRequest({
262
+ type: 'req',
263
+ method: 'sessions.list',
264
+ params: p
265
+ });
266
+ }
267
+ async sessionsGet(params) {
268
+ const p = {
269
+ sessionKey: params.sessionKey,
270
+ key: params.key,
271
+ limit: params.limit
272
+ };
273
+ return this.sendRequest({
274
+ type: 'req',
275
+ method: 'sessions.get',
276
+ params: p
277
+ });
278
+ }
279
+ async sessionsDescribe(params) {
280
+ return this.sendRequest({
281
+ type: 'req',
282
+ method: 'sessions.describe',
283
+ params
284
+ });
285
+ }
286
+ async sessionsResolve(params) {
287
+ const p = {
288
+ key: params.sessionKey,
289
+ sessionId: params.sessionId,
290
+ agentId: params.agentId,
291
+ includeGlobal: params.includeGlobal, //Bool
292
+ includeUnknown: params.includeUnknown // Bool
293
+ };
294
+ if (params.sessionLabel) {
295
+ p.label = params.sessionLabel;
296
+ }
297
+ if (params.spawnedBy) {
298
+ p.spawnedBy = params.spawnedBy;
299
+ }
300
+ return this.sendRequest({
301
+ type: 'req',
302
+ method: 'sessions.resolve',
303
+ params: p
304
+ });
305
+ }
306
+ async sessionReset(params) {
307
+ return this.sendRequest({
308
+ type: 'req',
309
+ method: 'sessions.reset',
310
+ params
311
+ });
312
+ }
313
+ async chatHistories(params) {
314
+ return this.sendRequest({
315
+ type: 'req',
316
+ method: 'chat.history',
317
+ params
318
+ });
319
+ }
320
+ // ==================== Skills API ====================
321
+ async listSkills(agentId) {
322
+ const params = {};
323
+ if (agentId)
324
+ params.agentId = agentId;
325
+ return this.sendRequest({
326
+ type: 'req',
327
+ method: 'skills.status',
328
+ params
329
+ });
330
+ }
331
+ async searchSkills(params) {
332
+ return this.sendRequest({
333
+ type: 'req',
334
+ method: 'skills.search',
335
+ params: params.query ? { query: params.query, limit: params.limit || 20 } : { limit: params.limit || 20 }
336
+ });
337
+ }
338
+ async getSkillDetail(slug) {
339
+ return this.sendRequest({
340
+ type: 'req',
341
+ method: 'skills.detail',
342
+ params: { slug }
343
+ });
344
+ }
345
+ async installSkill(slug, version) {
346
+ return this.sendRequest({
347
+ type: 'req',
348
+ method: 'skills.install',
349
+ params: { source: 'clawhub', slug, ...(version ? { version } : {}) }
350
+ });
351
+ }
352
+ async updateSkillConfig(skillKey, enabled) {
353
+ return this.sendRequest({
354
+ type: 'req',
355
+ method: 'skills.update',
356
+ params: { skillKey, enabled }
357
+ });
358
+ }
359
+ /**
360
+ * 取消当前Session的Agent执行
361
+ * 发送 /queue interrupt 到Gateway中断活跃run
362
+ * @param channelId 频道ID(如 dlink),由services层传入,避免硬编码
363
+ * @param chatType 会话类型 direct | group,由WorkRelay客户端传入
364
+ */
365
+ // ==================== 会话管理 API(CRUD) ====================
366
+ /**
367
+ * 创建新会话
368
+ * @param params.key - 完整的 sessionKey
369
+ */
370
+ async sessionsCreate(params) {
371
+ return this.sendRequest({
372
+ type: 'req',
373
+ method: 'sessions.create',
374
+ params: { key: params.key }
375
+ });
376
+ }
377
+ /**
378
+ * 更新会话元数据(归档/恢复等)
379
+ * @param params.key - sessionKey
380
+ * @param params.patch - 要更新的字段,如 { archived: true }
381
+ */
382
+ async sessionsPatch(params) {
383
+ return this.sendRequest({
384
+ type: 'req',
385
+ method: 'sessions.patch',
386
+ params: { key: params.key, ...params.patch }
387
+ });
388
+ }
389
+ /**
390
+ * 删除会话
391
+ * @param params.key - sessionKey
392
+ * @param params.deleteFiles - 是否同时删除会话相关文件(貌似不支持该参数)
393
+ */
394
+ async sessionsDelete(params) {
395
+ return this.sendRequest({
396
+ type: 'req',
397
+ method: 'sessions.delete',
398
+ params: { key: params.key, emitLifecycleHooks: true }
399
+ });
400
+ }
401
+ /** 订阅会话变更事件(需在握手成功后调用) */
402
+ async subscribeSessions() {
403
+ return this.sendRequest({
404
+ type: 'req',
405
+ method: 'sessions.subscribe'
406
+ });
407
+ }
408
+ async cancelSession({ sessionKey }) {
409
+ //const sessionKey = `agent:${agentId}:${channelId}:${chatType}:${userId.toLowerCase()}`;
410
+ const agentId = sessionKey.split(':')[1];
411
+ const messageId = `cancel_${Date.now()}`;
412
+ return this.sendRequest({
413
+ type: 'req',
414
+ method: 'agent',
415
+ params: {
416
+ agentId,
417
+ message: '/queue interrupt',
418
+ sessionKey,
419
+ deliver: false,
420
+ idempotencyKey: messageId
421
+ }
422
+ });
423
+ }
424
+ /**
425
+ * 发送 /stop 到Gateway,强制中止活跃run(比 /queue interrupt 更快)
426
+ * 对应 cmd 通道 agent.stop 指令
427
+ */
428
+ async stopSession({ sessionKey }) {
429
+ //const sessionKey = `agent:${agentId}:${channelId}:${chatType}:${userId.toLowerCase()}`;
430
+ const agentId = sessionKey.split(':')[1];
431
+ const messageId = `stop_${Date.now()}`;
432
+ return this.sendRequest({
433
+ type: 'req',
434
+ method: 'agent',
435
+ params: {
436
+ agentId,
437
+ message: '/stop',
438
+ sessionKey,
439
+ deliver: false,
440
+ idempotencyKey: messageId
441
+ }
442
+ });
443
+ }
444
+ async chatAbort(params) {
445
+ const { sessionKey, runId } = params;
446
+ return this.sendRequest({
447
+ type: 'req',
448
+ method: 'chat.abort',
449
+ params: {
450
+ sessionKey,
451
+ runId
452
+ }
453
+ });
454
+ }
455
+ async getSkillSecurityVerdicts(agentId) {
456
+ const params = {};
457
+ if (agentId)
458
+ params.agentId = agentId;
459
+ return this.sendRequest({
460
+ type: 'req',
461
+ method: 'skills.securityVerdicts',
462
+ params
463
+ });
464
+ }
465
+ // 仪表盘:按日期范围聚合成本
466
+ async getUsageCost(params) {
467
+ return this.sendRequest({
468
+ type: 'req',
469
+ method: 'usage.cost',
470
+ params
471
+ });
472
+ }
473
+ // 仪表盘:每 session 详细使用统计
474
+ async getSessionsUsage(params) {
475
+ return this.sendRequest({
476
+ type: 'req',
477
+ method: 'sessions.usage',
478
+ params
479
+ });
480
+ }
481
+ // 仪表盘:每消息级别使用日志
482
+ async getSessionsUsageLogs(params) {
483
+ return this.sendRequest({
484
+ type: 'req',
485
+ method: 'sessions.usage.logs',
486
+ params
487
+ });
488
+ }
489
+ // ==================== 工作区 / Artifact API ====================
490
+ async artifactsList(params) {
491
+ return this.sendRequest({
492
+ type: 'req',
493
+ method: 'artifacts.list',
494
+ params
495
+ });
496
+ }
497
+ async artifactsGet(params) {
498
+ return this.sendRequest({
499
+ type: 'req',
500
+ method: 'artifacts.get',
501
+ params
502
+ });
503
+ }
504
+ async artifactsDownload(params) {
505
+ return this.sendRequest({
506
+ type: 'req',
507
+ method: 'artifacts.download',
508
+ params
509
+ });
510
+ }
511
+ /** 列出 Agent 工作区文件(agents.files.list) */
512
+ async agentFilesList(agentId) {
513
+ return this.sendRequest({
514
+ type: 'req',
515
+ method: 'agents.files.list',
516
+ params: { agentId }
517
+ });
518
+ }
519
+ /** 获取 Agent 工作区文件内容(agents.files.get) */
520
+ async agentFilesGet(agentId, filePath) {
521
+ return this.sendRequest({
522
+ type: 'req',
523
+ method: 'agents.files.get',
524
+ params: { agentId, name: filePath }
525
+ });
526
+ }
527
+ // ===== 2026-07-31 新增:本地文件系统直接读写(绕过 Gateway RPC 的文件白名单限制) =====
528
+ /** 跳过 Gateway RPC,直接用 Node.js fs 读取 agent 工作区文件内容
529
+ * 2026-07-31 base64 fix:文本文件 utf-8,二进制文件 base64 编码避免损坏 */
530
+ async agentFilesGetLocal(agentId, filePath) {
531
+ const workspaceDir = this.resolveWorkspaceDir(agentId);
532
+ const fullPath = path.join(workspaceDir, filePath);
533
+ const stat = await fs.stat(fullPath);
534
+ const isText = this.isTextFile(filePath);
535
+ const encoding = isText ? 'utf-8' : 'base64';
536
+ let content;
537
+ if (isText) {
538
+ content = await fs.readFile(fullPath, 'utf-8');
539
+ }
540
+ else {
541
+ const buffer = await fs.readFile(fullPath);
542
+ content = buffer.toString('base64');
543
+ }
544
+ return {
545
+ agentId,
546
+ workspace: workspaceDir,
547
+ file: {
548
+ name: filePath,
549
+ path: fullPath,
550
+ missing: false,
551
+ size: stat.size,
552
+ updatedAtMs: Math.floor(stat.mtimeMs),
553
+ content,
554
+ encoding,
555
+ },
556
+ };
557
+ }
558
+ /** 根据扩展名判断是否文本文件 */
559
+ isTextFile(filename) {
560
+ const ext = path.extname(filename).slice(1).toLowerCase();
561
+ const textExts = new Set([
562
+ 'md', 'mdx', 'txt', 'log', 'json', 'xml', 'yaml', 'yml', 'toml',
563
+ 'ts', 'tsx', 'js', 'jsx', 'jsm', 'py', 'java', 'go', 'rs', 'c', 'cpp', 'h', 'hpp',
564
+ 'css', 'scss', 'less', 'html', 'htm', 'svg', 'csv', 'env', 'cfg', 'ini',
565
+ 'sh', 'bash', 'zsh', 'sql', 'rb', 'php', 'swift', 'kt', 'r', 'lua',
566
+ 'conf', 'properties', 'gitignore', 'editorconfig',
567
+ ]);
568
+ return textExts.has(ext);
569
+ }
570
+ /** 跳过 Gateway RPC,直接用 Node.js fs 递归扫描 agent 工作区目录 */
571
+ async agentFilesListLocal(agentId) {
572
+ const workspaceDir = this.resolveWorkspaceDir(agentId);
573
+ const files = await this.scanWorkspaceDir(workspaceDir);
574
+ return {
575
+ agentId,
576
+ workspace: workspaceDir,
577
+ files,
578
+ };
579
+ }
580
+ /** 根据 agentId 推断 workspace 目录路径 */
581
+ resolveWorkspaceDir(agentId) {
582
+ const home = os.homedir();
583
+ if (agentId === 'main') {
584
+ return path.join(home, '.openclaw', 'workspace');
585
+ }
586
+ // 其他 agent 尝试 workspace-{agentId} 命名
587
+ return path.join(home, '.openclaw', `workspace-${agentId}`);
588
+ }
589
+ /** 递归扫描工作区目录,跳过隐藏目录(.git, .clawhub, .openclaw 等) */
590
+ async scanWorkspaceDir(dirPath, relativePrefix = '') {
591
+ const results = [];
592
+ let entries;
593
+ try {
594
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
595
+ }
596
+ catch {
597
+ // 目录不存在或无权限 → 返回空
598
+ return results;
599
+ }
600
+ for (const entry of entries) {
601
+ // 跳过隐藏目录
602
+ if (entry.isDirectory() && entry.name.startsWith('.'))
603
+ continue;
604
+ const fullPath = path.join(dirPath, entry.name);
605
+ const relativePath = relativePrefix
606
+ ? path.join(relativePrefix, entry.name)
607
+ : entry.name;
608
+ let stat;
609
+ try {
610
+ stat = await fs.stat(fullPath);
611
+ }
612
+ catch {
613
+ continue; // 无法 stat 的文件跳过
614
+ }
615
+ if (entry.isDirectory()) {
616
+ // 先注册目录自身(方便客户端导航)
617
+ results.push({
618
+ name: entry.name,
619
+ path: fullPath,
620
+ relativePath,
621
+ type: 'directory',
622
+ size: 0,
623
+ modifiedAt: stat.mtimeMs,
624
+ });
625
+ // 递归子目录
626
+ const subFiles = await this.scanWorkspaceDir(fullPath, relativePath);
627
+ results.push(...subFiles);
628
+ }
629
+ else {
630
+ results.push({
631
+ name: entry.name,
632
+ path: fullPath,
633
+ relativePath,
634
+ type: 'file',
635
+ size: stat.size,
636
+ modifiedAt: stat.mtimeMs,
637
+ });
638
+ }
639
+ }
640
+ return results;
641
+ }
642
+ // ==================== 私有方法 ====================
643
+ send(message) {
644
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
645
+ throw new Error('WebSocket is not connected');
646
+ }
647
+ this.ws.send(JSON.stringify(message));
648
+ }
649
+ //握手
650
+ async sendHandshake(nonce) {
651
+ const { deviceToken, deviceId, publicKey, privateKeyPem } = this.deviceConfig;
652
+ const opts = {
653
+ type: 'req',
654
+ id: this.generateId(),
655
+ method: 'connect',
656
+ params: {
657
+ minProtocol: 3,
658
+ maxProtocol: 4,
659
+ client: {
660
+ id: 'gateway-client', //cli->mode=operator,gateway-client->node,
661
+ instanceId: "dlink-cmd-channel",
662
+ version: '1.0.0',
663
+ platform: 'web',
664
+ mode: 'backend' //'node'
665
+ },
666
+ role: 'operator',
667
+ scopes: ['operator.read', 'operator.write'],
668
+ auth: {},
669
+ device: {}
670
+ }
671
+ };
672
+ const extScopes = ['operator.approvals', 'operator.talk.secrets', 'operator.admin'];
673
+ if (this.token && this.token.length > 0) {
674
+ //console.log("使用共享Token握手 >>> ")
675
+ const { signature, signedAtMs, } = signConnectPayload({
676
+ deviceId,
677
+ privateKeyPem,
678
+ token: this.token,
679
+ clientId: opts.params.client.id,
680
+ clientMode: opts.params.client.mode,
681
+ role: opts.params.role,
682
+ scopes: [...opts.params.scopes, ...extScopes],
683
+ platform: opts.params.client.platform,
684
+ nonce: nonce
685
+ });
686
+ opts.params = {
687
+ ...opts.params,
688
+ scopes: [...opts.params.scopes, ...extScopes],
689
+ auth: { token: this.token },
690
+ device: {
691
+ id: deviceId,
692
+ publicKey,
693
+ signature,
694
+ signedAt: signedAtMs,
695
+ nonce
696
+ }
697
+ };
698
+ }
699
+ else if (!deviceToken || deviceToken.length === 0) {
700
+ const bootstrapToken = await reqBootstrapToken(this.gatewayUrl);
701
+ this.log("使用bootstrapToken握手", 'debug');
702
+ const { signature, signedAtMs, } = signConnectPayload({
703
+ deviceId,
704
+ privateKeyPem,
705
+ token: bootstrapToken,
706
+ clientId: opts.params.client.id,
707
+ clientMode: opts.params.client.mode,
708
+ role: opts.params.role,
709
+ scopes: [...opts.params.scopes],
710
+ platform: opts.params.client.platform,
711
+ nonce: nonce
712
+ });
713
+ opts.params = {
714
+ ...opts.params,
715
+ auth: { bootstrapToken: bootstrapToken },
716
+ device: {
717
+ id: deviceId,
718
+ publicKey,
719
+ signature,
720
+ signedAt: signedAtMs,
721
+ nonce
722
+ }
723
+ };
724
+ }
725
+ else {
726
+ this.log('使用deviceToken握手..', 'debug');
727
+ const { signature, signedAtMs, } = signConnectPayload({
728
+ deviceId,
729
+ privateKeyPem,
730
+ token: deviceToken,
731
+ clientId: opts.params.client.id,
732
+ clientMode: opts.params.client.mode,
733
+ role: opts.params.role,
734
+ scopes: [...opts.params.scopes, ...extScopes],
735
+ platform: opts.params.client.platform,
736
+ nonce: nonce
737
+ });
738
+ opts.params = {
739
+ ...opts.params,
740
+ scopes: [...opts.params.scopes, ...extScopes],
741
+ auth: {
742
+ deviceToken: deviceToken
743
+ },
744
+ device: {
745
+ id: deviceId,
746
+ publicKey,
747
+ signature,
748
+ signedAt: signedAtMs,
749
+ nonce
750
+ }
751
+ };
752
+ }
753
+ this.send(opts);
754
+ }
755
+ sendRequest(message) {
756
+ const id = this.generateId();
757
+ const fullMessage = { ...message, id };
758
+ return new Promise((resolve, reject) => {
759
+ const timeout = setTimeout(() => {
760
+ this.callbacks.delete(id);
761
+ reject(new Error('Request timeout after 30 seconds'));
762
+ }, 30000);
763
+ this.callbacks.set(id, { resolve, reject, timeout });
764
+ this.send(fullMessage);
765
+ });
766
+ }
767
+ async handleMessage(message) {
768
+ if (message.type === 'event') {
769
+ const event = message.event;
770
+ switch (event) {
771
+ case 'tick':
772
+ //心跳消息
773
+ this.emit('onTick', message.payload);
774
+ break;
775
+ case 'health':
776
+ //健康装态消息
777
+ this.emit('onHealth', message.payload);
778
+ break;
779
+ case 'connect.challenge':
780
+ //WS连接成功,开始握手
781
+ //console.log('gateway token >>> ', this.token)
782
+ this.log('WS连接成功,开始握手...', 'info');
783
+ this.sendHandshake(message.payload.nonce);
784
+ break;
785
+ case 'agent':
786
+ //agent回复
787
+ this.log(`onAgentReply >>> ${message.payload}`, 'warn');
788
+ this.emit('onAgentReply', message.payload);
789
+ break;
790
+ case 'chat':
791
+ this.log(`onAgentChat >>> ${message.payload}`, 'warn');
792
+ this.emit('onAgentChat', message.payload);
793
+ break;
794
+ case 'cron':
795
+ // 定时任务生命周期事件(创建/更新/删除/执行完成等)
796
+ this.log(`CRON事件 >>> ${message.payload}`, 'warn');
797
+ this.emit('onCron', message.payload);
798
+ break;
799
+ case 'shutdown':
800
+ // 网关关闭通知
801
+ this.log('收到网关关闭通知', 'warn'); //好像没有效果
802
+ this.emit('onShutdown', message.payload);
803
+ break;
804
+ case 'sessions.changed':
805
+ // 会话变更通知
806
+ this.log(`会话变更通知 >>> ${message.payload}`, 'warn');
807
+ this.emit('onSessionsChanged', message.payload);
808
+ break;
809
+ case 'session.message':
810
+ this.log(`会话消息,含中间回复 >>> ${message.payload}`, 'warn');
811
+ this.emit('onSessionMessage', message.payload);
812
+ break;
813
+ case 'presence':
814
+ //该事件触发频率密度高,一些平台及环境信息,可以忽略
815
+ break;
816
+ case 'heartbeat':
817
+ this.emit('onHeartbeat', message.payload);
818
+ break;
819
+ default:
820
+ this.log(`Ws监听未知/未处理的事件 >>> ${event}, payload=${JSON.stringify(message.payload)}`, 'warn');
821
+ }
822
+ return;
823
+ }
824
+ if (message.type === 'res') {
825
+ const callback = this.callbacks.get(message.id);
826
+ if (message.ok) {
827
+ const payload = message.payload;
828
+ if (payload) {
829
+ const type = payload.type;
830
+ switch (type) {
831
+ case 'hello-ok':
832
+ this.log('WS握手成功...', 'info');
833
+ const dToken = payload.auth.deviceToken;
834
+ if (dToken && dToken.length > 0) {
835
+ if (!this.deviceConfig.deviceToken || this.deviceConfig.deviceToken.length === 0) {
836
+ saveDeviceConfig({
837
+ ...this.deviceConfig,
838
+ deviceToken: dToken
839
+ });
840
+ }
841
+ }
842
+ this.isHandshaked = true;
843
+ this.isConnected = true;
844
+ this.emit('onHandshaked', payload);
845
+ break;
846
+ }
847
+ }
848
+ callback?.resolve(payload);
849
+ }
850
+ else {
851
+ const error = message.error;
852
+ if (error) {
853
+ callback?.reject(new Error(`${error.code}, ${error.message}`));
854
+ }
855
+ else {
856
+ callback?.reject(new Error('Request failed'));
857
+ }
858
+ }
859
+ this.callbacks.delete(message.id);
860
+ }
861
+ }
862
+ generateId() {
863
+ return `msg_${this.messageId++}_${Date.now()}`;
864
+ }
865
+ }
866
+ export default WSClient;