@honor-claw/yoyo 1.2.1-beta.2 → 1.2.1-beta.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/package.json
CHANGED
|
@@ -66,7 +66,7 @@ express_logistics_query
|
|
|
66
66
|
### 示例 1: 通用物流查询(未提供单号)
|
|
67
67
|
**用户输入**: "查一下我的快递"
|
|
68
68
|
|
|
69
|
-
**工具**: "
|
|
69
|
+
**工具**: "express_logistics_query"
|
|
70
70
|
|
|
71
71
|
**JSON 参数**:
|
|
72
72
|
```json
|
|
@@ -75,12 +75,12 @@ express_logistics_query
|
|
|
75
75
|
|
|
76
76
|
**Windows (Cmd) 执行命令**:
|
|
77
77
|
```bash
|
|
78
|
-
cmd /c 'openclaw nodes invoke --node <ID> --command
|
|
78
|
+
cmd /c 'openclaw nodes invoke --node <ID> --command mcp.tool.call --params "{\"name\":\"express_logistics_query\",\"appPkg\":\"com.hihonor.magicvoice\",\"arguments\":{}}"'
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**Linux (Bash) 执行命令**:
|
|
82
82
|
```bash
|
|
83
|
-
openclaw nodes invoke --node <ID> --command
|
|
83
|
+
openclaw nodes invoke --node <ID> --command mcp.tool.call --params '{"name":"express_logistics_query","appPkg":"com.hihonor.magicvoice","arguments":{}}'
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
---
|
|
@@ -88,7 +88,7 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
88
88
|
### 示例 2: 签收状态核实(未提供单号)
|
|
89
89
|
**用户输入**: "我的快递签收了吗"
|
|
90
90
|
|
|
91
|
-
**工具**: "
|
|
91
|
+
**工具**: "express_logistics_query"
|
|
92
92
|
|
|
93
93
|
**JSON 参数**:
|
|
94
94
|
```json
|
|
@@ -97,12 +97,12 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
97
97
|
|
|
98
98
|
**Windows (Cmd) 执行命令**:
|
|
99
99
|
```bash
|
|
100
|
-
cmd /c 'openclaw nodes invoke --node <ID> --command
|
|
100
|
+
cmd /c 'openclaw nodes invoke --node <ID> --command mcp.tool.call --params "{\"name\":\"express_logistics_query\",\"appPkg\":\"com.hihonor.magicvoice\",\"arguments\":{}}"'
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
**Linux (Bash) 执行命令**:
|
|
104
104
|
```bash
|
|
105
|
-
openclaw nodes invoke --node <ID> --command
|
|
105
|
+
openclaw nodes invoke --node <ID> --command mcp.tool.call --params '{"name":"express_logistics_query","appPkg":"com.hihonor.magicvoice","arguments":{}}'
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
---
|
|
@@ -110,7 +110,7 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
110
110
|
### 示例 3: 指定运单号查询
|
|
111
111
|
**用户输入**: "看看快递单号为123456789的快递"
|
|
112
112
|
|
|
113
|
-
**工具**: "
|
|
113
|
+
**工具**: "express_logistics_query"
|
|
114
114
|
|
|
115
115
|
**JSON 参数**:
|
|
116
116
|
```json
|
|
@@ -119,12 +119,12 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
119
119
|
|
|
120
120
|
**Windows (Cmd) 执行命令**:
|
|
121
121
|
```bash
|
|
122
|
-
cmd /c 'openclaw nodes invoke --node <ID> --command
|
|
122
|
+
cmd /c 'openclaw nodes invoke --node <ID> --command mcp.tool.call --params "{\"name\":\"express_logistics_query\",\"appPkg\":\"com.hihonor.magicvoice\",\"arguments\":{}}"'
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
**Linux (Bash) 执行命令**:
|
|
126
126
|
```bash
|
|
127
|
-
openclaw nodes invoke --node <ID> --command
|
|
127
|
+
openclaw nodes invoke --node <ID> --command mcp.tool.call --params '{"name":"express_logistics_query","appPkg":"com.hihonor.magicvoice","arguments":{}}'
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
---
|
|
@@ -132,7 +132,7 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
132
132
|
### 示例 4: 指定快递公司与单号查询
|
|
133
133
|
**用户输入**: "帮我查一下顺丰快递单号987654321现在到哪了"
|
|
134
134
|
|
|
135
|
-
**工具**: "
|
|
135
|
+
**工具**: "express_logistics_query"
|
|
136
136
|
|
|
137
137
|
**JSON 参数**:
|
|
138
138
|
```json
|
|
@@ -141,12 +141,12 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
141
141
|
|
|
142
142
|
**Windows (Cmd) 执行命令**:
|
|
143
143
|
```bash
|
|
144
|
-
cmd /c 'openclaw nodes invoke --node <ID> --command
|
|
144
|
+
cmd /c 'openclaw nodes invoke --node <ID> --command mcp.tool.call --params "{\"name\":\"express_logistics_query\",\"appPkg\":\"com.hihonor.magicvoice\",\"arguments\":{}}"'
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
**Linux (Bash) 执行命令**:
|
|
148
148
|
```bash
|
|
149
|
-
openclaw nodes invoke --node <ID> --command
|
|
149
|
+
openclaw nodes invoke --node <ID> --command mcp.tool.call --params '{"name":"express_logistics_query","appPkg":"com.hihonor.magicvoice","arguments":{}}'
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
---
|
|
@@ -154,7 +154,7 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
154
154
|
### 示例 5: 到件/签收意图识别
|
|
155
155
|
**用户输入**: "查询我的包裹到了吗"
|
|
156
156
|
|
|
157
|
-
**工具**: "
|
|
157
|
+
**工具**: "express_logistics_query"
|
|
158
158
|
|
|
159
159
|
**JSON 参数**:
|
|
160
160
|
```json
|
|
@@ -163,10 +163,10 @@ openclaw nodes invoke --node <ID> --command express-logistics-query --params '{}
|
|
|
163
163
|
|
|
164
164
|
**Windows (Cmd) 执行命令**:
|
|
165
165
|
```bash
|
|
166
|
-
cmd /c 'openclaw nodes invoke --node <ID> --command
|
|
166
|
+
cmd /c 'openclaw nodes invoke --node <ID> --command mcp.tool.call --params "{\"name\":\"express_logistics_query\",\"appPkg\":\"com.hihonor.magicvoice\",\"arguments\":{}}"'
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
**Linux (Bash) 执行命令**:
|
|
170
170
|
```bash
|
|
171
|
-
openclaw nodes invoke --node <ID> --command
|
|
171
|
+
openclaw nodes invoke --node <ID> --command mcp.tool.call --params '{"name":"express_logistics_query","appPkg":"com.hihonor.magicvoice","arguments":{}}'
|
|
172
172
|
```
|
|
@@ -1,51 +1,241 @@
|
|
|
1
|
-
|
|
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>
|
|
2
44
|
<html lang="zh-CN">
|
|
3
45
|
<head>
|
|
4
46
|
<meta charset="UTF-8" />
|
|
5
47
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title
|
|
48
|
+
<title>${escapeHtml(options.pageTitle)}</title>
|
|
7
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
|
+
|
|
8
67
|
body {
|
|
68
|
+
margin: 0;
|
|
69
|
+
min-height: 100vh;
|
|
70
|
+
display: grid;
|
|
71
|
+
place-items: center;
|
|
72
|
+
padding: 24px;
|
|
9
73
|
font-family:
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
12
94
|
display: flex;
|
|
13
|
-
justify-content: center;
|
|
14
95
|
align-items: center;
|
|
15
|
-
|
|
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 {
|
|
16
138
|
margin: 0;
|
|
17
|
-
|
|
139
|
+
font-size: 26px;
|
|
140
|
+
line-height: 1.2;
|
|
141
|
+
font-weight: 700;
|
|
18
142
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
padding:
|
|
143
|
+
|
|
144
|
+
.details {
|
|
145
|
+
margin: 18px 0 0;
|
|
146
|
+
padding: 0;
|
|
23
147
|
border-radius: 16px;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
27
152
|
}
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
30
161
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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;
|
|
36
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
|
+
|
|
37
182
|
.hint {
|
|
38
|
-
font-size: 14px;
|
|
39
|
-
color: #999;
|
|
40
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
|
+
}
|
|
41
223
|
}
|
|
42
224
|
</style>
|
|
43
225
|
</head>
|
|
44
226
|
<body>
|
|
45
|
-
<
|
|
46
|
-
<div class="
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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>
|
|
50
239
|
</body>
|
|
51
240
|
</html>`;
|
|
241
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
6
|
-
import { URL } from "url";
|
|
7
|
-
import {
|
|
6
|
+
import { URL } from "node:url";
|
|
7
|
+
import { getResultHtml } from "./auth-result-html.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* 回调服务器选项
|
|
@@ -20,15 +20,6 @@ export interface CallbackServerOptions {
|
|
|
20
20
|
onError?: (error: Error) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
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>`;
|
|
24
|
-
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>`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function getResultHtml(message: string, success: boolean): string {
|
|
28
|
-
const icon = success ? SUCCESS_ICON_SVG : FAILURE_ICON_SVG;
|
|
29
|
-
return AUTH_RESULT_HTML.replace("{{ICON_SVG}}", icon).replace("{{MESSAGE}}", message);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
23
|
/**
|
|
33
24
|
* 启动本地回调服务器
|
|
34
25
|
*/
|
|
@@ -40,74 +31,148 @@ export function startCallbackServer(options: CallbackServerOptions): Promise<voi
|
|
|
40
31
|
let hasReceivedCode = false;
|
|
41
32
|
let promiseResolved = false;
|
|
42
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
|
+
|
|
43
60
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
44
61
|
try {
|
|
45
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
|
+
|
|
46
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") || "";
|
|
47
80
|
|
|
48
81
|
if (code && !hasReceivedCode) {
|
|
49
82
|
hasReceivedCode = true;
|
|
50
83
|
|
|
51
84
|
// 返回成功响应
|
|
52
85
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
53
|
-
res.end(
|
|
86
|
+
res.end(
|
|
87
|
+
getResultHtml({
|
|
88
|
+
pageTitle: "授权成功",
|
|
89
|
+
statusClass: "success",
|
|
90
|
+
title: "登录已完成",
|
|
91
|
+
hintText: "请返回 OpenClaw,登录流程会继续完成。",
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
54
94
|
|
|
55
95
|
// 延迟关闭服务器,确保响应已发送
|
|
56
|
-
|
|
57
|
-
if (!serverClosed) {
|
|
58
|
-
serverClosed = true;
|
|
59
|
-
if (timeoutId) {
|
|
60
|
-
clearTimeout(timeoutId);
|
|
61
|
-
}
|
|
62
|
-
server.close();
|
|
63
|
-
}
|
|
64
|
-
}, 100);
|
|
96
|
+
closeServerSoon();
|
|
65
97
|
|
|
66
98
|
// 调用回调
|
|
67
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);
|
|
68
124
|
} else if (!hasReceivedCode) {
|
|
69
|
-
//
|
|
125
|
+
// 未拿到授权码时,直接提示并结束,避免一直等待到超时。
|
|
70
126
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
71
|
-
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);
|
|
72
139
|
} else {
|
|
73
140
|
// 重复请求,返回成功响应但不处理
|
|
74
141
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
75
|
-
res.end(
|
|
142
|
+
res.end(
|
|
143
|
+
getResultHtml({
|
|
144
|
+
pageTitle: "登录已完成",
|
|
145
|
+
statusClass: "success",
|
|
146
|
+
title: "授权已经处理完成",
|
|
147
|
+
hintText: "请返回 OpenClaw,登录流程会继续完成。",
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
76
150
|
}
|
|
77
151
|
} catch (error) {
|
|
78
152
|
console.error("authorize callback error:", error);
|
|
79
153
|
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
80
|
-
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);
|
|
81
165
|
}
|
|
82
166
|
});
|
|
83
167
|
|
|
84
168
|
server.on("error", (error) => {
|
|
85
|
-
|
|
86
|
-
serverClosed = true;
|
|
87
|
-
if (timeoutId) {
|
|
88
|
-
clearTimeout(timeoutId);
|
|
89
|
-
}
|
|
90
|
-
onError?.(error);
|
|
91
|
-
if (!promiseResolved) {
|
|
92
|
-
promiseResolved = true;
|
|
93
|
-
reject(error);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
169
|
+
failAuthorization(error);
|
|
96
170
|
});
|
|
97
171
|
|
|
98
172
|
server.listen(port, () => {
|
|
99
173
|
// 设置超时
|
|
100
174
|
timeoutId = setTimeout(() => {
|
|
101
|
-
|
|
102
|
-
serverClosed = true;
|
|
103
|
-
server.close();
|
|
104
|
-
const error = new Error("authorize timeout");
|
|
105
|
-
onError?.(error);
|
|
106
|
-
if (!promiseResolved) {
|
|
107
|
-
promiseResolved = true;
|
|
108
|
-
reject(error);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
175
|
+
failAuthorization(new Error("authorize timeout"));
|
|
111
176
|
}, timeout);
|
|
112
177
|
});
|
|
113
178
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|