@lingyao037/openclaw-lingyao-cli 0.4.1 → 0.5.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.
- package/cli.mjs +186 -4
- package/dist/{accounts-DeDlxyS9.d.ts → accounts-BNuShH7y.d.ts} +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.js +41 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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(' •
|
|
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) {
|
package/dist/cli.d.ts
CHANGED
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-
|
|
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-
|
|
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
|