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,1066 @@
1
+ /**
2
+ * Claw-Services 主服务接口
3
+ * 为 duolian 插件提供 OpenClaw API 封装
4
+ */
5
+ import EventEmitter from 'events';
6
+ import { exec, spawn } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import WSClient from './ws-client.js';
11
+ import { getOpenclawPath, getExtCapabilitiesDir } from '../utils.js';
12
+ import { applyModelOverrideToSessionEntry } from 'openclaw/plugin-sdk/model-session-runtime';
13
+ import { getSessionEntry, patchSessionEntry, resolveStorePath } from 'openclaw/plugin-sdk/config-runtime';
14
+ import { approveChannelPairingCode } from 'openclaw/plugin-sdk/conversation-runtime';
15
+ import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/core';
16
+ import { markRunStopped } from '../runtime.js';
17
+ /**
18
+ * Claw-Services 主类
19
+ * 处理 CMD 消息并调用相应的 OpenClaw API
20
+ */
21
+ export class ClawServices extends EventEmitter {
22
+ client = null;
23
+ options;
24
+ isInitialized = false;
25
+ logger;
26
+ constructor(options, logger) {
27
+ super();
28
+ this.options = options;
29
+ this.logger = logger;
30
+ }
31
+ log(msg, type) {
32
+ switch (type) {
33
+ case 'info':
34
+ this.logger.info(`✅ CmdService ${msg}`);
35
+ break;
36
+ case 'warn':
37
+ this.logger.warn(`❗️CmdService ${msg}`);
38
+ break;
39
+ case 'error':
40
+ this.logger.error(`❌️ CmdService ${msg}`);
41
+ break;
42
+ case 'debug':
43
+ if (this.logger.debug) {
44
+ this.logger.debug(`〰️ CmdService ${msg}`);
45
+ }
46
+ else {
47
+ console.debug(`〰️ CmdService ${msg}`);
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * 初始化服务
53
+ */
54
+ async initialize() {
55
+ if (this.isInitialized) {
56
+ return true;
57
+ }
58
+ const channelId = this.options.channelId;
59
+ try {
60
+ this.client = new WSClient({
61
+ gatewayUrl: this.options.gatewayUrl,
62
+ token: this.options.token
63
+ });
64
+ this.client.on('onConnected', () => {
65
+ this.log(`ws连接成功`, 'info');
66
+ this.emit('onConnectState', true);
67
+ });
68
+ this.client.on('onDisconnect', () => {
69
+ this.log(`ws连接已断开,5秒后重连...`, 'warn');
70
+ this.emit('onConnectState', false);
71
+ setTimeout(() => this.client?.connect(), 5000);
72
+ });
73
+ this.client.on('onTick', (message) => {
74
+ this.log(`onTick >>> ${message.ts}`, 'debug');
75
+ message.channel = channelId;
76
+ this.emit('onTick', message);
77
+ });
78
+ this.client.on('onHealth', (message) => {
79
+ message.channel = channelId;
80
+ this.emit('onHealth', message);
81
+ });
82
+ this.client.on('onHandshaked', (message) => {
83
+ // WS 握手成功后订阅会话变更事件,实时感知归档/恢复/删除/新建
84
+ this.client?.subscribeSessions()
85
+ .then(() => this.log('已订阅 sessions.changed 事件', 'info'))
86
+ .catch((e) => this.log(`订阅 sessions.changed 失败: ${e}`, 'warn'));
87
+ this.emit('onHandshaked', message);
88
+ });
89
+ this.client.on('onAgentReply', (message) => this.emit('onAgentReply', message));
90
+ this.client.on('onAgentChat', (message) => this.emit('onAgentChat', message));
91
+ this.client.on('onCron', (message) => this.emit('onCron', message));
92
+ this.client.on('onShutdown', (message) => this.emit('onShutdown', message));
93
+ this.client.on('onSessionsChanged', (message) => this.emit('onSessionsChanged', message));
94
+ this.client.on('onSessionMessage', (message) => this.emit('onSessionMessage', message));
95
+ this.client.on('onHeartbeat', (message) => this.emit('onHeartbeat', message));
96
+ this.client.on('onError', (error) => {
97
+ this.log('发生错误:' + String(error), 'error');
98
+ this.emit('error', error);
99
+ });
100
+ await this.client.connect();
101
+ this.isInitialized = true;
102
+ return true;
103
+ }
104
+ catch (error) {
105
+ this.log('❌ Claw-Services 初始化失败:' + String(error), 'error');
106
+ this.isInitialized = false;
107
+ throw error;
108
+ }
109
+ }
110
+ /**
111
+ * 检查服务是否已初始化
112
+ */
113
+ isReady() {
114
+ return this.isInitialized && (this.client?.isReady() || false);
115
+ }
116
+ /**
117
+ * 处理 CMD 消息
118
+ */
119
+ async handleCmdMessage(cmdMessage) {
120
+ if (!this.isReady()) {
121
+ throw new Error('Claw-Services 未初始化');
122
+ }
123
+ const startTime = Date.now();
124
+ try {
125
+ let result;
126
+ // 根据命令类型调用相应的 API
127
+ switch (cmdMessage.cmd) {
128
+ // Agent管理
129
+ case 'agent.ask':
130
+ result = await this.handleAskAgent(cmdMessage.params);
131
+ break;
132
+ case 'agent.list':
133
+ result = await this.handleAgentList();
134
+ break;
135
+ case 'agent.create':
136
+ const agentId = cmdMessage.params['id'];
137
+ const agentList = await this.handleAgentList();
138
+ if (agentList && agentList.length > 0) {
139
+ const hasAgent = agentList.find(rec => rec.id === agentId);
140
+ if (hasAgent) {
141
+ result = hasAgent;
142
+ }
143
+ }
144
+ if (!result) {
145
+ result = await this.handleAgentCreate(cmdMessage.params);
146
+ }
147
+ break;
148
+ case 'agent.update':
149
+ result = await this.handleAgentUpdate(cmdMessage.params);
150
+ break;
151
+ case 'agent.delete':
152
+ result = await this.handleAgentDelete(cmdMessage.params);
153
+ break;
154
+ case 'agent.get':
155
+ result = await this.handleAgentGet(cmdMessage.params);
156
+ break;
157
+ // 任务管理
158
+ case 'task.list':
159
+ result = await this.handleTaskList(cmdMessage.params);
160
+ break;
161
+ case 'task.create':
162
+ result = await this.handleTaskCreate(cmdMessage.params);
163
+ break;
164
+ case 'task.update':
165
+ result = await this.handleTaskUpdate(cmdMessage.params);
166
+ break;
167
+ case 'task.delete':
168
+ result = await this.handleTaskDelete(cmdMessage.params);
169
+ break;
170
+ case 'task.run':
171
+ result = await this.handleTaskRun(cmdMessage.params);
172
+ break;
173
+ // 状态监控命令
174
+ case 'status.health':
175
+ result = await this.handleStatusHealth();
176
+ break;
177
+ case 'status.system':
178
+ result = await this.handleStatusSystem();
179
+ break;
180
+ case 'status.usage':
181
+ result = await this.handleStatusUsage();
182
+ break;
183
+ // 模型管理
184
+ case 'models.list':
185
+ result = await this.handleListModels();
186
+ break;
187
+ // 会话
188
+ case 'sessions.list':
189
+ result = await this.handleSessionList(cmdMessage.params);
190
+ break;
191
+ case 'sessions.get':
192
+ result = await this.handleSessionGet(cmdMessage.params);
193
+ break;
194
+ case 'sessions.describe':
195
+ result = await this.handleSessionsDescribe(cmdMessage.params);
196
+ break;
197
+ case 'sessions.resolve':
198
+ result = await this.handleSessionsResolve(cmdMessage.params);
199
+ break;
200
+ case 'sessions.create':
201
+ result = await this.handleSessionCreate(cmdMessage.params);
202
+ break;
203
+ case 'sessions.archive':
204
+ result = await this.handleSessionArchive(cmdMessage.params);
205
+ break;
206
+ case 'sessions.restore':
207
+ result = await this.handleSessionRestore(cmdMessage.params);
208
+ break;
209
+ case 'sessions.delete':
210
+ result = await this.handleSessionDelete(cmdMessage.params);
211
+ break;
212
+ case 'sessions.reset':
213
+ result = await this.handleSessionReset(cmdMessage.params);
214
+ break;
215
+ case 'sessions.patch':
216
+ result = await this.handleUpdateSessionModel(cmdMessage.params);
217
+ break;
218
+ case 'sessions.currentModel':
219
+ result = await this.handleCurrentSessionModel(cmdMessage.params);
220
+ break;
221
+ // 当前会话聊天历史记录
222
+ case 'chat.history':
223
+ result = await this.handleChatHistories(cmdMessage.params);
224
+ break;
225
+ // Skills 管理
226
+ case 'skills.status':
227
+ result = await this.handleSkillsStatus(cmdMessage.params);
228
+ break;
229
+ case 'skills.search':
230
+ result = await this.handleSkillsSearch(cmdMessage.params);
231
+ break;
232
+ case 'skills.detail':
233
+ result = await this.handleSkillDetail(cmdMessage.params);
234
+ break;
235
+ case 'skills.install':
236
+ result = await this.handleSkillInstall(cmdMessage.params);
237
+ break;
238
+ case 'skills.update':
239
+ result = await this.handleSkillUpdate(cmdMessage.params);
240
+ break;
241
+ case 'skills.securityVerdicts':
242
+ result = await this.handleSkillsSecurityVerdicts(cmdMessage.params);
243
+ break;
244
+ case 'usage.cost':
245
+ result = await this.handleUsageCost(cmdMessage.params);
246
+ break;
247
+ case 'sessions.usage':
248
+ result = await this.handleSessionsUsage(cmdMessage.params);
249
+ break;
250
+ case 'sessions.usage.logs':
251
+ result = await this.handleSessionsUsageLogs(cmdMessage.params);
252
+ break;
253
+ case 'cancelMessage':
254
+ result = await this.handleCancelMessage(cmdMessage.params);
255
+ break;
256
+ case 'agent.stop':
257
+ result = await this.handleStopAgent(cmdMessage.params);
258
+ break;
259
+ case 'chat.abort':
260
+ // 新增 cmd 通道 chat.abort , 以上两个/stop, /queue interrupt都不能立即终止执行中的会话
261
+ result = await this.handlChatAbout(cmdMessage.params);
262
+ break;
263
+ case 'pairing.approve':
264
+ const { code, empId, accountId } = cmdMessage.params ?? {};
265
+ //注意:accountId目前只能为 DEFAULT_ACCOUNT_ID, 本插件不需要使用多账户,服务端提供多帐户服务(openId提供AGENT路由),在安装插件时需提醒使用"default"
266
+ result = await this.handleApprove({ code, empId, accountId: accountId || DEFAULT_ACCOUNT_ID });
267
+ break;
268
+ // 工作区 / Artifact
269
+ case 'artifacts.list':
270
+ result = await this.handleArtifactsList(cmdMessage);
271
+ break;
272
+ case 'artifacts.get':
273
+ result = await this.handleArtifactsGet(cmdMessage);
274
+ break;
275
+ case 'artifacts.download':
276
+ result = await this.handleArtifactsDownload(cmdMessage);
277
+ break;
278
+ // Agent 工作区文件
279
+ case 'agents.files.list':
280
+ result = await this.handleAgentFilesList(cmdMessage);
281
+ break;
282
+ case 'agents.files.get':
283
+ result = await this.handleAgentFilesGet(cmdMessage);
284
+ break;
285
+ // 扩展能力(ext_capabilities):语音合成 TTS / 语音转录 STT
286
+ case 'extCapabilities.status':
287
+ result = await this.handleExtCapabilityStatus(cmdMessage.params);
288
+ break;
289
+ case 'extCapabilities.check':
290
+ result = await this.handleExtCapabilityCheck(cmdMessage.params);
291
+ break;
292
+ case 'extCapabilities.install':
293
+ result = await this.handleExtCapabilityInstall(cmdMessage.params);
294
+ break;
295
+ default:
296
+ throw new Error(`未知的命令: ${cmdMessage.cmd}`);
297
+ }
298
+ const duration = Date.now() - startTime;
299
+ this.log(`CMD 处理完成: ${cmdMessage.cmd} (${duration}ms)`, 'info');
300
+ return {
301
+ success: true,
302
+ data: result,
303
+ timestamp: Date.now()
304
+ };
305
+ }
306
+ catch (error) {
307
+ const duration = Date.now() - startTime;
308
+ this.log(`CMD 处理失败: ${cmdMessage.cmd} (${duration}ms), error=${String(error)}`, 'error');
309
+ return {
310
+ success: false,
311
+ error: error instanceof Error ? error.message : String(error),
312
+ timestamp: Date.now()
313
+ };
314
+ }
315
+ }
316
+ async handleAskAgent(params) {
317
+ return await this.client?.askAgent(params);
318
+ }
319
+ /**
320
+ * 更新
321
+ * @param agentId
322
+ * @param userId
323
+ */
324
+ async handleUpdateSessionModel(params) {
325
+ const { sessionKey, provider, model } = params;
326
+ if (!provider || provider.length === 0 || !sessionKey || sessionKey.length === 0) {
327
+ return null;
328
+ }
329
+ const agentId = sessionKey.split(':')[1];
330
+ const { cfg } = this.options;
331
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
332
+ await patchSessionEntry({
333
+ storePath,
334
+ sessionKey,
335
+ update: (entry) => {
336
+ applyModelOverrideToSessionEntry({
337
+ entry,
338
+ selection: {
339
+ provider: provider,
340
+ model: model,
341
+ },
342
+ selectionSource: 'user',
343
+ markLiveSwitchPending: true, // 让当前 run 立即生效
344
+ });
345
+ return { ...entry };
346
+ },
347
+ });
348
+ return { provider, model };
349
+ }
350
+ /**
351
+ * 查询当前会话的模型覆盖(用于 ChatPage 恢复选中状态)
352
+ */
353
+ async handleCurrentSessionModel(params) {
354
+ const { agentId, userId } = params;
355
+ if (!userId || userId.length === 0) {
356
+ return null;
357
+ }
358
+ const { cfg, channelId } = this.options;
359
+ const sessionKey = `agent:${agentId}:${channelId}:direct:${userId.toLocaleLowerCase()}`;
360
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
361
+ const entry = getSessionEntry({ storePath, sessionKey });
362
+ if (!entry)
363
+ return null;
364
+ // 只返回用户主动设置的覆盖,不返回 auto fallback
365
+ if (entry.modelOverrideSource === 'user' && entry.modelOverride) {
366
+ return {
367
+ provider: entry.providerOverride ?? '',
368
+ model: entry.modelOverride,
369
+ };
370
+ }
371
+ return null;
372
+ }
373
+ //#region ==================== Agent 管理处理器 ====================
374
+ async handleAgentList() {
375
+ const agents = await this.client.listAgents();
376
+ return agents.agents.map(agent => ({
377
+ id: agent.id,
378
+ name: agent.name || agent.id,
379
+ emoji: agent.identity?.emoji,
380
+ model: agent.model?.primary,
381
+ workspace: agent.workspace,
382
+ fileCount: 0, // 需要额外API调用获取
383
+ lastActivity: undefined,
384
+ sessionCount: 0 // 需要额外API调用获取
385
+ }));
386
+ }
387
+ async handleAgentCreate(params) {
388
+ const workspace_path = getOpenclawPath('workspace', params.id.toLowerCase().replace(/\s+/g, '-'));
389
+ const createParams = {
390
+ name: params.id,
391
+ workspace: workspace_path,
392
+ emoji: params.emoji,
393
+ avatar: (params.avatar && params.avatar.length > 0) ? this.options.baseUrl + params.avatar : undefined
394
+ };
395
+ const result = await this.client.createAgent(createParams);
396
+ const provider = params.provider ?? '';
397
+ const udpateParams = {
398
+ agentId: result.agentId,
399
+ name: params.name,
400
+ workspace: workspace_path,
401
+ model: provider.length > 0 ? `${provider}/${params.model}` : params.model,
402
+ };
403
+ await this.client?.updateAgent(udpateParams);
404
+ const sessionKey = `agent:${params.agentId}:${this.options.channelId}:direct:${params.userId.toLowerCase()}`;
405
+ await this.handleUpdateSessionModel({
406
+ sessionKey,
407
+ provider: provider,
408
+ model: params.model
409
+ });
410
+ return {
411
+ id: result.agentId,
412
+ name: params.name
413
+ };
414
+ }
415
+ async handleAgentUpdate(params) {
416
+ const agents = await this.client.listAgents();
417
+ const agent = agents.agents.find(a => a.id === params.agentId);
418
+ const provider = params.provider ?? '';
419
+ if (agent) {
420
+ //agent存在直接更新
421
+ const result = await this.client.updateAgent({
422
+ agentId: params.agentId,
423
+ name: params.name,
424
+ workspace: params.workspace,
425
+ model: provider.length > 0 ? `${provider}/${params.model}` : params.model,
426
+ avatar: params.avatar
427
+ });
428
+ const sessionKey = `agent:${params.agentId}:${this.options.channelId}:direct:${params.userId.toLowerCase()}`;
429
+ await this.handleUpdateSessionModel({
430
+ sessionKey,
431
+ provider: provider,
432
+ model: params.model
433
+ });
434
+ return { id: result.agentId };
435
+ }
436
+ //不存在直接创建一个
437
+ const p = {
438
+ id: params.agentId,
439
+ name: params.name,
440
+ provider,
441
+ model: params.model,
442
+ userId: params.userId
443
+ };
444
+ const result = await this.handleAgentCreate(p);
445
+ return {
446
+ id: result.id
447
+ };
448
+ }
449
+ async handleAgentDelete(params) {
450
+ const result = await this.client.deleteAgent({
451
+ agentId: params.agentId,
452
+ deleteFiles: params.deleteFiles || false
453
+ });
454
+ return { success: result.ok };
455
+ }
456
+ async handleAgentGet(params) {
457
+ const agents = await this.client.listAgents();
458
+ const agent = agents.agents.find(a => a.id === params.agentId);
459
+ if (!agent) {
460
+ throw new Error(`Agent ${params.agentId} 未找到`);
461
+ }
462
+ return agent;
463
+ }
464
+ //#endregion
465
+ //#region ==================== 任务管理处理器 ====================
466
+ async handleTaskList(params) {
467
+ const channelId = this.options.channelId;
468
+ const res = await this.client.listCronTasks({
469
+ limit: params?.limit ?? 200,
470
+ offset: params?.offset ?? 0,
471
+ });
472
+ const agentId_list = params?.agentIds ?? [];
473
+ if (agentId_list.length > 0) {
474
+ const jobs = res.jobs.filter(item => agentId_list.includes(item.agentId)).map(item => {
475
+ if (item.delivery && item.delivery.to && item.delivery.to.startsWith(`${channelId}:`)) {
476
+ return {
477
+ ...item,
478
+ delivery: {
479
+ ...item.delivery,
480
+ to: item.delivery.to.replaceAll(`${channelId}:`, '')
481
+ }
482
+ };
483
+ }
484
+ return item;
485
+ });
486
+ return {
487
+ ...res,
488
+ total: jobs.length,
489
+ jobs
490
+ };
491
+ }
492
+ const jobs = (res.jobs ?? []).map(item => {
493
+ if (item.delivery && item.delivery.to && item.delivery.to.startsWith(`${channelId}:`)) {
494
+ return {
495
+ ...item,
496
+ delivery: {
497
+ ...item.delivery,
498
+ to: item.delivery.to.replaceAll(`${channelId}:`, '')
499
+ }
500
+ };
501
+ }
502
+ return item;
503
+ });
504
+ return {
505
+ ...res,
506
+ total: res.jobs.length,
507
+ jobs
508
+ };
509
+ }
510
+ async handleTaskCreate(params) {
511
+ const res = await this.client.addCronTask(params);
512
+ console.log('创建任务 >>> ', JSON.stringify(res));
513
+ return res;
514
+ }
515
+ // task.update — 更新定时任务(通过 OpenClaw cron.update)
516
+ async handleTaskUpdate(params) {
517
+ const result = await this.client.updateCronTask({
518
+ jobId: params.jobId,
519
+ patch: params.patch,
520
+ });
521
+ console.log('更新任务 >>> ', JSON.stringify(result));
522
+ return result;
523
+ }
524
+ async handleTaskDelete(params) {
525
+ const result = await this.client.removeCronTask({
526
+ jobId: params.jobId
527
+ });
528
+ return { success: result.ok };
529
+ }
530
+ async handleTaskRun(params) {
531
+ const result = await this.client.runCronTask({
532
+ jobId: params.jobId
533
+ });
534
+ return {
535
+ success: result.ok,
536
+ runId: result.runId
537
+ };
538
+ }
539
+ //#endregion
540
+ //#region ==================== 状态监控处理器 ====================
541
+ async handleStatusHealth() {
542
+ const health = await this.client.getHealthStatus();
543
+ return {
544
+ status: health.status,
545
+ version: health.version,
546
+ uptime: this.formatUptime(health.uptime),
547
+ agentCount: Object.keys(health.agents).length,
548
+ channelCount: Object.keys(health.channels).length,
549
+ memoryUsage: health.memory ? `${Math.round(health.memory.percent)}%` : '未知',
550
+ totalTokens: 0, // 需要 usage API
551
+ totalCost: 0 // 需要 usage API
552
+ };
553
+ }
554
+ async handleStatusSystem() {
555
+ const system = await this.client.getSystemStatus();
556
+ return system;
557
+ }
558
+ async handleStatusUsage() {
559
+ const usage = await this.client.getUsageStatus();
560
+ return usage;
561
+ }
562
+ async handleListModels() {
563
+ const res = await this.client?.listModels();
564
+ return res?.models ?? [];
565
+ }
566
+ //#endregion
567
+ //#region ===================== 会话处理器 ======================
568
+ async handleSessionList(params) {
569
+ return await this.client?.sessionsList(params);
570
+ }
571
+ async handleSessionGet(params) {
572
+ return await this.client?.sessionsGet(params);
573
+ }
574
+ async handleSessionsDescribe(params) {
575
+ return await this.client?.sessionsDescribe(params);
576
+ }
577
+ async handleSessionsResolve(params) {
578
+ return await this.client?.sessionsResolve(params);
579
+ }
580
+ /**
581
+ * 创建新会话
582
+ * sessionKey: agent:<agentId>:<channelId>-<suffix>:<chatType>:<userId>
583
+ * 例: agent:main:workrelay-00001:direct:I35916630580137984
584
+ */
585
+ async handleSessionCreate(params) {
586
+ const { sessionKey } = params;
587
+ if (!sessionKey) {
588
+ throw new Error('sessions.create 缺少 sessionKey 参数');
589
+ }
590
+ return await this.client?.sessionsCreate({ key: sessionKey });
591
+ }
592
+ /**
593
+ * 归档会话
594
+ * 入参: { sessionKey }
595
+ * - sessionKey: 完整的 sessionKey,如 "agent:main:workrelay-00001:direct:i35916630580137984"
596
+ */
597
+ async handleSessionArchive(params) {
598
+ const { sessionKey } = params;
599
+ if (!sessionKey) {
600
+ throw new Error('sessions.archive 缺少 sessionKey 参数');
601
+ }
602
+ this.log(`sessions.archive sessionKey=${sessionKey}`, 'info');
603
+ return await this.client?.sessionsPatch({ key: sessionKey, patch: { archived: true } });
604
+ }
605
+ /**
606
+ * 恢复(取消归档)会话
607
+ * 入参: { sessionKey }
608
+ * - sessionKey: 完整的 sessionKey
609
+ */
610
+ async handleSessionRestore(params) {
611
+ const { sessionKey } = params;
612
+ if (!sessionKey) {
613
+ throw new Error('sessions.restore 缺少 sessionKey 参数');
614
+ }
615
+ this.log(`sessions.restore sessionKey=${sessionKey}`, 'info');
616
+ return await this.client?.sessionsPatch({ key: sessionKey, patch: { archived: false } });
617
+ }
618
+ /**
619
+ * 删除会话
620
+ * 入参: { sessionKey, deleteFiles? }
621
+ * - sessionKey: 完整的 sessionKey
622
+ * - deleteFiles: 是否同时删除会话相关文件(可选,默认 false)
623
+ */
624
+ async handleSessionDelete(params) {
625
+ const sessionKey = params.sessionKey;
626
+ if (!sessionKey) {
627
+ throw new Error('sessions.delete 缺少 sessionKey 参数');
628
+ }
629
+ this.log(`sessions.delete sessionKey=${sessionKey}`, 'info');
630
+ const arr = sessionKey.split(':');
631
+ const userId = arr[arr.length - 1];
632
+ const effective_userId = userId.length <= 20 && userId.startsWith('i');
633
+ if (sessionKey.startsWith(`agent:main:${this.options.channelId}:`) && effective_userId) {
634
+ throw new Error('Deleting the default session is prohibited.');
635
+ }
636
+ return await this.client?.sessionsDelete({ key: sessionKey });
637
+ }
638
+ async handleSessionReset(params) {
639
+ return await this.client?.sessionReset(params);
640
+ }
641
+ async handleChatHistories(params) {
642
+ return await this.client?.chatHistories(params);
643
+ }
644
+ //#endregion
645
+ //#region ==================== Skills 管理处理器 ====================
646
+ async handleSkillsStatus(params) {
647
+ return await this.client?.listSkills(params?.agentId);
648
+ }
649
+ async handleSkillsSearch(params) {
650
+ return await this.client?.searchSkills({
651
+ query: params.query,
652
+ limit: params.limit,
653
+ });
654
+ }
655
+ async handleSkillDetail(params) {
656
+ if (!params?.slug) {
657
+ throw new Error('skills.detail 缺少 slug 参数');
658
+ }
659
+ return await this.client?.getSkillDetail(params.slug);
660
+ }
661
+ async handleSkillInstall(params) {
662
+ if (!params.slug) {
663
+ throw new Error('skills.install 缺少 slug 参数');
664
+ }
665
+ return await this.client?.installSkill(params.slug, params.version);
666
+ }
667
+ async handleSkillUpdate(params) {
668
+ if (!params.skillKey) {
669
+ throw new Error('skills.update 缺少 skillKey 参数');
670
+ }
671
+ return await this.client?.updateSkillConfig(params.skillKey, params.enabled);
672
+ }
673
+ async handleSkillsSecurityVerdicts(params) {
674
+ return await this.client?.getSkillSecurityVerdicts(params?.agentId);
675
+ }
676
+ // 仪表盘:按日期范围聚合成本
677
+ async handleUsageCost(params) {
678
+ const result = await this.client.getUsageCost(params);
679
+ return { data: result };
680
+ }
681
+ // 仪表盘:每 session 详细使用统计
682
+ async handleSessionsUsage(params) {
683
+ const result = await this.client.getSessionsUsage(params);
684
+ return { data: result };
685
+ }
686
+ // 仪表盘:每消息级别使用日志
687
+ async handleSessionsUsageLogs(params) {
688
+ const result = await this.client.getSessionsUsageLogs(params);
689
+ return { data: result };
690
+ }
691
+ /**
692
+ * 取消当前Agent执行,通过Gateway /queue interrupt 中断活跃run
693
+ */
694
+ async handleCancelMessage(params) {
695
+ const { sessionKey } = params;
696
+ if (!sessionKey) {
697
+ throw new Error('cancelMessage 缺少 sessionKey 参数');
698
+ }
699
+ // channelId 从配置获取,不硬编码
700
+ return await this.client?.cancelSession({ sessionKey });
701
+ }
702
+ /**
703
+ * 强制停止当前Agent run(/stop,比 /queue interrupt 更快)
704
+ * 新增 cmd 通道 agent.stop 指令
705
+ */
706
+ async handleStopAgent(params) {
707
+ const { sessionKey } = params;
708
+ if (!sessionKey) {
709
+ throw new Error('agent.stop 缺少 sessionKey 参数');
710
+ }
711
+ return await this.client?.stopSession({
712
+ sessionKey
713
+ });
714
+ }
715
+ async handlChatAbout(params) {
716
+ const { sessionKey, runId } = params;
717
+ if (!sessionKey) {
718
+ throw new Error('chat.bort 缺少 sessionKey, runId 参数');
719
+ }
720
+ // 2026-08-24 立即终止优化:先标记该 runId 已停止,
721
+ // before_tool_call 钩子会 block 此 run 后续的工具调用(chat.abort 是协作式取消,无法打断正在执行的工具)
722
+ if (runId) {
723
+ markRunStopped(runId);
724
+ }
725
+ return await this.client?.chatAbort({ sessionKey, runId });
726
+ }
727
+ //#endregion
728
+ //#region -----------对码审批--------------
729
+ async handleApprove(params) {
730
+ const { code, empId, accountId } = params;
731
+ const res = await approveChannelPairingCode({
732
+ channel: this.options.channelId,
733
+ code,
734
+ accountId,
735
+ });
736
+ if (res) {
737
+ this.emit('onPairingResult', {
738
+ approved: true,
739
+ senderId: res.id,
740
+ empId,
741
+ code
742
+ });
743
+ return {
744
+ success: true,
745
+ data: {
746
+ ...res
747
+ },
748
+ timestamp: Date.now()
749
+ };
750
+ }
751
+ throw new Error('审批失败:验证码无效或已过期!');
752
+ }
753
+ //#endregion
754
+ //#region ======================== 工作区 / Artifact 处理器 ========================
755
+ /** artifacts.list - 列出会话工件 */
756
+ async handleArtifactsList(cmdMessage) {
757
+ const { sessionKey } = cmdMessage.params || {};
758
+ if (!sessionKey || sessionKey.length === 0) {
759
+ throw new Error('artifacts.list 缺少 sessionKey 参数');
760
+ }
761
+ this.log(`artifacts.list sessionKey=${sessionKey}`, 'info');
762
+ return await this.client?.artifactsList({ sessionKey });
763
+ }
764
+ /** artifacts.get - 获取单个工件详情 */
765
+ async handleArtifactsGet(cmdMessage) {
766
+ const params = cmdMessage.params;
767
+ this.log(`artifacts.get params=${JSON.stringify(params)}`, 'info');
768
+ return await this.client?.artifactsGet(params);
769
+ }
770
+ /** artifacts.download - 下载工件 */
771
+ async handleArtifactsDownload(cmdMessage) {
772
+ const params = cmdMessage.params;
773
+ this.log(`artifacts.download params=${JSON.stringify(params)}`, 'info');
774
+ const result = await this.client?.artifactsDownload(params);
775
+ this.log(`artifacts.download result size=${result?.size ?? 'unknown'}`, 'info');
776
+ return result;
777
+ }
778
+ // ======================== Agent 工作区文件处理器 ========================
779
+ /** agents.files.list - 列出 Agent 工作区文件 */
780
+ async handleAgentFilesList(cmdMessage) {
781
+ const params = cmdMessage.params;
782
+ this.log(`agents.files.list params=${JSON.stringify(params)}`, 'info');
783
+ // 2026-07-31 新增:优先尝试本地文件系统直读(递归扫描所有文件),
784
+ // 目录受限/无权限时自动回退到 Gateway RPC(保留原有兜底逻辑)
785
+ try {
786
+ const result = await this.client?.agentFilesListLocal(params.agentId);
787
+ this.log(`agents.files.list (local scan) result count=${result?.files?.length ?? 0}`, 'info');
788
+ return result;
789
+ }
790
+ catch (err) {
791
+ this.log(`agents.files.list (local scan) failed: ${err}, falling back to Gateway RPC`, 'warn');
792
+ const result = await this.client?.agentFilesList(params.agentId);
793
+ this.log(`agents.files.list (Gateway RPC fallback) result count=${result?.files?.length ?? 0}`, 'info');
794
+ return result;
795
+ }
796
+ }
797
+ /** agents.files.get - 获取工作区文件内容 */
798
+ async handleAgentFilesGet(cmdMessage) {
799
+ const params = cmdMessage.params;
800
+ this.log(`agents.files.get params=${JSON.stringify(params)}`, 'info');
801
+ // 2026-07-31 新增:优先本地文件系统直读(绕过 Gateway 文件白名单限制),
802
+ // 权限不足/文件不存在时自动回退到 Gateway RPC
803
+ try {
804
+ const result = await this.client?.agentFilesGetLocal(params.agentId, params.name);
805
+ this.log(`agents.files.get (local) result name=${params.name}`, 'info');
806
+ return result;
807
+ }
808
+ catch (err) {
809
+ this.log(`agents.files.get (local) failed: ${err}, falling back to Gateway RPC`, 'warn');
810
+ const result = await this.client?.agentFilesGet(params.agentId, params.name);
811
+ this.log(`agents.files.get (Gateway RPC fallback) result name=${params.name}`, 'info');
812
+ return result;
813
+ }
814
+ }
815
+ //#endregion
816
+ //#region ==================== 扩展能力处理器(ext_capabilities)====================
817
+ /**
818
+ * extCapabilities.status - 查询扩展能力安装状态
819
+ * params: { name: 'workrelay-speak' | 'workrelay-transcribe' }
820
+ * 执行 ext_capabilities/<name>/status.sh,返回 JSON 供客户端展示
821
+ */
822
+ async handleExtCapabilityStatus(params) {
823
+ const name = params?.name;
824
+ if (!name) {
825
+ throw new Error('extCapabilities.status 缺少 name 参数');
826
+ }
827
+ const extDir = getExtCapabilitiesDir();
828
+ const scriptPath = path.join(extDir, name, 'status.sh');
829
+ this.log(`extCapabilities.status script=${scriptPath}`, 'info');
830
+ if (!fs.existsSync(scriptPath)) {
831
+ return {
832
+ status: 'disabled',
833
+ description: `能力 ${name} 未安装(脚本不存在)`,
834
+ checks: {}
835
+ };
836
+ }
837
+ try {
838
+ const execAsync = promisify(exec);
839
+ const { stdout } = await execAsync(`bash "${scriptPath}"`, { timeout: 15000, encoding: 'utf-8' });
840
+ // status.sh 输出 JSON(尾部可能带换行/日志),取最后一个 { } 块解析
841
+ const match = stdout.match(/\{[\s\S]*\}/);
842
+ let result;
843
+ if (match) {
844
+ result = JSON.parse(match[0]);
845
+ }
846
+ else {
847
+ result = { status: 'unknown', description: stdout.trim() || '状态解析失败', checks: {} };
848
+ }
849
+ // 2026-08-10 安装存活检测:判断 install.sh 进程是否真实在跑。
850
+ // 客户端 store 持久化的 installing 状态在重启后可能残留(假死),
851
+ // 客户端据此自动解除“安装中”,恢复可重新安装。
852
+ result.installing = await this.isExtCapabilityInstalling(name);
853
+ // 2026-08-18 日志位置随状态返回:客户端界面直接显示安装日志路径,方便排查。
854
+ result.logFile = getOpenclawPath('logs', `ext-capabilities-${name}-install.log`);
855
+ return result;
856
+ }
857
+ catch (err) {
858
+ // status.sh 非 0 退出(如未安装返回 1),但 stdout 仍有 JSON
859
+ const stdout = err?.stdout;
860
+ const match = stdout?.match(/\{[\s\S]*\}/);
861
+ if (match) {
862
+ try {
863
+ const result = JSON.parse(match[0]);
864
+ result.installing = await this.isExtCapabilityInstalling(name);
865
+ // 2026-08-18 日志位置随状态返回(与上方成功分支一致)
866
+ result.logFile = getOpenclawPath('logs', `ext-capabilities-${name}-install.log`);
867
+ return result;
868
+ }
869
+ catch { /* ignore */ }
870
+ }
871
+ this.log(`extCapabilities.status 执行失败: ${String(err)}`, 'warn');
872
+ return { status: 'unknown', description: '状态查询失败', checks: {}, installing: false };
873
+ }
874
+ }
875
+ /**
876
+ * 2026-08-10 检测指定扩展能力的安装进程是否真实在跑(存活检测)
877
+ * 注意:不能用 pgrep -f "...install.sh" —— 该命令经 bash -c 执行时,bash 进程
878
+ * 自身命令行包含 install.sh 字符串,pgrep 会永远自匹配,导致 installing 恒为 true。
879
+ * 改用 ps + grep 方括号技巧(grep 自身命令行不含字面 install.sh,不会自匹配)。
880
+ */
881
+ async isExtCapabilityInstalling(name) {
882
+ try {
883
+ const extDir = getExtCapabilitiesDir();
884
+ const installScript = path.join(extDir, name, 'install.sh');
885
+ const execAsync = promisify(exec);
886
+ // [i]nstall.sh 方括号技巧:grep 进程自身命令行是 "grep [i]nstall.sh",不含字面 install.sh
887
+ const { stdout } = await execAsync(`ps -eo args | grep "[i]nstall\\.sh" | grep -v grep`, { timeout: 3000, encoding: 'utf-8' });
888
+ // 只认跑在脚本路径上的进程,避免匹配到其它无关 install.sh
889
+ const lines = stdout.split('\n').filter((line) => line.trim().length > 0);
890
+ return lines.some((line) => line.includes(installScript) || line.includes('bash ' + installScript));
891
+ }
892
+ catch {
893
+ return false; // 出错 → 视为未在安装
894
+ }
895
+ }
896
+ /**
897
+ * extCapabilities.check - 检查扩展能力安装前置条件
898
+ * params: { name: 'workrelay-speak' | 'workrelay-transcribe' }
899
+ * 读取 ext_capabilities/<name>/manifest.json,逐条执行 requirements[].check(shell 表达式),
900
+ * exit 0 = 条件满足。返回每条条件是否满足,客户端据此展示"安装条件"清单。
901
+ * 2026-08-11 新增:通用适配——每个能力自带前置条件清单,用户安装前即可看到环境是否满足。
902
+ */
903
+ async handleExtCapabilityCheck(params) {
904
+ const name = params?.name;
905
+ if (!name) {
906
+ throw new Error('extCapabilities.check 缺少 name 参数');
907
+ }
908
+ const extDir = getExtCapabilitiesDir();
909
+ const manifestPath = path.join(extDir, name, 'manifest.json');
910
+ this.log(`extCapabilities.check manifest=${manifestPath}`, 'info');
911
+ if (!fs.existsSync(manifestPath)) {
912
+ return { name, displayName: name, allMet: true, requirements: [], note: '该能力未提供前置条件清单' };
913
+ }
914
+ let manifest;
915
+ try {
916
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
917
+ }
918
+ catch (err) {
919
+ this.log(`extCapabilities.check manifest 解析失败: ${String(err)}`, 'warn');
920
+ return { name, displayName: name, allMet: true, requirements: [], note: '前置条件清单解析失败' };
921
+ }
922
+ const requirements = manifest.requirements ?? [];
923
+ const execAsync = promisify(exec);
924
+ const results = [];
925
+ for (const req of requirements) {
926
+ const item = {
927
+ id: req.id,
928
+ label: req.label,
929
+ expect: req.expect,
930
+ hint: req.hint,
931
+ met: false,
932
+ };
933
+ try {
934
+ // check 是 shell 表达式,exit 0 = 满足;10s 超时防止命令卡死
935
+ await execAsync(req.check, { timeout: 10000, encoding: 'utf-8' });
936
+ item.met = true;
937
+ }
938
+ catch (err) {
939
+ item.met = false;
940
+ item.detail = String(err?.message ?? err).split('\n')[0] ?? undefined;
941
+ }
942
+ results.push(item);
943
+ }
944
+ return {
945
+ name: manifest.name ?? name,
946
+ displayName: manifest.displayName ?? name,
947
+ allMet: results.every((r) => r.met === true),
948
+ requirements: results,
949
+ };
950
+ }
951
+ /**
952
+ * extCapabilities.install - 安装扩展能力(长任务,后台执行)
953
+ * params: { name: 'workrelay-speak' | 'workrelay-transcribe' }
954
+ * install.sh 可能需要 5-15 分钟(下载模型),后台 spawn 立即返回"已开始",
955
+ * 客户端通过轮询 extCapabilities.status 感知进度
956
+ */
957
+ async handleExtCapabilityInstall(params) {
958
+ const name = params?.name;
959
+ if (!name) {
960
+ throw new Error('extCapabilities.install 缺少 name 参数');
961
+ }
962
+ const extDir = getExtCapabilitiesDir();
963
+ const scriptPath = path.join(extDir, name, 'install.sh');
964
+ this.log(`extCapabilities.install script=${scriptPath}`, 'info');
965
+ if (!fs.existsSync(scriptPath)) {
966
+ throw new Error(`能力 ${name} 的安装脚本不存在: ${scriptPath}`);
967
+ }
968
+ // 后台 spawn,detached 脱离父进程生命周期
969
+ // 2026-08-10 修改:stdio 不再 ignore,stdout/stderr 重定向到日志文件,方便排查安装失败原因
970
+ const logFile = getOpenclawPath('logs', `ext-capabilities-${name}-install.log`);
971
+ try {
972
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
973
+ }
974
+ catch { /* ignore */ }
975
+ const outFd = fs.openSync(logFile, 'a');
976
+ const child = spawn('bash', [scriptPath], {
977
+ detached: true,
978
+ stdio: ['ignore', outFd, outFd],
979
+ env: { ...process.env },
980
+ });
981
+ // 2026-08-10 修复:spawn 成功 ≠ 脚本执行成功。脚本内部立即失败(权限/依赖检查)
982
+ // 时 error 事件不会触发,失败输出全进日志文件,客户端只看到 started:true → 错误被吞。
983
+ // 方案:监听 exit 事件,3 秒窗口内脚本退出且退出码非 0 → 视为安装失败,读日志尾部报回。
984
+ const quickFail = await new Promise((resolve) => {
985
+ let settled = false;
986
+ const finish = (failed, code) => {
987
+ if (settled)
988
+ return;
989
+ settled = true;
990
+ resolve({ failed, code });
991
+ };
992
+ const timer = setTimeout(() => finish(false, null), 3000); // 3 秒没退出 → 长任务在跑
993
+ child.once('error', (err) => {
994
+ clearTimeout(timer);
995
+ this.log(`extCapabilities.install spawn 错误: ${err.message}`, 'error');
996
+ finish(true, -1);
997
+ });
998
+ child.once('exit', (code, _signal) => {
999
+ clearTimeout(timer);
1000
+ finish(code !== 0, code ?? -1);
1001
+ });
1002
+ });
1003
+ if (quickFail.failed) {
1004
+ // 读取日志尾部作为具体错误信息报回客户端(不吞错)
1005
+ const tail = this.readLogTail(logFile);
1006
+ this.log(`extCapabilities.install 执行失败(exit=${quickFail.code}): ${tail}`, 'error');
1007
+ return {
1008
+ started: false,
1009
+ name,
1010
+ error: `安装脚本执行失败(exit=${quickFail.code ?? 'unknown'}): ${tail}`,
1011
+ logFile,
1012
+ };
1013
+ }
1014
+ // 长任务在跑:监听后续 error/exit 记日志,unref 脱离父进程生命周期
1015
+ child.on('error', (err) => {
1016
+ this.log(`extCapabilities.install 运行错误: ${err.message}`, 'error');
1017
+ });
1018
+ child.unref();
1019
+ // 记录日志文件位置,便于客户端/日志排查
1020
+ this.log(`extCapabilities.install 日志: ${logFile}`, 'info');
1021
+ return {
1022
+ started: true,
1023
+ name,
1024
+ message: `安装已启动,请稍后刷新状态(${name} 首次安装需下载模型,约 5-15 分钟)`,
1025
+ logFile,
1026
+ };
1027
+ }
1028
+ //#endregion
1029
+ //#region ==================== 工具函数 ==========================
1030
+ /**
1031
+ * 2026-08-10 读取日志文件尾部内容(供安装失败报错)
1032
+ */
1033
+ readLogTail(logFile, maxLines = 20) {
1034
+ try {
1035
+ const content = fs.readFileSync(logFile, 'utf-8');
1036
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
1037
+ return lines.slice(-maxLines).join('\n').trim() || '(日志为空)';
1038
+ }
1039
+ catch (err) {
1040
+ return `(无法读取日志: ${String(err)})`;
1041
+ }
1042
+ }
1043
+ formatUptime(seconds) {
1044
+ const days = Math.floor(seconds / 86400);
1045
+ const hours = Math.floor((seconds % 86400) / 3600);
1046
+ const minutes = Math.floor((seconds % 3600) / 60);
1047
+ if (days > 0) {
1048
+ return `${days}天${hours}小时`;
1049
+ }
1050
+ else if (hours > 0) {
1051
+ return `${hours}小时${minutes}分钟`;
1052
+ }
1053
+ else {
1054
+ return `${minutes}分钟`;
1055
+ }
1056
+ }
1057
+ cleanup() {
1058
+ if (this.client) {
1059
+ this.client.disconnect();
1060
+ this.client.removeAllListeners();
1061
+ this.client = null;
1062
+ }
1063
+ this.isInitialized = false;
1064
+ }
1065
+ }
1066
+ export default ClawServices;