@lingyao037/openclaw-lingyao-cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -15,10 +15,14 @@ import { spawnSync, spawn, execSync } from 'child_process';
15
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'fs';
16
16
  import { join, dirname } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
+ import https from 'https';
19
+ import QRCode from 'qrcode';
18
20
 
19
21
  const __filename = fileURLToPath(import.meta.url);
20
22
  const __dirname = dirname(__filename);
21
23
 
24
+ const LINGYAO_API = 'https://api.lingyao.live/lyoc';
25
+
22
26
  // 颜色
23
27
  const GREEN = '\x1b[32m';
24
28
  const YELLOW = '\x1b[33m';
@@ -33,6 +37,8 @@ class LingyaoInstaller {
33
37
  this.skipDeps = skipDeps;
34
38
  this.pluginName = '@lingyao/openclaw-lingyao-cli';
35
39
  this.channelId = 'lingyao';
40
+ this.gatewayId = null;
41
+ this.gatewayToken = null;
36
42
  }
37
43
 
38
44
  setOpenClawPath(path) {
@@ -317,11 +323,15 @@ class LingyaoInstaller {
317
323
  }
318
324
 
319
325
  // 启用 lingyao 插件(serverUrl 已内置,用户不可配置)
326
+ const gatewayId = this.gatewayId || this.generateGatewayId();
327
+ this.gatewayId = gatewayId;
328
+
320
329
  config.plugins.entries.lingyao = {
321
330
  enabled: true,
322
331
  config: {
323
332
  maxOfflineMessages: 100,
324
- tokenExpiryDays: 30
333
+ tokenExpiryDays: 30,
334
+ gatewayId,
325
335
  }
326
336
  };
327
337
 
@@ -382,7 +392,7 @@ class LingyaoInstaller {
382
392
  this.warn('注意事项:');
383
393
  this.log(' • 确保 OpenClaw Gateway 正在运行');
384
394
  this.log(' • 需要可访问的 lingyao.live 中转服务');
385
- this.log(' • 配对与设备接入通过灵爻 App 和中转服务完成');
395
+ this.log(' • 如需重新配对,请再次运行安装命令');
386
396
  this.log('');
387
397
 
388
398
  this.info('架构说明:');
@@ -396,6 +406,175 @@ class LingyaoInstaller {
396
406
  this.log('');
397
407
  }
398
408
 
409
+ // HTTP helpers
410
+ httpsPost(path, body) {
411
+ return new Promise((resolve, reject) => {
412
+ const data = JSON.stringify(body);
413
+ const url = new URL(path, LINGYAO_API);
414
+ const headers = { 'Content-Type': 'application/json' };
415
+ if (this.gatewayToken) {
416
+ headers['Authorization'] = `Bearer ${this.gatewayToken}`;
417
+ }
418
+ const req = https.request(url, { method: 'POST', headers }, (res) => {
419
+ let chunk = '';
420
+ res.on('data', (c) => chunk += c);
421
+ res.on('end', () => {
422
+ try { resolve(JSON.parse(chunk)); }
423
+ catch { reject(new Error(`Invalid JSON: ${chunk}`)); }
424
+ });
425
+ });
426
+ req.on('error', reject);
427
+ req.write(data);
428
+ req.end();
429
+ });
430
+ }
431
+
432
+ httpsGet(path) {
433
+ return new Promise((resolve, reject) => {
434
+ const url = new URL(path, LINGYAO_API);
435
+ https.get(url, (res) => {
436
+ let chunk = '';
437
+ res.on('data', (c) => chunk += c);
438
+ res.on('end', () => {
439
+ try { resolve(JSON.parse(chunk)); }
440
+ catch { reject(new Error(`Invalid JSON: ${chunk}`)); }
441
+ });
442
+ }).on('error', reject);
443
+ });
444
+ }
445
+
446
+ // Generate a stable gateway ID
447
+ generateGatewayId() {
448
+ const os = require('os');
449
+ const host = os.hostname().split('.')[0].replace(/[^a-z0-9]/gi, '').toLowerCase();
450
+ const suffix = Math.random().toString(36).substring(2, 8);
451
+ return `gw_openclaw_${host}_default_${suffix}`;
452
+ }
453
+
454
+ // Pair device flow: register gateway, init pairing, show QR, poll status
455
+ async pairDevice() {
456
+ this.log('\n═══════════════════════════════════════════════════', CYAN);
457
+ this.log(' 设备配对', CYAN);
458
+ this.log('═══════════════════════════════════════════════════\n', CYAN);
459
+
460
+ try {
461
+ // 1. Register gateway
462
+ this.info('正在注册到 lingyao.live...');
463
+ const regResult = await this.httpsPost('/gateway/register', {
464
+ gatewayId: this.gatewayId,
465
+ version: '0.4.1',
466
+ capabilities: { websocket: true },
467
+ });
468
+
469
+ if (!regResult.gatewayToken) {
470
+ this.error('Gateway 注册失败');
471
+ return false;
472
+ }
473
+
474
+ this.gatewayToken = regResult.gatewayToken;
475
+ this.success('Gateway 注册成功');
476
+
477
+ // 2. Init pairing
478
+ this.info('正在生成配对码...');
479
+ const pairResult = await this.httpsPost('/pairing/init', {});
480
+
481
+ if (!pairResult.session) {
482
+ this.error('配对码生成失败');
483
+ return false;
484
+ }
485
+
486
+ // 3. Display QR code and pairing code
487
+ this.log('');
488
+ this.log('╔═══════════════════════════════════════════════════════════╗', CYAN);
489
+ this.log('║ 请使用灵爻 App 扫描下方二维码 ║', CYAN);
490
+ this.log('╚═══════════════════════════════════════════════════════════╝', CYAN);
491
+ this.log('');
492
+
493
+ try {
494
+ const qr = await QRCode.toString(pairResult.qrData, {
495
+ type: 'terminal',
496
+ small: true,
497
+ });
498
+ this.log(qr);
499
+ } catch {
500
+ this.info(`配对链接: ${pairResult.qrData}`);
501
+ }
502
+
503
+ this.log('');
504
+ this.log(` 接入码: ${YELLOW}${pairResult.code}${NC}`, NC);
505
+ this.log('');
506
+ this.info('等待灵爻 App 扫码确认... (Ctrl+C 跳过)');
507
+ this.log('');
508
+
509
+ // 4. Poll pairing status
510
+ const pollInterval = 3000;
511
+ const maxAttempts = 600; // 30 minutes max
512
+ for (let i = 0; i < maxAttempts; i++) {
513
+ await new Promise(r => setTimeout(r, pollInterval));
514
+
515
+ try {
516
+ const status = await this.httpsGet(`/pairing/status/${pairResult.session}`);
517
+
518
+ if (status.status === 'completed') {
519
+ this.log('');
520
+ this.success('配对成功!');
521
+ this.log('');
522
+ this.log(` 设备 ID: ${status.deviceInfo?.name ?? 'unknown'}`);
523
+ this.log(` 平台: ${status.deviceInfo?.platform ?? 'unknown'}`);
524
+ this.log(` 版本: ${status.deviceInfo?.version ?? 'unknown'}`);
525
+ this.log('');
526
+
527
+ // Save device info to OpenClaw config
528
+ this.saveDeviceInfo(status.deviceInfo);
529
+ return true;
530
+ }
531
+
532
+ if (status.status === 'expired') {
533
+ this.error('配对码已过期');
534
+ return false;
535
+ }
536
+
537
+ // Still pending - show spinner dot
538
+ process.stdout.write('.');
539
+ } catch {
540
+ // Network error during polling, just retry
541
+ }
542
+ }
543
+
544
+ this.warn('\n配对等待超时,可稍后通过灵爻 App 重新配对');
545
+ return false;
546
+
547
+ } catch (error) {
548
+ this.error(`配对失败: ${error.message}`);
549
+ return false;
550
+ }
551
+ }
552
+
553
+ // Save paired device info to OpenClaw config
554
+ saveDeviceInfo(deviceInfo) {
555
+ if (!this.openclawPath) return;
556
+
557
+ const configFiles = [
558
+ join(this.openclawPath, 'openclaw.json'),
559
+ join(this.openclawPath, 'config.json'),
560
+ ];
561
+
562
+ for (const file of configFiles) {
563
+ if (!existsSync(file)) continue;
564
+ try {
565
+ const config = JSON.parse(readFileSync(file, 'utf-8'));
566
+ if (config.plugins?.entries?.lingyao?.config) {
567
+ config.plugins.entries.lingyao.config.pairedDevice = deviceInfo;
568
+ writeFileSync(file, JSON.stringify(config, null, 2), 'utf-8');
569
+ this.success('设备信息已保存到配置');
570
+ }
571
+ } catch {
572
+ // Ignore config write errors
573
+ }
574
+ break; // Only write to first found config file
575
+ }
576
+ }
577
+
399
578
  // 主安装流程
400
579
  async install() {
401
580
  try {
@@ -409,7 +588,7 @@ class LingyaoInstaller {
409
588
  return false;
410
589
  }
411
590
 
412
- // 步骤 3: 配置插件
591
+ // 步骤 3: 配置插件(含 gatewayId 生成)
413
592
  if (!this.configurePlugin()) {
414
593
  return false;
415
594
  }
@@ -417,7 +596,10 @@ class LingyaoInstaller {
417
596
  // 步骤 4: 重启网关
418
597
  const restarted = this.restartGateway();
419
598
 
420
- // 步骤 5: 显示说明
599
+ // 步骤 5: 设备配对
600
+ await this.pairDevice();
601
+
602
+ // 步骤 6: 显示说明
421
603
  this.showInstructions();
422
604
 
423
605
  if (!restarted) {
@@ -179,6 +179,7 @@ interface LingyaoAccountConfig {
179
179
  allowFrom?: string[];
180
180
  maxOfflineMessages?: number;
181
181
  tokenExpiryDays?: number;
182
+ gatewayId?: string;
182
183
  }
183
184
  /**
184
185
  * Channel configuration
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { L as LingyaoRuntime, H as HealthStatus, A as AccountManager } from './accounts-DeDlxyS9.js';
1
+ import { L as LingyaoRuntime, H as HealthStatus, A as AccountManager } from './accounts-BNuShH7y.js';
2
2
 
3
3
  /**
4
4
  * Probe status levels
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as openclaw_plugin_sdk_core from 'openclaw/plugin-sdk/core';
2
2
  import * as openclaw_plugin_sdk from 'openclaw/plugin-sdk';
3
- import { L as LingyaoRuntime, D as DeviceInfo, a as DeviceToken, A as AccountManager, S as SyncRequest, b as SyncResponse, c as LingyaoMessage, d as LingyaoConfig, N as NotifyPayload, H as HealthStatus } from './accounts-DeDlxyS9.js';
4
- export { e as AckRequest, f as DiarySyncPayload, F as FailedEntry, g as LINGYAO_SERVER_URL, h as LingyaoAccount, i as LingyaoAccountConfig, M as MemorySyncPayload, j as MessageType, k as NotifyAction, l as NotifyRequest, P as PairingCode, m as PairingConfirmRequest, n as PairingConfirmResponse, o as PollRequest, p as PollResponse, Q as QueuedMessage, T as TokenRefreshRequest, q as TokenRefreshResponse, W as WebSocketConnection } from './accounts-DeDlxyS9.js';
3
+ import { L as LingyaoRuntime, D as DeviceInfo, a as DeviceToken, A as AccountManager, S as SyncRequest, b as SyncResponse, c as LingyaoMessage, d as LingyaoConfig, N as NotifyPayload, H as HealthStatus } from './accounts-BNuShH7y.js';
4
+ export { e as AckRequest, f as DiarySyncPayload, F as FailedEntry, g as LINGYAO_SERVER_URL, h as LingyaoAccount, i as LingyaoAccountConfig, M as MemorySyncPayload, j as MessageType, k as NotifyAction, l as NotifyRequest, P as PairingCode, m as PairingConfirmRequest, n as PairingConfirmResponse, o as PollRequest, p as PollResponse, Q as QueuedMessage, T as TokenRefreshRequest, q as TokenRefreshResponse, W as WebSocketConnection } from './accounts-BNuShH7y.js';
5
5
 
6
6
  /**
7
7
  * 错误处理模块
@@ -431,6 +431,7 @@ declare enum WSMessageType {
431
431
  MESSAGE_FAILED = "message_failed",
432
432
  APP_MESSAGE = "app_message",// 来自 App 的消息
433
433
  DEVICE_ONLINE = "device_online",
434
+ PAIRING_COMPLETED = "pairing_completed",
434
435
  ERROR = "error"
435
436
  }
436
437
  /**
@@ -478,6 +479,15 @@ type WSClientEvent = {
478
479
  type: "appMessage";
479
480
  deviceId: string;
480
481
  message: AppMessage["payload"]["message"];
482
+ } | {
483
+ type: "pairing_completed";
484
+ deviceId: string;
485
+ deviceInfo: {
486
+ name: string;
487
+ platform: string;
488
+ version: string;
489
+ };
490
+ sessionId: string;
481
491
  };
482
492
  /**
483
493
  * WebSocket 客户端配置
@@ -574,6 +584,10 @@ declare class LingyaoWSClient {
574
584
  * 处理消息发送成功
575
585
  */
576
586
  private handleMessageDelivered;
587
+ /**
588
+ * 处理配对完成通知(来自 lingyao.live 服务器)
589
+ */
590
+ private handlePairingCompleted;
577
591
  /**
578
592
  * 处理消息发送失败
579
593
  */
package/dist/index.js CHANGED
@@ -97,6 +97,7 @@ function createConfigAdapter() {
97
97
  enabled: accountConfig?.enabled !== false,
98
98
  dmPolicy: accountConfig?.dmPolicy ?? "paired",
99
99
  allowFrom: accountConfig?.allowFrom ?? [],
100
+ gatewayId: accountConfig?.gatewayId,
100
101
  rawConfig: accountConfig
101
102
  };
102
103
  },
@@ -660,6 +661,7 @@ var LingyaoWSClient = class {
660
661
  this.registerMessageHandler("message_delivered" /* MESSAGE_DELIVERED */, this.handleMessageDelivered.bind(this));
661
662
  this.registerMessageHandler("message_failed" /* MESSAGE_FAILED */, this.handleMessageFailed.bind(this));
662
663
  this.registerMessageHandler("app_message" /* APP_MESSAGE */, this.handleAppMessage.bind(this));
664
+ this.registerMessageHandler("pairing_completed" /* PAIRING_COMPLETED */, this.handlePairingCompleted.bind(this));
663
665
  this.registerMessageHandler("error" /* ERROR */, this.handleError.bind(this));
664
666
  }
665
667
  /**
@@ -908,6 +910,24 @@ var LingyaoWSClient = class {
908
910
  messageId: message.id
909
911
  });
910
912
  }
913
+ /**
914
+ * 处理配对完成通知(来自 lingyao.live 服务器)
915
+ */
916
+ handlePairingCompleted(message) {
917
+ const payload = message.payload;
918
+ this.logger.info("Pairing completed", {
919
+ deviceId: payload?.deviceId,
920
+ sessionId: payload?.sessionId
921
+ });
922
+ if (this.config.eventHandler) {
923
+ this.config.eventHandler({
924
+ type: "pairing_completed",
925
+ deviceId: payload?.deviceId,
926
+ deviceInfo: payload?.deviceInfo,
927
+ sessionId: payload?.sessionId
928
+ });
929
+ }
930
+ }
911
931
  /**
912
932
  * 处理消息发送失败
913
933
  */
@@ -2505,7 +2525,7 @@ var MultiAccountOrchestrator = class {
2505
2525
  return;
2506
2526
  }
2507
2527
  this.runtime.logger.info(`Starting account "${accountId}"`);
2508
- const gatewayId = generateGatewayId(accountId);
2528
+ const gatewayId = account.gatewayId ?? generateGatewayId(accountId);
2509
2529
  const storagePrefix = `lingyao:${accountId}`;
2510
2530
  const accountManager = new AccountManager(this.runtime);
2511
2531
  const messageProcessor = new MessageProcessor(this.runtime, accountManager);
@@ -2756,9 +2776,29 @@ var MultiAccountOrchestrator = class {
2756
2776
  });
2757
2777
  state.errorHandler.handleError(event.error);
2758
2778
  break;
2779
+ case "pairing_completed":
2780
+ this.handlePairingCompleted(state, event);
2781
+ break;
2759
2782
  }
2760
2783
  };
2761
2784
  }
2785
+ /**
2786
+ * Handle pairing completed event from WS — auto-bind device.
2787
+ */
2788
+ async handlePairingCompleted(state, event) {
2789
+ const { deviceId, deviceInfo } = event;
2790
+ this.runtime.logger.info(`[${state.accountId}] Pairing completed, auto-binding device`, { deviceId, deviceInfo });
2791
+ try {
2792
+ await state.accountManager.addDevice(deviceId, {
2793
+ name: deviceInfo?.name ?? deviceId,
2794
+ platform: deviceInfo?.platform ?? "harmonyos",
2795
+ version: deviceInfo?.version ?? ""
2796
+ });
2797
+ this.runtime.logger.info(`[${state.accountId}] Device auto-bound: ${deviceId}`);
2798
+ } catch (error) {
2799
+ this.runtime.logger.error(`[${state.accountId}] Failed to auto-bind device: ${deviceId}`, error);
2800
+ }
2801
+ }
2762
2802
  };
2763
2803
 
2764
2804
  // src/channel.ts