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/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, execution-history timeline,
90
- * and a manual event tester all served by the core plugin's /dsh-hooks/*
91
- * routes. Degrades gracefully: fetch failures show an inline notice, never
92
- * a crash. Registered into the shell's `web-ui.plugin.item` slot.
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([fetchStatus(), fetchHistory(30)]);
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
+ }