dsh-hooks 0.5.0 → 0.7.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
@@ -100,6 +100,60 @@ window.__ModuleLoader__.load({
100
100
  async function postFeishuTest(fetchFn = fetch) {
101
101
  return postFeishu("/dsh-hooks/feishu/test", {}, fetchFn);
102
102
  }
103
+ /** POST a JSON action; returns the envelope value plus its error message. */
104
+ async function postValue(path, body, fetchFn) {
105
+ try {
106
+ const response = await fetchFn(path, {
107
+ method: "POST",
108
+ headers: {
109
+ "content-type": "application/json",
110
+ accept: "application/json"
111
+ },
112
+ body: JSON.stringify(body)
113
+ });
114
+ const envelope = await response.json();
115
+ if (!response.ok || !envelope.ok) return {
116
+ ok: false,
117
+ error: envelope.error?.message ?? `HTTP ${response.status}`
118
+ };
119
+ const value = envelope.value ?? {};
120
+ return {
121
+ ok: true,
122
+ message: typeof value.message === "string" ? value.message : void 0,
123
+ preview: typeof value.preview === "string" ? value.preview : void 0,
124
+ hookCount: typeof value.hookCount === "number" ? value.hookCount : void 0,
125
+ patchFile: typeof value.patchFile === "string" ? value.patchFile : void 0,
126
+ backupPath: typeof value.backupPath === "string" ? value.backupPath : void 0,
127
+ disconnected: typeof value.disconnected === "boolean" ? value.disconnected : void 0,
128
+ existed: typeof value.existed === "boolean" ? value.existed : void 0,
129
+ removedHooks: typeof value.removedHooks === "boolean" ? value.removedHooks : void 0
130
+ };
131
+ } catch (error) {
132
+ console.warn(`[dsh-hooks-ui] POST ${path} failed: ${error instanceof Error ? error.message : String(error)}`);
133
+ return {
134
+ ok: false,
135
+ error: "网络请求失败"
136
+ };
137
+ }
138
+ }
139
+ async function postNotifyTest(channel, url, slack, fetchFn = fetch) {
140
+ const body = { channel };
141
+ if (url !== void 0 && url !== "") body.url = url;
142
+ if (slack) body.slack = true;
143
+ return postValue("/dsh-hooks/notify/test", body, fetchFn);
144
+ }
145
+ async function postHooksSave(profile, hooks, fetchFn = fetch) {
146
+ return postValue("/dsh-hooks/hooks/save", {
147
+ profile,
148
+ hooks
149
+ }, fetchFn);
150
+ }
151
+ async function postFeishuDisconnect(profile, removeHooks, fetchFn = fetch) {
152
+ return postValue("/dsh-hooks/feishu/disconnect", {
153
+ profile,
154
+ removeHooks
155
+ }, fetchFn);
156
+ }
103
157
  /** `HH:MM:SS` local time for a timestamp. */
104
158
  function formatTime(ts) {
105
159
  const date = new Date(ts);
@@ -134,12 +188,14 @@ window.__ModuleLoader__.load({
134
188
  //#endregion
135
189
  //#region src/client/settings-card.tsx
136
190
  /**
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
191
+ * The dsh-hooks settings card: status badges (incl. live runner stats), a
192
+ * manual event tester, notify-channel quick tests, the Feishu connect flow
193
+ * (QR scan + truncation editor with preview + test card + disconnect), the
194
+ * hook list / editor (saves back to cordis.patch.yml with a backup), and a
139
195
  * collapsed-by-default execution-history timeline at the bottom — all
140
196
  * 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.
197
+ * fetch failures show an inline notice with a retry button, never a crash.
198
+ * Registered into the shell's `settings.section` slot.
143
199
  */
144
200
  const EVENTS = [
145
201
  "turn/start",
@@ -157,19 +213,108 @@ window.__ModuleLoader__.load({
157
213
  "agent/error",
158
214
  "agent/status"
159
215
  ];
216
+ const TURN_END_REASONS = [
217
+ "completed",
218
+ "error",
219
+ "aborted",
220
+ "blocked",
221
+ "max-tokens",
222
+ "interrupted"
223
+ ];
160
224
  const DEFAULT_TRUNCATE = 300;
225
+ function loadStored(key, fallback) {
226
+ try {
227
+ return localStorage.getItem(key) ?? fallback;
228
+ } catch {
229
+ return fallback;
230
+ }
231
+ }
232
+ function storeValue(key, value) {
233
+ try {
234
+ localStorage.setItem(key, value);
235
+ } catch {}
236
+ }
237
+ /** Serialize one hook as a cordis.patch.yml snippet for copy-paste. */
238
+ function hookToYaml(hook) {
239
+ const q = (s) => `'${s.replace(/'/g, "''")}'`;
240
+ const lines = [`- on: ${q(hook.on)}`];
241
+ if (hook.when !== void 0 && hook.when !== "") lines.push(` when: ${q(hook.when)}`);
242
+ if (hook.match !== void 0 && Object.keys(hook.match).length > 0) {
243
+ lines.push(" match:");
244
+ for (const [field, pattern] of Object.entries(hook.match)) lines.push(` ${field}: ${q(pattern)}`);
245
+ }
246
+ if (hook.notify !== void 0 && hook.notify !== null) {
247
+ lines.push(" notify:");
248
+ lines.push(` channel: ${q(hook.notify.channel)}`);
249
+ if (hook.notify.url !== void 0 && hook.notify.url !== "") lines.push(` url: ${q(hook.notify.url)}`);
250
+ if (hook.notify.slack === true) lines.push(" slack: true");
251
+ } else if (hook.run !== void 0 && hook.run !== "") lines.push(` run: ${q(hook.run)}`);
252
+ if (hook.timeoutMs !== void 0 && hook.timeoutMs !== 1e4) lines.push(` timeoutMs: ${hook.timeoutMs}`);
253
+ if (hook.retries !== void 0 && hook.retries !== 0) lines.push(` retries: ${hook.retries}`);
254
+ if (hook.retryDelayMs !== void 0 && hook.retryDelayMs !== 500) lines.push(` retryDelayMs: ${hook.retryDelayMs}`);
255
+ return lines.join("\n");
256
+ }
257
+ async function copyText(text) {
258
+ try {
259
+ await navigator.clipboard.writeText(text);
260
+ return true;
261
+ } catch {
262
+ try {
263
+ const area = document.createElement("textarea");
264
+ area.value = text;
265
+ document.body.appendChild(area);
266
+ area.select();
267
+ const ok = document.execCommand("copy");
268
+ document.body.removeChild(area);
269
+ return ok;
270
+ } catch {
271
+ return false;
272
+ }
273
+ }
274
+ }
275
+ function parseNum(value) {
276
+ const parsed = Number(value);
277
+ return Number.isFinite(parsed) ? parsed : void 0;
278
+ }
279
+ function toWire(draft) {
280
+ return {
281
+ on: draft.on,
282
+ when: draft.when === "" ? void 0 : draft.when,
283
+ match: draft.match !== void 0 && Object.keys(draft.match).length > 0 ? draft.match : void 0,
284
+ run: draft.notify === void 0 || draft.notify === null ? draft.run?.trim() || void 0 : void 0,
285
+ notify: draft.notify ?? null,
286
+ timeoutMs: draft.timeoutMs,
287
+ retries: draft.retries,
288
+ retryDelayMs: draft.retryDelayMs
289
+ };
290
+ }
291
+ const NEW_HOOK = {
292
+ on: "turn/end",
293
+ when: "completed",
294
+ run: ""
295
+ };
161
296
  /** Settings-slot component; the shell's slot machinery supplies the props. */
162
297
  function HooksSettingsCard(_props) {
163
298
  const [status, setStatus] = (0, react.useState)(null);
164
299
  const [history, setHistory] = (0, react.useState)(null);
165
- const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
300
+ const [historyOpen, setHistoryOpenState] = (0, react.useState)(() => {
301
+ try {
302
+ return localStorage.getItem("dsh-hooks.historyOpen") === "1";
303
+ } catch {
304
+ return false;
305
+ }
306
+ });
166
307
  const [loadError, setLoadError] = (0, react.useState)(false);
167
308
  const [event, setEvent] = (0, react.useState)("turn/end");
168
309
  const [reason, setReason] = (0, react.useState)("completed");
169
310
  const [tool, setTool] = (0, react.useState)("");
170
311
  const [testResult, setTestResult] = (0, react.useState)(null);
312
+ const [notifyChannel, setNotifyChannel] = (0, react.useState)("webhook");
313
+ const [notifyUrl, setNotifyUrl] = (0, react.useState)("");
314
+ const [notifySlack, setNotifySlack] = (0, react.useState)(false);
315
+ const [notifyResult, setNotifyResult] = (0, react.useState)(null);
171
316
  const [feishu, setFeishu] = (0, react.useState)(null);
172
- const [profile, setProfile] = (0, react.useState)("web");
317
+ const [profile, setProfile] = (0, react.useState)(() => loadStored("dsh-hooks.profile", "web"));
173
318
  const [setupTruncate, setSetupTruncate] = (0, react.useState)(String(DEFAULT_TRUNCATE));
174
319
  const [truncateDraft, setTruncateDraft] = (0, react.useState)(null);
175
320
  const [configMessage, setConfigMessage] = (0, react.useState)(null);
@@ -177,6 +322,14 @@ window.__ModuleLoader__.load({
177
322
  const [feishuError, setFeishuError] = (0, react.useState)(null);
178
323
  const [testMessage, setTestMessage] = (0, react.useState)(null);
179
324
  const [countdown, setCountdown] = (0, react.useState)(null);
325
+ const [editing, setEditing] = (0, react.useState)(false);
326
+ const [draftHooks, setDraftHooks] = (0, react.useState)([]);
327
+ const [saveMessage, setSaveMessage] = (0, react.useState)(null);
328
+ const [copiedIndex, setCopiedIndex] = (0, react.useState)(null);
329
+ const setHistoryOpen = (open) => {
330
+ setHistoryOpenState(open);
331
+ storeValue("dsh-hooks.historyOpen", open ? "1" : "0");
332
+ };
180
333
  const refresh = (0, react.useCallback)(async () => {
181
334
  const [statusInfo, records, feishuInfo] = await Promise.all([
182
335
  fetchStatus(),
@@ -209,6 +362,13 @@ window.__ModuleLoader__.load({
209
362
  feishu?.setup?.expiresAtMs,
210
363
  refresh
211
364
  ]);
365
+ (0, react.useEffect)(() => {
366
+ setTestResult(null);
367
+ }, [
368
+ event,
369
+ reason,
370
+ tool
371
+ ]);
212
372
  const runTest = async (execute) => {
213
373
  const result = await postTest({
214
374
  event,
@@ -219,6 +379,19 @@ window.__ModuleLoader__.load({
219
379
  setTestResult(result);
220
380
  if (execute) refresh();
221
381
  };
382
+ const sendNotifyTest = async () => {
383
+ setNotifyResult(null);
384
+ const result = await postNotifyTest(notifyChannel, notifyUrl.trim() || void 0, notifySlack);
385
+ if (!result.ok) setNotifyResult({
386
+ ok: false,
387
+ text: result.error ?? "发送失败"
388
+ });
389
+ else setNotifyResult({
390
+ ok: true,
391
+ text: result.message ?? "已发送",
392
+ preview: result.preview
393
+ });
394
+ };
222
395
  const connectFeishu = async () => {
223
396
  setFeishuError(null);
224
397
  setTestMessage(null);
@@ -237,7 +410,8 @@ window.__ModuleLoader__.load({
237
410
  targetKind: null,
238
411
  target: null,
239
412
  setup: result.setup,
240
- resultMaxChars: Number.isFinite(parsed) ? parsed : DEFAULT_TRUNCATE
413
+ resultMaxChars: Number.isFinite(parsed) ? parsed : DEFAULT_TRUNCATE,
414
+ preview: ""
241
415
  });
242
416
  setCountdown(remainingSeconds(result.setup.expiresAtMs));
243
417
  }
@@ -277,6 +451,145 @@ window.__ModuleLoader__.load({
277
451
  });
278
452
  refresh();
279
453
  };
454
+ const disconnectFeishu = async () => {
455
+ if (!window.confirm("确定断开飞书连接?将删除本机凭据文件。")) return;
456
+ const removeHooks = window.confirm("同时从 cordis.patch.yml 移除飞书通知 hooks?\n(推荐移除,否则这些 hook 每次触发都会失败)");
457
+ setFeishuError(null);
458
+ const result = await postFeishuDisconnect(profile.trim() !== "" ? profile.trim() : "web", removeHooks);
459
+ if (!result.ok) setFeishuError(result.error ?? "断开失败");
460
+ else setConfigMessage({
461
+ ok: true,
462
+ text: result.message ?? "已断开"
463
+ });
464
+ refresh();
465
+ };
466
+ const startEdit = () => {
467
+ if (editing) {
468
+ setEditing(false);
469
+ setDraftHooks([]);
470
+ setSaveMessage(null);
471
+ return;
472
+ }
473
+ const current = status?.hooks ?? [];
474
+ setDraftHooks(current.map((hook) => ({
475
+ on: hook.on,
476
+ when: hook.when,
477
+ match: hook.match !== void 0 ? { ...hook.match } : void 0,
478
+ run: hook.run,
479
+ notify: hook.notify === void 0 ? null : {
480
+ channel: hook.notify.channel,
481
+ url: hook.notify.url,
482
+ slack: hook.notify.slack
483
+ },
484
+ timeoutMs: hook.timeoutMs,
485
+ retries: hook.retries,
486
+ retryDelayMs: hook.retryDelayMs
487
+ })));
488
+ setSaveMessage(null);
489
+ setEditing(true);
490
+ };
491
+ const patchDraft = (index, patch) => {
492
+ setDraftHooks((list) => list.map((hook, i) => i === index ? {
493
+ ...hook,
494
+ ...patch
495
+ } : hook));
496
+ };
497
+ const patchAction = (index, channel) => {
498
+ setDraftHooks((list) => list.map((hook, i) => {
499
+ if (i !== index) return hook;
500
+ if (channel === "") return {
501
+ ...hook,
502
+ notify: null,
503
+ run: hook.run ?? ""
504
+ };
505
+ return {
506
+ ...hook,
507
+ notify: {
508
+ channel,
509
+ url: hook.notify?.url,
510
+ slack: hook.notify?.slack
511
+ },
512
+ run: void 0
513
+ };
514
+ }));
515
+ };
516
+ const patchMatchKey = (index, oldKey, newKey) => {
517
+ setDraftHooks((list) => list.map((hook, i) => {
518
+ if (i !== index || hook.match === void 0) return hook;
519
+ const match = { ...hook.match };
520
+ if (oldKey !== newKey) {
521
+ delete match[oldKey];
522
+ if (newKey !== "") match[newKey] = hook.match[oldKey];
523
+ }
524
+ return {
525
+ ...hook,
526
+ match
527
+ };
528
+ }));
529
+ };
530
+ const patchMatchValue = (index, field, value) => {
531
+ setDraftHooks((list) => list.map((hook, i) => i === index ? {
532
+ ...hook,
533
+ match: {
534
+ ...hook.match ?? {},
535
+ [field]: value
536
+ }
537
+ } : hook));
538
+ };
539
+ const removeMatch = (index, field) => {
540
+ setDraftHooks((list) => list.map((hook, i) => {
541
+ if (i !== index || hook.match === void 0) return hook;
542
+ const match = { ...hook.match };
543
+ delete match[field];
544
+ return {
545
+ ...hook,
546
+ match
547
+ };
548
+ }));
549
+ };
550
+ const addMatch = (index) => {
551
+ setDraftHooks((list) => list.map((hook, i) => i === index ? {
552
+ ...hook,
553
+ match: {
554
+ ...hook.match ?? {},
555
+ "": ""
556
+ }
557
+ } : hook));
558
+ };
559
+ const removeDraft = (index) => {
560
+ setDraftHooks((list) => list.filter((_, i) => i !== index));
561
+ };
562
+ const saveHooks = async () => {
563
+ const wire = draftHooks.map(toWire);
564
+ for (const [i, hook] of wire.entries()) if ((hook.run === void 0 || hook.run === "") && (hook.notify === void 0 || hook.notify === null)) {
565
+ setSaveMessage({
566
+ ok: false,
567
+ text: `hook #${i + 1}:请填写 run 命令或选择通知渠道`
568
+ });
569
+ return;
570
+ }
571
+ const result = await postHooksSave(profile.trim() !== "" ? profile.trim() : "web", wire);
572
+ if (!result.ok) {
573
+ setSaveMessage({
574
+ ok: false,
575
+ text: result.error ?? "保存失败"
576
+ });
577
+ return;
578
+ }
579
+ setSaveMessage({
580
+ ok: true,
581
+ text: result.message ?? "已保存"
582
+ });
583
+ setEditing(false);
584
+ setDraftHooks([]);
585
+ refresh();
586
+ };
587
+ const copyHookYaml = async (hook, index) => {
588
+ if (await copyText(hookToYaml(hook))) {
589
+ setCopiedIndex(index);
590
+ setTimeout(() => setCopiedIndex((current) => current === index ? null : current), 1500);
591
+ }
592
+ };
280
593
  const truncateValue = truncateDraft ?? String(feishu?.resultMaxChars ?? DEFAULT_TRUNCATE);
281
594
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
282
595
  className: "dh-card",
@@ -300,13 +613,29 @@ window.__ModuleLoader__.load({
300
613
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
301
614
  className: "dh-badge",
302
615
  children: [status.historyCount, " 记录"]
616
+ }),
617
+ status.stats.inFlight > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
618
+ className: "dh-badge",
619
+ children: [status.stats.inFlight, " 运行中"]
620
+ }),
621
+ status.stats.recentFailures > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
622
+ className: "dh-badge dh-badge-bad",
623
+ children: [status.stats.recentFailures, " 失败"]
303
624
  })
304
625
  ] })
305
626
  })]
306
627
  }),
307
- loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
628
+ loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
308
629
  className: "dh-error-banner",
309
- children: "无法访问 /dsh-hooks/* 路由:请确认 dsh-hooks 核心插件已安装且 dsh web 已重启。"
630
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "无法访问 /dsh-hooks/* 路由:请确认 dsh-hooks 核心插件已安装且 dsh web 已重启。" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
631
+ type: "button",
632
+ className: "dh-button dh-error-retry",
633
+ onClick: () => {
634
+ setLoadError(false);
635
+ refresh();
636
+ },
637
+ children: "重试"
638
+ })]
310
639
  }),
311
640
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
312
641
  className: "dh-section-title",
@@ -399,6 +728,70 @@ window.__ModuleLoader__.load({
399
728
  })
400
729
  ]
401
730
  })] }),
731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
732
+ className: "dh-section-title",
733
+ children: "通知渠道测试"
734
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
735
+ className: "dh-test-form",
736
+ children: [
737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
738
+ className: "dh-test-row",
739
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
740
+ className: "dh-field",
741
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
742
+ className: "dh-field-label",
743
+ children: "渠道"
744
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
745
+ className: "dh-select",
746
+ value: notifyChannel,
747
+ onChange: (e) => setNotifyChannel(e.target.value),
748
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
749
+ value: "webhook",
750
+ children: "webhook(HTTP JSON)"
751
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
752
+ value: "desktop",
753
+ children: "desktop(系统通知)"
754
+ })]
755
+ })]
756
+ }), notifyChannel === "webhook" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
757
+ className: "dh-field",
758
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
759
+ className: "dh-field-label",
760
+ children: "URL(留空用 DSH_HOOKS_WEBHOOK_URL)"
761
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
762
+ className: "dh-input",
763
+ value: notifyUrl,
764
+ onChange: (e) => setNotifyUrl(e.target.value),
765
+ placeholder: "https://hooks.slack.com/…"
766
+ })]
767
+ })]
768
+ }),
769
+ notifyChannel === "webhook" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
770
+ className: "dh-check",
771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
772
+ type: "checkbox",
773
+ checked: notifySlack,
774
+ onChange: (e) => setNotifySlack(e.target.checked)
775
+ }), "Slack 风格单行摘要"]
776
+ }),
777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
778
+ className: "dh-buttons",
779
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
780
+ type: "button",
781
+ className: "dh-button dh-button-primary",
782
+ onClick: () => void sendNotifyTest(),
783
+ children: "发送测试通知"
784
+ })
785
+ }),
786
+ notifyResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
787
+ className: notifyResult.ok ? "dh-test-line dh-test-line-match" : "dh-feishu-error",
788
+ children: notifyResult.text
789
+ }), notifyResult.ok && notifyResult.preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
790
+ className: "dh-feishu-hint",
791
+ children: ["发送内容:", notifyResult.preview]
792
+ })] })
793
+ ]
794
+ })] }),
402
795
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
403
796
  className: "dh-section-title",
404
797
  children: "飞书通知"
@@ -465,23 +858,37 @@ window.__ModuleLoader__.load({
465
858
  children: "保存"
466
859
  })]
467
860
  }),
861
+ feishu.preview !== void 0 && feishu.preview !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
862
+ className: "dh-feishu-preview",
863
+ title: "按当前截断长度生成的卡片正文预览",
864
+ children: ["卡片预览:", feishu.preview]
865
+ }),
468
866
  configMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
469
867
  className: configMessage.ok ? "dh-feishu-line dh-feishu-ok" : "dh-feishu-error",
470
868
  children: configMessage.text
471
869
  }),
472
870
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
473
871
  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
- })]
872
+ children: [
873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
874
+ type: "button",
875
+ className: "dh-button dh-button-primary",
876
+ onClick: () => void sendTestCard(),
877
+ children: "发送测试卡片"
878
+ }),
879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
880
+ type: "button",
881
+ className: "dh-button",
882
+ onClick: () => setReconnecting(true),
883
+ children: "重新连接"
884
+ }),
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
886
+ type: "button",
887
+ className: "dh-button dh-button-danger",
888
+ onClick: () => void disconnectFeishu(),
889
+ children: "断开连接"
890
+ })
891
+ ]
485
892
  }),
486
893
  testMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
487
894
  className: "dh-feishu-line dh-feishu-ok",
@@ -519,7 +926,10 @@ window.__ModuleLoader__.load({
519
926
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
520
927
  className: "dh-input",
521
928
  value: profile,
522
- onChange: (e) => setProfile(e.target.value),
929
+ onChange: (e) => {
930
+ setProfile(e.target.value);
931
+ storeValue("dsh-hooks.profile", e.target.value);
932
+ },
523
933
  placeholder: "web"
524
934
  })]
525
935
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -562,6 +972,300 @@ window.__ModuleLoader__.load({
562
972
  ]
563
973
  })
564
974
  })] }),
975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
976
+ className: "dh-section-head",
977
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
978
+ className: "dh-section-title",
979
+ children: ["当前 hooks", status !== null ? ` · ${status.hookCount}` : ""]
980
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
981
+ type: "button",
982
+ className: "dh-button dh-toggle",
983
+ onClick: startEdit,
984
+ children: editing ? "取消编辑" : "编辑"
985
+ })]
986
+ }), editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
987
+ className: "dh-hook-editor",
988
+ children: [
989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
990
+ className: "dh-test-row",
991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
992
+ className: "dh-field dh-field-narrow",
993
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
994
+ className: "dh-field-label",
995
+ children: "profile(写入哪个 cordis.patch.yml)"
996
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
997
+ className: "dh-input",
998
+ value: profile,
999
+ onChange: (e) => {
1000
+ setProfile(e.target.value);
1001
+ storeValue("dsh-hooks.profile", e.target.value);
1002
+ },
1003
+ placeholder: "web"
1004
+ })]
1005
+ })
1006
+ }),
1007
+ draftHooks.map((hook, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1008
+ className: "dh-hook dh-hook-editing",
1009
+ children: [
1010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1011
+ className: "dh-test-row",
1012
+ children: [
1013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1014
+ className: "dh-field",
1015
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ className: "dh-field-label",
1017
+ children: "事件"
1018
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1019
+ className: "dh-select",
1020
+ value: hook.on,
1021
+ onChange: (e) => patchDraft(index, {
1022
+ on: e.target.value,
1023
+ when: e.target.value === "turn/end" ? hook.when : void 0
1024
+ }),
1025
+ children: EVENTS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1026
+ value: name,
1027
+ children: name
1028
+ }, name))
1029
+ })]
1030
+ }),
1031
+ hook.on === "turn/end" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1032
+ className: "dh-field",
1033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1034
+ className: "dh-field-label",
1035
+ children: "when(可选)"
1036
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1037
+ className: "dh-select",
1038
+ value: hook.when ?? "",
1039
+ onChange: (e) => patchDraft(index, { when: e.target.value === "" ? void 0 : e.target.value }),
1040
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1041
+ value: "",
1042
+ children: "全部原因"
1043
+ }), TURN_END_REASONS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1044
+ value: name,
1045
+ children: name
1046
+ }, name))]
1047
+ })]
1048
+ }),
1049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1050
+ className: "dh-field",
1051
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1052
+ className: "dh-field-label",
1053
+ children: "动作"
1054
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1055
+ className: "dh-select",
1056
+ value: hook.notify !== void 0 && hook.notify !== null ? hook.notify.channel : "",
1057
+ onChange: (e) => patchAction(index, e.target.value),
1058
+ children: [
1059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1060
+ value: "",
1061
+ children: "执行命令(run)"
1062
+ }),
1063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1064
+ value: "webhook",
1065
+ children: "通知 webhook"
1066
+ }),
1067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1068
+ value: "desktop",
1069
+ children: "通知 desktop"
1070
+ })
1071
+ ]
1072
+ })]
1073
+ })
1074
+ ]
1075
+ }),
1076
+ hook.notify !== void 0 && hook.notify !== null ? hook.notify.channel === "webhook" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1077
+ className: "dh-test-row",
1078
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1079
+ className: "dh-field",
1080
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1081
+ className: "dh-field-label",
1082
+ children: "URL(留空用 DSH_HOOKS_WEBHOOK_URL)"
1083
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1084
+ className: "dh-input",
1085
+ value: hook.notify.url ?? "",
1086
+ onChange: (e) => patchDraft(index, { notify: {
1087
+ ...hook.notify,
1088
+ url: e.target.value
1089
+ } }),
1090
+ placeholder: "https://hooks.slack.com/…"
1091
+ })]
1092
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1093
+ className: "dh-check",
1094
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1095
+ type: "checkbox",
1096
+ checked: hook.notify.slack === true,
1097
+ onChange: (e) => patchDraft(index, { notify: {
1098
+ ...hook.notify,
1099
+ slack: e.target.checked
1100
+ } })
1101
+ }), "Slack 单行摘要"]
1102
+ })]
1103
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1104
+ className: "dh-feishu-hint",
1105
+ children: "desktop:弹出系统桌面通知(内容为事件摘要)。"
1106
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1107
+ className: "dh-input",
1108
+ value: hook.run ?? "",
1109
+ onChange: (e) => patchDraft(index, { run: e.target.value }),
1110
+ placeholder: "node notify-feishu.mjs"
1111
+ }),
1112
+ Object.entries(hook.match ?? {}).map(([field, pattern], mi) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ className: "dh-test-row",
1114
+ children: [
1115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1116
+ className: "dh-input dh-match-key",
1117
+ value: field,
1118
+ onChange: (e) => patchMatchKey(index, field, e.target.value),
1119
+ placeholder: "字段(tool)"
1120
+ }),
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1122
+ className: "dh-input",
1123
+ value: pattern,
1124
+ onChange: (e) => patchMatchValue(index, field, e.target.value),
1125
+ placeholder: "正则(^(rm|git|ssh))"
1126
+ }),
1127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1128
+ type: "button",
1129
+ className: "dh-button",
1130
+ onClick: () => removeMatch(index, field),
1131
+ children: "×"
1132
+ })
1133
+ ]
1134
+ }, mi)),
1135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1136
+ className: "dh-test-row",
1137
+ children: [
1138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1139
+ type: "button",
1140
+ className: "dh-button dh-button-small",
1141
+ onClick: () => addMatch(index),
1142
+ children: "+ 添加匹配字段"
1143
+ }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1145
+ className: "dh-field",
1146
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1147
+ className: "dh-field-label",
1148
+ children: "timeoutMs"
1149
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1150
+ className: "dh-input",
1151
+ type: "number",
1152
+ value: hook.timeoutMs ?? "",
1153
+ onChange: (e) => patchDraft(index, { timeoutMs: parseNum(e.target.value) }),
1154
+ placeholder: "10000"
1155
+ })]
1156
+ }),
1157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1158
+ className: "dh-field",
1159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1160
+ className: "dh-field-label",
1161
+ children: "retries"
1162
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1163
+ className: "dh-input",
1164
+ type: "number",
1165
+ value: hook.retries ?? "",
1166
+ onChange: (e) => patchDraft(index, { retries: parseNum(e.target.value) }),
1167
+ placeholder: "0"
1168
+ })]
1169
+ }),
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1171
+ className: "dh-field",
1172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1173
+ className: "dh-field-label",
1174
+ children: "retryDelayMs"
1175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1176
+ className: "dh-input",
1177
+ type: "number",
1178
+ value: hook.retryDelayMs ?? "",
1179
+ onChange: (e) => patchDraft(index, { retryDelayMs: parseNum(e.target.value) }),
1180
+ placeholder: "500"
1181
+ })]
1182
+ }),
1183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1184
+ type: "button",
1185
+ className: "dh-button dh-button-danger",
1186
+ onClick: () => removeDraft(index),
1187
+ children: "删除"
1188
+ })
1189
+ ]
1190
+ })
1191
+ ]
1192
+ }, index)),
1193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1194
+ className: "dh-buttons",
1195
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1196
+ type: "button",
1197
+ className: "dh-button",
1198
+ onClick: () => setDraftHooks((list) => [...list, { ...NEW_HOOK }]),
1199
+ children: "+ 新增 hook"
1200
+ })
1201
+ }),
1202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1203
+ className: "dh-buttons",
1204
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1205
+ type: "button",
1206
+ className: "dh-button dh-button-primary",
1207
+ onClick: () => void saveHooks(),
1208
+ children: "保存到 cordis.patch.yml"
1209
+ })
1210
+ }),
1211
+ saveMessage !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1212
+ className: saveMessage.ok ? "dh-test-line dh-test-line-match" : "dh-feishu-error",
1213
+ children: saveMessage.text
1214
+ }),
1215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1216
+ className: "dh-feishu-hint",
1217
+ children: "保存后写回配置文件并自动备份原文件;如未立即生效请重启 dsh web。"
1218
+ })
1219
+ ]
1220
+ }) : status === null || status.hooks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1221
+ className: "dh-empty",
1222
+ children: status === null ? "加载中…" : "暂无 hook,点「编辑」添加"
1223
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1224
+ className: "dh-hook-list",
1225
+ children: status.hooks.map((hook) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1226
+ className: "dh-hook",
1227
+ children: [
1228
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1229
+ className: "dh-hook-head",
1230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1231
+ className: "dh-hook-event",
1232
+ children: [
1233
+ "[",
1234
+ hook.index,
1235
+ "] ",
1236
+ hook.on,
1237
+ hook.when !== void 0 && hook.when !== "" ? ` · ${hook.when}` : ""
1238
+ ]
1239
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1240
+ className: "dh-hook-action",
1241
+ title: hook.run ?? `notify:${hook.notify?.channel}`,
1242
+ children: hook.run !== void 0 && hook.run !== "" ? `run: ${hook.run}` : `notify: ${hook.notify?.channel ?? "?"}${hook.notify?.url ? ` ${hook.notify.url}` : ""}${hook.notify?.slack ? " (slack)" : ""}`
1243
+ })]
1244
+ }),
1245
+ hook.match !== void 0 && Object.keys(hook.match).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1246
+ className: "dh-hook-match",
1247
+ children: Object.entries(hook.match).map(([field, pattern]) => `${field} =~ /${pattern}/`).join(",")
1248
+ }),
1249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1250
+ className: "dh-hook-meta",
1251
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1252
+ type: "button",
1253
+ className: "dh-button dh-button-small",
1254
+ onClick: () => void copyHookYaml(hook, hook.index),
1255
+ children: copiedIndex === hook.index ? "已复制 ✓" : "复制 YAML"
1256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1257
+ className: "dh-feishu-hint",
1258
+ children: [
1259
+ "timeout ",
1260
+ hook.timeoutMs ?? 1e4,
1261
+ "ms · retries ",
1262
+ hook.retries ?? 0
1263
+ ]
1264
+ })]
1265
+ })
1266
+ ]
1267
+ }, hook.index))
1268
+ })] }),
565
1269
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
566
1270
  className: "dh-section-head",
567
1271
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
@@ -570,7 +1274,7 @@ window.__ModuleLoader__.load({
570
1274
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
571
1275
  type: "button",
572
1276
  className: "dh-button dh-toggle",
573
- onClick: () => setHistoryOpen((open) => !open),
1277
+ onClick: () => setHistoryOpen(!historyOpen),
574
1278
  "aria-expanded": historyOpen,
575
1279
  children: historyOpen ? "收起 ▲" : "展开 ▼"
576
1280
  })]
@@ -632,7 +1336,7 @@ window.__ModuleLoader__.load({
632
1336
  }
633
1337
  //#endregion
634
1338
  //#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";
1339
+ 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\n.dh-badge-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-error-retry {\n flex-shrink: 0;\n margin-left: 8px;\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-check {\n color: var(--dsh-hooks-muted);\n align-items: center;\n gap: 6px;\n font-size: 12px;\n display: flex;\n}\n\n.dh-button-small {\n padding: 2px 8px;\n font-size: 11px;\n}\n\n.dh-button-danger {\n color: var(--dsh-hooks-bad);\n border-color: #e5534b80;\n}\n\n.dh-button-danger:hover {\n background: #e5534b24;\n}\n\n.dh-hook-list, .dh-hook-editor {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-hook {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n flex-direction: column;\n gap: 4px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-hook-editing {\n border-color: #4d8df766;\n gap: 8px;\n padding: 9px;\n}\n\n.dh-hook-head {\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n display: flex;\n}\n\n.dh-hook-event {\n white-space: nowrap;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-hook-action {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n flex: 1;\n min-width: 0;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-hook-match {\n color: var(--dsh-hooks-accent);\n word-break: break-all;\n font-size: 11px;\n}\n\n.dh-hook-meta {\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dh-match-key {\n flex: 0 0 130px;\n}\n\n.dh-feishu-preview {\n color: var(--dsh-hooks-muted);\n border-left: 2px solid var(--dsh-hooks-border);\n word-break: break-all;\n max-height: 120px;\n padding: 4px 8px;\n font-size: 11px;\n overflow: hidden;\n}\n";
636
1340
  //#endregion
637
1341
  //#region src/client/index.ts
638
1342
  const name = "dsh-hooks";