@voko/lite 0.3.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 (62) hide show
  1. package/package.json +32 -0
  2. package/scripts/build-native.js +72 -0
  3. package/src/bankHeadOffices.js +20543 -0
  4. package/src/channels/email.js +35 -0
  5. package/src/channels/feishu.js +31 -0
  6. package/src/channels/qq-email.js +30 -0
  7. package/src/channels/registry.js +279 -0
  8. package/src/channels/telegram.js +28 -0
  9. package/src/channels/voko-email.js +7 -0
  10. package/src/channels/wechat.js +35 -0
  11. package/src/cli.js +120 -0
  12. package/src/context.js +164 -0
  13. package/src/core/access-control-api.js +150 -0
  14. package/src/core/access-control.js +56 -0
  15. package/src/core/agent-registration.js +319 -0
  16. package/src/core/api-signature.js +33 -0
  17. package/src/core/audit.js +133 -0
  18. package/src/core/database.js +1409 -0
  19. package/src/core/did-auth.js +54 -0
  20. package/src/core/hermes-paths.js +57 -0
  21. package/src/core/invitation.js +49 -0
  22. package/src/core/lite-bus.js +16 -0
  23. package/src/core/llm-client.js +1032 -0
  24. package/src/core/messenger.js +456 -0
  25. package/src/core/notifier.js +99 -0
  26. package/src/core/offline-sync.js +150 -0
  27. package/src/core/payment.js +285 -0
  28. package/src/core/publish-agent.js +166 -0
  29. package/src/core/register-capabilities.js +119 -0
  30. package/src/core/search-capabilities.js +136 -0
  31. package/src/core/send-message.js +85 -0
  32. package/src/core/set-agent-status.js +65 -0
  33. package/src/core/update-agent-profile.js +102 -0
  34. package/src/core/worker-manager.js +332 -0
  35. package/src/endpoints.json +21 -0
  36. package/src/index.js +712 -0
  37. package/src/mcp/CLAUDE_TEST.md +82 -0
  38. package/src/mcp/FULL_TEST.md +139 -0
  39. package/src/mcp/TEST.md +124 -0
  40. package/src/mcp/TEST_STEPS.md +75 -0
  41. package/src/mcp/server.js +612 -0
  42. package/src/mcp/tools.js +1367 -0
  43. package/src/mcp/transport/http.js +95 -0
  44. package/src/mcp/transport/stdio.js +20 -0
  45. package/src/preload.js +27 -0
  46. package/src/server/agent-email-api.js +120 -0
  47. package/src/server/agent-manager.js +580 -0
  48. package/src/server/email-handler.js +329 -0
  49. package/src/server/feishu-handler.js +249 -0
  50. package/src/server/hermes-api-client.js +166 -0
  51. package/src/server/hermes-discovery.js +80 -0
  52. package/src/server/hermes-handler.js +287 -0
  53. package/src/server/openclaw-handler-cli.js +131 -0
  54. package/src/server/openclaw-websocket-handler.js +1290 -0
  55. package/src/server/oss.js +186 -0
  56. package/src/server/owner-intervention-notifier.js +320 -0
  57. package/src/server/release-page.html +204 -0
  58. package/src/server/telegram-handler.js +208 -0
  59. package/src/server/voko-email-handler.js +68 -0
  60. package/src/server/wechat-handler.js +439 -0
  61. package/src/workers/agent-worker.js +378 -0
  62. package/src/workers/message-content.js +51 -0
@@ -0,0 +1,1032 @@
1
+ /**
2
+ * LLM Client - 支持自动检测(OpenClaw)和手动配置两种模式
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { getHermesDir, getHermesProfilesDir, getHermesEnvPath, getHermesConfigPath } = require('./hermes-paths');
9
+ let app;
10
+ try { app = require('electron').app; } catch (_) { app = null; }
11
+ const http = require('http');
12
+ const { execSync, spawnSync } = require('child_process');
13
+
14
+ // 模型名称 → 真实 API 端点映射表
15
+ const MODEL_ENDPOINT_PRESETS = [
16
+ { match: /deepseek/i, baseUrl: 'https://api.deepseek.com' },
17
+ { match: /moonshot|kimi/i, baseUrl: 'https://api.moonshot.cn/v1' },
18
+ { match: /glm|zhipu|chatglm/i, baseUrl: 'https://open.bigmodel.cn/api/coding/paas/v4' },
19
+ { match: /qwen|dashscope|tongyi/i, baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
20
+ { match: /stepfun|step/i, baseUrl: 'https://api.stepfun.com/step_plan/v1' },
21
+ { match: /ark|volces/i, baseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3' },
22
+ { match: /modelscope/i, baseUrl: 'https://api-inference.modelscope.cn/v1' },
23
+ { match: /baidu|qianfan/i, baseUrl: 'https://qianfan.baidubce.com/v2/coding' },
24
+ ];
25
+
26
+ /**
27
+ * OpenClaw 路由别名 → 真实 API 模型名映射
28
+ * OpenClaw 配置中的 model.primary 可能是 provider/model 格式的路由别名,
29
+ * 需要映射为 API 真实接受的模型名。
30
+ */
31
+ const ROUTING_ALIAS_MAP = {
32
+ 'deepseek/deepseek-chat': 'deepseek-v4-flash',
33
+ 'deepseek-chat': 'deepseek-v4-flash',
34
+ };
35
+
36
+ /**
37
+ * 从 openclaw.json 读取 gateway 端口,不存在则返回默认 18789
38
+ */
39
+ function getOpenclawPort() {
40
+ try {
41
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
42
+ if (fs.existsSync(configPath)) {
43
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
44
+ if (config.gateway?.port) return config.gateway.port;
45
+ }
46
+ } catch (_) { /* ignore */ }
47
+ return 18789;
48
+ }
49
+
50
+ /** 判断端点是否为本地地址(localhost / 127.x) */
51
+ function isLocalEndpoint(url) {
52
+ if (!url) return true;
53
+ try {
54
+ const hostname = typeof url === 'string' ? url.replace(/^https?:\/\//, '').split('/')[0].split(':')[0] : '';
55
+ return !hostname || hostname === 'localhost' || hostname.startsWith('127.') || hostname === '0.0.0.0' || hostname === '::1';
56
+ } catch (_) { return true; }
57
+ }
58
+
59
+ /** 根据模型名称匹配预设端点 */
60
+ function resolveEndpoint(modelName, configEndpoint) {
61
+ // 如果配置文件提供了真实端点(非本地地址),优先使用
62
+ if (configEndpoint && !isLocalEndpoint(configEndpoint)) {
63
+ let normalized = configEndpoint.replace(/\/+$/, '');
64
+ if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
65
+ normalized = 'https://' + normalized;
66
+ }
67
+ return normalized;
68
+ }
69
+ // 否则匹配预设表
70
+ if (modelName) {
71
+ for (const preset of MODEL_ENDPOINT_PRESETS) {
72
+ if (preset.match.test(modelName)) return preset.baseUrl;
73
+ }
74
+ }
75
+ // 都不匹配则返回 null
76
+ return null;
77
+ }
78
+
79
+ class LLMClient {
80
+ constructor(configPath) {
81
+ this.configPath = configPath || this.getDefaultConfigPath();
82
+ this.config = this.loadConfig();
83
+ }
84
+
85
+ getDefaultConfigPath() {
86
+ const userDataPath = app ? app.getPath('userData') : process.env.APPDATA || process.env.HOME;
87
+ return path.join(userDataPath, 'llm-config.json');
88
+ }
89
+
90
+ loadConfig() {
91
+ try {
92
+ if (fs.existsSync(this.configPath)) {
93
+ const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
94
+ return config;
95
+ }
96
+ } catch (e) {
97
+ console.error('[LLM] 加载配置失败:', e.message);
98
+ }
99
+ return this.getDefaultConfig();
100
+ }
101
+
102
+ getDefaultConfig() {
103
+ return {
104
+ mode: 'auto', // 'auto' | 'manual'
105
+ providers: [],
106
+ activeProviderId: null
107
+ };
108
+ }
109
+
110
+ /**
111
+ * 检测 OpenClaw 本地代理
112
+ * 首先尝试 HTTP 检测,如果失败则从配置文件读取
113
+ */
114
+ async detectOpenClawProxy() {
115
+ // 先尝试 HTTP 检测(如果 OpenClaw 正在运行)
116
+ const httpResult = await this._detectViaHttp();
117
+ if (httpResult.detected) {
118
+ return httpResult;
119
+ }
120
+
121
+ // 如果 HTTP 检测失败,尝试从配置文件读取
122
+ const configResult = this._detectViaConfigFile();
123
+ if (configResult.detected) {
124
+ return configResult;
125
+ }
126
+
127
+ return {
128
+ detected: false,
129
+ error: 'OpenClaw 未运行或配置未找到'
130
+ };
131
+ }
132
+
133
+ /**
134
+ * 通过 HTTP API 检测 OpenClaw
135
+ */
136
+ _detectViaHttp() {
137
+ return new Promise((resolve) => {
138
+ const ocPort = getOpenclawPort();
139
+ const options = {
140
+ hostname: '127.0.0.1',
141
+ port: ocPort,
142
+ path: '/coding/v1/models',
143
+ method: 'GET',
144
+ headers: {
145
+ 'Authorization': 'Bearer proxy-managed'
146
+ },
147
+ timeout: 3000
148
+ };
149
+
150
+ const req = http.request(options, (res) => {
151
+ let data = '';
152
+ res.on('data', chunk => data += chunk);
153
+ res.on('end', () => {
154
+ if (res.statusCode === 200) {
155
+ try {
156
+ const models = JSON.parse(data);
157
+ const modelList = models.data || [];
158
+ const firstModel = modelList[0];
159
+
160
+ if (firstModel) {
161
+ resolve({
162
+ detected: true,
163
+ provider: {
164
+ id: 'openclaw-kimi',
165
+ name: `OpenClaw / ${firstModel.id}`,
166
+ baseUrl: `http://127.0.0.1:${getOpenclawPort()}/coding`,
167
+ apiKey: 'proxy-managed',
168
+ modelId: firstModel.id,
169
+ apiType: 'anthropic-messages'
170
+ }
171
+ });
172
+ return;
173
+ }
174
+ } catch (e) {
175
+ console.error('[LLM] 解析 models 响应失败:', e);
176
+ }
177
+ }
178
+ resolve({ detected: false, error: '无法获取模型列表' });
179
+ });
180
+ });
181
+
182
+ req.on('error', (err) => {
183
+ console.log('[LLM] OpenClaw 检测失败:', err.message);
184
+ resolve({ detected: false, error: 'OpenClaw 未运行或端口不通' });
185
+ });
186
+
187
+ req.on('timeout', () => {
188
+ req.destroy();
189
+ resolve({ detected: false, error: '连接超时' });
190
+ });
191
+
192
+ req.end();
193
+ });
194
+ }
195
+
196
+ /**
197
+ * 通过 LLM_chat config.json 检测 OpenClaw 配置
198
+ */
199
+ _detectViaConfigFile() {
200
+ try {
201
+ // 尝试多个可能的 config.json 路径
202
+ const possiblePaths = [
203
+ // LLM_chat 项目目录
204
+ path.join(process.cwd(), '..', 'LLM_chat', 'config.json'),
205
+ path.join(process.cwd(), '..', '..', 'LLM_chat', 'config.json'),
206
+ // 用户主目录下的 LLM_chat
207
+ path.join(os.homedir(), 'kimi_code', 'LLM_chat', 'config.json'),
208
+ path.join(os.homedir(), 'LLM_chat', 'config.json'),
209
+ ];
210
+
211
+ let configPath = null;
212
+ for (const p of possiblePaths) {
213
+ if (fs.existsSync(p)) {
214
+ configPath = p;
215
+ break;
216
+ }
217
+ }
218
+
219
+ if (!configPath) {
220
+ return { detected: false, error: '未找到 LLM_chat/config.json' };
221
+ }
222
+
223
+ console.log('[LLM] 找到 LLM_chat 配置:', configPath);
224
+ const llmChatConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
225
+
226
+ // 获取活跃的 provider
227
+ const activeProviderId = llmChatConfig.active_provider_id;
228
+ const providers = llmChatConfig.providers || [];
229
+
230
+ if (!providers.length) {
231
+ return { detected: false, error: '配置中没有 providers' };
232
+ }
233
+
234
+ // 找到活跃的 provider,或第一个可用的
235
+ let provider = providers.find(p => p.id === activeProviderId && p.is_active);
236
+ if (!provider) {
237
+ provider = providers.find(p => p.is_active);
238
+ }
239
+ if (!provider) {
240
+ provider = providers[0];
241
+ }
242
+
243
+ return {
244
+ detected: true,
245
+ provider: {
246
+ id: 'openclaw-kimi',
247
+ name: `OpenClaw / ${provider.name || provider.model_id}`,
248
+ baseUrl: provider.base_url || `http://127.0.0.1:${getOpenclawPort()}/coding`,
249
+ apiKey: provider.api_key || 'proxy-managed',
250
+ modelId: provider.model_id,
251
+ apiType: provider.api_type || 'anthropic-messages'
252
+ }
253
+ };
254
+ } catch (e) {
255
+ console.error('[LLM] 读取 LLM_chat 配置失败:', e.message);
256
+ return { detected: false, error: '读取配置失败: ' + e.message };
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 初始化配置(自动检测或加载已有)
262
+ */
263
+ async initConfig() {
264
+ // 如果已有活跃配置,直接使用
265
+ if (this.config.activeProviderId) {
266
+ return { success: true, mode: this.config.mode, initialized: true };
267
+ }
268
+
269
+ // 自动模式:尝试检测 OpenClaw
270
+ if (this.config.mode === 'auto') {
271
+ const detection = await this.detectOpenClawProxy();
272
+
273
+ if (detection.detected) {
274
+ this.config.providers = [detection.provider];
275
+ this.config.activeProviderId = detection.provider.id;
276
+ this.saveConfig();
277
+
278
+ return {
279
+ success: true,
280
+ mode: 'auto',
281
+ provider: detection.provider,
282
+ message: '已自动连接到 OpenClaw'
283
+ };
284
+ } else {
285
+ return {
286
+ success: false,
287
+ mode: 'auto',
288
+ error: detection.error,
289
+ message: '自动检测失败,请切换到手动模式'
290
+ };
291
+ }
292
+ }
293
+
294
+ // 手动模式:检查是否有手动配置的 provider
295
+ const manualProvider = this.config.providers.find(p => p.id !== 'openclaw-kimi');
296
+ if (manualProvider) {
297
+ this.config.activeProviderId = manualProvider.id;
298
+ this.saveConfig();
299
+ return {
300
+ success: true,
301
+ mode: 'manual',
302
+ provider: manualProvider
303
+ };
304
+ }
305
+
306
+ return {
307
+ success: false,
308
+ mode: this.config.mode,
309
+ error: '未配置 LLM Provider'
310
+ };
311
+ }
312
+
313
+ /**
314
+ * 添加手动配置的 Provider
315
+ */
316
+ addManualProvider(config) {
317
+ const provider = {
318
+ id: 'manual-' + Date.now(),
319
+ name: config.name || 'Manual / ' + config.modelId,
320
+ baseUrl: config.baseUrl,
321
+ apiKey: config.apiKey,
322
+ modelId: config.modelId,
323
+ apiType: config.apiType || 'openai'
324
+ };
325
+
326
+ // 移除旧的 manual provider
327
+ this.config.providers = this.config.providers.filter(p => !p.id.startsWith('manual-'));
328
+
329
+ this.config.providers.push(provider);
330
+ this.config.activeProviderId = provider.id;
331
+ this.config.mode = 'manual';
332
+
333
+ this.saveConfig();
334
+ return provider;
335
+ }
336
+
337
+ /**
338
+ * 切换到指定 Provider
339
+ */
340
+ switchProvider(providerId) {
341
+ const provider = this.config.providers.find(p => p.id === providerId);
342
+ if (provider) {
343
+ this.config.activeProviderId = providerId;
344
+ this.saveConfig();
345
+ return { success: true, provider };
346
+ }
347
+ return { success: false, error: 'Provider 不存在' };
348
+ }
349
+
350
+ /**
351
+ * 获取当前活跃配置
352
+ */
353
+ getActiveProvider() {
354
+ if (!this.config.activeProviderId) return null;
355
+ return this.config.providers.find(p => p.id === this.config.activeProviderId);
356
+ }
357
+
358
+ /**
359
+ * 获取所有配置信息
360
+ */
361
+ getConfig() {
362
+ return {
363
+ mode: this.config.mode,
364
+ activeProviderId: this.config.activeProviderId,
365
+ providers: this.config.providers.map(p => ({
366
+ ...p,
367
+ apiKey: p.apiKey ? '****' + p.apiKey.slice(-4) : ''
368
+ })),
369
+ isConfigured: !!this.config.activeProviderId
370
+ };
371
+ }
372
+
373
+ /**
374
+ * 保存配置
375
+ */
376
+ saveConfig() {
377
+ try {
378
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
379
+ return { success: true };
380
+ } catch (e) {
381
+ return { success: false, error: e.message };
382
+ }
383
+ }
384
+
385
+ /**
386
+ * 设置模式(auto/manual)
387
+ */
388
+ setMode(mode) {
389
+ if (mode !== 'auto' && mode !== 'manual') {
390
+ return { success: false, error: '无效模式' };
391
+ }
392
+
393
+ this.config.mode = mode;
394
+
395
+ // 切换模式时重置 active provider
396
+ if (mode === 'auto') {
397
+ this.config.activeProviderId = null;
398
+ }
399
+
400
+ this.saveConfig();
401
+ return { success: true };
402
+ }
403
+
404
+ /**
405
+ * 调用 LLM API
406
+ */
407
+ async chat(messages, options = {}) {
408
+ const provider = this.getActiveProvider();
409
+ if (!provider) {
410
+ throw new Error('未配置 LLM Provider');
411
+ }
412
+
413
+ const timeout = (options.timeout || 30) * 1000;
414
+
415
+ return new Promise((resolve, reject) => {
416
+ const payload = this.buildPayload(provider, messages, options);
417
+ const endpoint = this.getEndpoint(provider);
418
+
419
+ const url = new URL(endpoint);
420
+
421
+ const headers = {
422
+ 'Content-Type': 'application/json',
423
+ 'Content-Length': Buffer.byteLength(JSON.stringify(payload))
424
+ };
425
+ if (provider.apiKey) {
426
+ headers['Authorization'] = `Bearer ${provider.apiKey}`;
427
+ }
428
+ const requestOptions = {
429
+ hostname: url.hostname,
430
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
431
+ path: url.pathname + url.search,
432
+ method: 'POST',
433
+ headers,
434
+ timeout: timeout
435
+ };
436
+
437
+ const client = url.protocol === 'https:' ? require('https') : http;
438
+
439
+ const req = client.request(requestOptions, (res) => {
440
+ let data = '';
441
+ res.on('data', chunk => data += chunk);
442
+ res.on('end', () => {
443
+ try {
444
+ if (res.statusCode >= 200 && res.statusCode < 300) {
445
+ const result = JSON.parse(data);
446
+ const content = this.parseResponse(provider, result);
447
+ resolve(content);
448
+ } else {
449
+ reject(new Error(`API ${res.statusCode}: ${data}`));
450
+ }
451
+ } catch (e) {
452
+ reject(new Error('解析响应失败: ' + e.message));
453
+ }
454
+ });
455
+ });
456
+
457
+ req.on('error', reject);
458
+ req.on('timeout', () => {
459
+ req.destroy();
460
+ reject(new Error('请求超时'));
461
+ });
462
+
463
+ req.write(JSON.stringify(payload));
464
+ req.end();
465
+ });
466
+ }
467
+
468
+ /**
469
+ * 构建请求体
470
+ */
471
+ buildPayload(provider, messages, options) {
472
+ if (provider.apiType === 'anthropic-messages') {
473
+ const payload = {
474
+ model: provider.modelId,
475
+ messages: messages.map(m => ({
476
+ role: m.role === 'system' ? 'user' : m.role,
477
+ content: m.content
478
+ })),
479
+ max_tokens: options.max_tokens || 4096,
480
+ stream: false
481
+ };
482
+
483
+ // 提取 system 消息
484
+ const systemMsg = messages.find(m => m.role === 'system');
485
+ if (systemMsg) {
486
+ payload.system = systemMsg.content;
487
+ }
488
+
489
+ return payload;
490
+ } else {
491
+ // OpenAI 格式
492
+ return {
493
+ model: provider.modelId,
494
+ messages: messages,
495
+ temperature: options.temperature || 0.7,
496
+ max_tokens: options.max_tokens || 2000,
497
+ stream: false
498
+ };
499
+ }
500
+ }
501
+
502
+ /**
503
+ * 获取 API 端点
504
+ */
505
+ getEndpoint(provider) {
506
+ const baseUrl = provider.baseUrl.replace(/\/$/, '');
507
+
508
+ if (provider.apiType === 'anthropic-messages') {
509
+ return `${baseUrl}/v1/messages`;
510
+ } else {
511
+ return `${baseUrl}/v1/chat/completions`;
512
+ }
513
+ }
514
+
515
+ /**
516
+ * 解析响应
517
+ */
518
+ parseResponse(provider, data) {
519
+ if (provider.apiType === 'anthropic-messages') {
520
+ const content = data.content || [];
521
+ if (content.length > 0) {
522
+ return content[0].text || '';
523
+ }
524
+ } else {
525
+ const choices = data.choices || [];
526
+ if (choices.length > 0) {
527
+ return choices[0].message?.content || '';
528
+ }
529
+ }
530
+ return '';
531
+ }
532
+
533
+ /**
534
+ * 带重试的调用
535
+ */
536
+ async chatWithRetry(messages, maxRetries = 2, options = {}) {
537
+ let lastError;
538
+
539
+ for (let i = 0; i <= maxRetries; i++) {
540
+ try {
541
+ const content = await this.chat(messages, options);
542
+ return { success: true, content };
543
+ } catch (err) {
544
+ lastError = err.message;
545
+ console.error(`[LLM] 调用失败 (${i + 1}/${maxRetries + 1}):`, err.message);
546
+
547
+ if (i < maxRetries) {
548
+ await new Promise(r => setTimeout(r, 1000 * (i + 1)));
549
+ }
550
+ }
551
+ }
552
+
553
+ return { success: false, error: lastError };
554
+ }
555
+
556
+ /**
557
+ * 测试连接
558
+ */
559
+ async testConnection() {
560
+ try {
561
+ const messages = [{ role: 'user', content: '你好,请回复"连接成功"' }];
562
+ const result = await this.chatWithRetry(messages, 0);
563
+ return {
564
+ success: result.success,
565
+ message: result.success ? '连接成功' : result.error,
566
+ response: result.success ? result.content?.substring(0, 100) : null
567
+ };
568
+ } catch (e) {
569
+ return { success: false, message: e.message };
570
+ }
571
+ }
572
+
573
+ /**
574
+ * 设置活跃 Provider(外部授权或配置导入后调用)
575
+ */
576
+ setActiveProvider(provider) {
577
+ this.config.providers = [provider];
578
+ this.config.activeProviderId = provider.id;
579
+ this.config.mode = 'manual';
580
+ this.saveConfig();
581
+ return { success: true };
582
+ }
583
+
584
+ /**
585
+ * 测试指定 provider 的连接(不改变当前活跃 provider)
586
+ */
587
+ async testProvider(provider) {
588
+ const prevId = this.config.activeProviderId;
589
+ const prevProviders = [...this.config.providers];
590
+
591
+ try {
592
+ // 临时切换
593
+ this.config.providers = [provider];
594
+ this.config.activeProviderId = provider.id;
595
+
596
+ const result = await this.chatWithRetry(
597
+ [{ role: 'user', content: '你好,请回复"连接成功"(回复不要超过10个字)' }],
598
+ 1
599
+ );
600
+
601
+ return {
602
+ success: result.success,
603
+ message: result.success ? '连接成功' : result.error,
604
+ response: result.success ? result.content?.substring(0, 100) : null
605
+ };
606
+ } catch (e) {
607
+ return { success: false, message: e.message };
608
+ } finally {
609
+ // 恢复原 provider
610
+ this.config.providers = prevProviders;
611
+ this.config.activeProviderId = prevId;
612
+ this.saveConfig();
613
+ }
614
+ }
615
+
616
+ /**
617
+ * 从所有可用的本地来源发现 LLM Provider
618
+ * 返回 [{ id, name, baseUrl, apiKey, modelId, apiType, source, sourceLabel }]
619
+ */
620
+ async discoverAllModels() {
621
+ const providers = [];
622
+ const seenIds = new Set();
623
+
624
+ // Source 1: OpenClaw HTTP 代理(端口 18789)
625
+ try {
626
+ const httpResult = await this._detectViaHttp();
627
+ if (httpResult.detected && httpResult.provider) {
628
+ const p = { ...httpResult.provider, source: 'openclaw-proxy', sourceLabel: 'OpenClaw 运行中' };
629
+ if (!seenIds.has(p.id)) {
630
+ providers.push(p);
631
+ seenIds.add(p.id);
632
+ }
633
+ }
634
+ } catch (e) { /* ignore */ }
635
+
636
+ // Source 2: LLM_chat config.json
637
+ try {
638
+ const configResult = this._detectViaConfigFile();
639
+ if (configResult.detected && configResult.provider) {
640
+ const p = { ...configResult.provider, source: 'llm-chat', sourceLabel: 'LLM_chat 配置' };
641
+ if (!seenIds.has(p.id)) {
642
+ providers.push(p);
643
+ seenIds.add(p.id);
644
+ }
645
+ }
646
+ } catch (e) { /* ignore */ }
647
+
648
+ // Source 3: OpenClaw openclaw.json
649
+ try {
650
+ const ocResult = this._detectViaOpenclawJson();
651
+ if (ocResult.detected && ocResult.provider) {
652
+ const p = { ...ocResult.provider, source: 'openclaw-config', sourceLabel: 'OpenClaw 配置' };
653
+ if (!seenIds.has(p.id)) {
654
+ providers.push(p);
655
+ seenIds.add(p.id);
656
+ }
657
+ }
658
+ } catch (e) { /* ignore */ }
659
+
660
+ // Source 4: Hermes profiles
661
+ try {
662
+ const hermesResult = this._detectViaHermes();
663
+ if (hermesResult.detected && hermesResult.providers) {
664
+ for (const p of hermesResult.providers) {
665
+ if (!seenIds.has(p.id)) {
666
+ providers.push(p);
667
+ seenIds.add(p.id);
668
+ }
669
+ }
670
+ }
671
+ } catch (e) { /* ignore */ }
672
+
673
+ return providers;
674
+ }
675
+
676
+ /**
677
+ * 发现唯一模型列表(按 modelId 去重,聚合来源信息)
678
+ * 返回 [{ modelId, sources: [...], providers: [...] }]
679
+ */
680
+ async discoverUniqueModels() {
681
+ const providers = await this.discoverAllModels();
682
+ const modelMap = new Map();
683
+
684
+ for (const p of providers) {
685
+ const key = p.modelId || 'unknown';
686
+ if (!modelMap.has(key)) {
687
+ modelMap.set(key, {
688
+ modelId: key,
689
+ modelName: p.modelId || '未知模型',
690
+ displayName: p.name || p.modelId || '未知模型',
691
+ sources: [],
692
+ providers: []
693
+ });
694
+ }
695
+ const entry = modelMap.get(key);
696
+ if (!entry.sources.includes(p.sourceLabel || p.source)) {
697
+ entry.sources.push(p.sourceLabel || p.source);
698
+ }
699
+ entry.providers.push(p);
700
+ }
701
+
702
+ return Array.from(modelMap.values());
703
+ }
704
+
705
+ /**
706
+ * 从 openclaw.json 的任意层级搜索 api_key 相关字段
707
+ */
708
+ _extractKeyFromObject(obj, depth = 0) {
709
+ if (!obj || typeof obj !== 'object' || depth > 5) return null;
710
+ const keyPatterns = ['api_key', 'apiKey', 'apikey', 'API_KEY', 'apikey', 'secret_key', 'secret'];
711
+ for (const [k, v] of Object.entries(obj)) {
712
+ const kl = k.toLowerCase();
713
+ if (keyPatterns.some(p => kl.includes(p.toLowerCase())) && typeof v === 'string' && v.length > 8) {
714
+ return v;
715
+ }
716
+ if (typeof v === 'object') {
717
+ const found = this._extractKeyFromObject(v, depth + 1);
718
+ if (found) return found;
719
+ }
720
+ }
721
+ return null;
722
+ }
723
+
724
+ /**
725
+ * 读取 Hermes profile config.yaml 中的配置(简易 key-value 解析)
726
+ */
727
+ _parseHermesConfig(yamlContent) {
728
+ const result = {};
729
+ for (const line of yamlContent.split('\n')) {
730
+ const match = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*?)\s*$/);
731
+ if (match) {
732
+ result[match[1]] = match[2].replace(/^['"]|['"]$/g, '');
733
+ }
734
+ }
735
+ return result;
736
+ }
737
+
738
+ /**
739
+ * 读取 ~/.hermes/.env 中的环境变量(共享方法,供多个检测路径使用)
740
+ */
741
+ _readHermesEnv() {
742
+ const envPath = getHermesEnvPath();
743
+ const envVars = {};
744
+ try {
745
+ if (fs.existsSync(envPath)) {
746
+ const raw = fs.readFileSync(envPath, 'utf-8');
747
+ for (const line of raw.split('\n')) {
748
+ const trimmed = line.trim();
749
+ if (!trimmed || trimmed.startsWith('#')) continue;
750
+ const eqIdx = trimmed.indexOf('=');
751
+ if (eqIdx === -1) continue;
752
+ const key = trimmed.slice(0, eqIdx).trim();
753
+ const val = trimmed.slice(eqIdx + 1).trim();
754
+ envVars[key] = val.replace(/^["']|["']$/g, '');
755
+ }
756
+ }
757
+ } catch (_) { /* ignore */ }
758
+ return envVars;
759
+ }
760
+
761
+ /**
762
+ * 通过 openclaw.json 检测模型配置(含 API Key 提取)
763
+ */
764
+ _detectViaOpenclawJson() {
765
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
766
+ if (!fs.existsSync(configPath)) return { detected: false };
767
+
768
+ try {
769
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
770
+ let modelName = null;
771
+
772
+ // 尝试多个位置获取模型名称
773
+ if (config.agents?.defaults?.model?.primary) modelName = config.agents.defaults.model.primary;
774
+ else if (config.agents?.defaultModel) modelName = config.agents.defaultModel;
775
+ else if (config.gateway?.model) modelName = config.gateway.model;
776
+
777
+ // 也检查 auth.profiles 中的模型
778
+ if (!modelName) {
779
+ const profiles = config.auth?.profiles || {};
780
+ for (const key of Object.keys(profiles)) {
781
+ const profile = profiles[key];
782
+ if (profile.model?.primary) { modelName = profile.model.primary; break; }
783
+ if (profile.model) { modelName = profile.model; break; }
784
+ }
785
+ }
786
+
787
+ if (!modelName) return { detected: false };
788
+
789
+ // 尝试从 openclaw.json 中提取 API Key(独立 try-catch,不影响模型检测)
790
+ let apiKey = null;
791
+ try {
792
+ apiKey = this._extractKeyFromObject(config);
793
+ } catch (_) { /* ignore */ }
794
+ if (!apiKey) {
795
+ // 尝试从 ~/.hermes/.env 中读取(最可能存 Key 的地方)
796
+ const envVars = this._readHermesEnv();
797
+ const nameLower = modelName.toLowerCase();
798
+ if (nameLower.includes('deepseek')) {
799
+ apiKey = envVars.DEEPSEEK_API_KEY || process.env.DEEPSEEK_API_KEY || null;
800
+ } else if (nameLower.includes('claude') || nameLower.includes('anthropic')) {
801
+ apiKey = envVars.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || null;
802
+ } else {
803
+ apiKey = envVars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || null;
804
+ }
805
+ // 最终保底
806
+ if (!apiKey) {
807
+ apiKey = envVars.DEEPSEEK_API_KEY || envVars.ANTHROPIC_API_KEY || envVars.OPENAI_API_KEY
808
+ || process.env.DEEPSEEK_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY
809
+ || null;
810
+ }
811
+ }
812
+
813
+ // 确定 apiType: 如果模型名包含 claude/anthropic 则用 anthropic-messages,否则 openai
814
+ const nameLower = modelName.toLowerCase();
815
+ const apiType = (nameLower.includes('claude') || nameLower.includes('anthropic'))
816
+ ? 'anthropic-messages' : 'openai';
817
+
818
+ // 端点: 从配置中提取真实地址(排除 localhost)
819
+ let configEndpoint = null;
820
+ try {
821
+ JSON.parse(fs.readFileSync(configPath, 'utf-8'), (k, v) => {
822
+ if (typeof v === 'string' && !configEndpoint) {
823
+ const kl = k.toLowerCase();
824
+ if ((kl.includes('url') || kl.includes('endpoint') || kl.includes('end_point') || kl.includes('base_url')) && v.length > 5) {
825
+ configEndpoint = v;
826
+ }
827
+ }
828
+ return v;
829
+ });
830
+ } catch (_) { /* ignore */ }
831
+ const baseUrl = resolveEndpoint(modelName, configEndpoint) || `http://127.0.0.1:${getOpenclawPort()}/coding`;
832
+
833
+ return {
834
+ detected: true,
835
+ provider: {
836
+ id: 'openclaw-' + modelName.replace(/[^a-zA-Z0-9]/g, '-'),
837
+ name: `OpenClaw / ${modelName}`,
838
+ baseUrl,
839
+ apiKey: apiKey || 'proxy-managed',
840
+ modelId: ROUTING_ALIAS_MAP[modelName] || modelName,
841
+ apiType
842
+ }
843
+ };
844
+ } catch (e) {
845
+ return { detected: false };
846
+ }
847
+ }
848
+
849
+ /**
850
+ * 通过 Hermes profiles 检测模型配置(含 API Key 提取)
851
+ */
852
+ _detectViaHermes() {
853
+ const profiles = [];
854
+
855
+ // 方案1: hermes profile list CLI(用 spawnSync 避免 Windows cmd.exe 乱码)
856
+ try {
857
+ const result = spawnSync('hermes', ['profile', 'list'], {
858
+ encoding: 'utf-8',
859
+ timeout: 3000,
860
+ windowsHide: true,
861
+ stdio: ['ignore', 'pipe', 'pipe']
862
+ });
863
+ if (result.error || result.status !== 0) throw result.error || new Error('non-zero exit');
864
+ const output = result.stdout;
865
+ const lines = output.split('\n').filter(l => l.trim());
866
+ let parsing = false;
867
+ for (const line of lines) {
868
+ if (line.includes('───')) { parsing = true; continue; }
869
+ if (!parsing) continue;
870
+ const trimmed = line.trim();
871
+ if (!trimmed) continue;
872
+ const isDefault = trimmed.startsWith('◆');
873
+ const clean = trimmed.replace(/^◆\s*/, '');
874
+ const parts = clean.split(/\s{2,}/).map(s => s.trim());
875
+ if (parts.length >= 1) {
876
+ profiles.push({
877
+ name: parts[0],
878
+ model: parts[1] || 'unknown',
879
+ isDefault
880
+ });
881
+ }
882
+ }
883
+ } catch (e) {
884
+ // CLI 不可用,尝试目录发现
885
+ }
886
+
887
+ // 方案2: 读取 ~/.hermes/profiles/ 目录
888
+ const profilesDir = getHermesProfilesDir();
889
+ if (profiles.length === 0) {
890
+ try {
891
+ if (fs.existsSync(profilesDir)) {
892
+ const dirs = fs.readdirSync(profilesDir);
893
+ for (const dir of dirs) {
894
+ const configPath = path.join(profilesDir, dir, 'config.yaml');
895
+ let model = 'unknown';
896
+ if (fs.existsSync(configPath)) {
897
+ const content = fs.readFileSync(configPath, 'utf-8');
898
+ const match = content.match(/default:\s*(\S+)/);
899
+ if (match) model = match[1];
900
+ }
901
+ profiles.push({ name: dir, model, isDefault: false });
902
+ }
903
+ }
904
+ } catch (e) { /* ignore */ }
905
+ }
906
+
907
+ // ─── 读取 ~/.hermes/.env(共享方法,用于 API Key 提取)───
908
+ const hermesDir = getHermesDir();
909
+ const envVars = this._readHermesEnv();
910
+
911
+ // 方案3: 读取根目录 config.yaml(当 profiles 为空时兜底)
912
+ const hermesConfigPath = getHermesConfigPath();
913
+ let rootConfigModel = null;
914
+ let rootConfigProvider = null;
915
+ let rootConfigBaseUrl = null;
916
+ let rootApiKey = '';
917
+
918
+ if (profiles.length === 0) {
919
+ try {
920
+ if (fs.existsSync(hermesConfigPath)) {
921
+ const content = fs.readFileSync(hermesConfigPath, 'utf-8');
922
+ const lines = content.split('\n');
923
+
924
+ // 定向提取 model: 块下的 default / provider / base_url
925
+ let inModelBlock = false;
926
+ for (const line of lines) {
927
+ const trimmed = line.trim();
928
+ if (trimmed === 'model:' || trimmed.startsWith('model:')) {
929
+ inModelBlock = true;
930
+ continue;
931
+ }
932
+ if (inModelBlock) {
933
+ // 如果遇到非缩进的新 key,表示已离开 model 块
934
+ if (trimmed && !line.startsWith(' ') && !line.startsWith('\t') && trimmed.includes(':')) {
935
+ inModelBlock = false;
936
+ continue;
937
+ }
938
+ const subMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*?)$/);
939
+ if (subMatch) {
940
+ const val = subMatch[2].replace(/^["']|["']$/g, '');
941
+ if (subMatch[1] === 'default') rootConfigModel = val;
942
+ else if (subMatch[1] === 'provider') rootConfigProvider = val;
943
+ else if (subMatch[1] === 'base_url') rootConfigBaseUrl = val;
944
+ }
945
+ }
946
+ }
947
+
948
+ // 从 .env 中根据 provider 类型构造 key 名(如 DEEPSEEK_API_KEY)
949
+ if (rootConfigProvider) {
950
+ const envKeyName = rootConfigProvider.toUpperCase() + '_API_KEY';
951
+ if (envVars[envKeyName]) {
952
+ rootApiKey = envVars[envKeyName];
953
+ }
954
+ }
955
+ // 再从 .env 中尝试常见 key 名
956
+ if (!rootApiKey) {
957
+ rootApiKey = envVars.DEEPSEEK_API_KEY || envVars.ANTHROPIC_API_KEY
958
+ || envVars.OPENAI_API_KEY || envVars.API_KEY || '';
959
+ }
960
+
961
+ if (rootConfigModel) {
962
+ profiles.push({
963
+ name: rootConfigProvider || 'hermes',
964
+ model: rootConfigModel,
965
+ isDefault: true
966
+ });
967
+ }
968
+ }
969
+ } catch (e) { /* ignore */ }
970
+ }
971
+
972
+ if (profiles.length === 0) return { detected: false };
973
+
974
+ const providers = profiles.map((p, i) => {
975
+ // 尝试从 config.yaml 提取 API Key
976
+ let apiKey = '';
977
+ let configBaseUrl = null;
978
+ const configPath = path.join(profilesDir, p.name, 'config.yaml');
979
+ try {
980
+ if (fs.existsSync(configPath)) {
981
+ const content = fs.readFileSync(configPath, 'utf-8');
982
+ const kv = this._parseHermesConfig(content);
983
+ // API Key — 补充 DEEPSEEK 查找
984
+ apiKey = kv.anthropic_api_key || kv.openai_api_key || kv.api_key
985
+ || kv.ANTHROPIC_API_KEY || kv.OPENAI_API_KEY || kv.API_KEY
986
+ || kv.DEEPSEEK_API_KEY || kv.deepseek_api_key
987
+ || '';
988
+ // 真实端点
989
+ configBaseUrl = kv.base_url || kv.api_base || kv.openai_api_base || null;
990
+ }
991
+ } catch (e) { /* ignore */ }
992
+
993
+ // 如果 profile config 中没找到 key,用根配置的 key(方案3提取的)
994
+ if (!apiKey) {
995
+ apiKey = rootApiKey;
996
+ }
997
+
998
+ // 如果 config 中没有,检查环境变量 — 补充 DEEPSEEK_API_KEY
999
+ if (!apiKey) {
1000
+ apiKey = process.env.DEEPSEEK_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || '';
1001
+ }
1002
+
1003
+ // 如果还没有,从 .env 中读取
1004
+ if (!apiKey) {
1005
+ apiKey = envVars.DEEPSEEK_API_KEY || envVars.ANTHROPIC_API_KEY
1006
+ || envVars.OPENAI_API_KEY || envVars.API_KEY || '';
1007
+ }
1008
+
1009
+ const baseUrl = rootConfigBaseUrl || resolveEndpoint(p.model, configBaseUrl) || `http://127.0.0.1:${8642 + i}`;
1010
+
1011
+ // 根据模型名判断 apiType
1012
+ const modelLower = (p.model || '').toLowerCase();
1013
+ const apiType = (modelLower.includes('claude') || modelLower.includes('anthropic'))
1014
+ ? 'anthropic-messages' : 'openai';
1015
+
1016
+ return {
1017
+ id: 'hermes-' + p.name.replace(/[^a-zA-Z0-9]/g, '-'),
1018
+ name: `Hermes / ${p.name}${p.model !== 'unknown' ? ' (' + p.model + ')' : ''}`,
1019
+ baseUrl,
1020
+ apiKey,
1021
+ modelId: p.model !== 'unknown' ? p.model : 'hermes-agent',
1022
+ apiType: 'openai',
1023
+ source: 'hermes',
1024
+ sourceLabel: 'Hermes Agent'
1025
+ };
1026
+ });
1027
+
1028
+ return { detected: true, providers };
1029
+ }
1030
+ }
1031
+
1032
+ module.exports = { LLMClient };