@lumachat/lumachat 0.15.2 → 0.15.4
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/README.md +36 -44
- package/openclaw.plugin.json +3 -2
- package/package.json +8 -7
- package/src/index.ts +335 -136
package/README.md
CHANGED
|
@@ -1,60 +1,52 @@
|
|
|
1
|
-
# LumaChat OpenClaw
|
|
1
|
+
# LumaChat OpenClaw 通道插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
该插件用于将 OpenClaw 接入 LumaChat App,当前配对方式为 **OAuth 2.0 Device Flow**(授权码 + 扫码)。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
- 完整配对流程(请求配对码+安全码、轮询状态、保存 token 到 `channels.clawdchat`)。
|
|
7
|
-
- 与 LumaChat 后端保持 WebSocket 长连接。
|
|
8
|
-
- 入站消息接入(LumaChat -> OpenClaw),按 `request_id` 回传回复。
|
|
9
|
-
- CLI 配对入口(`openclaw channels login --channel lumachat`)。
|
|
5
|
+
## 版本与安装
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
先确认 Node.js 版本满足 `>=22.12.0`(`openclaw` npm 包要求):
|
|
13
|
-
```
|
|
14
|
-
node -v
|
|
15
|
-
```
|
|
7
|
+
当前版本:`0.15.4`
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
```bash
|
|
10
|
+
pnpm openclaw plugins install @lumachat/lumachat@0.15.4
|
|
11
|
+
# 或升级
|
|
12
|
+
pnpm openclaw plugins update lumachat
|
|
13
|
+
openclaw gateway restart
|
|
21
14
|
```
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
## 配对流程(OAuth 设备授权)
|
|
17
|
+
|
|
18
|
+
1. 在 Clawdbot 机器执行:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
openclaw channels login --channel lumachat
|
|
26
22
|
```
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
24
|
+
2. 终端会输出:
|
|
25
|
+
- 二维码(可直接扫码)
|
|
26
|
+
- 6 位授权码(`user_code`,可手动输入)
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
-
|
|
28
|
+
3. 在 LumaChat App 中进入:`设置 -> Clawdbot 配对`
|
|
29
|
+
- 可扫码授权
|
|
30
|
+
- 或手动输入 6 位授权码
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
- 修复配对等待在部分时区立即超时的问题(将过期时间按 UTC 解析)。
|
|
32
|
+
4. 授权成功后,插件会自动保存 `channelToken` 并建立连接。
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
- 插件对外品牌升级为 LumaChat(npm 包名改为 `@lumachat/lumachat`)。
|
|
40
|
-
- 支持返回并展示 6 位安全码(pairing confirm code),与 App 双码配对流程保持一致。
|
|
41
|
-
- 补齐 clawdchat 消息目标解析(`chat:<uuid>` / `clawdchat:<uuid>`),避免 `Unknown target` 导致回复丢失。
|
|
34
|
+
## 常见问题
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```
|
|
36
|
+
- 提示 `授权码已过期`:重新执行 `openclaw channels login --channel lumachat`
|
|
37
|
+
- 提示 `OAuth client_id 无效`:升级插件和后端到最新版本
|
|
38
|
+
- 一直等待授权:确认 App 已登录并在配对页完成授权
|
|
47
39
|
|
|
48
|
-
##
|
|
49
|
-
1. 在 Clawdbot 机器上执行:`openclaw channels login --channel lumachat`。
|
|
50
|
-
2. 终端会显示 6 位配对码和 6 位安全码。
|
|
51
|
-
3. 在 LumaChat App 中:设置 -> Clawdbot 配对,先登录账号,再输入配对码和安全码。
|
|
52
|
-
4. 插件会保存通道 token 并自动连接后端。
|
|
40
|
+
## 可选环境变量
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
42
|
+
- `LUMACHAT_API_BASE_URL`:后端地址(默认 `https://api-love2.huaizuo2029.cn`)
|
|
43
|
+
- `LUMACHAT_CHANNEL_KEY` / `LUMACHAT_SHARED_KEY`:后端要求 `X-Clawdchat-Channel-Key` 时使用
|
|
44
|
+
- `CLAWDCHAT_API_BASE_URL`:同上(兼容旧变量)
|
|
45
|
+
- `CLAWDCHAT_CHANNEL_KEY` / `CLAWDCHAT_SHARED_KEY`:同上(兼容旧变量)
|
|
57
46
|
|
|
58
47
|
## 备注
|
|
59
|
-
|
|
60
|
-
-
|
|
48
|
+
|
|
49
|
+
- 插件 OAuth 相关接口:
|
|
50
|
+
- `POST /oauth/device/code`
|
|
51
|
+
- `POST /oauth/token`
|
|
52
|
+
- WebSocket 连接地址:`/ws/channel/clawdchat`
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumachat/lumachat",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
4
4
|
"description": "LumaChat channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"package.json"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"qrcode": "^1.5.4",
|
|
17
18
|
"ws": "^8.18.0"
|
|
18
19
|
},
|
|
19
20
|
"openclaw": {
|
|
@@ -21,14 +22,14 @@
|
|
|
21
22
|
"./src/index.ts"
|
|
22
23
|
],
|
|
23
24
|
"channel": {
|
|
24
|
-
"id": "
|
|
25
|
+
"id": "lumachat",
|
|
25
26
|
"label": "LumaChat",
|
|
26
|
-
"selectionLabel": "LumaChat (
|
|
27
|
-
"docsPath": "/channels/
|
|
28
|
-
"blurb": "
|
|
27
|
+
"selectionLabel": "LumaChat (授权码/扫码)",
|
|
28
|
+
"docsPath": "/channels/lumachat",
|
|
29
|
+
"blurb": "通过 OAuth 授权码或扫码把 Clawdbot 接入 LumaChat。",
|
|
29
30
|
"aliases": [
|
|
30
|
-
"
|
|
31
|
-
"
|
|
31
|
+
"lumachat",
|
|
32
|
+
"clawdchat"
|
|
32
33
|
]
|
|
33
34
|
},
|
|
34
35
|
"install": {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import QRCode from "qrcode";
|
|
2
3
|
import WebSocket from "ws";
|
|
3
4
|
import {
|
|
4
5
|
createReplyPrefixContext,
|
|
@@ -15,13 +16,19 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-
|
|
|
15
16
|
const HEARTBEAT_INTERVAL_MS = 25_000;
|
|
16
17
|
const RECONNECT_BASE_MS = 2_000;
|
|
17
18
|
const RECONNECT_MAX_MS = 20_000;
|
|
18
|
-
const PAIRING_POLL_INTERVAL_MS = 2_000;
|
|
19
19
|
const PAIRING_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
20
|
+
const DEVICE_AUTH_POLL_DEFAULT_INTERVAL_SECONDS = 5;
|
|
21
|
+
const DEVICE_AUTH_POLL_MIN_INTERVAL_MS = 1_000;
|
|
22
|
+
const DEVICE_AUTH_SLOW_DOWN_ADD_MS = 2_000;
|
|
23
|
+
const OAUTH_DEVICE_CLIENT_ID = "clawdchat-gateway";
|
|
24
|
+
const OAUTH_DEVICE_SCOPE = "channel:bind";
|
|
25
|
+
const OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
26
|
+
const CHANNEL_ID = "lumachat";
|
|
27
|
+
const LEGACY_CHANNEL_ID = "clawdchat";
|
|
20
28
|
|
|
21
29
|
const HEADER_CHANNEL_KEY = "X-Clawdchat-Channel-Key";
|
|
22
|
-
const HEADER_PAIRING_SECRET = "X-Pairing-Secret";
|
|
23
30
|
|
|
24
|
-
const
|
|
31
|
+
const deviceAuthSessions = new Map<string, DeviceAuthSession>();
|
|
25
32
|
const stopControllers = new Map<string, AbortController>();
|
|
26
33
|
|
|
27
34
|
type ClawdchatConfigSection = {
|
|
@@ -47,25 +54,35 @@ type ResolvedClawdchatAccount = {
|
|
|
47
54
|
pairedAt?: string;
|
|
48
55
|
};
|
|
49
56
|
|
|
50
|
-
type
|
|
57
|
+
type DeviceAuthSession = {
|
|
51
58
|
accountId: string;
|
|
52
59
|
baseUrl: string;
|
|
53
60
|
sharedKey?: string;
|
|
54
61
|
gatewayId: string;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
deviceCode: string;
|
|
63
|
+
userCode: string;
|
|
64
|
+
verificationUri: string;
|
|
65
|
+
verificationUriComplete: string;
|
|
66
|
+
pollIntervalSeconds: number;
|
|
59
67
|
expiresAt?: string;
|
|
60
68
|
};
|
|
61
69
|
|
|
62
|
-
type
|
|
63
|
-
status: "
|
|
70
|
+
type DeviceTokenSuccess = {
|
|
71
|
+
status: "ok";
|
|
64
72
|
channelToken?: string;
|
|
65
73
|
bindingId?: string;
|
|
66
|
-
expiresAt?: string;
|
|
67
74
|
};
|
|
68
75
|
|
|
76
|
+
type DeviceTokenError = {
|
|
77
|
+
status: "error";
|
|
78
|
+
error: string;
|
|
79
|
+
errorDescription?: string;
|
|
80
|
+
statusCode: number;
|
|
81
|
+
rawBody?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type DeviceTokenPollResult = DeviceTokenSuccess | DeviceTokenError;
|
|
85
|
+
|
|
69
86
|
type InboundPayload = {
|
|
70
87
|
type?: string;
|
|
71
88
|
request_id?: string;
|
|
@@ -83,15 +100,18 @@ const normalizeValue = (value?: string | null): string | undefined => {
|
|
|
83
100
|
};
|
|
84
101
|
|
|
85
102
|
const resolveClawdchatSection = (cfg: OpenClawConfig): ClawdchatConfigSection =>
|
|
86
|
-
(cfg.channels?.clawdchat ?? {}) as ClawdchatConfigSection;
|
|
103
|
+
(cfg.channels?.lumachat ?? cfg.channels?.clawdchat ?? {}) as ClawdchatConfigSection;
|
|
87
104
|
|
|
88
105
|
const resolveBaseUrl = (cfg: OpenClawConfig): string =>
|
|
89
106
|
normalizeValue(resolveClawdchatSection(cfg).baseUrl) ??
|
|
107
|
+
normalizeValue(process.env.LUMACHAT_API_BASE_URL) ??
|
|
90
108
|
normalizeValue(process.env.CLAWDCHAT_API_BASE_URL) ??
|
|
91
109
|
DEFAULT_BASE_URL;
|
|
92
110
|
|
|
93
111
|
const resolveSharedKey = (cfg: OpenClawConfig): string | undefined =>
|
|
94
112
|
normalizeValue(resolveClawdchatSection(cfg).sharedKey) ??
|
|
113
|
+
normalizeValue(process.env.LUMACHAT_CHANNEL_KEY) ??
|
|
114
|
+
normalizeValue(process.env.LUMACHAT_SHARED_KEY) ??
|
|
95
115
|
normalizeValue(process.env.CLAWDCHAT_CHANNEL_KEY) ??
|
|
96
116
|
normalizeValue(process.env.CLAWDCHAT_SHARED_KEY);
|
|
97
117
|
|
|
@@ -101,7 +121,7 @@ const buildConfigPatch = (cfg: OpenClawConfig, patch: Partial<ClawdchatConfigSec
|
|
|
101
121
|
...cfg,
|
|
102
122
|
channels: {
|
|
103
123
|
...cfg.channels,
|
|
104
|
-
|
|
124
|
+
lumachat: {
|
|
105
125
|
...current,
|
|
106
126
|
...patch,
|
|
107
127
|
},
|
|
@@ -111,14 +131,17 @@ const buildConfigPatch = (cfg: OpenClawConfig, patch: Partial<ClawdchatConfigSec
|
|
|
111
131
|
|
|
112
132
|
const removeClawdchatFields = (cfg: OpenClawConfig, fields: Array<keyof ClawdchatConfigSection>) => {
|
|
113
133
|
const current = { ...resolveClawdchatSection(cfg) } as Record<string, unknown>;
|
|
134
|
+
const legacy = { ...(cfg.channels?.clawdchat ?? {}) } as Record<string, unknown>;
|
|
114
135
|
for (const field of fields) {
|
|
115
136
|
delete current[field as string];
|
|
137
|
+
delete legacy[field as string];
|
|
116
138
|
}
|
|
117
139
|
return {
|
|
118
140
|
...cfg,
|
|
119
141
|
channels: {
|
|
120
142
|
...cfg.channels,
|
|
121
|
-
|
|
143
|
+
lumachat: current,
|
|
144
|
+
clawdchat: legacy,
|
|
122
145
|
},
|
|
123
146
|
} as OpenClawConfig;
|
|
124
147
|
};
|
|
@@ -191,20 +214,39 @@ const parseUtcTimestamp = (value: string | undefined) => {
|
|
|
191
214
|
return Number.isFinite(ms) ? ms : null;
|
|
192
215
|
};
|
|
193
216
|
|
|
217
|
+
const parseJsonObject = (raw: string): Record<string, unknown> => {
|
|
218
|
+
if (!raw.trim()) {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(raw);
|
|
223
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
224
|
+
return parsed as Record<string, unknown>;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// ignore
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
230
|
+
};
|
|
231
|
+
|
|
194
232
|
const fetchJson = async (url: string, options: RequestInit) => {
|
|
195
233
|
const response = await fetch(url, options);
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
const payload = parseJsonObject(text);
|
|
196
236
|
if (!response.ok) {
|
|
197
|
-
const
|
|
198
|
-
|
|
237
|
+
const detail = typeof payload.detail === "string" && payload.detail.trim()
|
|
238
|
+
? payload.detail.trim()
|
|
239
|
+
: text.trim();
|
|
240
|
+
throw new Error(`Request failed (${response.status}): ${detail || "Unknown error"}`);
|
|
199
241
|
}
|
|
200
|
-
return
|
|
242
|
+
return payload;
|
|
201
243
|
};
|
|
202
244
|
|
|
203
245
|
const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedClawdchatAccount> => {
|
|
204
246
|
const resolveAccount = (cfg: OpenClawConfig, accountId?: string | null): ResolvedClawdchatAccount => {
|
|
205
247
|
const section = resolveClawdchatSection(cfg);
|
|
206
248
|
const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
207
|
-
const name = section.name?.trim() || "
|
|
249
|
+
const name = section.name?.trim() || "LumaChat";
|
|
208
250
|
return {
|
|
209
251
|
accountId: resolvedId,
|
|
210
252
|
name,
|
|
@@ -229,38 +271,196 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
229
271
|
return { cfg: nextCfg, gatewayId };
|
|
230
272
|
};
|
|
231
273
|
|
|
232
|
-
const
|
|
274
|
+
const buildVerificationUriComplete = (verificationUri: string, userCode: string, existing?: string) => {
|
|
275
|
+
if (existing?.trim()) {
|
|
276
|
+
return existing.trim();
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(verificationUri);
|
|
280
|
+
url.searchParams.set("user_code", userCode);
|
|
281
|
+
return url.toString();
|
|
282
|
+
} catch {
|
|
283
|
+
const joiner = verificationUri.includes("?") ? "&" : "?";
|
|
284
|
+
return `${verificationUri}${joiner}user_code=${encodeURIComponent(userCode)}`;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const normalizePollIntervalSeconds = (value: unknown): number => {
|
|
289
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
290
|
+
return Math.max(1, Math.floor(value));
|
|
291
|
+
}
|
|
292
|
+
if (typeof value === "string" && value.trim()) {
|
|
293
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
294
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
295
|
+
return Math.max(1, parsed);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return DEVICE_AUTH_POLL_DEFAULT_INTERVAL_SECONDS;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const requestDeviceCode = async (baseUrl: string, sharedKey: string | undefined, gatewayId: string) => {
|
|
233
302
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
234
303
|
if (sharedKey) {
|
|
235
304
|
headers[HEADER_CHANNEL_KEY] = sharedKey;
|
|
236
305
|
}
|
|
237
|
-
const payload = await fetchJson(`${baseUrl}/
|
|
306
|
+
const payload = await fetchJson(`${baseUrl}/oauth/device/code`, {
|
|
238
307
|
method: "POST",
|
|
239
308
|
headers,
|
|
240
|
-
body: JSON.stringify({
|
|
309
|
+
body: JSON.stringify({
|
|
310
|
+
gateway_id: gatewayId,
|
|
311
|
+
client_id: OAUTH_DEVICE_CLIENT_ID,
|
|
312
|
+
scope: OAUTH_DEVICE_SCOPE,
|
|
313
|
+
}),
|
|
241
314
|
});
|
|
315
|
+
|
|
316
|
+
const deviceCode = normalizeValue(typeof payload.device_code === "string" ? payload.device_code : undefined);
|
|
317
|
+
const userCode = normalizeValue(typeof payload.user_code === "string" ? payload.user_code : undefined);
|
|
318
|
+
const verificationUri = normalizeValue(
|
|
319
|
+
typeof payload.verification_uri === "string" ? payload.verification_uri : undefined,
|
|
320
|
+
);
|
|
321
|
+
const expiresAt = normalizeValue(typeof payload.expires_at === "string" ? payload.expires_at : undefined);
|
|
322
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
323
|
+
throw new Error("OAuth device code response is missing required fields.");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const verificationUriComplete = buildVerificationUriComplete(
|
|
327
|
+
verificationUri,
|
|
328
|
+
userCode,
|
|
329
|
+
typeof payload.verification_uri_complete === "string" ? payload.verification_uri_complete : undefined,
|
|
330
|
+
);
|
|
331
|
+
|
|
242
332
|
return {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
333
|
+
deviceCode,
|
|
334
|
+
userCode,
|
|
335
|
+
verificationUri,
|
|
336
|
+
verificationUriComplete,
|
|
337
|
+
pollIntervalSeconds: normalizePollIntervalSeconds(payload.interval),
|
|
338
|
+
expiresAt,
|
|
248
339
|
};
|
|
249
340
|
};
|
|
250
341
|
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
342
|
+
const pollDeviceToken = async (session: DeviceAuthSession): Promise<DeviceTokenPollResult> => {
|
|
343
|
+
const body = new URLSearchParams({
|
|
344
|
+
grant_type: OAUTH_DEVICE_GRANT_TYPE,
|
|
345
|
+
device_code: session.deviceCode,
|
|
346
|
+
client_id: OAUTH_DEVICE_CLIENT_ID,
|
|
347
|
+
});
|
|
348
|
+
const response = await fetch(`${session.baseUrl}/oauth/token`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
352
|
+
},
|
|
353
|
+
body,
|
|
354
|
+
});
|
|
355
|
+
const rawBody = await response.text();
|
|
356
|
+
const payload = parseJsonObject(rawBody);
|
|
357
|
+
|
|
358
|
+
if (response.ok) {
|
|
359
|
+
const channelToken = normalizeValue(
|
|
360
|
+
typeof payload.channel_token === "string"
|
|
361
|
+
? payload.channel_token
|
|
362
|
+
: typeof payload.access_token === "string"
|
|
363
|
+
? payload.access_token
|
|
364
|
+
: undefined,
|
|
365
|
+
);
|
|
366
|
+
return {
|
|
367
|
+
status: "ok",
|
|
368
|
+
channelToken,
|
|
369
|
+
bindingId: normalizeValue(typeof payload.binding_id === "string" ? payload.binding_id : undefined),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
status: "error",
|
|
375
|
+
error: normalizeValue(typeof payload.error === "string" ? payload.error : undefined) ?? "unknown_error",
|
|
376
|
+
errorDescription: normalizeValue(
|
|
377
|
+
typeof payload.error_description === "string" ? payload.error_description : undefined,
|
|
378
|
+
),
|
|
379
|
+
statusCode: response.status,
|
|
380
|
+
rawBody: rawBody.trim() || undefined,
|
|
254
381
|
};
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const formatTokenError = (result: DeviceTokenError) => {
|
|
385
|
+
switch (result.error) {
|
|
386
|
+
case "authorization_pending":
|
|
387
|
+
return "authorization_pending";
|
|
388
|
+
case "slow_down":
|
|
389
|
+
return "slow_down";
|
|
390
|
+
case "expired_token":
|
|
391
|
+
return "授权码已过期,请重新执行登录。";
|
|
392
|
+
case "invalid_client":
|
|
393
|
+
return "OAuth client_id 无效,请升级插件或检查后端配置。";
|
|
394
|
+
case "invalid_grant":
|
|
395
|
+
return "device_code 无效或已失效,请重新执行登录。";
|
|
396
|
+
default:
|
|
397
|
+
return result.errorDescription
|
|
398
|
+
? `OAuth 授权失败:${result.errorDescription} (${result.error})`
|
|
399
|
+
: `OAuth 授权失败:${result.error}${result.rawBody ? ` | ${result.rawBody}` : ""}`;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const renderQrDataUrl = async (content: string): Promise<string | undefined> => {
|
|
404
|
+
try {
|
|
405
|
+
return await QRCode.toDataURL(content, { margin: 1, width: 360 });
|
|
406
|
+
} catch {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const renderTerminalQr = async (content: string): Promise<string | undefined> => {
|
|
412
|
+
try {
|
|
413
|
+
return await QRCode.toString(content, { type: "terminal", small: true });
|
|
414
|
+
} catch {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const waitForDeviceAuthorization = async (
|
|
420
|
+
session: DeviceAuthSession,
|
|
421
|
+
deadline: number,
|
|
422
|
+
onProgress?: (message: string) => void,
|
|
423
|
+
) => {
|
|
424
|
+
let pollIntervalMs = Math.max(DEVICE_AUTH_POLL_MIN_INTERVAL_MS, session.pollIntervalSeconds * 1_000);
|
|
425
|
+
while (Date.now() < deadline) {
|
|
426
|
+
const result = await pollDeviceToken(session);
|
|
427
|
+
if (result.status === "ok") {
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
const message = formatTokenError(result);
|
|
431
|
+
if (message === "authorization_pending") {
|
|
432
|
+
if (onProgress) {
|
|
433
|
+
onProgress(`等待授权确认中(授权码 ${session.userCode})...`);
|
|
434
|
+
}
|
|
435
|
+
} else if (message === "slow_down") {
|
|
436
|
+
pollIntervalMs += DEVICE_AUTH_SLOW_DOWN_ADD_MS;
|
|
437
|
+
if (onProgress) {
|
|
438
|
+
onProgress(`轮询过快,已调整为 ${Math.round(pollIntervalMs / 1000)} 秒间隔。`);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
throw new Error(message);
|
|
442
|
+
}
|
|
443
|
+
await sleep(pollIntervalMs);
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const persistAuthorizedBinding = async (tokenResult: DeviceTokenSuccess) => {
|
|
449
|
+
const channelToken = normalizeValue(tokenResult.channelToken);
|
|
450
|
+
if (!channelToken) {
|
|
451
|
+
throw new Error("OAuth token 响应缺少 channel_token/access_token。");
|
|
452
|
+
}
|
|
453
|
+
const latestCfg = api.runtime.config.loadConfig();
|
|
454
|
+
const nextCfg = buildConfigPatch(latestCfg, {
|
|
455
|
+
enabled: true,
|
|
456
|
+
channelToken,
|
|
457
|
+
bindingId: tokenResult.bindingId,
|
|
458
|
+
pairedAt: new Date().toISOString(),
|
|
459
|
+
});
|
|
460
|
+
await api.runtime.config.writeConfigFile(nextCfg);
|
|
259
461
|
return {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
bindingId: payload.binding_id ? String(payload.binding_id) : undefined,
|
|
263
|
-
expiresAt: payload.expires_at ? String(payload.expires_at) : undefined,
|
|
462
|
+
channelToken,
|
|
463
|
+
bindingId: tokenResult.bindingId,
|
|
264
464
|
};
|
|
265
465
|
};
|
|
266
466
|
|
|
@@ -299,18 +499,18 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
299
499
|
};
|
|
300
500
|
|
|
301
501
|
if (!requestId || !conversationId || !content) {
|
|
302
|
-
await respondError("invalid_message", "Invalid
|
|
502
|
+
await respondError("invalid_message", "Invalid lumachat payload.");
|
|
303
503
|
return;
|
|
304
504
|
}
|
|
305
505
|
|
|
306
506
|
const timestampMs = payload.timestamp ? Date.parse(payload.timestamp) : Date.now();
|
|
307
507
|
const route = api.runtime.channel.routing.resolveAgentRoute({
|
|
308
508
|
cfg,
|
|
309
|
-
channel:
|
|
509
|
+
channel: CHANNEL_ID,
|
|
310
510
|
peer: { kind: "dm", id: conversationId },
|
|
311
511
|
});
|
|
312
512
|
|
|
313
|
-
const senderLabel =
|
|
513
|
+
const senderLabel = `${CHANNEL_ID}:${conversationId}`;
|
|
314
514
|
const storePath = api.runtime.channel.session.resolveStorePath(cfg.session?.store, {
|
|
315
515
|
agentId: route.agentId,
|
|
316
516
|
});
|
|
@@ -320,7 +520,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
320
520
|
});
|
|
321
521
|
const envelopeOptions = api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
322
522
|
const body = api.runtime.channel.reply.formatAgentEnvelope({
|
|
323
|
-
channel: "
|
|
523
|
+
channel: "LumaChat",
|
|
324
524
|
from: senderLabel,
|
|
325
525
|
timestamp: timestampMs,
|
|
326
526
|
previousTimestamp,
|
|
@@ -340,13 +540,13 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
340
540
|
ConversationLabel: senderLabel,
|
|
341
541
|
SenderName: senderLabel,
|
|
342
542
|
SenderId: conversationId,
|
|
343
|
-
Provider:
|
|
344
|
-
Surface:
|
|
543
|
+
Provider: CHANNEL_ID as const,
|
|
544
|
+
Surface: CHANNEL_ID as const,
|
|
345
545
|
MessageSid: requestId,
|
|
346
546
|
Timestamp: timestampMs,
|
|
347
547
|
CommandAuthorized: true,
|
|
348
548
|
CommandSource: "text" as const,
|
|
349
|
-
OriginatingChannel:
|
|
549
|
+
OriginatingChannel: CHANNEL_ID as const,
|
|
350
550
|
OriginatingTo: `chat:${conversationId}`,
|
|
351
551
|
});
|
|
352
552
|
|
|
@@ -394,7 +594,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
394
594
|
}
|
|
395
595
|
},
|
|
396
596
|
onError: (err) => {
|
|
397
|
-
log?.warn?.(`
|
|
597
|
+
log?.warn?.(`lumachat reply error: ${String(err)}`);
|
|
398
598
|
},
|
|
399
599
|
});
|
|
400
600
|
|
|
@@ -420,7 +620,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
420
620
|
}
|
|
421
621
|
}
|
|
422
622
|
} catch (err) {
|
|
423
|
-
log?.error?.(`
|
|
623
|
+
log?.error?.(`lumachat dispatch failed: ${String(err)}`);
|
|
424
624
|
await respondError("assistant_unavailable", "Assistant unavailable.");
|
|
425
625
|
}
|
|
426
626
|
};
|
|
@@ -466,7 +666,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
466
666
|
heartbeat = setInterval(() => {
|
|
467
667
|
sendJson(socket, { type: "heartbeat" }).catch(() => undefined);
|
|
468
668
|
}, HEARTBEAT_INTERVAL_MS);
|
|
469
|
-
log?.info?.("
|
|
669
|
+
log?.info?.("lumachat channel connected");
|
|
470
670
|
});
|
|
471
671
|
|
|
472
672
|
socket.on("message", (data) => {
|
|
@@ -475,7 +675,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
475
675
|
try {
|
|
476
676
|
payload = JSON.parse(text) as InboundPayload;
|
|
477
677
|
} catch (err) {
|
|
478
|
-
log?.warn?.(`
|
|
678
|
+
log?.warn?.(`lumachat payload parse error: ${String(err)}`);
|
|
479
679
|
return;
|
|
480
680
|
}
|
|
481
681
|
if (payload.type === "heartbeat") {
|
|
@@ -495,7 +695,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
495
695
|
});
|
|
496
696
|
|
|
497
697
|
socket.on("error", (err) => {
|
|
498
|
-
log?.warn?.(`
|
|
698
|
+
log?.warn?.(`lumachat socket error: ${String(err)}`);
|
|
499
699
|
});
|
|
500
700
|
|
|
501
701
|
socket.on("close", (code, reason) => {
|
|
@@ -503,7 +703,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
503
703
|
signal.removeEventListener("abort", abortHandler);
|
|
504
704
|
update({ running: false, lastStopAt: new Date().toISOString() });
|
|
505
705
|
const reasonText = reason?.toString?.() ?? "";
|
|
506
|
-
log?.warn?.(`
|
|
706
|
+
log?.warn?.(`lumachat channel closed (${code}) ${reasonText}`);
|
|
507
707
|
resolve();
|
|
508
708
|
});
|
|
509
709
|
|
|
@@ -557,14 +757,14 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
557
757
|
};
|
|
558
758
|
|
|
559
759
|
return {
|
|
560
|
-
id:
|
|
760
|
+
id: CHANNEL_ID,
|
|
561
761
|
meta: {
|
|
562
|
-
id:
|
|
563
|
-
label: "
|
|
564
|
-
selectionLabel: "
|
|
565
|
-
docsPath: "/channels/
|
|
566
|
-
blurb: "Pair
|
|
567
|
-
aliases: [
|
|
762
|
+
id: CHANNEL_ID,
|
|
763
|
+
label: "LumaChat",
|
|
764
|
+
selectionLabel: "LumaChat (pair code)",
|
|
765
|
+
docsPath: "/channels/lumachat",
|
|
766
|
+
blurb: "Pair LumaChat with a 6-digit code.",
|
|
767
|
+
aliases: [CHANNEL_ID, LEGACY_CHANNEL_ID],
|
|
568
768
|
},
|
|
569
769
|
capabilities: {
|
|
570
770
|
chatTypes: ["direct"],
|
|
@@ -580,7 +780,10 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
580
780
|
if (!trimmed) return undefined;
|
|
581
781
|
const lower = trimmed.toLowerCase();
|
|
582
782
|
if (lower.startsWith("chat:")) return trimmed;
|
|
583
|
-
if (lower.startsWith(
|
|
783
|
+
if (lower.startsWith(`${CHANNEL_ID}:`)) return `chat:${trimmed.slice(`${CHANNEL_ID}:`.length)}`;
|
|
784
|
+
if (lower.startsWith(`${LEGACY_CHANNEL_ID}:`)) {
|
|
785
|
+
return `chat:${trimmed.slice(`${LEGACY_CHANNEL_ID}:`.length)}`;
|
|
786
|
+
}
|
|
584
787
|
if (UUID_REGEX.test(trimmed)) return `chat:${trimmed}`;
|
|
585
788
|
return undefined;
|
|
586
789
|
},
|
|
@@ -588,13 +791,20 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
588
791
|
looksLikeId: (raw, normalized) => {
|
|
589
792
|
const lower = raw.trim().toLowerCase();
|
|
590
793
|
if (!lower) return false;
|
|
591
|
-
if (
|
|
794
|
+
if (
|
|
795
|
+
lower.startsWith("chat:") ||
|
|
796
|
+
lower.startsWith(`${CHANNEL_ID}:`) ||
|
|
797
|
+
lower.startsWith(`${LEGACY_CHANNEL_ID}:`)
|
|
798
|
+
) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
592
801
|
if (UUID_REGEX.test(lower)) return true;
|
|
593
802
|
return Boolean(normalized && normalized.toLowerCase().startsWith("chat:"));
|
|
594
803
|
},
|
|
595
|
-
hint: "使用
|
|
804
|
+
hint: "使用 lumachat 会话 ID(形如 chat:<uuid>)。",
|
|
596
805
|
},
|
|
597
|
-
formatTargetDisplay: ({ target }) =>
|
|
806
|
+
formatTargetDisplay: ({ target }) =>
|
|
807
|
+
target.replace(/^chat:/i, "").replace(/^lumachat:/i, "").replace(/^clawdchat:/i, ""),
|
|
598
808
|
},
|
|
599
809
|
config: {
|
|
600
810
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
@@ -616,7 +826,7 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
616
826
|
outbound: {
|
|
617
827
|
deliveryMode: "direct",
|
|
618
828
|
sendText: async () => {
|
|
619
|
-
throw new Error("
|
|
829
|
+
throw new Error("LumaChat outbound is only supported for inbound replies.");
|
|
620
830
|
},
|
|
621
831
|
},
|
|
622
832
|
status: {
|
|
@@ -632,10 +842,10 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
632
842
|
for (const account of accounts) {
|
|
633
843
|
if (!account.configured) {
|
|
634
844
|
issues.push({
|
|
635
|
-
channel:
|
|
845
|
+
channel: CHANNEL_ID,
|
|
636
846
|
accountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
637
847
|
kind: "config",
|
|
638
|
-
message: "
|
|
848
|
+
message: "LumaChat channel not paired",
|
|
639
849
|
});
|
|
640
850
|
}
|
|
641
851
|
}
|
|
@@ -673,60 +883,52 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
673
883
|
const cfg = api.runtime.config.loadConfig();
|
|
674
884
|
const account = resolveAccount(cfg, resolvedId);
|
|
675
885
|
if (account.channelToken && !force) {
|
|
676
|
-
return { message: "
|
|
886
|
+
return { message: "LumaChat already paired." };
|
|
677
887
|
}
|
|
678
888
|
const baseUrl = account.baseUrl;
|
|
679
889
|
const sharedKey = account.sharedKey;
|
|
680
890
|
const { gatewayId } = await ensureGatewayId(cfg);
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
throw new Error("Failed to request pairing code.");
|
|
684
|
-
}
|
|
685
|
-
const session: PairingSession = {
|
|
891
|
+
const deviceCode = await requestDeviceCode(baseUrl, sharedKey, gatewayId);
|
|
892
|
+
const session: DeviceAuthSession = {
|
|
686
893
|
accountId: resolvedId,
|
|
687
894
|
baseUrl,
|
|
688
895
|
sharedKey,
|
|
689
896
|
gatewayId,
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
897
|
+
deviceCode: deviceCode.deviceCode,
|
|
898
|
+
userCode: deviceCode.userCode,
|
|
899
|
+
verificationUri: deviceCode.verificationUri,
|
|
900
|
+
verificationUriComplete: deviceCode.verificationUriComplete,
|
|
901
|
+
pollIntervalSeconds: deviceCode.pollIntervalSeconds,
|
|
902
|
+
expiresAt: deviceCode.expiresAt,
|
|
695
903
|
};
|
|
696
|
-
|
|
697
|
-
const
|
|
904
|
+
deviceAuthSessions.set(resolvedId, session);
|
|
905
|
+
const qrDataUrl = await renderQrDataUrl(session.verificationUriComplete);
|
|
906
|
+
const expires = session.expiresAt ? ` (expires ${session.expiresAt})` : "";
|
|
698
907
|
return {
|
|
699
|
-
|
|
908
|
+
qrDataUrl,
|
|
909
|
+
message: `OAuth user code: ${session.userCode}${expires}`,
|
|
700
910
|
};
|
|
701
911
|
},
|
|
702
912
|
loginWithQrWait: async ({ accountId, timeoutMs }) => {
|
|
703
913
|
const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
704
|
-
const session =
|
|
914
|
+
const session = deviceAuthSessions.get(resolvedId);
|
|
705
915
|
if (!session) {
|
|
706
|
-
return { connected: false, message: "No
|
|
916
|
+
return { connected: false, message: "No OAuth device authorization in progress." };
|
|
707
917
|
}
|
|
708
918
|
const deadline = Date.now() + Math.max(1_000, timeoutMs ?? 60_000);
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
const nextCfg = buildConfigPatch(cfg, {
|
|
714
|
-
enabled: true,
|
|
715
|
-
channelToken: status.channelToken,
|
|
716
|
-
bindingId: status.bindingId,
|
|
717
|
-
pairedAt: new Date().toISOString(),
|
|
718
|
-
});
|
|
719
|
-
await api.runtime.config.writeConfigFile(nextCfg);
|
|
720
|
-
pairingSessions.delete(resolvedId);
|
|
721
|
-
return { connected: true, message: "Clawdchat paired." };
|
|
722
|
-
}
|
|
723
|
-
if (status.status === "expired") {
|
|
724
|
-
pairingSessions.delete(resolvedId);
|
|
725
|
-
return { connected: false, message: "Pairing code expired." };
|
|
919
|
+
try {
|
|
920
|
+
const tokenResult = await waitForDeviceAuthorization(session, deadline);
|
|
921
|
+
if (!tokenResult) {
|
|
922
|
+
return { connected: false, message: "Waiting for OAuth authorization confirmation." };
|
|
726
923
|
}
|
|
727
|
-
await
|
|
924
|
+
await persistAuthorizedBinding(tokenResult);
|
|
925
|
+
return { connected: true, message: "LumaChat paired." };
|
|
926
|
+
} catch (error) {
|
|
927
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
928
|
+
return { connected: false, message };
|
|
929
|
+
} finally {
|
|
930
|
+
deviceAuthSessions.delete(resolvedId);
|
|
728
931
|
}
|
|
729
|
-
return { connected: false, message: "Waiting for pairing confirmation." };
|
|
730
932
|
},
|
|
731
933
|
logoutAccount: async ({ cfg, accountId }) => {
|
|
732
934
|
const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -744,62 +946,59 @@ const createClawdchatPlugin = (api: OpenClawPluginApi): ChannelPlugin<ResolvedCl
|
|
|
744
946
|
const warn = runtime.error ?? console.warn;
|
|
745
947
|
|
|
746
948
|
if (account.channelToken) {
|
|
747
|
-
log("
|
|
949
|
+
log("LumaChat 已配对。若需重新配对,请先执行 `openclaw channels logout --channel lumachat`。");
|
|
748
950
|
return;
|
|
749
951
|
}
|
|
750
952
|
|
|
751
953
|
const baseUrl = account.baseUrl;
|
|
752
954
|
const sharedKey = account.sharedKey;
|
|
753
955
|
const { gatewayId } = await ensureGatewayId(cfg);
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
throw new Error("Failed to request pairing code.");
|
|
757
|
-
}
|
|
758
|
-
log(`Clawdchat 配对码:${pairing.pairingCode}`);
|
|
759
|
-
log(`Clawdchat 安全码:${pairing.pairingConfirmCode}`);
|
|
760
|
-
log("请在 clawdchat App -> 设置 -> Clawdbot 配对 中输入配对码与安全码。");
|
|
761
|
-
|
|
762
|
-
const session: PairingSession = {
|
|
956
|
+
const deviceCode = await requestDeviceCode(baseUrl, sharedKey, gatewayId);
|
|
957
|
+
const session: DeviceAuthSession = {
|
|
763
958
|
accountId: resolvedId,
|
|
764
959
|
baseUrl,
|
|
765
960
|
sharedKey,
|
|
766
961
|
gatewayId,
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
962
|
+
deviceCode: deviceCode.deviceCode,
|
|
963
|
+
userCode: deviceCode.userCode,
|
|
964
|
+
verificationUri: deviceCode.verificationUri,
|
|
965
|
+
verificationUriComplete: deviceCode.verificationUriComplete,
|
|
966
|
+
pollIntervalSeconds: deviceCode.pollIntervalSeconds,
|
|
967
|
+
expiresAt: deviceCode.expiresAt,
|
|
772
968
|
};
|
|
969
|
+
deviceAuthSessions.set(resolvedId, session);
|
|
970
|
+
|
|
971
|
+
const terminalQr = await renderTerminalQr(session.verificationUriComplete);
|
|
972
|
+
if (terminalQr?.trim()) {
|
|
973
|
+
log("LumaChat 扫码授权二维码:");
|
|
974
|
+
log(terminalQr);
|
|
975
|
+
} else {
|
|
976
|
+
warn("二维码生成失败,请改用 6 位授权码手动输入。");
|
|
977
|
+
}
|
|
978
|
+
log(`LumaChat 授权码:${session.userCode}`);
|
|
979
|
+
log("请在 LumaChat App -> 设置 -> Clawdbot 配对 中输入授权码,或直接扫码授权。");
|
|
980
|
+
log(`授权链接:${session.verificationUriComplete}`);
|
|
773
981
|
|
|
774
|
-
const expiresAtMs = parseUtcTimestamp(
|
|
982
|
+
const expiresAtMs = parseUtcTimestamp(session.expiresAt);
|
|
775
983
|
const deadline = expiresAtMs && Number.isFinite(expiresAtMs)
|
|
776
984
|
? Math.max(Date.now(), expiresAtMs)
|
|
777
985
|
: Date.now() + PAIRING_WAIT_TIMEOUT_MS;
|
|
778
986
|
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
});
|
|
789
|
-
await api.runtime.config.writeConfigFile(nextCfg);
|
|
790
|
-
log("Clawdchat 配对成功。");
|
|
987
|
+
try {
|
|
988
|
+
const tokenResult = await waitForDeviceAuthorization(
|
|
989
|
+
session,
|
|
990
|
+
deadline,
|
|
991
|
+
verbose ? log : undefined,
|
|
992
|
+
);
|
|
993
|
+
if (tokenResult) {
|
|
994
|
+
await persistAuthorizedBinding(tokenResult);
|
|
995
|
+
log("LumaChat 配对成功。");
|
|
791
996
|
return;
|
|
792
997
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (verbose) {
|
|
797
|
-
log("等待配对确认中...");
|
|
798
|
-
}
|
|
799
|
-
await sleep(PAIRING_POLL_INTERVAL_MS);
|
|
998
|
+
warn("等待授权超时,请确认已在 App 中完成授权后重试。");
|
|
999
|
+
} finally {
|
|
1000
|
+
deviceAuthSessions.delete(resolvedId);
|
|
800
1001
|
}
|
|
801
|
-
|
|
802
|
-
warn("等待配对超时,请确认已在 App 中输入配对码后重试。");
|
|
803
1002
|
},
|
|
804
1003
|
},
|
|
805
1004
|
};
|