@honor-claw/yoyo 1.2.1-beta.1 → 1.2.1-beta.3
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/package.json +1 -1
- package/src/cloud-channel/client.ts +20 -8
- package/src/cloud-channel/types.ts +4 -3
- package/src/honor-auth/auth-result-html.ts +241 -0
- package/src/honor-auth/callback-server.ts +111 -53
- package/src/modules/device/providers/windows.ts +28 -26
- package/src/vite-env.d.ts +1 -0
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export class ClawCloudSocketClient {
|
|
|
26
26
|
private retryTimer: NodeJS.Timeout | null = null;
|
|
27
27
|
private isManualClose = false;
|
|
28
28
|
private isRetryPaused = false; // 重试状态
|
|
29
|
-
// ping
|
|
29
|
+
// ping 定时器(原生 ping + 业务 pingMessage)
|
|
30
30
|
private pingTimer: NodeJS.Timeout | null = null;
|
|
31
31
|
// 当前连接上下文
|
|
32
32
|
private currentTraceId = "";
|
|
@@ -59,7 +59,6 @@ export class ClawCloudSocketClient {
|
|
|
59
59
|
this.ws = new WebSocket(url, wsOptions);
|
|
60
60
|
this.ws.on("open", this.handleOpen.bind(this, url, isRetry));
|
|
61
61
|
this.ws.on("message", this.onMessage);
|
|
62
|
-
this.ws.on("ping", this.handlePing);
|
|
63
62
|
this.ws.on("pong", this.handlePong);
|
|
64
63
|
this.ws.on("close", this.handleClose);
|
|
65
64
|
this.ws.on("error", this.handleError);
|
|
@@ -90,10 +89,6 @@ export class ClawCloudSocketClient {
|
|
|
90
89
|
this.options.onOpen?.();
|
|
91
90
|
};
|
|
92
91
|
|
|
93
|
-
private handlePing = (): void => {
|
|
94
|
-
useClawLogger().debug?.("[claw-cloud-socket] received ping from server");
|
|
95
|
-
};
|
|
96
|
-
|
|
97
92
|
private handlePong = (): void => {
|
|
98
93
|
useClawLogger().debug?.("[claw-cloud-socket] received pong from server");
|
|
99
94
|
};
|
|
@@ -299,7 +294,7 @@ export class ClawCloudSocketClient {
|
|
|
299
294
|
}
|
|
300
295
|
|
|
301
296
|
/**
|
|
302
|
-
* 启动 ping
|
|
297
|
+
* 启动 ping 定时器,同时发送原生 ping 和业务 pingMessage
|
|
303
298
|
*/
|
|
304
299
|
private startPingTimer(): void {
|
|
305
300
|
this.pingTimer = setInterval(() => {
|
|
@@ -308,9 +303,26 @@ export class ClawCloudSocketClient {
|
|
|
308
303
|
return;
|
|
309
304
|
}
|
|
310
305
|
|
|
311
|
-
//
|
|
306
|
+
// 原生 ping
|
|
312
307
|
this.ws.ping();
|
|
313
308
|
useClawLogger().debug?.("[claw-cloud-socket] sent ping to server");
|
|
309
|
+
|
|
310
|
+
// 业务 pingMessage
|
|
311
|
+
const { deviceInfo } = this.options;
|
|
312
|
+
const pingMessage: YOYOClawServiceEvent = {
|
|
313
|
+
msgType: "pingMessage",
|
|
314
|
+
sourceRole: "yoyoclaw",
|
|
315
|
+
sourceDeviceId: deviceInfo.deviceId,
|
|
316
|
+
targetRole: "node",
|
|
317
|
+
port: deviceInfo.port,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
this.ws.send(JSON.stringify(pingMessage));
|
|
322
|
+
useClawLogger().debug?.("[claw-cloud-socket] sent pingMessage to server");
|
|
323
|
+
} catch {
|
|
324
|
+
useClawLogger().error("[claw-cloud-socket] failed to send pingMessage");
|
|
325
|
+
}
|
|
314
326
|
}, PING_INTERVAL);
|
|
315
327
|
}
|
|
316
328
|
|
|
@@ -18,8 +18,8 @@ export interface YOYOClawServiceEvent {
|
|
|
18
18
|
sourceDeviceId: string;
|
|
19
19
|
sourceDeviceInfo?: ClawDeviceInfo;
|
|
20
20
|
targetRole?: DeviceRole;
|
|
21
|
-
targetDeviceId
|
|
22
|
-
traceInfo
|
|
21
|
+
targetDeviceId?: string;
|
|
22
|
+
traceInfo?: object;
|
|
23
23
|
port: number | string;
|
|
24
24
|
data?: string; // openclaw原生消息
|
|
25
25
|
msgType:
|
|
@@ -28,7 +28,8 @@ export interface YOYOClawServiceEvent {
|
|
|
28
28
|
| "deviceUnPairMessage"
|
|
29
29
|
| "fetchContexts"
|
|
30
30
|
| "updateContexts"
|
|
31
|
-
| "deviceControl"
|
|
31
|
+
| "deviceControl"
|
|
32
|
+
| "pingMessage";
|
|
32
33
|
/**
|
|
33
34
|
* 会话轮次等追加信息,只有配对消息有
|
|
34
35
|
*/
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const SUCCESS_ICON_SVG = `<svg aria-hidden="true" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="32" cy="32" r="30" fill="currentColor" opacity="0.12"/><circle cx="32" cy="32" r="21" fill="currentColor"/><path d="M22.5 32.5L29 39L42.5 25.5" stroke="white" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
2
|
+
const FAILURE_ICON_SVG = `<svg aria-hidden="true" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="32" cy="32" r="30" fill="currentColor" opacity="0.12"/><circle cx="32" cy="32" r="21" fill="currentColor"/><path d="M24.5 24.5L39.5 39.5M39.5 24.5L24.5 39.5" stroke="white" stroke-width="4.5" stroke-linecap="round"/></svg>`;
|
|
3
|
+
|
|
4
|
+
export interface PageDetailItem {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResultPageOptions {
|
|
10
|
+
pageTitle: string;
|
|
11
|
+
statusClass: "success" | "error";
|
|
12
|
+
title: string;
|
|
13
|
+
hintText: string;
|
|
14
|
+
details?: PageDetailItem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeHtml(input: string): string {
|
|
18
|
+
return input
|
|
19
|
+
.replaceAll("&", "&")
|
|
20
|
+
.replaceAll("<", "<")
|
|
21
|
+
.replaceAll(">", ">")
|
|
22
|
+
.replaceAll('"', """)
|
|
23
|
+
.replaceAll("'", "'");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildDetailsHtml(details?: PageDetailItem[]): string {
|
|
27
|
+
if (!details || details.length === 0) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const detailRows = details
|
|
32
|
+
.map((item) => {
|
|
33
|
+
return `<div class="detail-item"><span class="detail-key">${escapeHtml(item.key)}</span><span class="detail-value">${escapeHtml(item.value)}</span></div>`;
|
|
34
|
+
})
|
|
35
|
+
.join("");
|
|
36
|
+
|
|
37
|
+
return `<div class="details">${detailRows}</div>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getResultHtml(options: ResultPageOptions): string {
|
|
41
|
+
const icon = options.statusClass === "success" ? SUCCESS_ICON_SVG : FAILURE_ICON_SVG;
|
|
42
|
+
|
|
43
|
+
return `<!doctype html>
|
|
44
|
+
<html lang="zh-CN">
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8" />
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
48
|
+
<title>${escapeHtml(options.pageTitle)}</title>
|
|
49
|
+
<style>
|
|
50
|
+
:root {
|
|
51
|
+
color-scheme: light;
|
|
52
|
+
--surface: rgba(255, 255, 255, 0.94);
|
|
53
|
+
--text: #172033;
|
|
54
|
+
--muted: #667085;
|
|
55
|
+
--faint: #98a2b3;
|
|
56
|
+
--border: rgba(23, 32, 51, 0.1);
|
|
57
|
+
--shadow: 0 22px 70px rgba(31, 41, 55, 0.14);
|
|
58
|
+
--success: #168255;
|
|
59
|
+
--error: #d14343;
|
|
60
|
+
--accent: #2563eb;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
* {
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
body {
|
|
68
|
+
margin: 0;
|
|
69
|
+
min-height: 100vh;
|
|
70
|
+
display: grid;
|
|
71
|
+
place-items: center;
|
|
72
|
+
padding: 24px;
|
|
73
|
+
font-family:
|
|
74
|
+
"Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
75
|
+
color: var(--text);
|
|
76
|
+
background:
|
|
77
|
+
linear-gradient(135deg, rgba(37, 99, 235, 0.09), transparent 34%),
|
|
78
|
+
linear-gradient(315deg, rgba(22, 130, 85, 0.12), transparent 38%),
|
|
79
|
+
#f6f8fb;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.card {
|
|
83
|
+
width: min(100%, 480px);
|
|
84
|
+
padding: 24px;
|
|
85
|
+
border-radius: 18px;
|
|
86
|
+
background: var(--surface);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
box-shadow: var(--shadow);
|
|
89
|
+
backdrop-filter: blur(10px);
|
|
90
|
+
text-align: center;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.brand {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
gap: 9px;
|
|
98
|
+
margin-bottom: 24px;
|
|
99
|
+
color: var(--text);
|
|
100
|
+
font-size: 15px;
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.brand-logo {
|
|
105
|
+
width: 26px;
|
|
106
|
+
height: 26px;
|
|
107
|
+
display: block;
|
|
108
|
+
border-radius: 8px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.icon-wrap {
|
|
112
|
+
width: 96px;
|
|
113
|
+
height: 96px;
|
|
114
|
+
margin: 0 auto 20px;
|
|
115
|
+
border-radius: 50%;
|
|
116
|
+
display: grid;
|
|
117
|
+
place-items: center;
|
|
118
|
+
background: transparent;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.icon-wrap.success {
|
|
122
|
+
color: var(--success);
|
|
123
|
+
filter: drop-shadow(0 16px 28px rgba(22, 130, 85, 0.18));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.icon-wrap.error {
|
|
127
|
+
color: var(--error);
|
|
128
|
+
filter: drop-shadow(0 16px 28px rgba(209, 67, 67, 0.18));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.icon-wrap svg {
|
|
132
|
+
display: block;
|
|
133
|
+
width: 78px;
|
|
134
|
+
height: 78px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
h1 {
|
|
138
|
+
margin: 0;
|
|
139
|
+
font-size: 26px;
|
|
140
|
+
line-height: 1.2;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.details {
|
|
145
|
+
margin: 18px 0 0;
|
|
146
|
+
padding: 0;
|
|
147
|
+
border-radius: 16px;
|
|
148
|
+
background: rgba(255, 255, 255, 0.72);
|
|
149
|
+
border: 1px solid rgba(29, 36, 51, 0.07);
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
text-align: left;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.detail-item {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: 64px minmax(0, 1fr);
|
|
157
|
+
gap: 10px;
|
|
158
|
+
padding: 12px 14px;
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
line-height: 1.5;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.detail-item + .detail-item {
|
|
164
|
+
border-top: 1px solid rgba(29, 36, 51, 0.07);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.detail-key {
|
|
168
|
+
color: #7a808d;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.detail-value {
|
|
172
|
+
color: var(--text);
|
|
173
|
+
word-break: break-word;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.footer {
|
|
177
|
+
margin-top: 22px;
|
|
178
|
+
padding-top: 16px;
|
|
179
|
+
border-top: 1px solid rgba(29, 36, 51, 0.08);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.hint {
|
|
183
|
+
margin: 0;
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
color: var(--muted);
|
|
186
|
+
line-height: 1.6;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.caption {
|
|
190
|
+
margin: 8px 0 0;
|
|
191
|
+
color: var(--faint);
|
|
192
|
+
font-size: 12px;
|
|
193
|
+
line-height: 1.5;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@media (max-width: 480px) {
|
|
197
|
+
body {
|
|
198
|
+
padding: 16px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.card {
|
|
202
|
+
padding: 22px 18px 18px;
|
|
203
|
+
border-radius: 16px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.brand {
|
|
207
|
+
margin-bottom: 22px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
h1 {
|
|
211
|
+
font-size: 23px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.icon-wrap {
|
|
215
|
+
width: 84px;
|
|
216
|
+
height: 84px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.icon-wrap svg {
|
|
220
|
+
width: 70px;
|
|
221
|
+
height: 70px;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<main class="card">
|
|
228
|
+
<div class="brand">
|
|
229
|
+
<span>YOYOClaw</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="icon-wrap ${options.statusClass}">${icon}</div>
|
|
232
|
+
<h1>${escapeHtml(options.title)}</h1>
|
|
233
|
+
${buildDetailsHtml(options.details)}
|
|
234
|
+
<div class="footer">
|
|
235
|
+
<p class="hint">${escapeHtml(options.hintText)}</p>
|
|
236
|
+
<p class="caption">浏览器不会自动关闭当前页面,保留或稍后关闭都不影响登录结果。</p>
|
|
237
|
+
</div>
|
|
238
|
+
</main>
|
|
239
|
+
</body>
|
|
240
|
+
</html>`;
|
|
241
|
+
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* 本地HTTP回调服务器 - 接收OAuth2授权回调
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import * as fs from "fs";
|
|
6
5
|
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
6
|
+
import { URL } from "node:url";
|
|
7
|
+
import { getResultHtml } from "./auth-result-html.js";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* 回调服务器选项
|
|
@@ -21,21 +20,6 @@ export interface CallbackServerOptions {
|
|
|
21
20
|
onError?: (error: Error) => void;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const SUCCESS_ICON_SVG = `<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M28 56C43.464 56 56 43.464 56 28C56 12.536 43.464 0 28 0C12.536 0 0 12.536 0 28C0 43.464 12.536 56 28 56Z" fill="#28a745"/><path d="M39.6667 20.3333L24.5 35.5L16.3333 27.3333" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
25
|
-
const FAILURE_ICON_SVG = `<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M28 56C43.464 56 56 43.464 56 28C56 12.536 43.464 0 28 0C12.536 0 0 12.536 0 28C0 43.464 12.536 56 28 56Z" fill="#dc3545"/><path d="M35 21L21 35M21 21L35 35" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
26
|
-
|
|
27
|
-
function getResultHtml(message: string, success: boolean): string {
|
|
28
|
-
try {
|
|
29
|
-
const template = fs.readFileSync(path.join(__dirname, "auth-result.html"), "utf-8");
|
|
30
|
-
const icon = success ? SUCCESS_ICON_SVG : FAILURE_ICON_SVG;
|
|
31
|
-
return template.replace("{{MESSAGE}}", message).replace("{{ICON_SVG}}", icon);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
// Fallback to simple text
|
|
34
|
-
console.error("Failed to read auth-result.html:", error);
|
|
35
|
-
return `<h1>${message}</h1>`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
23
|
/**
|
|
40
24
|
* 启动本地回调服务器
|
|
41
25
|
*/
|
|
@@ -47,74 +31,148 @@ export function startCallbackServer(options: CallbackServerOptions): Promise<voi
|
|
|
47
31
|
let hasReceivedCode = false;
|
|
48
32
|
let promiseResolved = false;
|
|
49
33
|
|
|
34
|
+
const closeServer = () => {
|
|
35
|
+
if (serverClosed) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
serverClosed = true;
|
|
39
|
+
if (timeoutId) {
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
}
|
|
42
|
+
server.close();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const failAuthorization = (error: Error) => {
|
|
46
|
+
closeServer();
|
|
47
|
+
onError?.(error);
|
|
48
|
+
if (!promiseResolved) {
|
|
49
|
+
promiseResolved = true;
|
|
50
|
+
reject(error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const closeServerSoon = (delayMs = 120) => {
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
closeServer();
|
|
57
|
+
}, delayMs);
|
|
58
|
+
};
|
|
59
|
+
|
|
50
60
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
51
61
|
try {
|
|
52
62
|
const url = new URL(req.url || "", `http://localhost:${port}`);
|
|
63
|
+
if (req.method !== "GET") {
|
|
64
|
+
res.writeHead(405, { "Content-Type": "text/html; charset=utf-8" });
|
|
65
|
+
res.end(
|
|
66
|
+
getResultHtml({
|
|
67
|
+
pageTitle: "请求方式不支持",
|
|
68
|
+
statusClass: "error",
|
|
69
|
+
title: "不支持的请求方式",
|
|
70
|
+
hintText: "请返回 OpenClaw 重新发起登录。",
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
const code = url.searchParams.get("code") || "";
|
|
77
|
+
const oauthError = url.searchParams.get("error") || "";
|
|
78
|
+
const oauthErrorDescription =
|
|
79
|
+
url.searchParams.get("error_description") || url.searchParams.get("error_message") || "";
|
|
54
80
|
|
|
55
81
|
if (code && !hasReceivedCode) {
|
|
56
82
|
hasReceivedCode = true;
|
|
57
83
|
|
|
58
84
|
// 返回成功响应
|
|
59
85
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
60
|
-
res.end(
|
|
86
|
+
res.end(
|
|
87
|
+
getResultHtml({
|
|
88
|
+
pageTitle: "授权成功",
|
|
89
|
+
statusClass: "success",
|
|
90
|
+
title: "登录已完成",
|
|
91
|
+
hintText: "请返回 OpenClaw,登录流程会继续完成。",
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
61
94
|
|
|
62
95
|
// 延迟关闭服务器,确保响应已发送
|
|
63
|
-
|
|
64
|
-
if (!serverClosed) {
|
|
65
|
-
serverClosed = true;
|
|
66
|
-
if (timeoutId) {
|
|
67
|
-
clearTimeout(timeoutId);
|
|
68
|
-
}
|
|
69
|
-
server.close();
|
|
70
|
-
}
|
|
71
|
-
}, 100);
|
|
96
|
+
closeServerSoon();
|
|
72
97
|
|
|
73
98
|
// 调用回调
|
|
74
99
|
onCodeReceived(code);
|
|
100
|
+
} else if (oauthError && !hasReceivedCode) {
|
|
101
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
102
|
+
res.end(
|
|
103
|
+
getResultHtml({
|
|
104
|
+
pageTitle: "授权失败",
|
|
105
|
+
statusClass: "error",
|
|
106
|
+
title: "未能完成授权",
|
|
107
|
+
hintText: "请返回 OpenClaw 重新发起登录。",
|
|
108
|
+
details: [
|
|
109
|
+
{ key: "错误码", value: oauthError },
|
|
110
|
+
...(oauthErrorDescription ? [{ key: "说明", value: oauthErrorDescription }] : []),
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
failAuthorization(
|
|
117
|
+
new Error(
|
|
118
|
+
oauthErrorDescription
|
|
119
|
+
? `authorization failed: ${oauthError} (${oauthErrorDescription})`
|
|
120
|
+
: `authorization failed: ${oauthError}`,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}, 120);
|
|
75
124
|
} else if (!hasReceivedCode) {
|
|
76
|
-
//
|
|
125
|
+
// 未拿到授权码时,直接提示并结束,避免一直等待到超时。
|
|
77
126
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
78
|
-
res.end(
|
|
127
|
+
res.end(
|
|
128
|
+
getResultHtml({
|
|
129
|
+
pageTitle: "授权失败",
|
|
130
|
+
statusClass: "error",
|
|
131
|
+
title: "回调参数不完整",
|
|
132
|
+
hintText: "请返回 OpenClaw 重新发起登录。",
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
failAuthorization(new Error("authorization failed: no code in callback"));
|
|
138
|
+
}, 120);
|
|
79
139
|
} else {
|
|
80
140
|
// 重复请求,返回成功响应但不处理
|
|
81
141
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
82
|
-
res.end(
|
|
142
|
+
res.end(
|
|
143
|
+
getResultHtml({
|
|
144
|
+
pageTitle: "登录已完成",
|
|
145
|
+
statusClass: "success",
|
|
146
|
+
title: "授权已经处理完成",
|
|
147
|
+
hintText: "请返回 OpenClaw,登录流程会继续完成。",
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
83
150
|
}
|
|
84
151
|
} catch (error) {
|
|
85
152
|
console.error("authorize callback error:", error);
|
|
86
153
|
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
87
|
-
res.end(
|
|
154
|
+
res.end(
|
|
155
|
+
getResultHtml({
|
|
156
|
+
pageTitle: "服务异常",
|
|
157
|
+
statusClass: "error",
|
|
158
|
+
title: "回调服务出现异常",
|
|
159
|
+
hintText: "请返回 OpenClaw 后重新发起登录。",
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
failAuthorization(new Error("authorization failed: callback server internal error"));
|
|
164
|
+
}, 120);
|
|
88
165
|
}
|
|
89
166
|
});
|
|
90
167
|
|
|
91
168
|
server.on("error", (error) => {
|
|
92
|
-
|
|
93
|
-
serverClosed = true;
|
|
94
|
-
if (timeoutId) {
|
|
95
|
-
clearTimeout(timeoutId);
|
|
96
|
-
}
|
|
97
|
-
onError?.(error);
|
|
98
|
-
if (!promiseResolved) {
|
|
99
|
-
promiseResolved = true;
|
|
100
|
-
reject(error);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
169
|
+
failAuthorization(error);
|
|
103
170
|
});
|
|
104
171
|
|
|
105
172
|
server.listen(port, () => {
|
|
106
173
|
// 设置超时
|
|
107
174
|
timeoutId = setTimeout(() => {
|
|
108
|
-
|
|
109
|
-
serverClosed = true;
|
|
110
|
-
server.close();
|
|
111
|
-
const error = new Error("authorize timeout");
|
|
112
|
-
onError?.(error);
|
|
113
|
-
if (!promiseResolved) {
|
|
114
|
-
promiseResolved = true;
|
|
115
|
-
reject(error);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
175
|
+
failAuthorization(new Error("authorize timeout"));
|
|
118
176
|
}, timeout);
|
|
119
177
|
});
|
|
120
178
|
|
|
@@ -87,31 +87,30 @@ export class WindowsDeviceInfoProvider implements DeviceInfoProvider {
|
|
|
87
87
|
private async _initializeCache(): Promise<void> {
|
|
88
88
|
try {
|
|
89
89
|
// 并行获取所有注册表值
|
|
90
|
-
const [systemManufacturer, biosVendor, biosVendor2, systemManufacturer2] =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
]);
|
|
90
|
+
const [systemManufacturer, biosVendor, biosVendor2, systemManufacturer2] = await Promise.all([
|
|
91
|
+
// 原有路径
|
|
92
|
+
getRegistryStringValueAsync(
|
|
93
|
+
Registry.HKLM,
|
|
94
|
+
"\\HARDWARE\\DESCRIPTION\\System",
|
|
95
|
+
"SystemManufacturer",
|
|
96
|
+
),
|
|
97
|
+
getRegistryStringValueAsync(
|
|
98
|
+
Registry.HKLM,
|
|
99
|
+
"\\HARDWARE\\DESCRIPTION\\System",
|
|
100
|
+
"SystemBiosVendor",
|
|
101
|
+
),
|
|
102
|
+
// 新增路径(BIOS 子键)
|
|
103
|
+
getRegistryStringValueAsync(
|
|
104
|
+
Registry.HKLM,
|
|
105
|
+
"\\HARDWARE\\DESCRIPTION\\System\\BIOS",
|
|
106
|
+
"Vendor",
|
|
107
|
+
),
|
|
108
|
+
getRegistryStringValueAsync(
|
|
109
|
+
Registry.HKLM,
|
|
110
|
+
"\\HARDWARE\\DESCRIPTION\\System\\BIOS",
|
|
111
|
+
"SystemManufacturer",
|
|
112
|
+
),
|
|
113
|
+
]);
|
|
115
114
|
|
|
116
115
|
// 缓存 deviceBrand(检查多个来源)
|
|
117
116
|
const manufacturerSources = [
|
|
@@ -126,7 +125,10 @@ export class WindowsDeviceInfoProvider implements DeviceInfoProvider {
|
|
|
126
125
|
this.cache.deviceBrand = "HONOR";
|
|
127
126
|
} else {
|
|
128
127
|
// 最后兜底:检查 HKLM:\SOFTWARE\HONOR 注册表键是否存在
|
|
129
|
-
const honorKeyExists = await registryKeyExistsAsync(
|
|
128
|
+
const honorKeyExists = await registryKeyExistsAsync(
|
|
129
|
+
Registry.HKLM,
|
|
130
|
+
"\\SOFTWARE\\HONOR\\PCManager",
|
|
131
|
+
);
|
|
130
132
|
if (honorKeyExists) {
|
|
131
133
|
this.cache.deviceBrand = "HONOR";
|
|
132
134
|
} else {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|