dsh-session-guard 0.1.2 → 0.1.5-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
@@ -1,271 +1,463 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-session-guard",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
- let react_jsx_runtime = require("react/jsx-runtime");
9
- //#region src/client/status-badge.tsx
10
- /**
11
- * dsh-session-guard — 状态徽标(纯展示,fail-open)。
12
- *
13
- * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末。
14
- * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
15
- */
16
- const LABELS = {
17
- peak: "高峰",
18
- "off-peak": "谷时",
19
- weekend: "周末"
20
- };
21
- const POLL_MS = 15e3;
22
- /** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
23
- function StatusBadge({ sessionId }) {
24
- const [status, setStatus] = (0, react.useState)(null);
25
- (0, react.useEffect)(() => {
26
- if (!sessionId) return;
27
- let cancelled = false;
28
- const poll = async () => {
29
- try {
30
- const body = await (await fetch("/session-guard/status")).json().catch(() => null);
31
- if (!cancelled && body?.ok && body.status) setStatus(body.status);
32
- } catch {}
33
- };
34
- poll();
35
- const timer = setInterval(poll, POLL_MS);
36
- return () => {
37
- cancelled = true;
38
- clearInterval(timer);
39
- };
40
- }, [sessionId]);
41
- if (!status || !status.enabled) return null;
42
- const cls = status.phase === "peak" ? "sg-peak" : status.phase === "weekend" ? "sg-weekend" : "sg-off";
43
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
44
- className: `sg-status ${cls}`,
45
- title: `${LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? " · 周末模式" : ""}`,
46
- "data-sg-phase": status.phase,
47
- children: LABELS[status.phase]
48
- });
49
- }
50
- //#endregion
51
- //#region src/client/settings-card.tsx
52
- /**
53
- * dsh-session-guard — 插件配置卡片(settings.plugin.item 面)。
54
- *
55
- * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,
56
- * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch
57
- * (track + thumb),不是复选框对勾。
58
- *
59
- * 通过 `settingsScope.bind({ namespace: NS })` 绑定 host 已注册的
60
- * `session-guard` 命名空间;每次变更立即经 scope 提交(无 staged form)。
61
- * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,
62
- * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。
63
- */
64
- /** 卡片样式,注入一次(保持 bundle CSS-free)。 */
65
- const CARD_CSS = `
66
- .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}
67
- .sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}
68
- .sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
69
- .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}
70
- .sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
71
- .sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
72
- .sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
73
- .sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
74
- .sgCardChevron-open{transform:rotate(180deg)}
75
- .sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}
76
- .sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}
77
- .sgRow:last-child{border-bottom:0}
78
- .sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
79
- .sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
80
- .sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
81
- .sgSwitch{position:relative;width:40px;height:22px;flex:none}
82
- .sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}
83
- .sgSwitch>input:disabled{cursor:not-allowed}
84
- .sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}
85
- .sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
86
- .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
- .sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}
88
- .sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}
89
- `;
90
- /** 注入一次卡片样式。 */
91
- function injectCss() {
92
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"session-guard-card\"]") === null) {
93
- const tag = document.createElement("style");
94
- tag.dataset.plugin = "dsh-session-guard";
95
- tag.dataset.pluginCss = "session-guard-card";
96
- tag.textContent = CARD_CSS;
97
- document.head.appendChild(tag);
98
- }
99
- }
100
- /** 一行 pill switch(滑块开关,绑定 scope)。 */
101
- function SwitchRow(props) {
102
- const { label, description, checked, disabled, onChange } = props;
103
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
104
- className: "sgRow",
105
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
106
- className: "sgRowText",
107
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
108
- className: "sgTitle",
109
- children: label
110
- }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
111
- className: "sgDesc",
112
- children: description
113
- })]
114
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
115
- className: "sgSwitch",
116
- children: [
117
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
118
- type: "checkbox",
119
- checked,
120
- disabled,
121
- onChange: (e) => onChange(e.currentTarget.checked)
122
- }),
123
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "sgSwitchTrack" }),
124
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "sgSwitchThumb" })
125
- ]
126
- })]
127
- });
128
- }
129
- /** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */
130
- function SessionGuardCard({ scope }) {
131
- const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
132
- const unavailable = snapshot.status === "unavailable";
133
- const readonly = unavailable || !snapshot.writable;
134
- const value = snapshot.value ?? {};
135
- const [open, setOpen] = (0, react.useState)(false);
136
- injectCss();
137
- const toggle = (field, next) => {
138
- scope.set(field, next);
139
- };
140
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
141
- className: "sgCard" + (open ? " sgCard-open" : ""),
142
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
143
- type: "button",
144
- className: "sgCardHeader",
145
- "aria-expanded": open,
146
- onClick: () => setOpen(!open),
147
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
148
- className: "sgCardHeadtext",
149
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
150
- className: "sgCardName",
151
- children: "会话守护门禁"
152
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
153
- className: "sgCardDesc",
154
- children: "高峰自动暂停运行会话,周末模式无视峰谷畅快跑"
155
- })]
156
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
157
- className: "sgCardChevron" + (open ? " sgCardChevron-open" : ""),
158
- viewBox: "0 0 14 14",
159
- width: 14,
160
- height: 14,
161
- fill: "none",
162
- "aria-hidden": "true",
163
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
164
- d: "M3.5 5.5L7 9l3.5-3.5",
165
- stroke: "currentColor",
166
- strokeWidth: 1.5,
167
- strokeLinecap: "round",
168
- strokeLinejoin: "round"
169
- })
170
- })]
171
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
172
- className: "sgCardBody",
173
- children: unavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
174
- style: {
175
- margin: "0",
176
- padding: "12px 0",
177
- fontSize: "13px",
178
- color: "var(--dsw-alias-label-tertiary)"
179
- },
180
- children: "设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。"
181
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
182
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
183
- label: "高峰自动暂停冻结会话",
184
- description: "高峰时段自动暂停运行会话",
185
- checked: value.enabled ?? true,
186
- disabled: readonly,
187
- onChange: (next) => toggle("enabled", next)
188
- }),
189
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
190
- label: "低谷自动恢复",
191
- description: "低峰时段自动恢复被暂停的会话",
192
- checked: value.offPeakAutoResume ?? true,
193
- disabled: readonly,
194
- onChange: (next) => toggle("offPeakAutoResume", next)
195
- }),
196
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
197
- label: "周末模式",
198
- description: "识别周末,无视峰谷畅快跑",
199
- checked: value.weekendMode ?? true,
200
- disabled: readonly,
201
- onChange: (next) => toggle("weekendMode", next)
202
- }),
203
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
204
- label: "回退锁队列",
205
- description: "无会话门时锁等待队列",
206
- checked: value.queueFallback ?? true,
207
- disabled: readonly,
208
- onChange: (next) => toggle("queueFallback", next)
209
- }),
210
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
211
- label: "自动重试",
212
- description: "后端重试,默认关(保守)",
213
- checked: value.retryEnabled ?? false,
214
- disabled: readonly,
215
- onChange: (next) => toggle("retryEnabled", next)
216
- }),
217
- !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
218
- className: "sgReadonly",
219
- children: "当前只读,无法修改。"
220
- })
221
- ] })
222
- })]
223
- });
224
- }
225
- //#endregion
226
- //#region src/client/index.ts
227
- /**
228
- * dsh-session-guard — 浏览器 half。
229
- *
230
- * 职责(全部 fail-open,D8):
231
- * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询
232
- * /session-guard/status;
233
- * - 注册 `settings.plugin.item` 设置卡片,经 `settingsScope.bind({ namespace })`
234
- * 绑定 host 已注册的 `session-guard` 命名空间——这正是“插件配置”面板显示本
235
- * 插件的**必要**机制(对齐 dsh-thinking-levels / dsh-context);
236
- * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc
237
- * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。
238
- *
239
- * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。
240
- */
241
- /** 客户端所需服务:slots(状态徽标 + 设置卡片)+ locale + settingsScope(设置卡片绑定)。 */
242
- const inject = [
243
- "slots",
244
- "locale",
245
- "settingsScope"
246
- ];
247
- /** host 侧 src/settings.js 注册的命名空间(保持一致)。 */
248
- const NS = "session-guard";
249
- function apply(ctx) {
250
- ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
251
- name: "conversation.input.right",
252
- id: "session-guard-status",
253
- order: 50,
254
- locale: "session-guard"
255
- }, StatusBadge));
256
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
257
- name: "settings.plugin.item",
258
- id: NS,
259
- key: NS,
260
- locale: "session-guard",
261
- inject: () => ({ scope: ctx.settingsScope.bind({ namespace: NS }) })
262
- }, SessionGuardCard));
263
- }
264
- //#endregion
265
- exports.apply = apply;
266
- exports.inject = inject;
267
- return module.exports;
268
- }
269
- });
270
-
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-session-guard",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/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
+ return `${base} · ${mode}${held > 0 || deferred > 0 ? ` · 挂起 ${held} · 延后 ${deferred}` : ""}`;
29
+ }
30
+ //#endregion
31
+ //#region src/client/status-badge.tsx
32
+ /**
33
+ * dsh-session-guard — 状态徽标(纯展示,fail-open)。
34
+ *
35
+ * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;
36
+ * 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。
37
+ * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
38
+ */
39
+ const POLL_MS = 15e3;
40
+ /** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
41
+ function StatusBadge({ sessionId }) {
42
+ const [status, setStatus] = (0, react.useState)(null);
43
+ (0, react.useEffect)(() => {
44
+ if (!sessionId) return;
45
+ let cancelled = false;
46
+ const poll = async () => {
47
+ try {
48
+ const body = await (await fetch("/session-guard/status")).json().catch(() => null);
49
+ if (!cancelled && body?.ok && body.status) setStatus(body.status);
50
+ } catch {}
51
+ };
52
+ poll();
53
+ const timer = setInterval(poll, POLL_MS);
54
+ return () => {
55
+ cancelled = true;
56
+ clearInterval(timer);
57
+ };
58
+ }, [sessionId]);
59
+ if (!status || !status.enabled) return null;
60
+ const cls = status.phase === "peak" ? "sg-peak" : status.phase === "weekend" ? "sg-weekend" : "sg-off";
61
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
62
+ className: `sg-status ${cls}`,
63
+ title: badgeTitle(status),
64
+ "data-sg-phase": status.phase,
65
+ "data-sg-provider-guard": status.providerGuard === true ? "on" : "off",
66
+ children: peakLabel(status)
67
+ });
68
+ }
69
+ //#endregion
70
+ //#region src/client/settings-card.tsx
71
+ /**
72
+ * dsh-session-guard — 插件配置卡片(settings.plugin.item 面)。
73
+ *
74
+ * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,
75
+ * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch
76
+ * (track + thumb),不是复选框对勾。
77
+ *
78
+ * 通过 `settingsScope.bind({ namespace: NS })` 绑定 host 已注册的
79
+ * `session-guard` 命名空间;每次变更立即经 scope 提交(无 staged form)。
80
+ * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,
81
+ * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。
82
+ */
83
+ /** 卡片样式,注入一次(保持 bundle CSS-free)。 */
84
+ const CARD_CSS = `
85
+ .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}
86
+ .sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}
87
+ .sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
88
+ .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}
89
+ .sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
90
+ .sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
91
+ .sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
92
+ .sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
93
+ .sgCardChevron-open{transform:rotate(180deg)}
94
+ .sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}
95
+ .sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}
96
+ .sgRow:last-child{border-bottom:0}
97
+ .sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
98
+ .sgRowText-wide{padding-right:0}
99
+ .sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
100
+ .sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
101
+ .sgSwitch{position:relative;width:40px;height:22px;flex:none}
102
+ .sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}
103
+ .sgSwitch>input:disabled{cursor:not-allowed}
104
+ .sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}
105
+ .sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
106
+ .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}
107
+ .sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}
108
+ .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}
109
+ .sgInput:disabled{opacity:.6;cursor:not-allowed}
110
+ .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}
111
+ .sgSelect:disabled{opacity:.6;cursor:not-allowed}
112
+ .sgHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}
113
+ .sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}
114
+ `;
115
+ /** 注入一次卡片样式。 */
116
+ function injectCss() {
117
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"session-guard-card\"]") === null) {
118
+ const tag = document.createElement("style");
119
+ tag.dataset.plugin = "dsh-session-guard";
120
+ tag.dataset.pluginCss = "session-guard-card";
121
+ tag.textContent = CARD_CSS;
122
+ document.head.appendChild(tag);
123
+ }
124
+ }
125
+ /** 一行 pill switch(滑块开关,绑定 scope)。 */
126
+ function SwitchRow(props) {
127
+ const { label, description, checked, disabled, onChange } = props;
128
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
129
+ className: "sgRow",
130
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
131
+ className: "sgRowText",
132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
133
+ className: "sgTitle",
134
+ children: label
135
+ }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
136
+ className: "sgDesc",
137
+ children: description
138
+ })]
139
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
140
+ className: "sgSwitch",
141
+ children: [
142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
143
+ type: "checkbox",
144
+ checked,
145
+ disabled,
146
+ onChange: (e) => onChange(e.currentTarget.checked)
147
+ }),
148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "sgSwitchTrack" }),
149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "sgSwitchThumb" })
150
+ ]
151
+ })]
152
+ });
153
+ }
154
+ /** 一行文本输入(失焦提交;外部值变化时同步草稿)。 */
155
+ function TextRow(props) {
156
+ const { label, description, value, placeholder, disabled, onCommit } = props;
157
+ const [draft, setDraft] = (0, react.useState)(value);
158
+ (0, react.useEffect)(() => {
159
+ setDraft(value);
160
+ }, [value]);
161
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
162
+ className: "sgRow",
163
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
164
+ className: "sgRowText sgRowText-wide",
165
+ children: [
166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
167
+ className: "sgTitle",
168
+ children: label
169
+ }),
170
+ description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
171
+ className: "sgDesc",
172
+ children: description
173
+ }),
174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
175
+ className: "sgInput",
176
+ type: "text",
177
+ value: draft,
178
+ placeholder,
179
+ disabled,
180
+ onChange: (e) => setDraft(e.currentTarget.value),
181
+ onBlur: () => onCommit(draft)
182
+ })
183
+ ]
184
+ })
185
+ });
186
+ }
187
+ /** 一行「逗号/换行分隔」的字符串列表(失焦提交为数组)。 */
188
+ function ListRow(props) {
189
+ const { label, description, values, placeholder, disabled, onCommit } = props;
190
+ const joined = values.join(", ");
191
+ const [draft, setDraft] = (0, react.useState)(joined);
192
+ (0, react.useEffect)(() => {
193
+ setDraft(joined);
194
+ }, [joined]);
195
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
196
+ className: "sgRow",
197
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
198
+ className: "sgRowText sgRowText-wide",
199
+ children: [
200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
201
+ className: "sgTitle",
202
+ children: label
203
+ }),
204
+ description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
205
+ className: "sgDesc",
206
+ children: description
207
+ }),
208
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
209
+ className: "sgInput",
210
+ type: "text",
211
+ value: draft,
212
+ placeholder,
213
+ disabled,
214
+ onChange: (e) => setDraft(e.currentTarget.value),
215
+ onBlur: () => {
216
+ const next = draft.split(/[,\n]/).map((s) => s.trim()).filter((s) => s !== "");
217
+ onCommit(next);
218
+ }
219
+ })
220
+ ]
221
+ })
222
+ });
223
+ }
224
+ /** 一行下拉选择。 */
225
+ function SelectRow(props) {
226
+ const { label, description, value, options, disabled, onChange } = props;
227
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
228
+ className: "sgRow",
229
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
+ className: "sgRowText",
231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
232
+ className: "sgTitle",
233
+ children: label
234
+ }), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
235
+ className: "sgDesc",
236
+ children: description
237
+ })]
238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
239
+ className: "sgSelect",
240
+ value,
241
+ disabled,
242
+ onChange: (e) => onChange(e.currentTarget.value),
243
+ children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
244
+ value: o.value,
245
+ children: o.label
246
+ }, o.value))
247
+ })]
248
+ });
249
+ }
250
+ /** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */
251
+ function SessionGuardCard({ scope }) {
252
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
253
+ const unavailable = snapshot.status === "unavailable";
254
+ const readonly = unavailable || !snapshot.writable;
255
+ const value = snapshot.value ?? {};
256
+ const [open, setOpen] = (0, react.useState)(false);
257
+ injectCss();
258
+ const toggle = (field, next) => {
259
+ scope.set(field, next);
260
+ };
261
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
262
+ className: "sgCard" + (open ? " sgCard-open" : ""),
263
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
264
+ type: "button",
265
+ className: "sgCardHeader",
266
+ "aria-expanded": open,
267
+ onClick: () => setOpen(!open),
268
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
269
+ className: "sgCardHeadtext",
270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
271
+ className: "sgCardName",
272
+ children: "会话守护门禁"
273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
274
+ className: "sgCardDesc",
275
+ children: "高峰自动暂停运行会话,周末模式无视峰谷畅快跑"
276
+ })]
277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
278
+ className: "sgCardChevron" + (open ? " sgCardChevron-open" : ""),
279
+ viewBox: "0 0 14 14",
280
+ width: 14,
281
+ height: 14,
282
+ fill: "none",
283
+ "aria-hidden": "true",
284
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
285
+ d: "M3.5 5.5L7 9l3.5-3.5",
286
+ stroke: "currentColor",
287
+ strokeWidth: 1.5,
288
+ strokeLinecap: "round",
289
+ strokeLinejoin: "round"
290
+ })
291
+ })]
292
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
293
+ className: "sgCardBody",
294
+ children: unavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
295
+ style: {
296
+ margin: "0",
297
+ padding: "12px 0",
298
+ fontSize: "13px",
299
+ color: "var(--dsw-alias-label-tertiary)"
300
+ },
301
+ children: "设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。"
302
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
304
+ label: "高峰自动暂停冻结会话",
305
+ description: "高峰时段自动暂停运行会话",
306
+ checked: value.enabled ?? true,
307
+ disabled: readonly,
308
+ onChange: (next) => toggle("enabled", next)
309
+ }),
310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
311
+ label: "官方源二维判定",
312
+ description: "高峰期只拦 DeepSeek 官方源;本地/第三方 provider 照常跑",
313
+ checked: value.providerGuard ?? true,
314
+ disabled: readonly,
315
+ onChange: (next) => toggle("providerGuard", next)
316
+ }),
317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
318
+ label: "追加官方 provider id",
319
+ description: "精确匹配,优先级最高(逗号分隔)",
320
+ values: value.officialProviders ?? [],
321
+ placeholder: "deepseek-official",
322
+ disabled: readonly,
323
+ onCommit: (next) => {
324
+ scope.set("officialProviders", next);
325
+ }
326
+ }),
327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
328
+ label: "官方端点名单",
329
+ description: "baseURL 归一化后的 host(逗号分隔)",
330
+ values: value.officialBaseURLs ?? [],
331
+ placeholder: "api.deepseek.com",
332
+ disabled: readonly,
333
+ onCommit: (next) => {
334
+ scope.set("officialBaseURLs", next);
335
+ }
336
+ }),
337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
338
+ label: "拦截方式",
339
+ description: "挂起等待不报错;或报错并记入延后队列",
340
+ value: value.deferredMode ?? "hold",
341
+ options: [{
342
+ value: "hold",
343
+ label: "挂起等待(退峰自动放行)"
344
+ }, {
345
+ value: "error",
346
+ label: "报错并延后(退峰续跑)"
347
+ }],
348
+ disabled: readonly,
349
+ onChange: (next) => {
350
+ scope.set("deferredMode", next);
351
+ }
352
+ }),
353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
354
+ label: "退峰自动继续",
355
+ description: "关闭后延后的请求/会话不自动续跑,需手动 /resume",
356
+ checked: value.deferredResume ?? true,
357
+ disabled: readonly,
358
+ onChange: (next) => toggle("deferredResume", next)
359
+ }),
360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextRow, {
361
+ label: "退峰续跑文案",
362
+ description: "error 模式退峰时发送的消息内容",
363
+ value: value.deferredResumeText ?? "",
364
+ placeholder: "继续(高峰已过,自动继续)",
365
+ disabled: readonly,
366
+ onCommit: (next) => {
367
+ scope.set("deferredResumeText", next);
368
+ }
369
+ }),
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
371
+ label: "纳入子代理请求",
372
+ description: "子代理请求同样计费,默认一并拦截",
373
+ checked: value.guardSubagents ?? true,
374
+ disabled: readonly,
375
+ onChange: (next) => toggle("guardSubagents", next)
376
+ }),
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
378
+ label: "低谷自动恢复",
379
+ description: "低峰时段自动恢复被暂停的会话",
380
+ checked: value.offPeakAutoResume ?? true,
381
+ disabled: readonly,
382
+ onChange: (next) => toggle("offPeakAutoResume", next)
383
+ }),
384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
385
+ label: "周末模式",
386
+ description: "识别周末,无视峰谷畅快跑",
387
+ checked: value.weekendMode ?? true,
388
+ disabled: readonly,
389
+ onChange: (next) => toggle("weekendMode", next)
390
+ }),
391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
392
+ label: "回退锁队列",
393
+ description: "无会话门时锁等待队列",
394
+ checked: value.queueFallback ?? true,
395
+ disabled: readonly,
396
+ onChange: (next) => toggle("queueFallback", next)
397
+ }),
398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
399
+ label: "自动重试",
400
+ description: "后端重试,默认关(保守)",
401
+ checked: value.retryEnabled ?? false,
402
+ disabled: readonly,
403
+ onChange: (next) => toggle("retryEnabled", next)
404
+ }),
405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
406
+ className: "sgHint",
407
+ children: "判定口径:显式 id 名单 → baseURL 端点 → catalog 默认端点 → 内置 id。 排查误判访问 /session-guard/provider?provider=<id> 看 matchedBy。"
408
+ }),
409
+ !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
410
+ className: "sgReadonly",
411
+ children: "当前只读,无法修改。"
412
+ })
413
+ ] })
414
+ })]
415
+ });
416
+ }
417
+ //#endregion
418
+ //#region src/client/index.ts
419
+ /**
420
+ * dsh-session-guard — 浏览器 half。
421
+ *
422
+ * 职责(全部 fail-open,D8):
423
+ * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询
424
+ * /session-guard/status;
425
+ * - 注册 `settings.plugin.item` 设置卡片,经 `settingsScope.bind({ namespace })`
426
+ * 绑定 host 已注册的 `session-guard` 命名空间——这正是“插件配置”面板显示本
427
+ * 插件的**必要**机制(对齐 dsh-thinking-levels / dsh-context);
428
+ * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc
429
+ * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。
430
+ *
431
+ * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。
432
+ */
433
+ /** 客户端所需服务:slots(状态徽标 + 设置卡片)+ locale + settingsScope(设置卡片绑定)。 */
434
+ const inject = [
435
+ "slots",
436
+ "locale",
437
+ "settingsScope"
438
+ ];
439
+ /** host 侧 src/settings.js 注册的命名空间(保持一致)。 */
440
+ const NS = "session-guard";
441
+ function apply(ctx) {
442
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
443
+ name: "conversation.input.right",
444
+ id: "session-guard-status",
445
+ order: 50,
446
+ locale: "session-guard"
447
+ }, StatusBadge));
448
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
449
+ name: "settings.plugin.item",
450
+ id: NS,
451
+ key: NS,
452
+ locale: "session-guard",
453
+ inject: () => ({ scope: ctx.settingsScope.bind({ namespace: NS }) })
454
+ }, SessionGuardCard));
455
+ }
456
+ //#endregion
457
+ exports.apply = apply;
458
+ exports.inject = inject;
459
+ return module.exports;
460
+ }
461
+ });
462
+
271
463
  //# sourceMappingURL=client.js.map