dsh-hooks 0.3.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 ADDED
@@ -0,0 +1,683 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-hooks",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/api.ts
10
+ async function getJson(path, fetchFn) {
11
+ try {
12
+ const response = await fetchFn(path, { headers: { accept: "application/json" } });
13
+ if (!response.ok) {
14
+ console.warn(`[dsh-hooks-ui] GET ${path} → HTTP ${response.status}`);
15
+ return null;
16
+ }
17
+ const envelope = await response.json();
18
+ if (!envelope.ok) {
19
+ console.warn(`[dsh-hooks-ui] GET ${path} → ${envelope.error?.message ?? "unknown error"}`);
20
+ return null;
21
+ }
22
+ return envelope.value ?? null;
23
+ } catch (error) {
24
+ console.warn(`[dsh-hooks-ui] GET ${path} failed: ${error instanceof Error ? error.message : String(error)}`);
25
+ return null;
26
+ }
27
+ }
28
+ async function fetchStatus(fetchFn = fetch) {
29
+ return getJson("/dsh-hooks/status", fetchFn);
30
+ }
31
+ async function fetchHistory(n = 50, fetchFn = fetch) {
32
+ return getJson(`/dsh-hooks/history?n=${Math.max(1, Math.min(500, Math.floor(n)))}`, fetchFn);
33
+ }
34
+ async function postTest(body, fetchFn = fetch) {
35
+ try {
36
+ const response = await fetchFn("/dsh-hooks/test", {
37
+ method: "POST",
38
+ headers: {
39
+ "content-type": "application/json",
40
+ accept: "application/json"
41
+ },
42
+ body: JSON.stringify(body)
43
+ });
44
+ const envelope = await response.json();
45
+ if (!response.ok || !envelope.ok) {
46
+ console.warn(`[dsh-hooks-ui] POST /dsh-hooks/test → ${envelope.error?.message ?? `HTTP ${response.status}`}`);
47
+ return null;
48
+ }
49
+ return envelope.value ?? null;
50
+ } catch (error) {
51
+ console.warn(`[dsh-hooks-ui] POST /dsh-hooks/test failed: ${error instanceof Error ? error.message : String(error)}`);
52
+ return null;
53
+ }
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
+ }
103
+ /** `HH:MM:SS` local time for a timestamp. */
104
+ function formatTime(ts) {
105
+ const date = new Date(ts);
106
+ const p = (value) => String(value).padStart(2, "0");
107
+ return `${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
108
+ }
109
+ /** Chinese outcome labels. */
110
+ const OUTCOME_LABELS = {
111
+ spawned: "已启动",
112
+ "spawn-failed": "启动失败",
113
+ timeout: "超时",
114
+ "exit-0": "成功",
115
+ "exit-nonzero": "失败",
116
+ sent: "已发送",
117
+ "send-failed": "发送失败"
118
+ };
119
+ function outcomeLabel(outcome) {
120
+ return OUTCOME_LABELS[outcome] ?? outcome;
121
+ }
122
+ const OUTCOME_TONES = {
123
+ "exit-0": "ok",
124
+ sent: "ok",
125
+ "exit-nonzero": "bad",
126
+ "spawn-failed": "bad",
127
+ "send-failed": "bad",
128
+ timeout: "warn",
129
+ spawned: "neutral"
130
+ };
131
+ function outcomeTone(outcome) {
132
+ return OUTCOME_TONES[outcome] ?? "neutral";
133
+ }
134
+ //#endregion
135
+ //#region src/client/settings-card.tsx
136
+ /**
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.
143
+ */
144
+ const EVENTS = [
145
+ "turn/start",
146
+ "turn/end",
147
+ "step/end",
148
+ "tool/call",
149
+ "tool/result",
150
+ "user/message",
151
+ "approval/asked",
152
+ "session/title",
153
+ "session/created",
154
+ "session/disposed",
155
+ "agent/created",
156
+ "agent/disposed",
157
+ "agent/error",
158
+ "agent/status"
159
+ ];
160
+ const DEFAULT_TRUNCATE = 300;
161
+ /** Settings-slot component; the shell's slot machinery supplies the props. */
162
+ function HooksSettingsCard(_props) {
163
+ const [status, setStatus] = (0, react.useState)(null);
164
+ const [history, setHistory] = (0, react.useState)(null);
165
+ const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
166
+ const [loadError, setLoadError] = (0, react.useState)(false);
167
+ const [event, setEvent] = (0, react.useState)("turn/end");
168
+ const [reason, setReason] = (0, react.useState)("completed");
169
+ const [tool, setTool] = (0, react.useState)("");
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);
180
+ const refresh = (0, react.useCallback)(async () => {
181
+ const [statusInfo, records, feishuInfo] = await Promise.all([
182
+ fetchStatus(),
183
+ fetchHistory(30),
184
+ fetchFeishuStatus()
185
+ ]);
186
+ setStatus(statusInfo);
187
+ setHistory(records);
188
+ setFeishu(feishuInfo);
189
+ setLoadError(statusInfo === null && records === null);
190
+ }, []);
191
+ (0, react.useEffect)(() => {
192
+ refresh();
193
+ const timer = setInterval(() => void refresh(), 5e3);
194
+ return () => clearInterval(timer);
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
+ ]);
212
+ const runTest = async (execute) => {
213
+ const result = await postTest({
214
+ event,
215
+ reason: event === "turn/end" && reason !== "" ? reason : void 0,
216
+ tool: tool !== "" ? tool : void 0,
217
+ execute
218
+ });
219
+ setTestResult(result);
220
+ if (execute) refresh();
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);
281
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
282
+ className: "dh-card",
283
+ children: [
284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
285
+ className: "dh-card-head",
286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
287
+ className: "dh-card-title",
288
+ children: "dsh-hooks"
289
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
290
+ className: "dh-badges",
291
+ children: status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
293
+ className: "dh-badge",
294
+ children: ["v", status.version]
295
+ }),
296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
297
+ className: "dh-badge",
298
+ children: [status.hookCount, " hooks"]
299
+ }),
300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
301
+ className: "dh-badge",
302
+ children: [status.historyCount, " 记录"]
303
+ })
304
+ ] })
305
+ })]
306
+ }),
307
+ loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
308
+ className: "dh-error-banner",
309
+ children: "无法访问 /dsh-hooks/* 路由:请确认 dsh-hooks 核心插件已安装且 dsh web 已重启。"
310
+ }),
311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
312
+ className: "dh-section-title",
313
+ children: "手动测试"
314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
315
+ className: "dh-test-form",
316
+ children: [
317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
318
+ className: "dh-test-row",
319
+ children: [
320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
321
+ className: "dh-field",
322
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
323
+ className: "dh-field-label",
324
+ children: "事件"
325
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
326
+ className: "dh-select",
327
+ value: event,
328
+ onChange: (e) => setEvent(e.target.value),
329
+ children: EVENTS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
330
+ value: name,
331
+ children: name
332
+ }, name))
333
+ })]
334
+ }),
335
+ event === "turn/end" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
336
+ className: "dh-field",
337
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
338
+ className: "dh-field-label",
339
+ children: "reason"
340
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
341
+ className: "dh-input",
342
+ value: reason,
343
+ onChange: (e) => setReason(e.target.value),
344
+ placeholder: "completed"
345
+ })]
346
+ }),
347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
348
+ className: "dh-field",
349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
350
+ className: "dh-field-label",
351
+ children: "tool(可选)"
352
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
353
+ className: "dh-input",
354
+ value: tool,
355
+ onChange: (e) => setTool(e.target.value),
356
+ placeholder: "pwsh"
357
+ })]
358
+ })
359
+ ]
360
+ }),
361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
362
+ className: "dh-buttons",
363
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
364
+ type: "button",
365
+ className: "dh-button",
366
+ onClick: () => void runTest(false),
367
+ children: "模拟(看匹配)"
368
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
369
+ type: "button",
370
+ className: "dh-button dh-button-primary",
371
+ onClick: () => void runTest(true),
372
+ children: "执行(真实触发)"
373
+ })]
374
+ }),
375
+ testResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
376
+ className: "dh-test-results",
377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
378
+ className: "dh-test-line",
379
+ children: [
380
+ testResult.event,
381
+ ":",
382
+ testResult.matched,
383
+ "/",
384
+ testResult.total,
385
+ " 个 hook 触发",
386
+ testResult.executed ? "(已执行)" : ""
387
+ ]
388
+ }, "head"), testResult.lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
389
+ className: `dh-test-line ${line.matched ? "dh-test-line-match" : "dh-test-line-skip"}`,
390
+ children: [
391
+ line.matched ? "✅" : "⏭",
392
+ " [",
393
+ line.index,
394
+ "] ",
395
+ line.summary,
396
+ !line.matched && line.why !== "" ? ` —— ${line.why}` : ""
397
+ ]
398
+ }, line.index))]
399
+ })
400
+ ]
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
+ }))] })
617
+ ]
618
+ });
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
+ }
625
+ function outcomeClass(outcome) {
626
+ switch (outcomeTone(outcome)) {
627
+ case "ok": return "dh-outcome-ok";
628
+ case "bad": return "dh-outcome-bad";
629
+ case "warn": return "dh-outcome-warn";
630
+ default: return "dh-outcome-neutral";
631
+ }
632
+ }
633
+ //#endregion
634
+ //#region src/client/settings-card.module.css?inline
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";
636
+ //#endregion
637
+ //#region src/client/index.ts
638
+ const name = "dsh-hooks";
639
+ /** Required services: the slot registry must be up before this plugin applies. */
640
+ const inject = ["slots"];
641
+ const STYLE_ID = "dsh-hooks-ui-style";
642
+ /** Single-application guard: first apply wins; later calls become no-ops. */
643
+ let applied = false;
644
+ function apply(ctx) {
645
+ if (typeof document === "undefined") return;
646
+ if (applied) return;
647
+ applied = true;
648
+ injectCardStyle();
649
+ try {
650
+ ctx.slots.inject("settings.section", () => {
651
+ const unregister = ctx.slots.register({
652
+ name: "settings.section",
653
+ id: "dsh-hooks",
654
+ order: 100,
655
+ label: "Hooks"
656
+ }, HooksSettingsCard);
657
+ return () => {
658
+ unregister();
659
+ };
660
+ });
661
+ } catch (error) {
662
+ console.error("[dsh-hooks-ui] slot registration failed:", error);
663
+ }
664
+ ctx.effect(() => () => {
665
+ applied = false;
666
+ document.getElementById(STYLE_ID)?.remove();
667
+ }, "dsh-hooks-ui: card");
668
+ }
669
+ /** Inject the card stylesheet once (bundled as a string via .css?inline). */
670
+ function injectCardStyle() {
671
+ if (document.getElementById(STYLE_ID) !== null) return;
672
+ const style = document.createElement("style");
673
+ style.id = STYLE_ID;
674
+ style.textContent = settings_card_module_default;
675
+ document.head.appendChild(style);
676
+ }
677
+ //#endregion
678
+ exports.apply = apply;
679
+ exports.inject = inject;
680
+ exports.name = name;
681
+ return module.exports;
682
+ }
683
+ });