dsh-session-guard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-session-guard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ <p align="center">
2
+ <strong>高峰自动会话门:周末模式 + 高峰自动暂停 + 会话级冻结 + 后端自动重试</strong>
3
+ </p>
4
+ <p align="center">
5
+ <a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
6
+ <img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" data-canonical-src="https://img.shields.io/badge/dsh--plugin-ready-478CBF?logo=deepseek&amp;logoColor=white" style="max-width: 100%;">
7
+ <img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
8
+ </p>
9
+
10
+ # dsh-session-guard
11
+
12
+ > 高峰时段自动暂停运行中的会话、低峰/周末自动续跑;配合 input-traffic 的冻结按钮做到**会话级**锁定;后端**自动重试**在冻结/门控期间让路。核心基于**自研会话门**(`agent.cancel keepInbox + goals.pause + session/event 安全边界 + followup 续跑`),不再依赖 dsh-task-control。
13
+
14
+ 无需修改 dsh 源码、无需提 PR:`dsh plugin` 命令组装 + bundle patch 装配的 cordis 插件。
15
+
16
+ > 💡 **为什么推荐**:DeepSeek 已于 2026-08-17 实行**峰谷计费**——高峰时段(北京时间 9:00-12:00、14:00-18:00)单价为闲时(含午间、夜间、周末与节假日)的 **2 倍**。本插件在高峰自动停住运行会话、退峰自动续跑,错峰长跑最多可省 **50%**;手动冻结(配 input-traffic 按钮)可进一步按会话精确控停。
17
+
18
+ ## 功能一览
19
+
20
+ - **周末模式**:识别周末(基于配置时区 `Intl.DateTimeFormat`,不踩裸 `getUTCDay()` 的北京边界 8 小时 bug)→ 周末无视峰谷、畅快跑。
21
+ - **高峰自动暂停(全局)**:进入高峰(且非周末)时,对所有 running root session 自动暂停;退峰自动恢复全部——**全局开关,无需手动**。
22
+ - **会话级冻结 / 恢复**:`sessionGuard` 冗余端口 + `POST /session-guard/rpc`,input-traffic 冻结按钮逐会话透传接入;也提供 `/pause /resume /cancel` 手动命令。
23
+ - **后端自动重试(D9)**:turn/end 瞬时失败(error/429/max-tokens)自适应退避自动续跑;永久失败停止;**冻结/门控期间让路**,绝不绕过会话门。
24
+ - **fail-open**:自研会话门不可用、session-guard 未装、设置服务缺失——均静默降级,绝不因依赖而崩。
25
+
26
+ ## 安装
27
+
28
+ ```bash
29
+ dsh plugin --profile web add github:<owner>/dsh-session-guard
30
+ ```
31
+
32
+ 装后重启 dsh web 并刷新页面。
33
+
34
+ ## 设置(设置 → 插件 → session-guard,简单开关)
35
+
36
+ | 开关 | 默认 | 说明 |
37
+ |---|---|---|
38
+ | `enabled` | on | **高峰自动暂停冻结会话**:高峰时段自动暂停运行会话 |
39
+ | `offPeakAutoResume` | on | **低谷自动恢复**:低峰时段自动恢复被暂停的会话;关掉则退峰不自动恢复(需手动) |
40
+ | `weekendMode` | on | **周末模式**:识别周末 → 周末不自动暂停(周末本无高峰,畅快跑) |
41
+ | `queueFallback` | on | 自研会话门不可用时回退锁等待队列(fail-open) |
42
+ | `retryEnabled` | off | **自动重试(后端)**:瞬时失败自动续跑(默认关,保守) |
43
+
44
+ 附属配置:
45
+
46
+ - `timezone`(默认 Asia/Shanghai)、`peakWindows`(默认 09:00–12:00 / 14:00–18:00);
47
+ - `pauseMode`(`safe`/`force`)、`pauseReason`(`wait`/`stop`)——暂停推进方式;
48
+ - 重试参数:`retryText`、`retryGraceMs`、`retryCooldownMs`、`retryBackoffFactor`、`retryBackoffMaxMs`、`retryMaxConsecutive`。
49
+
50
+ ## 行为
51
+
52
+ ### 高峰自动门(全局)
53
+
54
+ - **入峰**(且非周末):对所有 running root session 调 `gate.stopNextTurn`——自研会话门真暂停(不打断推理,推理完成/工具派发前落在安全边界暂停),或按 `queueFallback` 回退锁等待队列;
55
+ - **退峰 / 周末**:`gate.resume` **全部**会话(自动续跑,无需手动)——受 `offPeakAutoResume` 开关控制,关掉则退峰不自动恢复;
56
+ - 状态机:单实例 `NORMAL ↔ PAUSED_PEAK`(`scheduler.js`),由单一 30s tick 驱动。
57
+
58
+ ### 会话锁定(冻结)
59
+
60
+ - **冗余端口**:`ctx.provide('sessionGuard', service)`——`stopNextTurn(sessionId)` / `resume(sessionId)` / `lockQueue(sessionId)` / `unlockQueue(sessionId)` / `state(sessionId)`;
61
+ - **RPC 桥**:`POST /session-guard/rpc { action, sessionId }`——input-traffic 冻结按钮按 `sessionId` **逐会话**调用 `stopNextTurn` / `resume`;本插件未装则静默跳过(D8 fail-open);
62
+ - **手动命令**:`/pause [force|safe] [stop|wait]`、`/resume [confirm] [rerun|skip]`、`/cancel`——作用于调用它的会话(取 `invocation.agent.id`)。
63
+
64
+ ### 后端自动重试(D9)
65
+
66
+ 监听 `turn/end`,将失败分类:
67
+
68
+ - **瞬时失败**(error/429/max-tokens 等)→ 自适应退避自动 `followup(retryText)` 续跑;
69
+ - **永久失败**(鉴权/余额/模型/上下文超限)→ 停止;
70
+ - **冻结/门控期间让路**:`isFrozen(sessionId)` 为真(queueLocked / paused / taskControl paused)时不重试;
71
+ - 用户介入或成功回合重置连续失败计数。
72
+
73
+ ### 与 input-traffic 协作
74
+
75
+ - input-traffic 的**冻结按钮**触发时经 `sessionGuard.stopNextTurn`(RPC,会话级)透传服务端;
76
+ - input-traffic **只做冻结增强**(队列冻结/解冻 + composer block),重试归本插件后端;
77
+ - 两者共享「会话隔离」语义:input-traffic 冻结队列按 sessionId 隔离,session-guard RPC 同样按 sessionId 锁。
78
+
79
+ ## 冗余端口 `sessionGuard`
80
+
81
+ ```js
82
+ {
83
+ stopNextTurn(sessionId, opts), // 停掉 session 下一回合(自研会话门 / 回退锁队列)
84
+ resume(sessionId, opts), // 恢复(confirm + choice: rerun|skip)
85
+ lockQueue(sessionId, reason), // 显式锁队列
86
+ unlockQueue(sessionId), // 显式解锁
87
+ state(sessionId), // { queueLocked, lockReason, paused, taskControlAvailable, taskControl }
88
+ }
89
+ ```
90
+
91
+ ## HTTP 路由
92
+
93
+ - `GET /session-guard/state?session=<id>` — 会话状态
94
+ - `GET /session-guard/settings` — 设置 + taskControl 可用性
95
+ - `GET /session-guard/status` — 全局当前阶段(状态徽标轮询)
96
+ - `GET /session-guard/diag` — 运行时诊断
97
+ - `POST /session-guard/rpc` — `{ action: stopNextTurn|resume|lockQueue|unlockQueue|state, sessionId }`
98
+
99
+ ## 状态存储
100
+
101
+ 每会话 JSON:`$DSH_HOME/.dsh/session-guard/<sessionId>.json`(原子写;`DSH_SESSION_GUARD_STATE_DIR` 可覆盖)。
102
+
103
+ ## 测试
104
+
105
+ ```bash
106
+ npm test # node --test tests/*.test.mjs(时区/周末/状态机/会话门/桥接/重试)
107
+ ```
108
+
109
+ ## 模块
110
+
111
+ | 文件 | 职责 |
112
+ |---|---|
113
+ | `src/time.js` | 高峰/周末判定(时区正确) |
114
+ | `src/scheduler.js` | 纯状态机 NORMAL ↔ PAUSED_PEAK |
115
+ | `src/pause-gate.js` | 自研会话门引擎(agent.cancel keepInbox + goals.pause + 安全边界 + followup 续跑) |
116
+ | `src/pause-store.js` | 自研暂停状态持久化 |
117
+ | `src/gate.js` | 会话门驱动(自研真暂停 / 回退锁队列,fail-open) |
118
+ | `src/bridge.js` | `sessionGuard` 冗余端口 |
119
+ | `src/retry.js` | 后端自动重试(失败分类/退避/冻结让路) |
120
+ | `src/detect.js` | 自动检测(host taskControl / client input-traffic 桥) |
121
+ | `src/store.js` | 每会话持久化状态 |
122
+ | `src/settings.js` | 设置子板块(schemastery schema + fail-open 注册) |
123
+ | `src/index.js` | host apply(设置/路由/tick/提供服务/重试接线) |
124
+ | `src/client/` | 浏览器 half(状态徽标 + 设置卡片) |
125
+
126
+ ## License
127
+
128
+ MIT — 见 [LICENSE](LICENSE)。
@@ -0,0 +1,10 @@
1
+ # dsh-session-guard bundle patch:一行装配进 profile。
2
+ #
3
+ # host 半(src/index.js)注册 sessionGuard 服务(冗余端口,供 input-traffic 适配)、
4
+ # 高峰自动会话门(taskControl.pause/resume,缺失时回退锁等待队列)、
5
+ # /session-guard/* 路由与 设置→插件→session-guard 子板块(简单开关)。
6
+ # 浏览器半(src/client/index.js,经 package.json dsh.client 声明)提供
7
+ # 高峰状态徽标 + 无 input-traffic 时的回退冻结按钮。
8
+ - insert:
9
+ - id: session-guard
10
+ name: dsh-session-guard
package/lib/client.js ADDED
@@ -0,0 +1,271 @@
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
+
271
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "dsh-session-guard",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "高峰自动会话门:周末模式 + 高峰自动暂停(全局)+ 会话级冻结/恢复 + 后端自动重试;周末识别正确、退峰自动续跑。设置位于 设置 → 插件 → session-guard,简单开关。",
6
+ "keywords": [
7
+ "deepseek-harness",
8
+ "dsh",
9
+ "plugin",
10
+ "peak",
11
+ "off-peak",
12
+ "weekend",
13
+ "pause",
14
+ "freeze",
15
+ "retry",
16
+ "session-gate"
17
+ ],
18
+ "license": "MIT",
19
+ "main": "src/index.js",
20
+ "exports": {
21
+ ".": "./src/index.js",
22
+ "./client": "./lib/client.js",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "lib/",
28
+ "cordis.patch.yml",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "dsh": {
33
+ "client": {
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-runtime",
36
+ "@deepseek-ai/dsh-client-locale",
37
+ "@deepseek-ai/dsh-client-ui-slots",
38
+ "@deepseek-ai/dsh-client-ui-settings"
39
+ ],
40
+ "platform": "web"
41
+ },
42
+ "bundle": {
43
+ "patch": "./cordis.patch.yml"
44
+ }
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "lint": "eslint src tests",
49
+ "test": "node --test \"tests/*.test.mjs\""
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "dependencies": {
55
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
56
+ "@deepseek-ai/schemastery": "^3.18.1"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^10.0.1",
60
+ "@types/react": "~18.3.1",
61
+ "eslint": "^10.8.1",
62
+ "react": "^18.2.0",
63
+ "tsdown": "^0.22.2",
64
+ "typescript": "^5.6.0",
65
+ "typescript-eslint": "^8.67.0"
66
+ }
67
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * dsh-session-guard — sessionGuard 冗余端口(D5/D6/D8)。
3
+ *
4
+ * `ctx.provide('sessionGuard', service)`:
5
+ * - stopNextTurn / resume:会话门(透传 taskControl,或回退锁队列)。
6
+ * - lockQueue / unlockQueue:显式队列锁(供 input-traffic 冻结按钮桥接,
7
+ * 或未来手动触发)。
8
+ * - state(sessionId):{ queueLocked, lockReason, taskControlAvailable, taskControl }。
9
+ *
10
+ * input-traffic(方案 A,基于 feat/absorb-auto-continue 分支改造)在冻结按钮
11
+ * 触发时调用 stopNextTurn;本服务不存在时 input-traffic 静默跳过(fail-open,D8)。
12
+ */
13
+ import { idleState } from './store.js'
14
+
15
+ /**
16
+ * @param {object} ctx host context
17
+ * @param {ReturnType<import('./gate.js').createGate>} gate
18
+ * @param {ReturnType<import('./store.js').createStore>} store
19
+ */
20
+ export function createBridge(ctx, gate, store, pauseGate) {
21
+ function taskControlState(sessionId) {
22
+ let tcState = null
23
+ try {
24
+ const tc = ctx && typeof ctx.get === 'function' ? ctx.get('taskControl') : undefined
25
+ if (tc && typeof tc.state === 'function') {
26
+ const s = tc.state(sessionId)
27
+ tcState = {
28
+ status: s && s.status,
29
+ paused: !!(s && s.paused),
30
+ forced: !!(s && s.forced),
31
+ }
32
+ }
33
+ } catch {
34
+ /* 状态读取失败 → 保持 null,fail-open */
35
+ }
36
+ return tcState
37
+ }
38
+
39
+ return {
40
+ /** 停掉 session 下一回合(会话门 / 回退锁队列)。 */
41
+ stopNextTurn(sessionId, opts) {
42
+ return gate.stopNextTurn(sessionId, opts)
43
+ },
44
+ /** 恢复。 */
45
+ resume(sessionId, opts) {
46
+ return gate.resume(sessionId, opts)
47
+ },
48
+ /** 显式锁队列(手动触发)。 */
49
+ lockQueue(sessionId, reason = 'manual') {
50
+ const next = gate.lockQueue(sessionId, reason)
51
+ return { ok: true, state: next }
52
+ },
53
+ /** 显式解锁队列。 */
54
+ unlockQueue(sessionId) {
55
+ const next = gate.unlockQueue(sessionId)
56
+ return { ok: true, state: next }
57
+ },
58
+ /** 读一会话状态。 */
59
+ state(sessionId) {
60
+ const cur = store.get(sessionId)
61
+ const base = cur || idleState(sessionId)
62
+ const pausedState = pauseGate ? (pauseGate.state(sessionId) || {}) : {}
63
+ return {
64
+ sessionId: String(sessionId),
65
+ queueLocked: base.queueLocked === true,
66
+ lockReason: base.lockReason ?? null,
67
+ // 自研会话门真暂停状态(脱离 task-control)。
68
+ paused: pausedState.paused === true,
69
+ pausedForced: pausedState.forced === true,
70
+ taskControlAvailable: gate.taskControlAvailable(),
71
+ taskControl: taskControlState(sessionId),
72
+ updatedAt: base.updatedAt ?? null,
73
+ }
74
+ },
75
+ }
76
+ }