dsh-session-guard 3.2.4 → 4.0.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.
@@ -1,30 +1,30 @@
1
- /**
2
- * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。
3
- *
4
- * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线
5
- * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。
6
- * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件
7
- * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。
8
- */
9
-
10
- /** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */
11
- export const CLIENT_CSS = `
12
- .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}
13
- .sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}
14
- .sg-pause:disabled{opacity:.55;cursor:default}
15
- .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)}
16
- .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}
17
- .sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}
18
- .sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}
19
- `
20
-
21
- /** 注入一次(幂等;无 document 时静默跳过)。 */
22
- export function injectClientCss(): void {
23
- if (typeof document === 'undefined') return
24
- if (document.querySelector('style[data-plugin-css="session-guard-client"]') !== null) return
25
- const tag = document.createElement('style')
26
- tag.dataset.plugin = 'dsh-session-guard'
27
- tag.dataset.pluginCss = 'session-guard-client'
28
- tag.textContent = CLIENT_CSS
29
- document.head.appendChild(tag)
30
- }
1
+ /**
2
+ * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。
3
+ *
4
+ * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线
5
+ * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。
6
+ * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件
7
+ * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。
8
+ */
9
+
10
+ /** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */
11
+ export const CLIENT_CSS = `
12
+ .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}
13
+ .sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}
14
+ .sg-pause:disabled{opacity:.55;cursor:default}
15
+ .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)}
16
+ .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}
17
+ .sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}
18
+ .sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}
19
+ `
20
+
21
+ /** 注入一次(幂等;无 document 时静默跳过)。 */
22
+ export function injectClientCss(): void {
23
+ if (typeof document === 'undefined') return
24
+ if (document.querySelector('style[data-plugin-css="session-guard-client"]') !== null) return
25
+ const tag = document.createElement('style')
26
+ tag.dataset.plugin = 'dsh-session-guard'
27
+ tag.dataset.pluginCss = 'session-guard-client'
28
+ tag.textContent = CLIENT_CSS
29
+ document.head.appendChild(tag)
30
+ }
package/src/detect.js CHANGED
@@ -1,27 +1,27 @@
1
- /**
2
- * dsh-session-guard — 自动检测(D7,诚实边界)。
3
- *
4
- * - host 可自动探测:`ctx.get('taskControl')`(会话门)、`ctx.get('sessionGuard')`。
5
- * - client 可自动探测:input-traffic 补的「最小桥」标记 `window.__DSH_SESSION_GUARD_BRIDGE__`
6
- * (方案 A 中 input-traffic 冻结按钮触发时会调 sessionGuard.stopNextTurn)。
7
- * 探测不到 → 本插件客户端使用自带回退冻结(fallback),fail-open 不报错(D8)。
8
- */
9
-
10
- /** host:会话门(dsh-task-control)是否可用。 */
11
- export function detectTaskControl(ctx) {
12
- return !!(ctx && typeof ctx.get === 'function' && ctx.get('taskControl'))
13
- }
14
-
15
- /** host:sessionGuard 服务是否已提供(其他插件探测用)。 */
16
- export function detectSessionGuard(ctx) {
17
- return !!(ctx && typeof ctx.get === 'function' && ctx.get('sessionGuard'))
18
- }
19
-
20
- /**
21
- * client:input-traffic 最小桥是否已注册。
22
- * @param {object} [g] 显式传入全局对象(便于测试);默认 globalThis
23
- */
24
- export function detectInputTrafficBridge(g) {
25
- const root = g || (typeof globalThis !== 'undefined' ? globalThis : {})
26
- return !!(root && root.__DSH_SESSION_GUARD_BRIDGE__ && typeof root.__DSH_SESSION_GUARD_BRIDGE__.stopNextTurn === 'function')
27
- }
1
+ /**
2
+ * dsh-session-guard — 自动检测(D7,诚实边界)。
3
+ *
4
+ * - host 可自动探测:`ctx.get('taskControl')`(会话门)、`ctx.get('sessionGuard')`。
5
+ * - client 可自动探测:input-traffic 补的「最小桥」标记 `window.__DSH_SESSION_GUARD_BRIDGE__`
6
+ * (方案 A 中 input-traffic 冻结按钮触发时会调 sessionGuard.stopNextTurn)。
7
+ * 探测不到 → 本插件客户端使用自带回退冻结(fallback),fail-open 不报错(D8)。
8
+ */
9
+
10
+ /** host:会话门(dsh-task-control)是否可用。 */
11
+ export function detectTaskControl(ctx) {
12
+ return !!(ctx && typeof ctx.get === 'function' && ctx.get('taskControl'))
13
+ }
14
+
15
+ /** host:sessionGuard 服务是否已提供(其他插件探测用)。 */
16
+ export function detectSessionGuard(ctx) {
17
+ return !!(ctx && typeof ctx.get === 'function' && ctx.get('sessionGuard'))
18
+ }
19
+
20
+ /**
21
+ * client:input-traffic 最小桥是否已注册。
22
+ * @param {object} [g] 显式传入全局对象(便于测试);默认 globalThis
23
+ */
24
+ export function detectInputTrafficBridge(g) {
25
+ const root = g || (typeof globalThis !== 'undefined' ? globalThis : {})
26
+ return !!(root && root.__DSH_SESSION_GUARD_BRIDGE__ && typeof root.__DSH_SESSION_GUARD_BRIDGE__.stopNextTurn === 'function')
27
+ }
package/src/gate.js CHANGED
@@ -1,93 +1,93 @@
1
- /**
2
- * dsh-session-guard — 会话门驱动(D3/D5/D6,自研真实锁定)。
3
- *
4
- * **主路径**(替换 dsh-task-control):经自研 `pauseGate` 真暂停 session 推进——
5
- * `pauseGate.pause(id,{mode,reason})` 调用 agent.cancel / goals.pause / session/event
6
- * 安全边界落地(见 src/pause-gate.js),不再依赖 `ctx.get('taskControl')`,也不再退化成
7
- * 只写 `queueLocked` 标记。
8
- *
9
- * **降级路径**(fail-open,D8):仅当自研暂停因 agent 不可用而失败时,按 `queueFallback`
10
- * 回退为只锁等待队列(`queueLocked` 持久化标记,供前端/重试让路)。
11
- *
12
- * resume 对称:自研 resume(从暂停点续跑)或清 queueLock。
13
- */
14
- import { idleState } from './store.js'
15
-
16
- /**
17
- * @param {object} deps
18
- * @param {()=>object|null} deps.getCtx 取当前 host ctx(懒,便于测试)
19
- * @param {()=>object} deps.getSettings 读实时配置(pauseMode/pauseReason)
20
- * @param {ReturnType<import('./store.js').createStore>} deps.store 持久化队列锁
21
- * @param {ReturnType<import('./pause-gate.js').createPauseGate>} [deps.pauseGate] 自研会话门引擎
22
- * @param {(event:string, sessionId:string, payload?:object)=>void} [deps.emit] 状态变化通知
23
- */
24
- export function createGate({ getCtx: _getCtx, getSettings, store, pauseGate, emit }) {
25
- function lockQueue(sessionId, reason) {
26
- const cur = store.get(sessionId)
27
- const next = { ...(cur || idleState(sessionId)), queueLocked: true, lockReason: reason, updatedAt: Date.now() }
28
- store.set(sessionId, next)
29
- if (emit) emit('queue-lock', sessionId, { reason })
30
- return next
31
- }
32
-
33
- function unlockQueue(sessionId) {
34
- const cur = store.get(sessionId)
35
- const next = { ...(cur || idleState(sessionId)), queueLocked: false, lockReason: null, updatedAt: Date.now() }
36
- store.set(sessionId, next)
37
- if (emit) emit('queue-unlock', sessionId)
38
- return next
39
- }
40
-
41
- /**
42
- * 停掉 session 的下一回合(真实锁定推进)。
43
- * ① 自研 pauseGate.pause(真暂停);② 自研失败/agent 不可用 → 按 queueFallback 降级锁队列。
44
- * @param {string} sessionId
45
- * @param {object} [opts] { mode, reason }
46
- */
47
- async function stopNextTurn(sessionId, opts = {}) {
48
- const cfg = getSettings()
49
- if (pauseGate) {
50
- try {
51
- const mode = opts.mode ?? cfg.pauseMode ?? 'safe'
52
- const reason = opts.reason ?? cfg.pauseReason ?? 'wait'
53
- const r = pauseGate.pause(sessionId, { mode, reason })
54
- if (r && r.kind === 'success') return { ok: true, via: 'pauseGate', result: r }
55
- // kind === 'error'(如 no live agent)→ 落到降级,语义如实上报。
56
- } catch {
57
- // 自研内部异常 → fail-open 降级,绝不中断冻结动作。
58
- }
59
- }
60
- // 无自研门或自研失败 → 回退只锁等待队列(自带实现,D5)。
61
- if (cfg.queueFallback === false) {
62
- return { ok: false, via: 'queueLock', error: 'queueFallback disabled' }
63
- }
64
- lockQueue(sessionId, 'peak')
65
- return { ok: true, via: 'queueLock' }
66
- }
67
-
68
- /**
69
- * 恢复 session 推进(从暂停点续跑)。
70
- * ① 自研 resume(confirm + choice);② 降级清 queueLock。
71
- */
72
- async function resume(sessionId, opts = {}) {
73
- if (pauseGate) {
74
- try {
75
- const r = pauseGate.resume(sessionId, { confirm: true, choice: opts.choice ?? 'rerun' })
76
- if (r && r.kind === 'success') return { ok: true, via: 'pauseGate', result: r }
77
- } catch {
78
- // 降级清队列锁。
79
- }
80
- }
81
- unlockQueue(sessionId)
82
- return { ok: true, via: 'queueLock' }
83
- }
84
-
85
- return {
86
- stopNextTurn,
87
- resume,
88
- lockQueue,
89
- unlockQueue,
90
- /** 自研会话门是否可用(自研后恒视为可用;供 detect/UI 展示)。 */
91
- taskControlAvailable: () => (pauseGate ? pauseGate.taskControlAvailable() : false),
92
- }
93
- }
1
+ /**
2
+ * dsh-session-guard — 会话门驱动(D3/D5/D6,自研真实锁定)。
3
+ *
4
+ * **主路径**(替换 dsh-task-control):经自研 `pauseGate` 真暂停 session 推进——
5
+ * `pauseGate.pause(id,{mode,reason})` 调用 agent.cancel / goals.pause / session/event
6
+ * 安全边界落地(见 src/pause-gate.js),不再依赖 `ctx.get('taskControl')`,也不再退化成
7
+ * 只写 `queueLocked` 标记。
8
+ *
9
+ * **降级路径**(fail-open,D8):仅当自研暂停因 agent 不可用而失败时,按 `queueFallback`
10
+ * 回退为只锁等待队列(`queueLocked` 持久化标记,供前端/重试让路)。
11
+ *
12
+ * resume 对称:自研 resume(从暂停点续跑)或清 queueLock。
13
+ */
14
+ import { idleState } from './store.js'
15
+
16
+ /**
17
+ * @param {object} deps
18
+ * @param {()=>object|null} deps.getCtx 取当前 host ctx(懒,便于测试)
19
+ * @param {()=>object} deps.getSettings 读实时配置(pauseMode/pauseReason)
20
+ * @param {ReturnType<import('./store.js').createStore>} deps.store 持久化队列锁
21
+ * @param {ReturnType<import('./pause-gate.js').createPauseGate>} [deps.pauseGate] 自研会话门引擎
22
+ * @param {(event:string, sessionId:string, payload?:object)=>void} [deps.emit] 状态变化通知
23
+ */
24
+ export function createGate({ getCtx: _getCtx, getSettings, store, pauseGate, emit }) {
25
+ function lockQueue(sessionId, reason) {
26
+ const cur = store.get(sessionId)
27
+ const next = { ...(cur || idleState(sessionId)), queueLocked: true, lockReason: reason, updatedAt: Date.now() }
28
+ store.set(sessionId, next)
29
+ if (emit) emit('queue-lock', sessionId, { reason })
30
+ return next
31
+ }
32
+
33
+ function unlockQueue(sessionId) {
34
+ const cur = store.get(sessionId)
35
+ const next = { ...(cur || idleState(sessionId)), queueLocked: false, lockReason: null, updatedAt: Date.now() }
36
+ store.set(sessionId, next)
37
+ if (emit) emit('queue-unlock', sessionId)
38
+ return next
39
+ }
40
+
41
+ /**
42
+ * 停掉 session 的下一回合(真实锁定推进)。
43
+ * ① 自研 pauseGate.pause(真暂停);② 自研失败/agent 不可用 → 按 queueFallback 降级锁队列。
44
+ * @param {string} sessionId
45
+ * @param {object} [opts] { mode, reason }
46
+ */
47
+ async function stopNextTurn(sessionId, opts = {}) {
48
+ const cfg = getSettings()
49
+ if (pauseGate) {
50
+ try {
51
+ const mode = opts.mode ?? cfg.pauseMode ?? 'safe'
52
+ const reason = opts.reason ?? cfg.pauseReason ?? 'wait'
53
+ const r = pauseGate.pause(sessionId, { mode, reason })
54
+ if (r && r.kind === 'success') return { ok: true, via: 'pauseGate', result: r }
55
+ // kind === 'error'(如 no live agent)→ 落到降级,语义如实上报。
56
+ } catch {
57
+ // 自研内部异常 → fail-open 降级,绝不中断冻结动作。
58
+ }
59
+ }
60
+ // 无自研门或自研失败 → 回退只锁等待队列(自带实现,D5)。
61
+ if (cfg.queueFallback === false) {
62
+ return { ok: false, via: 'queueLock', error: 'queueFallback disabled' }
63
+ }
64
+ lockQueue(sessionId, 'peak')
65
+ return { ok: true, via: 'queueLock' }
66
+ }
67
+
68
+ /**
69
+ * 恢复 session 推进(从暂停点续跑)。
70
+ * ① 自研 resume(confirm + choice);② 降级清 queueLock。
71
+ */
72
+ async function resume(sessionId, opts = {}) {
73
+ if (pauseGate) {
74
+ try {
75
+ const r = pauseGate.resume(sessionId, { confirm: true, choice: opts.choice ?? 'rerun' })
76
+ if (r && r.kind === 'success') return { ok: true, via: 'pauseGate', result: r }
77
+ } catch {
78
+ // 降级清队列锁。
79
+ }
80
+ }
81
+ unlockQueue(sessionId)
82
+ return { ok: true, via: 'queueLock' }
83
+ }
84
+
85
+ return {
86
+ stopNextTurn,
87
+ resume,
88
+ lockQueue,
89
+ unlockQueue,
90
+ /** 自研会话门是否可用(自研后恒视为可用;供 detect/UI 展示)。 */
91
+ taskControlAvailable: () => (pauseGate ? pauseGate.taskControlAvailable() : false),
92
+ }
93
+ }