dsh-session-guard 0.1.2 → 0.2.0-beta.1

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
@@ -6,22 +6,72 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/badge-text.ts
10
+ /** 阶段文案。 */
11
+ const PHASE_LABELS = {
12
+ peak: "高峰",
13
+ "off-peak": "谷时",
14
+ weekend: "周末"
15
+ };
16
+ /** 高峰期的徽标文案:二维判定开启时区分「只拦官方」与「全部暂停」。 */
17
+ function peakLabel(status) {
18
+ if (status.phase !== "peak") return PHASE_LABELS[status.phase];
19
+ return status.providerGuard === true ? "高峰·拦官方" : "高峰·全部暂停";
20
+ }
21
+ /** 悬浮说明:把判定口径与当前挂起/延后数量写清楚。 */
22
+ function badgeTitle(status) {
23
+ const base = `${PHASE_LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? " · 周末模式" : ""}`;
24
+ if (status.phase !== "peak") return base;
25
+ const mode = status.providerGuard === true ? "仅拦截 DeepSeek 官方源" : "全部会话暂停(未启用二维判定)";
26
+ const held = status.held ?? 0;
27
+ const deferred = status.deferred ?? 0;
28
+ const stepHeld = status.stepHeld ?? 0;
29
+ return `${base} · ${mode}${held > 0 || deferred > 0 || stepHeld > 0 ? ` · 挂起 ${held} · 延后 ${deferred} · step 挂起 ${stepHeld}` : ""}`;
30
+ }
31
+ //#endregion
32
+ //#region src/client/styles.ts
33
+ /**
34
+ * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。
35
+ *
36
+ * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线
37
+ * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。
38
+ * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件
39
+ * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。
40
+ */
41
+ /** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */
42
+ const CLIENT_CSS = `
43
+ .sg-pause{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1;cursor:pointer;white-space:nowrap;flex:none}
44
+ .sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}
45
+ .sg-pause:disabled{opacity:.55;cursor:default}
46
+ .sg-pause[aria-pressed='true']{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d97706) 8%,transparent)}
47
+ .sg-status{display:inline-flex;align-items:center;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap;flex:none}
48
+ .sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}
49
+ .sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}
50
+ `;
51
+ /** 注入一次(幂等;无 document 时静默跳过)。 */
52
+ function injectClientCss() {
53
+ if (typeof document === "undefined") return;
54
+ if (document.querySelector("style[data-plugin-css=\"session-guard-client\"]") !== null) return;
55
+ const tag = document.createElement("style");
56
+ tag.dataset.plugin = "dsh-session-guard";
57
+ tag.dataset.pluginCss = "session-guard-client";
58
+ tag.textContent = CLIENT_CSS;
59
+ document.head.appendChild(tag);
60
+ }
61
+ //#endregion
9
62
  //#region src/client/status-badge.tsx
10
63
  /**
11
64
  * dsh-session-guard — 状态徽标(纯展示,fail-open)。
12
65
  *
13
- * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末。
66
+ * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;
67
+ * 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。
14
68
  * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
15
69
  */
16
- const LABELS = {
17
- peak: "高峰",
18
- "off-peak": "谷时",
19
- weekend: "周末"
20
- };
21
- const POLL_MS = 15e3;
70
+ const POLL_MS$1 = 15e3;
22
71
  /** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
23
72
  function StatusBadge({ sessionId }) {
24
73
  const [status, setStatus] = (0, react.useState)(null);
74
+ injectClientCss();
25
75
  (0, react.useEffect)(() => {
26
76
  if (!sessionId) return;
27
77
  let cancelled = false;
@@ -32,7 +82,7 @@ window.__ModuleLoader__.load({
32
82
  } catch {}
33
83
  };
34
84
  poll();
35
- const timer = setInterval(poll, POLL_MS);
85
+ const timer = setInterval(poll, POLL_MS$1);
36
86
  return () => {
37
87
  cancelled = true;
38
88
  clearInterval(timer);
@@ -42,9 +92,11 @@ window.__ModuleLoader__.load({
42
92
  const cls = status.phase === "peak" ? "sg-peak" : status.phase === "weekend" ? "sg-weekend" : "sg-off";
43
93
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44
94
  className: `sg-status ${cls}`,
45
- title: `${LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? " · 周末模式" : ""}`,
95
+ title: badgeTitle(status),
46
96
  "data-sg-phase": status.phase,
47
- children: LABELS[status.phase]
97
+ "data-sg-provider-guard": status.providerGuard === true ? "on" : "off",
98
+ "data-sg-step-held": String(status.stepHeld ?? 0),
99
+ children: peakLabel(status)
48
100
  });
49
101
  }
50
102
  //#endregion
@@ -76,6 +128,7 @@ window.__ModuleLoader__.load({
76
128
  .sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}
77
129
  .sgRow:last-child{border-bottom:0}
78
130
  .sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
131
+ .sgRowText-wide{padding-right:0}
79
132
  .sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
80
133
  .sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
81
134
  .sgSwitch{position:relative;width:40px;height:22px;flex:none}
@@ -85,6 +138,11 @@ window.__ModuleLoader__.load({
85
138
  .sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
86
139
  .sgSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}
87
140
  .sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}
141
+ .sgInput{appearance:none;min-width:0;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;margin-top:8px}
142
+ .sgInput:disabled{opacity:.6;cursor:not-allowed}
143
+ .sgSelect{appearance:none;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;min-width:160px;flex:none}
144
+ .sgSelect:disabled{opacity:.6;cursor:not-allowed}
145
+ .sgHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}
88
146
  .sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}
89
147
  `;
90
148
  /** 注入一次卡片样式。 */
@@ -126,6 +184,102 @@ window.__ModuleLoader__.load({
126
184
  })]
127
185
  });
128
186
  }
187
+ /** 一行文本输入(失焦提交;外部值变化时同步草稿)。 */
188
+ function TextRow(props) {
189
+ const { label, description, value, placeholder, disabled, onCommit } = props;
190
+ const [draft, setDraft] = (0, react.useState)(value);
191
+ (0, react.useEffect)(() => {
192
+ setDraft(value);
193
+ }, [value]);
194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
195
+ className: "sgRow",
196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
197
+ className: "sgRowText sgRowText-wide",
198
+ children: [
199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
200
+ className: "sgTitle",
201
+ children: label
202
+ }),
203
+ description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
204
+ className: "sgDesc",
205
+ children: description
206
+ }),
207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
208
+ className: "sgInput",
209
+ type: "text",
210
+ value: draft,
211
+ placeholder,
212
+ disabled,
213
+ onChange: (e) => setDraft(e.currentTarget.value),
214
+ onBlur: () => onCommit(draft)
215
+ })
216
+ ]
217
+ })
218
+ });
219
+ }
220
+ /** 一行「逗号/换行分隔」的字符串列表(失焦提交为数组)。 */
221
+ function ListRow(props) {
222
+ const { label, description, values, placeholder, disabled, onCommit } = props;
223
+ const joined = values.join(", ");
224
+ const [draft, setDraft] = (0, react.useState)(joined);
225
+ (0, react.useEffect)(() => {
226
+ setDraft(joined);
227
+ }, [joined]);
228
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
229
+ className: "sgRow",
230
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
231
+ className: "sgRowText sgRowText-wide",
232
+ children: [
233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
234
+ className: "sgTitle",
235
+ children: label
236
+ }),
237
+ description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
238
+ className: "sgDesc",
239
+ children: description
240
+ }),
241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
242
+ className: "sgInput",
243
+ type: "text",
244
+ value: draft,
245
+ placeholder,
246
+ disabled,
247
+ onChange: (e) => setDraft(e.currentTarget.value),
248
+ onBlur: () => {
249
+ const next = draft.split(/[,\n]/).map((s) => s.trim()).filter((s) => s !== "");
250
+ onCommit(next);
251
+ }
252
+ })
253
+ ]
254
+ })
255
+ });
256
+ }
257
+ /** 一行下拉选择。 */
258
+ function SelectRow(props) {
259
+ const { label, description, value, options, disabled, onChange } = props;
260
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
261
+ className: "sgRow",
262
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
263
+ className: "sgRowText",
264
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
265
+ className: "sgTitle",
266
+ children: label
267
+ }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
268
+ className: "sgDesc",
269
+ children: description
270
+ })]
271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
272
+ className: "sgSelect",
273
+ value,
274
+ disabled,
275
+ onChange: (e) => onChange(e.currentTarget.value),
276
+ children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
277
+ value: o.value,
278
+ children: o.label
279
+ }, o.value))
280
+ })]
281
+ });
282
+ }
129
283
  /** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */
130
284
  function SessionGuardCard({ scope }) {
131
285
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
@@ -186,6 +340,91 @@ window.__ModuleLoader__.load({
186
340
  disabled: readonly,
187
341
  onChange: (next) => toggle("enabled", next)
188
342
  }),
343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
344
+ label: "step 级门控",
345
+ description: "高峰在下一个 step 的模型请求前拉门(省 token 更彻底);关闭则回退为回合级暂停",
346
+ checked: value.stepLevelPause ?? true,
347
+ disabled: readonly,
348
+ onChange: (next) => toggle("stepLevelPause", next)
349
+ }),
350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextRow, {
351
+ label: "step 门控超时(毫秒)",
352
+ description: "到期释放 step 门并升级为回合级暂停(防死锁);0 或非法值用默认 300000",
353
+ value: String(value.stepGateTimeoutMs ?? 3e5),
354
+ placeholder: "300000",
355
+ disabled: readonly,
356
+ onCommit: (next) => {
357
+ const n = Number(String(next).trim());
358
+ scope.set("stepGateTimeoutMs", Number.isFinite(n) && n > 0 ? n : 3e5);
359
+ }
360
+ }),
361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
362
+ label: "官方源二维判定",
363
+ description: "高峰期只拦 DeepSeek 官方源;本地/第三方 provider 照常跑",
364
+ checked: value.providerGuard ?? true,
365
+ disabled: readonly,
366
+ onChange: (next) => toggle("providerGuard", next)
367
+ }),
368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
369
+ label: "追加官方 provider id",
370
+ description: "精确匹配,优先级最高(逗号分隔)",
371
+ values: value.officialProviders ?? [],
372
+ placeholder: "deepseek-official",
373
+ disabled: readonly,
374
+ onCommit: (next) => {
375
+ scope.set("officialProviders", next);
376
+ }
377
+ }),
378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
379
+ label: "官方端点名单",
380
+ description: "baseURL 归一化后的 host(逗号分隔)",
381
+ values: value.officialBaseURLs ?? [],
382
+ placeholder: "api.deepseek.com",
383
+ disabled: readonly,
384
+ onCommit: (next) => {
385
+ scope.set("officialBaseURLs", next);
386
+ }
387
+ }),
388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
389
+ label: "拦截方式",
390
+ description: "挂起等待不报错;或报错并记入延后队列",
391
+ value: value.deferredMode ?? "hold",
392
+ options: [{
393
+ value: "hold",
394
+ label: "挂起等待(退峰自动放行)"
395
+ }, {
396
+ value: "error",
397
+ label: "报错并延后(退峰续跑)"
398
+ }],
399
+ disabled: readonly,
400
+ onChange: (next) => {
401
+ scope.set("deferredMode", next);
402
+ }
403
+ }),
404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
405
+ label: "退峰自动继续",
406
+ description: "关闭后延后的请求/会话不自动续跑,需手动 /resume",
407
+ checked: value.deferredResume ?? true,
408
+ disabled: readonly,
409
+ onChange: (next) => toggle("deferredResume", next)
410
+ }),
411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextRow, {
412
+ label: "退峰续跑文案",
413
+ description: "error 模式退峰时发送的消息内容",
414
+ value: value.deferredResumeText ?? "",
415
+ placeholder: "继续(高峰已过,自动继续)",
416
+ disabled: readonly,
417
+ onCommit: (next) => {
418
+ scope.set("deferredResumeText", next);
419
+ }
420
+ }),
421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
422
+ label: "纳入子代理请求",
423
+ description: "子代理请求同样计费,默认一并拦截",
424
+ checked: value.guardSubagents ?? true,
425
+ disabled: readonly,
426
+ onChange: (next) => toggle("guardSubagents", next)
427
+ }),
189
428
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
190
429
  label: "低谷自动恢复",
191
430
  description: "低峰时段自动恢复被暂停的会话",
@@ -214,6 +453,10 @@ window.__ModuleLoader__.load({
214
453
  disabled: readonly,
215
454
  onChange: (next) => toggle("retryEnabled", next)
216
455
  }),
456
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
457
+ className: "sgHint",
458
+ children: "判定口径:显式 id 名单 → baseURL 端点 → catalog 默认端点 → 内置 id。 排查误判访问 /session-guard/provider?provider=<id> 看 matchedBy。"
459
+ }),
217
460
  !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
218
461
  className: "sgReadonly",
219
462
  children: "当前只读,无法修改。"
@@ -223,6 +466,143 @@ window.__ModuleLoader__.load({
223
466
  });
224
467
  }
225
468
  //#endregion
469
+ //#region src/client/pause-button-text.ts
470
+ /** 按钮文案。 */
471
+ function pauseButtonLabel(state) {
472
+ return state.paused === true ? "继续会话" : "暂停会话";
473
+ }
474
+ /** 悬浮说明:写清当前状态与点击后的语义。 */
475
+ function pauseButtonTitle(state) {
476
+ if (state.paused !== true) return "暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)";
477
+ const since = typeof state.heldSince === "number" && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null;
478
+ const at = since === null ? null : `${String(since.getHours()).padStart(2, "0")}:${String(since.getMinutes()).padStart(2, "0")}`;
479
+ const why = state.manual === true ? "手动暂停" : "高峰自动暂停";
480
+ return `${at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`} · 点击继续:放行当前 step,且本高峰内不再拦该会话`;
481
+ }
482
+ /** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */
483
+ function readPauseState(body) {
484
+ const b = body ?? {};
485
+ if (b.ok !== true) return {
486
+ paused: false,
487
+ heldSince: null,
488
+ manual: false
489
+ };
490
+ const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true;
491
+ const paused = b.paused?.step === true || b.state?.pausedStep === true || manual;
492
+ const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null;
493
+ return {
494
+ paused,
495
+ heldSince: typeof raw === "number" && Number.isFinite(raw) ? raw : null,
496
+ manual
497
+ };
498
+ }
499
+ /** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */
500
+ function readStepSnapshot(snapshot) {
501
+ const s = snapshot ?? {};
502
+ const manual = s.manual === true;
503
+ return {
504
+ paused: s.held === true || manual,
505
+ heldSince: typeof s.since === "number" && Number.isFinite(s.since) ? s.since : null,
506
+ manual
507
+ };
508
+ }
509
+ //#endregion
510
+ //#region src/client/pause-button.tsx
511
+ /**
512
+ * dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。
513
+ *
514
+ * 交互(v0.2.0 修订):
515
+ * - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停
516
+ * (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);
517
+ * - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;
518
+ * - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底
519
+ * (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。
520
+ *
521
+ * 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:
522
+ * 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。
523
+ */
524
+ /** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */
525
+ const POLL_MS = 1e4;
526
+ const IDLE = {
527
+ paused: false,
528
+ heldSince: null,
529
+ manual: false
530
+ };
531
+ /** 暂停 / 继续会话按钮。 */
532
+ function PauseButton({ sessionId }) {
533
+ const [state, setState] = (0, react.useState)(IDLE);
534
+ const [busy, setBusy] = (0, react.useState)(false);
535
+ injectClientCss();
536
+ (0, react.useEffect)(() => {
537
+ if (sessionId === void 0 || sessionId === "") return;
538
+ let cancelled = false;
539
+ const poll = async () => {
540
+ try {
541
+ const body = await (await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)).json().catch(() => null);
542
+ if (cancelled) return;
543
+ setState(readPauseState(body));
544
+ } catch {}
545
+ };
546
+ poll();
547
+ const timer = setInterval(() => {
548
+ poll();
549
+ }, POLL_MS);
550
+ let source;
551
+ try {
552
+ source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`);
553
+ source.onmessage = (ev) => {
554
+ try {
555
+ const msg = JSON.parse(ev.data);
556
+ if (cancelled || msg?.type !== "step") return;
557
+ setState(readStepSnapshot(msg.state));
558
+ } catch {}
559
+ };
560
+ source.onerror = () => void 0;
561
+ } catch {}
562
+ return () => {
563
+ cancelled = true;
564
+ clearInterval(timer);
565
+ try {
566
+ source?.close();
567
+ } catch {}
568
+ };
569
+ }, [sessionId]);
570
+ if (sessionId === void 0 || sessionId === "") return null;
571
+ const paused = state.paused === true;
572
+ const onClick = async () => {
573
+ if (busy) return;
574
+ setBusy(true);
575
+ try {
576
+ if ((await (await fetch("/session-guard/rpc", {
577
+ method: "POST",
578
+ headers: { "content-type": "application/json" },
579
+ body: JSON.stringify({
580
+ sessionId,
581
+ action: paused ? "stepResume" : "stepPause"
582
+ })
583
+ })).json().catch(() => null))?.ok === true) setState(paused ? IDLE : {
584
+ paused: true,
585
+ heldSince: null,
586
+ manual: true
587
+ });
588
+ } catch {} finally {
589
+ setBusy(false);
590
+ }
591
+ };
592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
593
+ type: "button",
594
+ className: "sg-pause",
595
+ disabled: busy,
596
+ "aria-pressed": paused || void 0,
597
+ title: pauseButtonTitle(state),
598
+ "data-sg-step-paused": paused ? "on" : "off",
599
+ onClick: () => {
600
+ onClick();
601
+ },
602
+ children: pauseButtonLabel(state)
603
+ });
604
+ }
605
+ //#endregion
226
606
  //#region src/client/index.ts
227
607
  /**
228
608
  * dsh-session-guard — 浏览器 half。
@@ -247,10 +627,16 @@ window.__ModuleLoader__.load({
247
627
  /** host 侧 src/settings.js 注册的命名空间(保持一致)。 */
248
628
  const NS = "session-guard";
249
629
  function apply(ctx) {
630
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
631
+ name: "conversation.input.right",
632
+ id: "session-guard-pause",
633
+ order: 20,
634
+ locale: "session-guard"
635
+ }, PauseButton));
250
636
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
251
637
  name: "conversation.input.right",
252
638
  id: "session-guard-status",
253
- order: 50,
639
+ order: 40,
254
640
  locale: "session-guard"
255
641
  }, StatusBadge));
256
642
  ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","useSyncExternalStore","useState"],"sources":["../src/client/status-badge.tsx","../src/client/settings-card.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * dsh-session-guard — 状态徽标(纯展示,fail-open)。\n *\n * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末。\n * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。\n */\nimport { useEffect, useState } from 'react'\n\n/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */\nexport interface StatusBadgeProps {\n sessionId?: string\n}\n\n/** /session-guard/status 返回的全局阶段。 */\nexport interface Status {\n phase: 'peak' | 'off-peak' | 'weekend'\n enabled: boolean\n weekendMode: boolean\n timezone: string\n}\n\nconst LABELS = {\n peak: '高峰',\n 'off-peak': '谷时',\n weekend: '周末',\n} as const\n\nconst POLL_MS = 15_000\n\n/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */\nexport function StatusBadge({ sessionId }: StatusBadgeProps) {\n const [status, setStatus] = useState<Status | null>(null)\n\n useEffect(() => {\n if (!sessionId) return\n let cancelled = false\n const poll = async () => {\n try {\n const res = await fetch('/session-guard/status')\n const body = await res.json().catch(() => null)\n if (!cancelled && body?.ok && body.status) setStatus(body.status)\n } catch {\n // fail-open:路由/网络不可达 → 静默,徽标隐藏。\n }\n }\n void poll()\n const timer = setInterval(poll, POLL_MS)\n return () => {\n cancelled = true\n clearInterval(timer)\n }\n }, [sessionId])\n\n if (!status || !status.enabled) return null\n\n const cls = status.phase === 'peak' ? 'sg-peak' : status.phase === 'weekend' ? 'sg-weekend' : 'sg-off'\n return (\n <span\n className={`sg-status ${cls}`}\n title={`${LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? ' · 周末模式' : ''}`}\n data-sg-phase={status.phase}\n >\n {LABELS[status.phase]}\n </span>\n )\n}\n","/**\n * dsh-session-guard — 插件配置卡片(settings.plugin.item 面)。\n *\n * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,\n * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch\n * (track + thumb),不是复选框对勾。\n *\n * 通过 `settingsScope.bind({ namespace: NS })` 绑定 host 已注册的\n * `session-guard` 命名空间;每次变更立即经 scope 提交(无 staged form)。\n * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,\n * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。\n */\nimport { useState, useSyncExternalStore } from 'react'\nimport type { JSX } from 'react'\nimport type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'\n\n/** 命名空间值形状(与 host 的 DEFAULT_SETTINGS 对应)。 */\nexport interface SessionGuardConfig {\n enabled: boolean\n offPeakAutoResume: boolean\n weekendMode: boolean\n queueFallback: boolean\n retryEnabled: boolean\n timezone: string\n pauseMode: 'safe' | 'force'\n pauseReason: 'wait' | 'stop'\n}\n\n/** 注入给卡片的面:绑定到 session-guard 命名空间的 settings scope。 */\nexport interface SettingsCardInjected {\n scope: SettingsScope<SessionGuardConfig>\n}\n\nexport type SettingsCardProps = SettingsCardInjected\n\n/** 卡片样式,注入一次(保持 bundle CSS-free)。 */\nconst CARD_CSS = `\n.sgCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}\n.sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}\n.sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}\n.sgCardHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}\n.sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}\n.sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}\n.sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\n.sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}\n.sgCardChevron-open{transform:rotate(180deg)}\n.sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}\n.sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}\n.sgRow:last-child{border-bottom:0}\n.sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\n.sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\n.sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\n.sgSwitch{position:relative;width:40px;height:22px;flex:none}\n.sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}\n.sgSwitch>input:disabled{cursor:not-allowed}\n.sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\n.sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\n.sgSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\n.sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}\n.sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}\n`\n\n/** 注入一次卡片样式。 */\nfunction injectCss(): void {\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"session-guard-card\"]') === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-card'\n tag.textContent = CARD_CSS\n document.head.appendChild(tag)\n }\n}\n\n/** 一行 pill switch(滑块开关,绑定 scope)。 */\nfunction SwitchRow(props: {\n label: string\n description?: string\n checked: boolean\n disabled: boolean\n onChange: (_next: boolean) => void\n}): JSX.Element {\n const { label, description, checked, disabled, onChange } = props\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n </div>\n <label className=\"sgSwitch\">\n <input\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e) => onChange(e.currentTarget.checked)}\n />\n <span className=\"sgSwitchTrack\" />\n <span className=\"sgSwitchThumb\" />\n </label>\n </div>\n )\n}\n\n/** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */\nexport function SessionGuardCard({ scope }: SettingsCardProps): JSX.Element {\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n )\n const unavailable = snapshot.status === 'unavailable'\n const readonly = unavailable || !snapshot.writable\n const value = (snapshot.value ?? {}) as Partial<SessionGuardConfig>\n const [open, setOpen] = useState(false)\n\n injectCss()\n\n const toggle = (field: keyof SessionGuardConfig, next: boolean) => { void scope.set(field, next) }\n\n return (\n <li className={'sgCard' + (open ? ' sgCard-open' : '')}>\n <button\n type=\"button\"\n className=\"sgCardHeader\"\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n <span className=\"sgCardHeadtext\">\n <span className=\"sgCardName\">会话守护门禁</span>\n <span className=\"sgCardDesc\">高峰自动暂停运行会话,周末模式无视峰谷畅快跑</span>\n </span>\n <svg\n className={'sgCardChevron' + (open ? ' sgCardChevron-open' : '')}\n viewBox=\"0 0 14 14\"\n width={14}\n height={14}\n fill=\"none\"\n aria-hidden=\"true\"\n >\n <path d=\"M3.5 5.5L7 9l3.5-3.5\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n {open && (\n <div className=\"sgCardBody\">\n {unavailable ? (\n <p style={{ margin: '0', padding: '12px 0', fontSize: '13px', color: 'var(--dsw-alias-label-tertiary)' }}>\n 设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。\n </p>\n ) : (\n <>\n <SwitchRow\n label=\"高峰自动暂停冻结会话\"\n description=\"高峰时段自动暂停运行会话\"\n checked={value.enabled ?? true}\n disabled={readonly}\n onChange={(next) => toggle('enabled', next)}\n />\n <SwitchRow\n label=\"低谷自动恢复\"\n description=\"低峰时段自动恢复被暂停的会话\"\n checked={value.offPeakAutoResume ?? true}\n disabled={readonly}\n onChange={(next) => toggle('offPeakAutoResume', next)}\n />\n <SwitchRow\n label=\"周末模式\"\n description=\"识别周末,无视峰谷畅快跑\"\n checked={value.weekendMode ?? true}\n disabled={readonly}\n onChange={(next) => toggle('weekendMode', next)}\n />\n <SwitchRow\n label=\"回退锁队列\"\n description=\"无会话门时锁等待队列\"\n checked={value.queueFallback ?? true}\n disabled={readonly}\n onChange={(next) => toggle('queueFallback', next)}\n />\n <SwitchRow\n label=\"自动重试\"\n description=\"后端重试,默认关(保守)\"\n checked={value.retryEnabled ?? false}\n disabled={readonly}\n onChange={(next) => toggle('retryEnabled', next)}\n />\n {!snapshot.writable && <p className=\"sgReadonly\">当前只读,无法修改。</p>}\n </>\n )}\n </div>\n )}\n </li>\n )\n}\n","/**\n * dsh-session-guard — 浏览器 half。\n *\n * 职责(全部 fail-open,D8):\n * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询\n * /session-guard/status;\n * - 注册 `settings.plugin.item` 设置卡片,经 `settingsScope.bind({ namespace })`\n * 绑定 host 已注册的 `session-guard` 命名空间——这正是“插件配置”面板显示本\n * 插件的**必要**机制(对齐 dsh-thinking-levels / dsh-context);\n * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc\n * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。\n *\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。\n */\nimport { StatusBadge } from './status-badge'\nimport { SessionGuardCard } from './settings-card'\n\n/** 客户端所需服务:slots(状态徽标 + 设置卡片)+ locale + settingsScope(设置卡片绑定)。 */\nexport const inject = ['slots', 'locale', 'settingsScope']\n\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\ninterface SlotsFace {\n inject: (_name: string, _fn: () => unknown) => () => void\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\n}\ninterface SettingsScopeFace {\n bind: (_spec: { namespace: string }) => unknown\n}\ninterface ClientCtx {\n slots: SlotsFace\n settingsScope: SettingsScopeFace\n}\n\n/** host 侧 src/settings.js 注册的命名空间(保持一致)。 */\nconst NS = 'session-guard'\n\nexport function apply(ctx: ClientCtx) {\n // 状态徽标:输入区右侧,纯展示。\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'session-guard-status',\n order: 50,\n locale: 'session-guard',\n }, StatusBadge))\n\n // 插件配置卡片:绑定 session-guard 命名空间,经 settingsScope 渲染并提交。\n ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({\n name: 'settings.plugin.item',\n id: NS,\n key: NS,\n locale: 'session-guard',\n inject: () => ({\n scope: ctx.settingsScope.bind({ namespace: NS }),\n }),\n }, SessionGuardCard))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["POLL_MS","useState","useState","useSyncExternalStore","useState"],"sources":["../src/client/badge-text.ts","../src/client/styles.ts","../src/client/status-badge.tsx","../src/client/settings-card.tsx","../src/client/pause-button-text.ts","../src/client/pause-button.tsx","../src/client/index.ts"],"sourcesContent":["/**\r\n * dsh-session-guard — 状态徽标文案(纯函数,零依赖,可单测)。\r\n *\r\n * 分离出来的原因:徽标语义(高峰期「只拦官方」vs「全部暂停」)需要单测,\r\n * 而组件文件是 TSX(含 React 运行时)。这里只有字符串逻辑。\r\n */\r\n\r\n/** /session-guard/status 返回的全局阶段(组件与测试共用)。 */\r\nexport interface Status {\r\n phase: 'peak' | 'off-peak' | 'weekend'\r\n enabled: boolean\r\n weekendMode: boolean\r\n /** 官方源二维判定是否开启(开启时高峰期只拦官方源)。 */\r\n providerGuard?: boolean\r\n /** 当前被挂起的请求数(hold 模式)。 */\r\n held?: number\r\n /** error 模式记下的延后会话数。 */\r\n deferred?: number\r\n /** 当前被 step 门挂起的会话数(v0.2.0)。 */\r\n stepHeld?: number\r\n timezone: string\r\n}\r\n\r\n/** 阶段文案。 */\r\nexport const PHASE_LABELS = {\r\n peak: '高峰',\r\n 'off-peak': '谷时',\r\n weekend: '周末',\r\n} as const\r\n\r\n/** 高峰期的徽标文案:二维判定开启时区分「只拦官方」与「全部暂停」。 */\r\nexport function peakLabel(status: Pick<Status, 'phase' | 'providerGuard'>): string {\r\n if (status.phase !== 'peak') return PHASE_LABELS[status.phase]\r\n return status.providerGuard === true ? '高峰·拦官方' : '高峰·全部暂停'\r\n}\r\n\r\n/** 悬浮说明:把判定口径与当前挂起/延后数量写清楚。 */\r\nexport function badgeTitle(status: Status): string {\r\n const base = `${PHASE_LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? ' · 周末模式' : ''}`\r\n if (status.phase !== 'peak') return base\r\n const mode = status.providerGuard === true ? '仅拦截 DeepSeek 官方源' : '全部会话暂停(未启用二维判定)'\r\n const held = status.held ?? 0\r\n const deferred = status.deferred ?? 0\r\n const stepHeld = status.stepHeld ?? 0\r\n const extra = held > 0 || deferred > 0 || stepHeld > 0\r\n ? ` · 挂起 ${held} · 延后 ${deferred} · step 挂起 ${stepHeld}`\r\n : ''\r\n return `${base} · ${mode}${extra}`\r\n}\r\n","/**\n * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。\n *\n * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线\n * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。\n * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件\n * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。\n */\n\n/** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */\nexport const CLIENT_CSS = `\n.sg-pause{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1;cursor:pointer;white-space:nowrap;flex:none}\n.sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}\n.sg-pause:disabled{opacity:.55;cursor:default}\n.sg-pause[aria-pressed='true']{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d97706) 8%,transparent)}\n.sg-status{display:inline-flex;align-items:center;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap;flex:none}\n.sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}\n.sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}\n`\n\n/** 注入一次(幂等;无 document 时静默跳过)。 */\nexport function injectClientCss(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"session-guard-client\"]') !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-client'\n tag.textContent = CLIENT_CSS\n document.head.appendChild(tag)\n}\n","/**\r\n * dsh-session-guard — 状态徽标(纯展示,fail-open)。\r\n *\r\n * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;\r\n * 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。\r\n * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。\r\n */\r\nimport { useEffect, useState } from 'react'\r\nimport { badgeTitle, peakLabel, type Status } from './badge-text'\r\nimport { injectClientCss } from './styles'\r\n\r\n/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */\r\nexport interface StatusBadgeProps {\r\n sessionId?: string\r\n}\r\n\r\nconst POLL_MS = 15_000\r\n\r\n/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */\r\nexport function StatusBadge({ sessionId }: StatusBadgeProps) {\r\n const [status, setStatus] = useState<Status | null>(null)\r\n\r\n injectClientCss()\r\n\r\n useEffect(() => {\r\n if (!sessionId) return\r\n let cancelled = false\r\n const poll = async () => {\r\n try {\r\n const res = await fetch('/session-guard/status')\r\n const body = await res.json().catch(() => null)\r\n if (!cancelled && body?.ok && body.status) setStatus(body.status)\r\n } catch {\r\n // fail-open:路由/网络不可达 → 静默,徽标隐藏。\r\n }\r\n }\r\n void poll()\r\n const timer = setInterval(poll, POLL_MS)\r\n return () => {\r\n cancelled = true\r\n clearInterval(timer)\r\n }\r\n }, [sessionId])\r\n\r\n if (!status || !status.enabled) return null\r\n\r\n const cls = status.phase === 'peak' ? 'sg-peak' : status.phase === 'weekend' ? 'sg-weekend' : 'sg-off'\r\n return (\r\n <span\r\n className={`sg-status ${cls}`}\r\n title={badgeTitle(status)}\r\n data-sg-phase={status.phase}\r\n data-sg-provider-guard={status.providerGuard === true ? 'on' : 'off'}\r\n data-sg-step-held={String(status.stepHeld ?? 0)}\r\n >\r\n {peakLabel(status)}\r\n </span>\r\n )\r\n}\r\n","/**\r\n * dsh-session-guard — 插件配置卡片(settings.plugin.item 面)。\r\n *\r\n * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,\r\n * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch\r\n * (track + thumb),不是复选框对勾。\r\n *\r\n * 通过 `settingsScope.bind({ namespace: NS })` 绑定 host 已注册的\r\n * `session-guard` 命名空间;每次变更立即经 scope 提交(无 staged form)。\r\n * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,\r\n * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。\r\n */\r\nimport { useEffect, useState, useSyncExternalStore } from 'react'\r\nimport type { JSX } from 'react'\r\nimport type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'\r\n\r\n/** 命名空间值形状(与 host 的 DEFAULT_SETTINGS 对应)。 */\r\nexport interface SessionGuardConfig {\r\n enabled: boolean\r\n offPeakAutoResume: boolean\r\n weekendMode: boolean\r\n queueFallback: boolean\r\n retryEnabled: boolean\r\n timezone: string\r\n pauseMode: 'safe' | 'force'\r\n pauseReason: 'wait' | 'stop'\r\n // ── step 级门控(v0.2.0)──\r\n stepLevelPause: boolean\r\n stepGateTimeoutMs: number\r\n // ── 官方 provider 二维判定(高峰 × 目标源)──\r\n providerGuard: boolean\r\n officialProviders: string[]\r\n officialBaseURLs: string[]\r\n deferredResume: boolean\r\n deferredResumeText: string\r\n deferredMode: 'hold' | 'error'\r\n deferredMaxHoldMs: number\r\n guardSubagents: boolean\r\n}\r\n\r\n/** 注入给卡片的面:绑定到 session-guard 命名空间的 settings scope。 */\r\nexport interface SettingsCardInjected {\r\n scope: SettingsScope<SessionGuardConfig>\r\n}\r\n\r\nexport type SettingsCardProps = SettingsCardInjected\r\n\r\n/** 卡片样式,注入一次(保持 bundle CSS-free)。 */\r\nconst CARD_CSS = `\r\n.sgCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}\r\n.sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}\r\n.sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}\r\n.sgCardHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}\r\n.sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}\r\n.sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}\r\n.sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\r\n.sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}\r\n.sgCardChevron-open{transform:rotate(180deg)}\r\n.sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}\r\n.sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}\r\n.sgRow:last-child{border-bottom:0}\r\n.sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\r\n.sgRowText-wide{padding-right:0}\r\n.sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\r\n.sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\r\n.sgSwitch{position:relative;width:40px;height:22px;flex:none}\r\n.sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}\r\n.sgSwitch>input:disabled{cursor:not-allowed}\r\n.sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\r\n.sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\r\n.sgSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\r\n.sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}\r\n.sgInput{appearance:none;min-width:0;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;margin-top:8px}\r\n.sgInput:disabled{opacity:.6;cursor:not-allowed}\r\n.sgSelect{appearance:none;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;min-width:160px;flex:none}\r\n.sgSelect:disabled{opacity:.6;cursor:not-allowed}\r\n.sgHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}\r\n.sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}\r\n`\r\n\r\n/** 注入一次卡片样式。 */\r\nfunction injectCss(): void {\r\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"session-guard-card\"]') === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-session-guard'\r\n tag.dataset.pluginCss = 'session-guard-card'\r\n tag.textContent = CARD_CSS\r\n document.head.appendChild(tag)\r\n }\r\n}\r\n\r\n/** 一行 pill switch(滑块开关,绑定 scope)。 */\r\nfunction SwitchRow(props: {\r\n label: string\r\n description?: string\r\n checked: boolean\r\n disabled: boolean\r\n onChange: (_next: boolean) => void\r\n}): JSX.Element {\r\n const { label, description, checked, disabled, onChange } = props\r\n return (\r\n <div className=\"sgRow\">\r\n <div className=\"sgRowText\">\r\n <div className=\"sgTitle\">{label}</div>\r\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\r\n </div>\r\n <label className=\"sgSwitch\">\r\n <input\r\n type=\"checkbox\"\r\n checked={checked}\r\n disabled={disabled}\r\n onChange={(e) => onChange(e.currentTarget.checked)}\r\n />\r\n <span className=\"sgSwitchTrack\" />\r\n <span className=\"sgSwitchThumb\" />\r\n </label>\r\n </div>\r\n )\r\n}\r\n\r\n/** 一行文本输入(失焦提交;外部值变化时同步草稿)。 */\r\nfunction TextRow(props: {\r\n label: string\r\n description?: string\r\n value: string\r\n placeholder?: string\r\n disabled: boolean\r\n onCommit: (_next: string) => void\r\n}): JSX.Element {\r\n const { label, description, value, placeholder, disabled, onCommit } = props\r\n const [draft, setDraft] = useState(value)\r\n useEffect(() => {\r\n setDraft(value)\r\n }, [value])\r\n return (\r\n <div className=\"sgRow\">\r\n <div className=\"sgRowText sgRowText-wide\">\r\n <div className=\"sgTitle\">{label}</div>\r\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\r\n <input\r\n className=\"sgInput\"\r\n type=\"text\"\r\n value={draft}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n onChange={(e) => setDraft(e.currentTarget.value)}\r\n onBlur={() => onCommit(draft)}\r\n />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/** 一行「逗号/换行分隔」的字符串列表(失焦提交为数组)。 */\r\nfunction ListRow(props: {\r\n label: string\r\n description?: string\r\n values: string[]\r\n placeholder?: string\r\n disabled: boolean\r\n onCommit: (_next: string[]) => void\r\n}): JSX.Element {\r\n const { label, description, values, placeholder, disabled, onCommit } = props\r\n const joined = values.join(', ')\r\n const [draft, setDraft] = useState(joined)\r\n useEffect(() => {\r\n setDraft(joined)\r\n }, [joined])\r\n return (\r\n <div className=\"sgRow\">\r\n <div className=\"sgRowText sgRowText-wide\">\r\n <div className=\"sgTitle\">{label}</div>\r\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\r\n <input\r\n className=\"sgInput\"\r\n type=\"text\"\r\n value={draft}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n onChange={(e) => setDraft(e.currentTarget.value)}\r\n onBlur={() => {\r\n const next = draft\r\n .split(/[,\\n]/)\r\n .map((s) => s.trim())\r\n .filter((s) => s !== '')\r\n onCommit(next)\r\n }}\r\n />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/** 一行下拉选择。 */\r\nfunction SelectRow<T extends string>(props: {\r\n label: string\r\n description?: string\r\n value: T\r\n options: readonly { value: T; label: string }[]\r\n disabled: boolean\r\n onChange: (_next: T) => void\r\n}): JSX.Element {\r\n const { label, description, value, options, disabled, onChange } = props\r\n return (\r\n <div className=\"sgRow\">\r\n <div className=\"sgRowText\">\r\n <div className=\"sgTitle\">{label}</div>\r\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\r\n </div>\r\n <select\r\n className=\"sgSelect\"\r\n value={value}\r\n disabled={disabled}\r\n onChange={(e) => onChange(e.currentTarget.value as T)}\r\n >\r\n {options.map((o) => (\r\n <option key={o.value} value={o.value}>\r\n {o.label}\r\n </option>\r\n ))}\r\n </select>\r\n </div>\r\n )\r\n}\r\n\r\n/** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */\r\nexport function SessionGuardCard({ scope }: SettingsCardProps): JSX.Element {\r\n const snapshot = useSyncExternalStore(\r\n (listener) => scope.subscribe(listener),\r\n () => scope.getSnapshot(),\r\n )\r\n const unavailable = snapshot.status === 'unavailable'\r\n const readonly = unavailable || !snapshot.writable\r\n const value = (snapshot.value ?? {}) as Partial<SessionGuardConfig>\r\n const [open, setOpen] = useState(false)\r\n\r\n injectCss()\r\n\r\n const toggle = (field: keyof SessionGuardConfig, next: boolean) => { void scope.set(field, next) }\r\n return (\r\n <li className={'sgCard' + (open ? ' sgCard-open' : '')}>\r\n <button\r\n type=\"button\"\r\n className=\"sgCardHeader\"\r\n aria-expanded={open}\r\n onClick={() => setOpen(!open)}\r\n >\r\n <span className=\"sgCardHeadtext\">\r\n <span className=\"sgCardName\">会话守护门禁</span>\r\n <span className=\"sgCardDesc\">高峰自动暂停运行会话,周末模式无视峰谷畅快跑</span>\r\n </span>\r\n <svg\r\n className={'sgCardChevron' + (open ? ' sgCardChevron-open' : '')}\r\n viewBox=\"0 0 14 14\"\r\n width={14}\r\n height={14}\r\n fill=\"none\"\r\n aria-hidden=\"true\"\r\n >\r\n <path d=\"M3.5 5.5L7 9l3.5-3.5\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n </button>\r\n {open && (\r\n <div className=\"sgCardBody\">\r\n {unavailable ? (\r\n <p style={{ margin: '0', padding: '12px 0', fontSize: '13px', color: 'var(--dsw-alias-label-tertiary)' }}>\r\n 设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。\r\n </p>\r\n ) : (\r\n <>\r\n <SwitchRow\r\n label=\"高峰自动暂停冻结会话\"\r\n description=\"高峰时段自动暂停运行会话\"\r\n checked={value.enabled ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('enabled', next)}\r\n />\r\n <SwitchRow\r\n label=\"step 级门控\"\r\n description=\"高峰在下一个 step 的模型请求前拉门(省 token 更彻底);关闭则回退为回合级暂停\"\r\n checked={value.stepLevelPause ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('stepLevelPause', next)}\r\n />\r\n <TextRow\r\n label=\"step 门控超时(毫秒)\"\r\n description=\"到期释放 step 门并升级为回合级暂停(防死锁);0 或非法值用默认 300000\"\r\n value={String(value.stepGateTimeoutMs ?? 300000)}\r\n placeholder=\"300000\"\r\n disabled={readonly}\r\n onCommit={(next) => {\r\n const n = Number(String(next).trim())\r\n void scope.set('stepGateTimeoutMs', Number.isFinite(n) && n > 0 ? n : 300000)\r\n }}\r\n />\r\n <SwitchRow\r\n label=\"官方源二维判定\"\r\n description=\"高峰期只拦 DeepSeek 官方源;本地/第三方 provider 照常跑\"\r\n checked={value.providerGuard ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('providerGuard', next)}\r\n />\r\n <ListRow\r\n label=\"追加官方 provider id\"\r\n description=\"精确匹配,优先级最高(逗号分隔)\"\r\n values={value.officialProviders ?? []}\r\n placeholder=\"deepseek-official\"\r\n disabled={readonly}\r\n onCommit={(next) => { void scope.set('officialProviders', next) }}\r\n />\r\n <ListRow\r\n label=\"官方端点名单\"\r\n description=\"baseURL 归一化后的 host(逗号分隔)\"\r\n values={value.officialBaseURLs ?? []}\r\n placeholder=\"api.deepseek.com\"\r\n disabled={readonly}\r\n onCommit={(next) => { void scope.set('officialBaseURLs', next) }}\r\n />\r\n <SelectRow<'hold' | 'error'>\r\n label=\"拦截方式\"\r\n description=\"挂起等待不报错;或报错并记入延后队列\"\r\n value={value.deferredMode ?? 'hold'}\r\n options={[\r\n { value: 'hold', label: '挂起等待(退峰自动放行)' },\r\n { value: 'error', label: '报错并延后(退峰续跑)' },\r\n ]}\r\n disabled={readonly}\r\n onChange={(next) => { void scope.set('deferredMode', next) }}\r\n />\r\n <SwitchRow\r\n label=\"退峰自动继续\"\r\n description=\"关闭后延后的请求/会话不自动续跑,需手动 /resume\"\r\n checked={value.deferredResume ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('deferredResume', next)}\r\n />\r\n <TextRow\r\n label=\"退峰续跑文案\"\r\n description=\"error 模式退峰时发送的消息内容\"\r\n value={value.deferredResumeText ?? ''}\r\n placeholder=\"继续(高峰已过,自动继续)\"\r\n disabled={readonly}\r\n onCommit={(next) => { void scope.set('deferredResumeText', next) }}\r\n />\r\n <SwitchRow\r\n label=\"纳入子代理请求\"\r\n description=\"子代理请求同样计费,默认一并拦截\"\r\n checked={value.guardSubagents ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('guardSubagents', next)}\r\n />\r\n <SwitchRow\r\n label=\"低谷自动恢复\"\r\n description=\"低峰时段自动恢复被暂停的会话\"\r\n checked={value.offPeakAutoResume ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('offPeakAutoResume', next)}\r\n />\r\n <SwitchRow\r\n label=\"周末模式\"\r\n description=\"识别周末,无视峰谷畅快跑\"\r\n checked={value.weekendMode ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('weekendMode', next)}\r\n />\r\n <SwitchRow\r\n label=\"回退锁队列\"\r\n description=\"无会话门时锁等待队列\"\r\n checked={value.queueFallback ?? true}\r\n disabled={readonly}\r\n onChange={(next) => toggle('queueFallback', next)}\r\n />\r\n <SwitchRow\r\n label=\"自动重试\"\r\n description=\"后端重试,默认关(保守)\"\r\n checked={value.retryEnabled ?? false}\r\n disabled={readonly}\r\n onChange={(next) => toggle('retryEnabled', next)}\r\n />\r\n <p className=\"sgHint\">\r\n 判定口径:显式 id 名单 → baseURL 端点 → catalog 默认端点 → 内置 id。\r\n 排查误判访问 /session-guard/provider?provider=&lt;id&gt; 看 matchedBy。\r\n </p>\r\n {!snapshot.writable && <p className=\"sgReadonly\">当前只读,无法修改。</p>}\r\n </>\r\n )}\r\n </div>\r\n )}\r\n </li>\r\n )\r\n}\r\n","/**\n * dsh-session-guard — 暂停/继续按钮文案与状态投影(纯函数,零依赖,可单测)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」(**可点**:点击 = 在下一次 step 边界手动暂停该会话);\n * - 已暂停(step 门已拉 / 已请求但未到边界)→ 「继续会话」(点击 = 放行并本峰不再拦)。\n * 按钮不置灰:step 暂停虽然会自动发生,但按钮同时是**手动暂停**入口。\n */\n\n/** 按钮状态。 */\nexport interface PauseButtonState {\n /** 是否处于暂停态(step 门已挂起,或已请求暂停但还没到边界) */\n paused: boolean\n /** 挂起起始时间戳(毫秒);未知为 null */\n heldSince?: number | null\n /** 暂停是否来自用户手动请求(而非高峰自动拉门) */\n manual?: boolean\n}\n\n/** 按钮文案。 */\nexport function pauseButtonLabel(state: PauseButtonState): string {\n return state.paused === true ? '继续会话' : '暂停会话'\n}\n\n/** 悬浮说明:写清当前状态与点击后的语义。 */\nexport function pauseButtonTitle(state: PauseButtonState): string {\n if (state.paused !== true) {\n return '暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)'\n }\n const since = typeof state.heldSince === 'number' && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null\n const at = since === null ? null : `${String(since.getHours()).padStart(2, '0')}:${String(since.getMinutes()).padStart(2, '0')}`\n const why = state.manual === true ? '手动暂停' : '高峰自动暂停'\n const base = at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`\n return `${base} · 点击继续:放行当前 step,且本高峰内不再拦该会话`\n}\n\n/** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */\nexport function readPauseState(body: unknown): PauseButtonState {\n const b = (body ?? {}) as {\n ok?: unknown\n paused?: { step?: unknown; manual?: unknown }\n stepGate?: { since?: unknown; manual?: unknown }\n state?: { pausedStep?: unknown; stepHeldSince?: unknown; stepManual?: unknown }\n }\n if (b.ok !== true) return { paused: false, heldSince: null, manual: false }\n const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true\n const paused = b.paused?.step === true || b.state?.pausedStep === true || manual\n const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null\n const heldSince = typeof raw === 'number' && Number.isFinite(raw) ? raw : null\n return { paused, heldSince, manual }\n}\n\n/** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */\nexport function readStepSnapshot(snapshot: unknown): PauseButtonState {\n const s = (snapshot ?? {}) as { held?: unknown; since?: unknown; manual?: unknown }\n const manual = s.manual === true\n const paused = s.held === true || manual\n const heldSince = typeof s.since === 'number' && Number.isFinite(s.since) ? s.since : null\n return { paused, heldSince, manual }\n}\n","/**\n * dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停\n * (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);\n * - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;\n * - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底\n * (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。\n *\n * 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:\n * 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。\n */\nimport { useEffect, useState } from 'react'\nimport {\n pauseButtonLabel,\n pauseButtonTitle,\n readPauseState,\n readStepSnapshot,\n type PauseButtonState,\n} from './pause-button-text'\nimport { injectClientCss } from './styles'\n\n/** slot 运行时注入的会话级 props。 */\nexport interface PauseButtonProps {\n sessionId?: string\n}\n\n/** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */\nconst POLL_MS = 10_000\n\nconst IDLE: PauseButtonState = { paused: false, heldSince: null, manual: false }\n\n/** 暂停 / 继续会话按钮。 */\nexport function PauseButton({ sessionId }: PauseButtonProps) {\n const [state, setState] = useState<PauseButtonState>(IDLE)\n const [busy, setBusy] = useState(false)\n\n injectClientCss()\n\n useEffect(() => {\n if (sessionId === undefined || sessionId === '') return\n let cancelled = false\n\n const poll = async (): Promise<void> => {\n try {\n const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)\n const body = await res.json().catch(() => null)\n if (cancelled) return\n setState(readPauseState(body))\n } catch {\n // fail-open:路由不存在 / 网络失败 → 保持上一次状态。\n }\n }\n void poll()\n const timer = setInterval(() => {\n void poll()\n }, POLL_MS)\n\n let source: EventSource | undefined\n try {\n source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`)\n source.onmessage = (ev: MessageEvent<string>) => {\n try {\n const msg = JSON.parse(ev.data) as { type?: string; state?: unknown }\n if (cancelled || msg?.type !== 'step') return\n setState(readStepSnapshot(msg.state))\n } catch {\n /* 非法负载:忽略,等下一次推送或轮询 */\n }\n }\n // onerror:EventSource 会自行重连;轮询同时兜底。\n source.onerror = () => undefined\n } catch {\n // 浏览器不支持 EventSource → 只靠轮询。\n }\n\n return () => {\n cancelled = true\n clearInterval(timer)\n try {\n source?.close()\n } catch {\n /* ignore */\n }\n }\n }, [sessionId])\n\n if (sessionId === undefined || sessionId === '') return null\n\n const paused = state.paused === true\n const onClick = async (): Promise<void> => {\n if (busy) return\n setBusy(true)\n try {\n const action = paused ? 'stepResume' : 'stepPause'\n const res = await fetch('/session-guard/rpc', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, action }),\n })\n const body = await res.json().catch(() => null)\n if (body?.ok === true) {\n // 立即乐观更新;SSE / 轮询随后校正。\n setState(paused ? IDLE : { paused: true, heldSince: null, manual: true })\n }\n } catch {\n // fail-open:下一轮轮询会纠正显示。\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <button\n type=\"button\"\n className=\"sg-pause\"\n disabled={busy}\n aria-pressed={paused || undefined}\n title={pauseButtonTitle(state)}\n data-sg-step-paused={paused ? 'on' : 'off'}\n onClick={() => {\n void onClick()\n }}\n >\n {pauseButtonLabel(state)}\n </button>\n )\n}\n","/**\n * dsh-session-guard — 浏览器 half。\n *\n * 职责(全部 fail-open,D8):\n * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询\n * /session-guard/status;\n * - 注册 `settings.plugin.item` 设置卡片,经 `settingsScope.bind({ namespace })`\n * 绑定 host 已注册的 `session-guard` 命名空间——这正是“插件配置”面板显示本\n * 插件的**必要**机制(对齐 dsh-thinking-levels / dsh-context);\n * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc\n * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。\n *\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。\n */\nimport { StatusBadge } from './status-badge'\nimport { SessionGuardCard } from './settings-card'\nimport { PauseButton } from './pause-button'\n\n/** 客户端所需服务:slots(状态徽标 + 设置卡片)+ locale + settingsScope(设置卡片绑定)。 */\nexport const inject = ['slots', 'locale', 'settingsScope']\n\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\ninterface SlotsFace {\n inject: (_name: string, _fn: () => unknown) => () => void\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\n}\ninterface SettingsScopeFace {\n bind: (_spec: { namespace: string }) => unknown\n}\ninterface ClientCtx {\n slots: SlotsFace\n settingsScope: SettingsScopeFace\n}\n\n/** host 侧 src/settings.js 注册的命名空间(保持一致)。 */\nconst NS = 'session-guard'\n\nexport function apply(ctx: ClientCtx) {\n // 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。\n // order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'session-guard-pause',\n order: 20,\n locale: 'session-guard',\n }, PauseButton))\n\n // 状态徽标:输入区右侧,纯展示。\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'session-guard-status',\n order: 40,\n locale: 'session-guard',\n }, StatusBadge))\n\n // 插件配置卡片:绑定 session-guard 命名空间,经 settingsScope 渲染并提交。\n ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({\n name: 'settings.plugin.item',\n id: NS,\n key: NS,\n locale: 'session-guard',\n inject: () => ({\n scope: ctx.settingsScope.bind({ namespace: NS }),\n }),\n }, SessionGuardCard))\n}\n"],"mappings":"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"}