dsh-session-guard 4.0.2 → 4.1.5
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 +21 -21
- package/README.de.md +375 -375
- package/README.es.md +374 -374
- package/README.fr.md +373 -373
- package/README.it.md +374 -374
- package/README.ru.md +374 -374
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +16 -16
- package/lib/client.js +179 -37
- package/lib/client.js.map +1 -1
- package/package.json +9 -11
- package/src/bridge.js +136 -136
- package/src/client/index.ts +125 -47
- package/src/client/pause-button-text.ts +60 -60
- package/src/client/pause-button.tsx +129 -129
- package/src/client/scope-face.ts +20 -20
- package/src/client/settings-card.tsx +391 -391
- package/src/client/shell-overlay.tsx +69 -0
- package/src/client/status-badge.tsx +17 -0
- package/src/client/styles.ts +30 -30
- package/src/detect.js +27 -27
- package/src/gate.js +93 -93
- package/src/index.js +36 -1
- package/src/pause-gate.js +544 -544
- package/src/pause-store.js +101 -101
- package/src/retry.js +227 -227
- package/src/scheduler.js +42 -42
- package/src/step-gate.js +399 -399
- package/src/store.js +77 -77
- package/src/wiring.js +305 -305
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 无设置页宿主(0.1.0/0.1.1)的兜底配置入口。
|
|
3
|
+
*
|
|
4
|
+
* 壳级 `shell.overlay` 席位渲染:右下角固定齿轮钮,点开固定定位浮层承载
|
|
5
|
+
* SessionGuardCard(page 形态,默认展开)。配置存储全在 host 侧(settings
|
|
6
|
+
* 服务),浮层与宿主设置页解耦——壳再老也不影响读写。有设置页的宿主上
|
|
7
|
+
* 该齿轮与行配置卡/家族 tab 并存,作为全局快捷入口。
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
import { SessionGuardCard } from './settings-card'
|
|
11
|
+
import type { SessionGuardConfig } from './settings-card'
|
|
12
|
+
import type { SettingsScope } from './scope-face'
|
|
13
|
+
|
|
14
|
+
export function ShellSettingsEntry(props: {
|
|
15
|
+
scope: SettingsScope<SessionGuardConfig>
|
|
16
|
+
}): JSX.Element {
|
|
17
|
+
const [open, setOpen] = useState(false)
|
|
18
|
+
return (
|
|
19
|
+
<div style={{ position: 'fixed', right: 16, bottom: 64, zIndex: 10000 }}>
|
|
20
|
+
{open && (
|
|
21
|
+
<div
|
|
22
|
+
onClick={() => setOpen(false)}
|
|
23
|
+
style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,.45)', zIndex: 10000 }}
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
{open && (
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
position: 'fixed',
|
|
30
|
+
top: '6vh',
|
|
31
|
+
left: '50%',
|
|
32
|
+
transform: 'translateX(-50%)',
|
|
33
|
+
width: 'min(760px, 92vw)',
|
|
34
|
+
maxHeight: '84vh',
|
|
35
|
+
overflowY: 'auto',
|
|
36
|
+
zIndex: 10001,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => setOpen(false)}
|
|
42
|
+
style={{ position: 'sticky', top: 6, float: 'right', marginRight: 8, zIndex: 2 }}
|
|
43
|
+
>
|
|
44
|
+
✕ 关闭
|
|
45
|
+
</button>
|
|
46
|
+
<SessionGuardCard scope={props.scope} />
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
title="会话守护设置"
|
|
52
|
+
onClick={() => setOpen(state => !state)}
|
|
53
|
+
style={{
|
|
54
|
+
width: 40,
|
|
55
|
+
height: 40,
|
|
56
|
+
borderRadius: '50%',
|
|
57
|
+
border: '1px solid rgba(127,127,127,.4)',
|
|
58
|
+
background: 'rgba(30,30,32,.85)',
|
|
59
|
+
color: 'inherit',
|
|
60
|
+
cursor: 'pointer',
|
|
61
|
+
fontSize: 16,
|
|
62
|
+
boxShadow: '0 4px 16px rgba(0,0,0,.35)',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
🛡
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;
|
|
5
5
|
* 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。
|
|
6
6
|
* 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
|
|
7
|
+
*
|
|
8
|
+
* 自适应退避(universal-rc):视口宽度不足(< PEAK_BADGE_MIN_VIEWPORT,窄窗/
|
|
9
|
+
* 分屏)或新空会话(sessionId 未落盘为 undefined)→ 整体退避隐藏。
|
|
7
10
|
*/
|
|
8
11
|
import { useEffect, useState } from 'react'
|
|
9
12
|
import { badgeTitle, peakLabel, type Status } from './badge-text'
|
|
@@ -15,10 +18,15 @@ export interface StatusBadgeProps {
|
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const POLL_MS = 15_000
|
|
21
|
+
/** 视口宽度退避阈值:低于此值隐藏徽标(窄窗/分屏下输入区放不下)。 */
|
|
22
|
+
const PEAK_BADGE_MIN_VIEWPORT = 960
|
|
18
23
|
|
|
19
24
|
/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
|
|
20
25
|
export function StatusBadge({ sessionId }: StatusBadgeProps) {
|
|
21
26
|
const [status, setStatus] = useState<Status | null>(null)
|
|
27
|
+
const [viewportNarrow, setViewportNarrow] = useState(
|
|
28
|
+
() => typeof window !== 'undefined' && window.innerWidth < PEAK_BADGE_MIN_VIEWPORT,
|
|
29
|
+
)
|
|
22
30
|
|
|
23
31
|
injectClientCss()
|
|
24
32
|
|
|
@@ -42,6 +50,15 @@ export function StatusBadge({ sessionId }: StatusBadgeProps) {
|
|
|
42
50
|
}
|
|
43
51
|
}, [sessionId])
|
|
44
52
|
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const onResize = () => setViewportNarrow(window.innerWidth < PEAK_BADGE_MIN_VIEWPORT)
|
|
55
|
+
window.addEventListener('resize', onResize)
|
|
56
|
+
return () => window.removeEventListener('resize', onResize)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
// 视口宽度不足:退避隐藏。新空会话隐藏需会话空态信号(UseChat 契约),
|
|
60
|
+
// 留待后续(sessionId 在新会话同样有值,不能作判定)。
|
|
61
|
+
if (viewportNarrow) return null
|
|
45
62
|
if (!status || !status.enabled) return null
|
|
46
63
|
|
|
47
64
|
const cls = status.phase === 'peak' ? 'sg-peak' : status.phase === 'weekend' ? 'sg-weekend' : 'sg-off'
|
package/src/client/styles.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -43,10 +43,12 @@ export function apply(ctx, config = {}) {
|
|
|
43
43
|
let lastState = null
|
|
44
44
|
|
|
45
45
|
/** 读实时设置(settings 服务不可用时回退默认)。 */
|
|
46
|
+
let configSource = config
|
|
46
47
|
/** 组合条目快照:volatile 字段解引,供默认值合并。 */
|
|
47
48
|
function resolveEntryConfig() {
|
|
49
|
+
const source = configSource ?? config
|
|
48
50
|
const out = {}
|
|
49
|
-
for (const key of Object.keys(DEFAULT_SETTINGS)) out[key] = readVolatileValue(
|
|
51
|
+
for (const key of Object.keys(DEFAULT_SETTINGS)) out[key] = readVolatileValue(source?.[key])
|
|
50
52
|
return out
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -59,6 +61,39 @@ export function apply(ctx, config = {}) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// ── ≤0.1.5(含 0.1.0/0.1.1)设置命名空间 legacy 注册(universal-rc)──────
|
|
65
|
+
// 0.1.7+ 由 loader 自动注册 ns,无需此块;老线必须经 settings 服务手动注册,
|
|
66
|
+
// 否则 client 配置卡报「设置命名空间不可用」。两条 legacy 路径按服务能力
|
|
67
|
+
// 自动分派:0.1.2/0.1.5 = 服务实例 installSection;0.1.0/0.1.1 = 模块级
|
|
68
|
+
// installSettingsSection + settingsNamespace。fail-open:失败仅告警。
|
|
69
|
+
try {
|
|
70
|
+
ctx.inject(['settings'], (sctx) => {
|
|
71
|
+
const service = sctx.settings
|
|
72
|
+
if (service === undefined || service === null) return
|
|
73
|
+
const hooks = {
|
|
74
|
+
setSource: (source) => {
|
|
75
|
+
configSource = typeof source === 'function' ? source() : source
|
|
76
|
+
},
|
|
77
|
+
onChange: () => {
|
|
78
|
+
// 状态机按 tick 读 readCfg(),无需即时动作。
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
if (typeof service.installSection === 'function') {
|
|
82
|
+
service.installSection(ctx, NS, SettingsSchema, resolveEntryConfig(), hooks)
|
|
83
|
+
console.warn('dsh-session-guard: settings registered via service.installSection (<=0.1.5)')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
void import('@deepseek-ai/dsh-settings').then((legacy) => {
|
|
87
|
+
if (typeof legacy.installSettingsSection === 'function' && typeof legacy.settingsNamespace === 'function') {
|
|
88
|
+
legacy.installSettingsSection(ctx, legacy.settingsNamespace(NS), SettingsSchema, resolveEntryConfig(), hooks)
|
|
89
|
+
console.warn('dsh-session-guard: settings registered via module installSettingsSection (0.1.0/0.1.1)')
|
|
90
|
+
}
|
|
91
|
+
}).catch(() => {})
|
|
92
|
+
})
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn('dsh-session-guard: legacy settings registration skipped:', error)
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
// ── SSE 推送(v0.2.0):step 门状态变化时立刻推给对应会话页面 ──
|
|
63
98
|
// 客户端「暂停会话 / 继续会话」按钮据此更新,无需等轮询。
|
|
64
99
|
const sseClients = new Set()
|