dsh-hooks 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/README.zh.md +25 -6
- package/bin/dsh-hooks.mjs +37 -197
- package/examples/notify-feishu.d.mts +31 -0
- package/lib/client.js +368 -50
- package/lib/feishu-session.d.ts +47 -0
- package/lib/feishu-session.js +94 -0
- package/lib/feishu.d.ts +119 -0
- package/lib/feishu.js +282 -0
- package/lib/history.d.ts +5 -0
- package/lib/history.js +102 -3
- package/lib/index.js +13 -4
- package/lib/server.d.ts +15 -3
- package/lib/server.js +93 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -52,6 +52,54 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/** POST a JSON action and surface the envelope result (error message included). */
|
|
56
|
+
async function postFeishu(path, body, fetchFn) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetchFn(path, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
accept: "application/json"
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body)
|
|
65
|
+
});
|
|
66
|
+
const envelope = await response.json();
|
|
67
|
+
if (!response.ok || !envelope.ok) return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: envelope.error?.message ?? `HTTP ${response.status}`
|
|
70
|
+
};
|
|
71
|
+
const value = envelope.value ?? {};
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
setup: value.setup,
|
|
75
|
+
message: typeof value.message === "string" ? value.message : void 0,
|
|
76
|
+
resultMaxChars: typeof value.resultMaxChars === "number" ? value.resultMaxChars : void 0
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn(`[dsh-hooks-ui] POST ${path} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: "网络请求失败"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchFeishuStatus(fetchFn = fetch) {
|
|
87
|
+
return getJson("/dsh-hooks/feishu/status", fetchFn);
|
|
88
|
+
}
|
|
89
|
+
async function postFeishuSetup(profile, resultMaxChars, fetchFn = fetch) {
|
|
90
|
+
const body = { profile };
|
|
91
|
+
if (resultMaxChars !== void 0) body.resultMaxChars = resultMaxChars;
|
|
92
|
+
return postFeishu("/dsh-hooks/feishu/setup", body, fetchFn);
|
|
93
|
+
}
|
|
94
|
+
async function postFeishuConfig(resultMaxChars, fetchFn = fetch) {
|
|
95
|
+
return postFeishu("/dsh-hooks/feishu/config", { resultMaxChars }, fetchFn);
|
|
96
|
+
}
|
|
97
|
+
async function postFeishuCancel(fetchFn = fetch) {
|
|
98
|
+
return postFeishu("/dsh-hooks/feishu/cancel", {}, fetchFn);
|
|
99
|
+
}
|
|
100
|
+
async function postFeishuTest(fetchFn = fetch) {
|
|
101
|
+
return postFeishu("/dsh-hooks/feishu/test", {}, fetchFn);
|
|
102
|
+
}
|
|
55
103
|
/** `HH:MM:SS` local time for a timestamp. */
|
|
56
104
|
function formatTime(ts) {
|
|
57
105
|
const date = new Date(ts);
|
|
@@ -86,10 +134,12 @@ window.__ModuleLoader__.load({
|
|
|
86
134
|
//#endregion
|
|
87
135
|
//#region src/client/settings-card.tsx
|
|
88
136
|
/**
|
|
89
|
-
* The dsh-hooks settings card: status badges,
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
137
|
+
* The dsh-hooks settings card: status badges, a manual event tester, the
|
|
138
|
+
* Feishu connect flow (QR scan + truncation length + test card), and a
|
|
139
|
+
* collapsed-by-default execution-history timeline at the bottom — all
|
|
140
|
+
* served by the core plugin's /dsh-hooks/* routes. Degrades gracefully:
|
|
141
|
+
* fetch failures show an inline notice, never a crash. Registered into the
|
|
142
|
+
* shell's `settings.section` slot.
|
|
93
143
|
*/
|
|
94
144
|
const EVENTS = [
|
|
95
145
|
"turn/start",
|
|
@@ -107,19 +157,35 @@ window.__ModuleLoader__.load({
|
|
|
107
157
|
"agent/error",
|
|
108
158
|
"agent/status"
|
|
109
159
|
];
|
|
160
|
+
const DEFAULT_TRUNCATE = 300;
|
|
110
161
|
/** Settings-slot component; the shell's slot machinery supplies the props. */
|
|
111
162
|
function HooksSettingsCard(_props) {
|
|
112
163
|
const [status, setStatus] = (0, react.useState)(null);
|
|
113
164
|
const [history, setHistory] = (0, react.useState)(null);
|
|
165
|
+
const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
|
|
114
166
|
const [loadError, setLoadError] = (0, react.useState)(false);
|
|
115
167
|
const [event, setEvent] = (0, react.useState)("turn/end");
|
|
116
168
|
const [reason, setReason] = (0, react.useState)("completed");
|
|
117
169
|
const [tool, setTool] = (0, react.useState)("");
|
|
118
170
|
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
171
|
+
const [feishu, setFeishu] = (0, react.useState)(null);
|
|
172
|
+
const [profile, setProfile] = (0, react.useState)("web");
|
|
173
|
+
const [setupTruncate, setSetupTruncate] = (0, react.useState)(String(DEFAULT_TRUNCATE));
|
|
174
|
+
const [truncateDraft, setTruncateDraft] = (0, react.useState)(null);
|
|
175
|
+
const [configMessage, setConfigMessage] = (0, react.useState)(null);
|
|
176
|
+
const [reconnecting, setReconnecting] = (0, react.useState)(false);
|
|
177
|
+
const [feishuError, setFeishuError] = (0, react.useState)(null);
|
|
178
|
+
const [testMessage, setTestMessage] = (0, react.useState)(null);
|
|
179
|
+
const [countdown, setCountdown] = (0, react.useState)(null);
|
|
119
180
|
const refresh = (0, react.useCallback)(async () => {
|
|
120
|
-
const [statusInfo, records] = await Promise.all([
|
|
181
|
+
const [statusInfo, records, feishuInfo] = await Promise.all([
|
|
182
|
+
fetchStatus(),
|
|
183
|
+
fetchHistory(30),
|
|
184
|
+
fetchFeishuStatus()
|
|
185
|
+
]);
|
|
121
186
|
setStatus(statusInfo);
|
|
122
187
|
setHistory(records);
|
|
188
|
+
setFeishu(feishuInfo);
|
|
123
189
|
setLoadError(statusInfo === null && records === null);
|
|
124
190
|
}, []);
|
|
125
191
|
(0, react.useEffect)(() => {
|
|
@@ -127,6 +193,22 @@ window.__ModuleLoader__.load({
|
|
|
127
193
|
const timer = setInterval(() => void refresh(), 5e3);
|
|
128
194
|
return () => clearInterval(timer);
|
|
129
195
|
}, [refresh]);
|
|
196
|
+
const pending = feishu?.setup?.status === "pending";
|
|
197
|
+
(0, react.useEffect)(() => {
|
|
198
|
+
if (!pending) return;
|
|
199
|
+
const poll = setInterval(() => void refresh(), 2e3);
|
|
200
|
+
const tick = setInterval(() => {
|
|
201
|
+
setCountdown(remainingSeconds(feishu?.setup?.expiresAtMs));
|
|
202
|
+
}, 1e3);
|
|
203
|
+
return () => {
|
|
204
|
+
clearInterval(poll);
|
|
205
|
+
clearInterval(tick);
|
|
206
|
+
};
|
|
207
|
+
}, [
|
|
208
|
+
pending,
|
|
209
|
+
feishu?.setup?.expiresAtMs,
|
|
210
|
+
refresh
|
|
211
|
+
]);
|
|
130
212
|
const runTest = async (execute) => {
|
|
131
213
|
const result = await postTest({
|
|
132
214
|
event,
|
|
@@ -137,6 +219,65 @@ window.__ModuleLoader__.load({
|
|
|
137
219
|
setTestResult(result);
|
|
138
220
|
if (execute) refresh();
|
|
139
221
|
};
|
|
222
|
+
const connectFeishu = async () => {
|
|
223
|
+
setFeishuError(null);
|
|
224
|
+
setTestMessage(null);
|
|
225
|
+
setConfigMessage(null);
|
|
226
|
+
setCountdown(null);
|
|
227
|
+
const parsed = Number(setupTruncate);
|
|
228
|
+
const result = await postFeishuSetup(profile.trim() !== "" ? profile.trim() : "web", Number.isFinite(parsed) ? parsed : void 0);
|
|
229
|
+
if (!result.ok) {
|
|
230
|
+
setFeishuError(result.error ?? "启动扫码失败");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (result.setup !== void 0) {
|
|
234
|
+
setFeishu({
|
|
235
|
+
configured: false,
|
|
236
|
+
appId: null,
|
|
237
|
+
targetKind: null,
|
|
238
|
+
target: null,
|
|
239
|
+
setup: result.setup,
|
|
240
|
+
resultMaxChars: Number.isFinite(parsed) ? parsed : DEFAULT_TRUNCATE
|
|
241
|
+
});
|
|
242
|
+
setCountdown(remainingSeconds(result.setup.expiresAtMs));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const cancelFeishu = async () => {
|
|
246
|
+
await postFeishuCancel();
|
|
247
|
+
refresh();
|
|
248
|
+
};
|
|
249
|
+
const sendTestCard = async () => {
|
|
250
|
+
setTestMessage(null);
|
|
251
|
+
setFeishuError(null);
|
|
252
|
+
const result = await postFeishuTest();
|
|
253
|
+
if (!result.ok) setFeishuError(result.error ?? "发送失败");
|
|
254
|
+
else setTestMessage(result.message ?? "已发送");
|
|
255
|
+
};
|
|
256
|
+
const saveTruncate = async () => {
|
|
257
|
+
const value = Number(truncateDraft);
|
|
258
|
+
if (!Number.isFinite(value)) {
|
|
259
|
+
setConfigMessage({
|
|
260
|
+
ok: false,
|
|
261
|
+
text: "请输入数字"
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const result = await postFeishuConfig(value);
|
|
266
|
+
if (!result.ok) {
|
|
267
|
+
setConfigMessage({
|
|
268
|
+
ok: false,
|
|
269
|
+
text: result.error ?? "保存失败"
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setTruncateDraft(null);
|
|
274
|
+
setConfigMessage({
|
|
275
|
+
ok: true,
|
|
276
|
+
text: `已保存:卡片内容最长 ${result.resultMaxChars} 字符`
|
|
277
|
+
});
|
|
278
|
+
refresh();
|
|
279
|
+
};
|
|
280
|
+
const truncateValue = truncateDraft ?? String(feishu?.resultMaxChars ?? DEFAULT_TRUNCATE);
|
|
140
281
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
141
282
|
className: "dh-card",
|
|
142
283
|
children: [
|
|
@@ -167,49 +308,6 @@ window.__ModuleLoader__.load({
|
|
|
167
308
|
className: "dh-error-banner",
|
|
168
309
|
children: "无法访问 /dsh-hooks/* 路由:请确认 dsh-hooks 核心插件已安装且 dsh web 已重启。"
|
|
169
310
|
}),
|
|
170
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
171
|
-
className: "dh-section-title",
|
|
172
|
-
children: "执行历史(最近 30 条)"
|
|
173
|
-
}), history === null || history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
174
|
-
className: "dh-empty",
|
|
175
|
-
children: history === null ? "加载中…" : "暂无记录"
|
|
176
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
177
|
-
className: "dh-timeline",
|
|
178
|
-
children: [...history].reverse().map((record, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
179
|
-
className: "dh-record",
|
|
180
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
181
|
-
className: "dh-record-main",
|
|
182
|
-
children: [
|
|
183
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
184
|
-
className: "dh-record-top",
|
|
185
|
-
children: [
|
|
186
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
187
|
-
className: "dh-record-time",
|
|
188
|
-
children: formatTime(record.ts)
|
|
189
|
-
}),
|
|
190
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
191
|
-
className: "dh-record-event",
|
|
192
|
-
children: record.event
|
|
193
|
-
}),
|
|
194
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
195
|
-
className: `dh-outcome ${outcomeClass(record.outcome)}`,
|
|
196
|
-
children: outcomeLabel(record.outcome)
|
|
197
|
-
})
|
|
198
|
-
]
|
|
199
|
-
}),
|
|
200
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
201
|
-
className: "dh-record-command",
|
|
202
|
-
title: record.command,
|
|
203
|
-
children: record.command
|
|
204
|
-
}),
|
|
205
|
-
record.error !== void 0 && record.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
206
|
-
className: "dh-record-error",
|
|
207
|
-
children: record.error.slice(0, 200)
|
|
208
|
-
})
|
|
209
|
-
]
|
|
210
|
-
})
|
|
211
|
-
}, `${record.ts}-${index}`))
|
|
212
|
-
})] }),
|
|
213
311
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
214
312
|
className: "dh-section-title",
|
|
215
313
|
children: "手动测试"
|
|
@@ -300,10 +398,230 @@ window.__ModuleLoader__.load({
|
|
|
300
398
|
}, line.index))]
|
|
301
399
|
})
|
|
302
400
|
]
|
|
303
|
-
})] })
|
|
401
|
+
})] }),
|
|
402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
403
|
+
className: "dh-section-title",
|
|
404
|
+
children: "飞书通知"
|
|
405
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
406
|
+
className: "dh-feishu",
|
|
407
|
+
children: pending ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
408
|
+
className: "dh-feishu-qr",
|
|
409
|
+
children: [
|
|
410
|
+
feishu?.setup?.qrDataUrl !== void 0 && feishu?.setup?.qrDataUrl !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
411
|
+
className: "dh-feishu-qr-img",
|
|
412
|
+
src: feishu.setup.qrDataUrl,
|
|
413
|
+
alt: "飞书扫码授权二维码"
|
|
414
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
415
|
+
className: "dh-feishu-link",
|
|
416
|
+
href: feishu?.setup?.qrUrl,
|
|
417
|
+
target: "_blank",
|
|
418
|
+
rel: "noreferrer",
|
|
419
|
+
children: "在浏览器中打开飞书授权链接"
|
|
420
|
+
}),
|
|
421
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
422
|
+
className: "dh-feishu-line",
|
|
423
|
+
children: ["请用飞书扫码", countdown !== null && countdown > 0 ? `(${countdown}s 内有效)` : ""]
|
|
424
|
+
}),
|
|
425
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
426
|
+
className: "dh-buttons",
|
|
427
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
428
|
+
type: "button",
|
|
429
|
+
className: "dh-button",
|
|
430
|
+
onClick: () => void cancelFeishu(),
|
|
431
|
+
children: "取消"
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
]
|
|
435
|
+
}) : feishu?.configured === true && !reconnecting ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
436
|
+
className: "dh-feishu-status",
|
|
437
|
+
children: [
|
|
438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
439
|
+
className: "dh-feishu-line dh-feishu-ok",
|
|
440
|
+
children: [
|
|
441
|
+
"✅ 已连接 · 应用 ",
|
|
442
|
+
feishu.appId ?? "?",
|
|
443
|
+
feishu.targetKind !== null && feishu.target !== null ? `(接收者 ${feishu.targetKind}: ${feishu.target})` : ""
|
|
444
|
+
]
|
|
445
|
+
}),
|
|
446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
447
|
+
className: "dh-feishu-row",
|
|
448
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
449
|
+
className: "dh-field dh-field-narrow",
|
|
450
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
451
|
+
className: "dh-field-label",
|
|
452
|
+
children: "卡片截断长度(50–5000 字符)"
|
|
453
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
454
|
+
className: "dh-input",
|
|
455
|
+
type: "number",
|
|
456
|
+
min: 50,
|
|
457
|
+
max: 5e3,
|
|
458
|
+
value: truncateValue,
|
|
459
|
+
onChange: (e) => setTruncateDraft(e.target.value)
|
|
460
|
+
})]
|
|
461
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
462
|
+
type: "button",
|
|
463
|
+
className: "dh-button",
|
|
464
|
+
onClick: () => void saveTruncate(),
|
|
465
|
+
children: "保存"
|
|
466
|
+
})]
|
|
467
|
+
}),
|
|
468
|
+
configMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
469
|
+
className: configMessage.ok ? "dh-feishu-line dh-feishu-ok" : "dh-feishu-error",
|
|
470
|
+
children: configMessage.text
|
|
471
|
+
}),
|
|
472
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
473
|
+
className: "dh-buttons",
|
|
474
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
475
|
+
type: "button",
|
|
476
|
+
className: "dh-button dh-button-primary",
|
|
477
|
+
onClick: () => void sendTestCard(),
|
|
478
|
+
children: "发送测试卡片"
|
|
479
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
480
|
+
type: "button",
|
|
481
|
+
className: "dh-button",
|
|
482
|
+
onClick: () => setReconnecting(true),
|
|
483
|
+
children: "重新连接"
|
|
484
|
+
})]
|
|
485
|
+
}),
|
|
486
|
+
testMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
487
|
+
className: "dh-feishu-line dh-feishu-ok",
|
|
488
|
+
children: testMessage
|
|
489
|
+
}),
|
|
490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
491
|
+
className: "dh-feishu-hint",
|
|
492
|
+
children: "截断长度即时生效;重新扫码会覆盖现有应用凭据与本 profile 的飞书 hooks。"
|
|
493
|
+
})
|
|
494
|
+
]
|
|
495
|
+
}) : feishu?.setup?.status === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
496
|
+
className: "dh-feishu-status",
|
|
497
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
498
|
+
className: "dh-feishu-error",
|
|
499
|
+
children: ["连接失败:", feishu.setup.error ?? "未知错误"]
|
|
500
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
501
|
+
className: "dh-buttons",
|
|
502
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
503
|
+
type: "button",
|
|
504
|
+
className: "dh-button dh-button-primary",
|
|
505
|
+
onClick: () => void connectFeishu(),
|
|
506
|
+
children: "重试"
|
|
507
|
+
})
|
|
508
|
+
})]
|
|
509
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
510
|
+
className: "dh-feishu-form",
|
|
511
|
+
children: [
|
|
512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
513
|
+
className: "dh-test-row",
|
|
514
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
515
|
+
className: "dh-field",
|
|
516
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
517
|
+
className: "dh-field-label",
|
|
518
|
+
children: "profile(写入哪个 profile 的 cordis.patch.yml)"
|
|
519
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
520
|
+
className: "dh-input",
|
|
521
|
+
value: profile,
|
|
522
|
+
onChange: (e) => setProfile(e.target.value),
|
|
523
|
+
placeholder: "web"
|
|
524
|
+
})]
|
|
525
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
526
|
+
className: "dh-field dh-field-narrow",
|
|
527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
528
|
+
className: "dh-field-label",
|
|
529
|
+
children: "卡片截断长度(50–5000)"
|
|
530
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
531
|
+
className: "dh-input",
|
|
532
|
+
type: "number",
|
|
533
|
+
min: 50,
|
|
534
|
+
max: 5e3,
|
|
535
|
+
value: setupTruncate,
|
|
536
|
+
onChange: (e) => setSetupTruncate(e.target.value)
|
|
537
|
+
})]
|
|
538
|
+
})]
|
|
539
|
+
}),
|
|
540
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
541
|
+
className: "dh-buttons",
|
|
542
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
className: "dh-button dh-button-primary",
|
|
545
|
+
onClick: () => void connectFeishu(),
|
|
546
|
+
children: "扫码连接飞书"
|
|
547
|
+
}), reconnecting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
548
|
+
type: "button",
|
|
549
|
+
className: "dh-button",
|
|
550
|
+
onClick: () => setReconnecting(false),
|
|
551
|
+
children: "返回"
|
|
552
|
+
})]
|
|
553
|
+
}),
|
|
554
|
+
feishuError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
555
|
+
className: "dh-feishu-error",
|
|
556
|
+
children: feishuError
|
|
557
|
+
}),
|
|
558
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
559
|
+
className: "dh-feishu-hint",
|
|
560
|
+
children: "将创建名为「DSH 通知机器人」的飞书应用(仅 im:message:send_as_bot 权限),扫码者本人接收通知卡片;配置写入 ~/.dsh/profiles/<profile>/cordis.patch.yml,重启 dsh web 后生效。"
|
|
561
|
+
})
|
|
562
|
+
]
|
|
563
|
+
})
|
|
564
|
+
})] }),
|
|
565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
566
|
+
className: "dh-section-head",
|
|
567
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
568
|
+
className: "dh-section-title",
|
|
569
|
+
children: ["执行历史(最近 30 条)", status !== null && status.historyCount > 0 ? ` · ${status.historyCount} 条` : ""]
|
|
570
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
571
|
+
type: "button",
|
|
572
|
+
className: "dh-button dh-toggle",
|
|
573
|
+
onClick: () => setHistoryOpen((open) => !open),
|
|
574
|
+
"aria-expanded": historyOpen,
|
|
575
|
+
children: historyOpen ? "收起 ▲" : "展开 ▼"
|
|
576
|
+
})]
|
|
577
|
+
}), historyOpen && (history === null || history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
578
|
+
className: "dh-empty",
|
|
579
|
+
children: history === null ? "加载中…" : "暂无记录"
|
|
580
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
581
|
+
className: "dh-timeline",
|
|
582
|
+
children: [...history].reverse().map((record, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
583
|
+
className: "dh-record",
|
|
584
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
585
|
+
className: "dh-record-main",
|
|
586
|
+
children: [
|
|
587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
588
|
+
className: "dh-record-top",
|
|
589
|
+
children: [
|
|
590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
591
|
+
className: "dh-record-time",
|
|
592
|
+
children: formatTime(record.ts)
|
|
593
|
+
}),
|
|
594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
595
|
+
className: "dh-record-event",
|
|
596
|
+
children: record.event
|
|
597
|
+
}),
|
|
598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
599
|
+
className: `dh-outcome ${outcomeClass(record.outcome)}`,
|
|
600
|
+
children: outcomeLabel(record.outcome)
|
|
601
|
+
})
|
|
602
|
+
]
|
|
603
|
+
}),
|
|
604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
605
|
+
className: "dh-record-command",
|
|
606
|
+
title: record.command,
|
|
607
|
+
children: record.command
|
|
608
|
+
}),
|
|
609
|
+
record.error !== void 0 && record.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
610
|
+
className: "dh-record-error",
|
|
611
|
+
children: record.error.slice(0, 200)
|
|
612
|
+
})
|
|
613
|
+
]
|
|
614
|
+
})
|
|
615
|
+
}, `${record.ts}-${index}`))
|
|
616
|
+
}))] })
|
|
304
617
|
]
|
|
305
618
|
});
|
|
306
619
|
}
|
|
620
|
+
/** Seconds until the QR expires, or null when unknown/expired. */
|
|
621
|
+
function remainingSeconds(expiresAtMs) {
|
|
622
|
+
if (expiresAtMs === void 0) return null;
|
|
623
|
+
return Math.max(0, Math.round((expiresAtMs - Date.now()) / 1e3));
|
|
624
|
+
}
|
|
307
625
|
function outcomeClass(outcome) {
|
|
308
626
|
switch (outcomeTone(outcome)) {
|
|
309
627
|
case "ok": return "dh-outcome-ok";
|
|
@@ -314,7 +632,7 @@ window.__ModuleLoader__.load({
|
|
|
314
632
|
}
|
|
315
633
|
//#endregion
|
|
316
634
|
//#region src/client/settings-card.module.css?inline
|
|
317
|
-
var settings_card_module_default = ":root {\n --dsh-hooks-border: #80849038;\n --dsh-hooks-muted: #767c85;\n --dsh-hooks-accent: #4d8df7;\n --dsh-hooks-ok: #3fb56b;\n --dsh-hooks-bad: #e5534b;\n --dsh-hooks-warn: #d9a13c;\n}\n\n.dh-card {\n flex-direction: column;\n gap: 14px;\n padding: 12px 4px;\n font-size: 13px;\n line-height: 1.5;\n display: flex;\n}\n\n.dh-card-head {\n align-items: center;\n gap: 10px;\n display: flex;\n}\n\n.dh-card-title {\n font-size: 14px;\n font-weight: 600;\n}\n\n.dh-badges {\n gap: 6px;\n display: flex;\n}\n\n.dh-badge {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n background: #80849029;\n border-radius: 9px;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-section-title {\n color: var(--dsh-hooks-muted);\n text-transform: uppercase;\n letter-spacing: .04em;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-timeline {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-record {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n gap: 8px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-record-main {\n flex: 1;\n min-width: 0;\n}\n\n.dh-record-top {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dh-record-time {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n font-size: 11px;\n}\n\n.dh-record-event {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-weight: 600;\n overflow: hidden;\n}\n\n.dh-record-command {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-outcome {\n white-space: nowrap;\n border-radius: 9px;\n align-self: flex-start;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-outcome-ok {\n color: var(--dsh-hooks-ok);\n background: #3fb56b29;\n}\n\n.dh-outcome-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-outcome-warn {\n color: var(--dsh-hooks-warn);\n background: #d9a13c29;\n}\n\n.dh-outcome-neutral {\n color: var(--dsh-hooks-muted);\n background: #80849029;\n}\n\n.dh-record-error {\n color: var(--dsh-hooks-bad);\n white-space: pre-wrap;\n word-break: break-all;\n margin-top: 4px;\n font-size: 11px;\n}\n\n.dh-empty {\n color: var(--dsh-hooks-muted);\n padding: 6px 2px;\n font-size: 12px;\n}\n\n.dh-test-form {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-test-row {\n gap: 8px;\n display: flex;\n}\n\n.dh-field {\n flex-direction: column;\n flex: 1;\n gap: 3px;\n min-width: 0;\n display: flex;\n}\n\n.dh-field-label {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-input, .dh-select {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n box-sizing: border-box;\n background: #8084901f;\n border-radius: 5px;\n outline: none;\n width: 100%;\n padding: 5px 8px;\n font-size: 12px;\n}\n\n.dh-input:focus, .dh-select:focus {\n border-color: var(--dsh-hooks-accent);\n}\n\n.dh-buttons {\n gap: 8px;\n display: flex;\n}\n\n.dh-button {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n cursor: pointer;\n background: #8084901f;\n border-radius: 5px;\n padding: 5px 12px;\n font-size: 12px;\n}\n\n.dh-button:hover {\n background: #80849038;\n}\n\n.dh-button-primary {\n background: var(--dsh-hooks-accent);\n border-color: var(--dsh-hooks-accent);\n color: #fff;\n}\n\n.dh-button-primary:hover {\n background: #3c7de8;\n}\n\n.dh-test-results {\n flex-direction: column;\n gap: 4px;\n display: flex;\n}\n\n.dh-test-line {\n word-break: break-all;\n border-radius: 5px;\n padding: 4px 8px;\n font-size: 12px;\n}\n\n.dh-test-line-match {\n color: var(--dsh-hooks-ok);\n background: #3fb56b24;\n}\n\n.dh-test-line-skip {\n color: var(--dsh-hooks-muted);\n background: #8084901a;\n}\n\n.dh-error-banner {\n color: var(--dsh-hooks-bad);\n background: #e5534b1f;\n border: 1px solid #e5534b66;\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n";
|
|
635
|
+
var settings_card_module_default = ":root {\n --dsh-hooks-border: #80849038;\n --dsh-hooks-muted: #767c85;\n --dsh-hooks-accent: #4d8df7;\n --dsh-hooks-ok: #3fb56b;\n --dsh-hooks-bad: #e5534b;\n --dsh-hooks-warn: #d9a13c;\n}\n\n.dh-card {\n flex-direction: column;\n gap: 14px;\n padding: 12px 4px;\n font-size: 13px;\n line-height: 1.5;\n display: flex;\n}\n\n.dh-card-head {\n align-items: center;\n gap: 10px;\n display: flex;\n}\n\n.dh-card-title {\n font-size: 14px;\n font-weight: 600;\n}\n\n.dh-badges {\n gap: 6px;\n display: flex;\n}\n\n.dh-badge {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n background: #80849029;\n border-radius: 9px;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-section-title {\n color: var(--dsh-hooks-muted);\n text-transform: uppercase;\n letter-spacing: .04em;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-section-head {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n margin: 0 0 8px;\n display: flex;\n}\n\n.dh-section-head .dh-section-title {\n margin: 0;\n}\n\n.dh-toggle {\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-field-narrow {\n flex: 0 0 150px;\n}\n\n.dh-feishu-row {\n align-items: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.dh-timeline {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-record {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n gap: 8px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-record-main {\n flex: 1;\n min-width: 0;\n}\n\n.dh-record-top {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dh-record-time {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n font-size: 11px;\n}\n\n.dh-record-event {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-weight: 600;\n overflow: hidden;\n}\n\n.dh-record-command {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-outcome {\n white-space: nowrap;\n border-radius: 9px;\n align-self: flex-start;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-outcome-ok {\n color: var(--dsh-hooks-ok);\n background: #3fb56b29;\n}\n\n.dh-outcome-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-outcome-warn {\n color: var(--dsh-hooks-warn);\n background: #d9a13c29;\n}\n\n.dh-outcome-neutral {\n color: var(--dsh-hooks-muted);\n background: #80849029;\n}\n\n.dh-record-error {\n color: var(--dsh-hooks-bad);\n white-space: pre-wrap;\n word-break: break-all;\n margin-top: 4px;\n font-size: 11px;\n}\n\n.dh-empty {\n color: var(--dsh-hooks-muted);\n padding: 6px 2px;\n font-size: 12px;\n}\n\n.dh-test-form {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-test-row {\n gap: 8px;\n display: flex;\n}\n\n.dh-field {\n flex-direction: column;\n flex: 1;\n gap: 3px;\n min-width: 0;\n display: flex;\n}\n\n.dh-field-label {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-input, .dh-select {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n box-sizing: border-box;\n background: #8084901f;\n border-radius: 5px;\n outline: none;\n width: 100%;\n padding: 5px 8px;\n font-size: 12px;\n}\n\n.dh-input:focus, .dh-select:focus {\n border-color: var(--dsh-hooks-accent);\n}\n\n.dh-buttons {\n gap: 8px;\n display: flex;\n}\n\n.dh-button {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n cursor: pointer;\n background: #8084901f;\n border-radius: 5px;\n padding: 5px 12px;\n font-size: 12px;\n}\n\n.dh-button:hover {\n background: #80849038;\n}\n\n.dh-button-primary {\n background: var(--dsh-hooks-accent);\n border-color: var(--dsh-hooks-accent);\n color: #fff;\n}\n\n.dh-button-primary:hover {\n background: #3c7de8;\n}\n\n.dh-test-results {\n flex-direction: column;\n gap: 4px;\n display: flex;\n}\n\n.dh-test-line {\n word-break: break-all;\n border-radius: 5px;\n padding: 4px 8px;\n font-size: 12px;\n}\n\n.dh-test-line-match {\n color: var(--dsh-hooks-ok);\n background: #3fb56b24;\n}\n\n.dh-test-line-skip {\n color: var(--dsh-hooks-muted);\n background: #8084901a;\n}\n\n.dh-error-banner {\n color: var(--dsh-hooks-bad);\n background: #e5534b1f;\n border: 1px solid #e5534b66;\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.dh-feishu {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-form, .dh-feishu-status, .dh-feishu-qr {\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-qr-img {\n border: 1px solid var(--dsh-hooks-border);\n box-sizing: border-box;\n background: #fff;\n border-radius: 8px;\n width: 220px;\n height: 220px;\n padding: 6px;\n}\n\n.dh-feishu-line {\n font-size: 12px;\n}\n\n.dh-feishu-ok {\n color: var(--dsh-hooks-ok);\n}\n\n.dh-feishu-error {\n color: var(--dsh-hooks-bad);\n word-break: break-all;\n font-size: 12px;\n}\n\n.dh-feishu-hint {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-feishu-link {\n color: var(--dsh-hooks-accent);\n font-size: 12px;\n}\n";
|
|
318
636
|
//#endregion
|
|
319
637
|
//#region src/client/index.ts
|
|
320
638
|
const name = "dsh-hooks";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu QR-scan session manager for the web routes: one in-flight
|
|
3
|
+
* `registerApp` flow at a time, polled by the settings card. The start
|
|
4
|
+
* promise resolves as soon as the QR authorization is ready (so the UI can
|
|
5
|
+
* render the code immediately), while the scan wait and file writes finish
|
|
6
|
+
* in the background and surface through `status()`.
|
|
7
|
+
*/
|
|
8
|
+
import { runFeishuSetup, type FeishuSetupPaths } from './feishu.js';
|
|
9
|
+
export type FeishuSetupStatus = 'pending' | 'succeeded' | 'failed';
|
|
10
|
+
/** Display-only snapshot; credentials never enter any field. */
|
|
11
|
+
export interface FeishuSetupSnapshot {
|
|
12
|
+
status: FeishuSetupStatus;
|
|
13
|
+
/** Epoch ms when the flow started (server clock). */
|
|
14
|
+
startedAt: number;
|
|
15
|
+
/** Epoch ms when the QR authorization expires (pending only). */
|
|
16
|
+
expiresAtMs?: number;
|
|
17
|
+
/** Feishu authorization URL (pending only). */
|
|
18
|
+
qrUrl?: string;
|
|
19
|
+
/** PNG data URL of the QR code (pending only; best-effort). */
|
|
20
|
+
qrDataUrl?: string;
|
|
21
|
+
/** Created app id (succeeded only, unmasked — it is not a secret). */
|
|
22
|
+
appId?: string;
|
|
23
|
+
/** Failure message (failed only). */
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const FEISHU_SETUP_BUSY = "\u5DF2\u6709\u8FDB\u884C\u4E2D\u7684\u626B\u7801\u4F1A\u8BDD\uFF0C\u8BF7\u5148\u53D6\u6D88\u6216\u7B49\u5F85\u5B8C\u6210";
|
|
27
|
+
/** Render the QR as a PNG data URL (the qrcode package loads lazily). */
|
|
28
|
+
export declare function renderFeishuQr(url: string): Promise<string>;
|
|
29
|
+
export interface FeishuSetupManagerDeps {
|
|
30
|
+
runSetup?: typeof runFeishuSetup;
|
|
31
|
+
renderQr?: (url: string) => Promise<string>;
|
|
32
|
+
/** Clock override for tests. */
|
|
33
|
+
now?: () => number;
|
|
34
|
+
paths?: FeishuSetupPaths;
|
|
35
|
+
}
|
|
36
|
+
export interface FeishuSetupManager {
|
|
37
|
+
/** Start one scan flow; rejects when another flow is still pending. */
|
|
38
|
+
start(profile?: string, options?: {
|
|
39
|
+
resultMaxChars?: number;
|
|
40
|
+
}): Promise<FeishuSetupSnapshot>;
|
|
41
|
+
/** Current snapshot, or null when idle. */
|
|
42
|
+
status(): FeishuSetupSnapshot | null;
|
|
43
|
+
/** Abort the pending flow. Returns false when nothing was pending. */
|
|
44
|
+
cancel(): boolean;
|
|
45
|
+
dispose(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createFeishuSetupManager(deps?: FeishuSetupManagerDeps): FeishuSetupManager;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu QR-scan session manager for the web routes: one in-flight
|
|
3
|
+
* `registerApp` flow at a time, polled by the settings card. The start
|
|
4
|
+
* promise resolves as soon as the QR authorization is ready (so the UI can
|
|
5
|
+
* render the code immediately), while the scan wait and file writes finish
|
|
6
|
+
* in the background and surface through `status()`.
|
|
7
|
+
*/
|
|
8
|
+
import { runFeishuSetup } from './feishu.js';
|
|
9
|
+
export const FEISHU_SETUP_BUSY = '已有进行中的扫码会话,请先取消或等待完成';
|
|
10
|
+
/** Render the QR as a PNG data URL (the qrcode package loads lazily). */
|
|
11
|
+
export async function renderFeishuQr(url) {
|
|
12
|
+
const { default: QRCode } = await import('qrcode');
|
|
13
|
+
return QRCode.toDataURL(url, { width: 320, margin: 1 });
|
|
14
|
+
}
|
|
15
|
+
export function createFeishuSetupManager(deps = {}) {
|
|
16
|
+
const runSetup = deps.runSetup ?? runFeishuSetup;
|
|
17
|
+
const renderQr = deps.renderQr ?? renderFeishuQr;
|
|
18
|
+
const now = deps.now ?? Date.now;
|
|
19
|
+
const paths = deps.paths;
|
|
20
|
+
let current = null;
|
|
21
|
+
let controller = null;
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
async function start(profile = 'web', options = {}) {
|
|
24
|
+
if (current?.status === 'pending')
|
|
25
|
+
throw new Error(FEISHU_SETUP_BUSY);
|
|
26
|
+
cancelled = false;
|
|
27
|
+
const ac = new AbortController();
|
|
28
|
+
controller = ac;
|
|
29
|
+
const signal = ac.signal;
|
|
30
|
+
const startedAt = now();
|
|
31
|
+
const snapshot = { status: 'pending', startedAt };
|
|
32
|
+
current = snapshot;
|
|
33
|
+
let resolveReady;
|
|
34
|
+
const ready = new Promise((resolve) => {
|
|
35
|
+
resolveReady = resolve;
|
|
36
|
+
});
|
|
37
|
+
const task = (async () => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await runSetup({
|
|
40
|
+
profile,
|
|
41
|
+
signal,
|
|
42
|
+
paths,
|
|
43
|
+
resultMaxChars: options.resultMaxChars,
|
|
44
|
+
onQRCodeReady: async (qr) => {
|
|
45
|
+
snapshot.qrUrl = qr.url;
|
|
46
|
+
snapshot.expiresAtMs = startedAt + qr.expireIn * 1000;
|
|
47
|
+
try {
|
|
48
|
+
snapshot.qrDataUrl = await renderQr(qr.url);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// QR 图像渲染失败不阻塞扫码:UI 回退为授权链接。
|
|
52
|
+
}
|
|
53
|
+
resolveReady({ ...snapshot });
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (cancelled)
|
|
57
|
+
return;
|
|
58
|
+
current = { status: 'succeeded', startedAt, appId: result.appId };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (cancelled)
|
|
62
|
+
return;
|
|
63
|
+
current = {
|
|
64
|
+
status: 'failed',
|
|
65
|
+
startedAt,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
// Resolve when the QR is ready; if the flow settles first (no QR callback
|
|
71
|
+
// or an immediate failure), report the terminal snapshot instead.
|
|
72
|
+
const cancelledOutcome = { status: 'failed', startedAt, error: '已取消' };
|
|
73
|
+
return await Promise.race([
|
|
74
|
+
ready,
|
|
75
|
+
task.then(() => current ?? cancelledOutcome),
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
function status() {
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
function cancel() {
|
|
82
|
+
if (current?.status !== 'pending')
|
|
83
|
+
return false;
|
|
84
|
+
cancelled = true;
|
|
85
|
+
controller?.abort();
|
|
86
|
+
controller = null;
|
|
87
|
+
current = null;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function dispose() {
|
|
91
|
+
cancel();
|
|
92
|
+
}
|
|
93
|
+
return { start, status, cancel, dispose };
|
|
94
|
+
}
|