dsh-session-guard 4.0.2 → 4.1.11
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/CHANGELOG.md +21 -0
- 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 +1 -1
- package/lib/client.js +222 -44
- package/lib/client.js.map +1 -1
- package/package.json +10 -10
- package/src/bridge.js +136 -136
- package/src/client/family-section.tsx +26 -6
- package/src/client/index.ts +176 -53
- 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
package/src/bridge.js
CHANGED
|
@@ -1,136 +1,136 @@
|
|
|
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
|
-
* @param {ReturnType<import('./pause-gate.js').createPauseGate>} [pauseGate]
|
|
20
|
-
* @param {ReturnType<import('./step-gate.js').createStepGate>} [stepGate] step 级门控(v0.2.0)
|
|
21
|
-
*/
|
|
22
|
-
export function createBridge(ctx, gate, store, pauseGate, stepGate) {
|
|
23
|
-
function taskControlState(sessionId) {
|
|
24
|
-
let tcState = null
|
|
25
|
-
try {
|
|
26
|
-
const tc = ctx && typeof ctx.get === 'function' ? ctx.get('taskControl') : undefined
|
|
27
|
-
if (tc && typeof tc.state === 'function') {
|
|
28
|
-
const s = tc.state(sessionId)
|
|
29
|
-
tcState = {
|
|
30
|
-
status: s && s.status,
|
|
31
|
-
paused: !!(s && s.paused),
|
|
32
|
-
forced: !!(s && s.forced),
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
/* 状态读取失败 → 保持 null,fail-open */
|
|
37
|
-
}
|
|
38
|
-
return tcState
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
/** 停掉 session 下一回合(会话门 / 回退锁队列)。 */
|
|
43
|
-
stopNextTurn(sessionId, opts) {
|
|
44
|
-
return gate.stopNextTurn(sessionId, opts)
|
|
45
|
-
},
|
|
46
|
-
/** 恢复。 */
|
|
47
|
-
resume(sessionId, opts) {
|
|
48
|
-
return gate.resume(sessionId, opts)
|
|
49
|
-
},
|
|
50
|
-
/** 显式锁队列(手动触发)。 */
|
|
51
|
-
lockQueue(sessionId, reason = 'manual') {
|
|
52
|
-
const next = gate.lockQueue(sessionId, reason)
|
|
53
|
-
return { ok: true, state: next }
|
|
54
|
-
},
|
|
55
|
-
/** 显式解锁队列。 */
|
|
56
|
-
unlockQueue(sessionId) {
|
|
57
|
-
const next = gate.unlockQueue(sessionId)
|
|
58
|
-
return { ok: true, state: next }
|
|
59
|
-
},
|
|
60
|
-
/**
|
|
61
|
-
* 手动请求 step 级暂停(「暂停会话」按钮)。
|
|
62
|
-
* 不打断当前 step:在下一次 `agent/pre-step` 边界拉门(step 1 也拦,且不受峰谷/provider 限制)。
|
|
63
|
-
* @param {string} sessionId
|
|
64
|
-
*/
|
|
65
|
-
stepPause(sessionId) {
|
|
66
|
-
if (!stepGate || typeof stepGate.requestPause !== 'function') {
|
|
67
|
-
return { requested: false, held: false, reason: 'no-step-gate' }
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
return stepGate.requestPause(sessionId)
|
|
71
|
-
} catch {
|
|
72
|
-
return { requested: false, held: false, reason: 'error' }
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
/**
|
|
76
|
-
* 解开 step 门(v0.2.0):放行被挂起的 step。
|
|
77
|
-
* 默认 `bypass=true`(用户手动「继续」)——本高峰窗口内不再拦该会话;
|
|
78
|
-
* 退峰/非官方切换等自动释放请用 `{ bypass: false }`。
|
|
79
|
-
* @param {string} sessionId
|
|
80
|
-
* @param {{bypass?:boolean, reason?:string}} [opts]
|
|
81
|
-
*/
|
|
82
|
-
stepResume(sessionId, opts = {}) {
|
|
83
|
-
if (!stepGate || typeof stepGate.release !== 'function') {
|
|
84
|
-
return { released: false, bypass: false, reason: 'no-step-gate' }
|
|
85
|
-
}
|
|
86
|
-
const reason = opts.reason ?? 'manual'
|
|
87
|
-
let r
|
|
88
|
-
try {
|
|
89
|
-
r = stepGate.release(sessionId, reason)
|
|
90
|
-
} catch {
|
|
91
|
-
return { released: false, bypass: false, reason }
|
|
92
|
-
}
|
|
93
|
-
const released = !!(r && r.released === true)
|
|
94
|
-
let bypassed = false
|
|
95
|
-
if (released && opts.bypass !== false && typeof stepGate.markBypass === 'function') {
|
|
96
|
-
try {
|
|
97
|
-
stepGate.markBypass(sessionId)
|
|
98
|
-
bypassed = true
|
|
99
|
-
} catch {
|
|
100
|
-
/* ignore */
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return { released, bypass: bypassed, reason }
|
|
104
|
-
},
|
|
105
|
-
/** 读一会话状态。 */
|
|
106
|
-
state(sessionId) {
|
|
107
|
-
const cur = store.get(sessionId)
|
|
108
|
-
const base = cur || idleState(sessionId)
|
|
109
|
-
const pausedState = pauseGate ? (pauseGate.state(sessionId) || {}) : {}
|
|
110
|
-
let stepState = {}
|
|
111
|
-
if (stepGate && typeof stepGate.state === 'function') {
|
|
112
|
-
try {
|
|
113
|
-
stepState = stepGate.state(sessionId) || {}
|
|
114
|
-
} catch {
|
|
115
|
-
stepState = {}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
sessionId: String(sessionId),
|
|
120
|
-
queueLocked: base.queueLocked === true,
|
|
121
|
-
lockReason: base.lockReason ?? null,
|
|
122
|
-
// 自研会话门真暂停状态(脱离 task-control)。
|
|
123
|
-
paused: pausedState.paused === true,
|
|
124
|
-
pausedForced: pausedState.forced === true,
|
|
125
|
-
// step 级门控(v0.2.0):paused 保持布尔以兼容既有消费者,step 态用独立字段。
|
|
126
|
-
pausedStep: stepState.held === true,
|
|
127
|
-
stepHeldSince: stepState.since ?? null,
|
|
128
|
-
stepBypass: stepState.bypass === true,
|
|
129
|
-
stepManual: stepState.manual === true,
|
|
130
|
-
taskControlAvailable: gate.taskControlAvailable(),
|
|
131
|
-
taskControl: taskControlState(sessionId),
|
|
132
|
-
updatedAt: base.updatedAt ?? null,
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
}
|
|
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
|
+
* @param {ReturnType<import('./pause-gate.js').createPauseGate>} [pauseGate]
|
|
20
|
+
* @param {ReturnType<import('./step-gate.js').createStepGate>} [stepGate] step 级门控(v0.2.0)
|
|
21
|
+
*/
|
|
22
|
+
export function createBridge(ctx, gate, store, pauseGate, stepGate) {
|
|
23
|
+
function taskControlState(sessionId) {
|
|
24
|
+
let tcState = null
|
|
25
|
+
try {
|
|
26
|
+
const tc = ctx && typeof ctx.get === 'function' ? ctx.get('taskControl') : undefined
|
|
27
|
+
if (tc && typeof tc.state === 'function') {
|
|
28
|
+
const s = tc.state(sessionId)
|
|
29
|
+
tcState = {
|
|
30
|
+
status: s && s.status,
|
|
31
|
+
paused: !!(s && s.paused),
|
|
32
|
+
forced: !!(s && s.forced),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* 状态读取失败 → 保持 null,fail-open */
|
|
37
|
+
}
|
|
38
|
+
return tcState
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
/** 停掉 session 下一回合(会话门 / 回退锁队列)。 */
|
|
43
|
+
stopNextTurn(sessionId, opts) {
|
|
44
|
+
return gate.stopNextTurn(sessionId, opts)
|
|
45
|
+
},
|
|
46
|
+
/** 恢复。 */
|
|
47
|
+
resume(sessionId, opts) {
|
|
48
|
+
return gate.resume(sessionId, opts)
|
|
49
|
+
},
|
|
50
|
+
/** 显式锁队列(手动触发)。 */
|
|
51
|
+
lockQueue(sessionId, reason = 'manual') {
|
|
52
|
+
const next = gate.lockQueue(sessionId, reason)
|
|
53
|
+
return { ok: true, state: next }
|
|
54
|
+
},
|
|
55
|
+
/** 显式解锁队列。 */
|
|
56
|
+
unlockQueue(sessionId) {
|
|
57
|
+
const next = gate.unlockQueue(sessionId)
|
|
58
|
+
return { ok: true, state: next }
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* 手动请求 step 级暂停(「暂停会话」按钮)。
|
|
62
|
+
* 不打断当前 step:在下一次 `agent/pre-step` 边界拉门(step 1 也拦,且不受峰谷/provider 限制)。
|
|
63
|
+
* @param {string} sessionId
|
|
64
|
+
*/
|
|
65
|
+
stepPause(sessionId) {
|
|
66
|
+
if (!stepGate || typeof stepGate.requestPause !== 'function') {
|
|
67
|
+
return { requested: false, held: false, reason: 'no-step-gate' }
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return stepGate.requestPause(sessionId)
|
|
71
|
+
} catch {
|
|
72
|
+
return { requested: false, held: false, reason: 'error' }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* 解开 step 门(v0.2.0):放行被挂起的 step。
|
|
77
|
+
* 默认 `bypass=true`(用户手动「继续」)——本高峰窗口内不再拦该会话;
|
|
78
|
+
* 退峰/非官方切换等自动释放请用 `{ bypass: false }`。
|
|
79
|
+
* @param {string} sessionId
|
|
80
|
+
* @param {{bypass?:boolean, reason?:string}} [opts]
|
|
81
|
+
*/
|
|
82
|
+
stepResume(sessionId, opts = {}) {
|
|
83
|
+
if (!stepGate || typeof stepGate.release !== 'function') {
|
|
84
|
+
return { released: false, bypass: false, reason: 'no-step-gate' }
|
|
85
|
+
}
|
|
86
|
+
const reason = opts.reason ?? 'manual'
|
|
87
|
+
let r
|
|
88
|
+
try {
|
|
89
|
+
r = stepGate.release(sessionId, reason)
|
|
90
|
+
} catch {
|
|
91
|
+
return { released: false, bypass: false, reason }
|
|
92
|
+
}
|
|
93
|
+
const released = !!(r && r.released === true)
|
|
94
|
+
let bypassed = false
|
|
95
|
+
if (released && opts.bypass !== false && typeof stepGate.markBypass === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
stepGate.markBypass(sessionId)
|
|
98
|
+
bypassed = true
|
|
99
|
+
} catch {
|
|
100
|
+
/* ignore */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { released, bypass: bypassed, reason }
|
|
104
|
+
},
|
|
105
|
+
/** 读一会话状态。 */
|
|
106
|
+
state(sessionId) {
|
|
107
|
+
const cur = store.get(sessionId)
|
|
108
|
+
const base = cur || idleState(sessionId)
|
|
109
|
+
const pausedState = pauseGate ? (pauseGate.state(sessionId) || {}) : {}
|
|
110
|
+
let stepState = {}
|
|
111
|
+
if (stepGate && typeof stepGate.state === 'function') {
|
|
112
|
+
try {
|
|
113
|
+
stepState = stepGate.state(sessionId) || {}
|
|
114
|
+
} catch {
|
|
115
|
+
stepState = {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
sessionId: String(sessionId),
|
|
120
|
+
queueLocked: base.queueLocked === true,
|
|
121
|
+
lockReason: base.lockReason ?? null,
|
|
122
|
+
// 自研会话门真暂停状态(脱离 task-control)。
|
|
123
|
+
paused: pausedState.paused === true,
|
|
124
|
+
pausedForced: pausedState.forced === true,
|
|
125
|
+
// step 级门控(v0.2.0):paused 保持布尔以兼容既有消费者,step 态用独立字段。
|
|
126
|
+
pausedStep: stepState.held === true,
|
|
127
|
+
stepHeldSince: stepState.since ?? null,
|
|
128
|
+
stepBypass: stepState.bypass === true,
|
|
129
|
+
stepManual: stepState.manual === true,
|
|
130
|
+
taskControlAvailable: gate.taskControlAvailable(),
|
|
131
|
+
taskControl: taskControlState(sessionId),
|
|
132
|
+
updatedAt: base.updatedAt ?? null,
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 与 thinking-levels 的 FamilySettingsSection 的差别仅在不需要 thinking-
|
|
11
11
|
* levels 自己的卡(宿主不在了,它的卡也不在):渲染循环没有 own-tab 特例。
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { useEffect, useState } from 'react'
|
|
14
14
|
import type { JSX } from 'react'
|
|
15
15
|
|
|
16
16
|
/** 与 FamilySectionInjected.hooks.tabs 同形的账本投影(结构化镜像)。 */
|
|
@@ -26,19 +26,39 @@ export interface FamilyTabEntry {
|
|
|
26
26
|
label: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** 组件 props
|
|
29
|
+
/** 组件 props:hooks 舱(宿主 bindInjectHooks 把 inject 面的 `hooks` 键剥掉
|
|
30
|
+
* 转成 `useTabs`——0.1.0 起各代皆然,故 useTabs 是正路、raw hooks 只是
|
|
31
|
+
* 无转换环境的兜底)+ ≤0.1.5 壳缺 renderSlot 时的直接挂载面。 */
|
|
30
32
|
export interface GuardFamilySectionProps {
|
|
31
|
-
renderSlot
|
|
32
|
-
hooks
|
|
33
|
+
renderSlot?: (key: 'dsh-family.tab', owner?: object, opts?: { only?: string; fallback?: JSX.Element | null }) => JSX.Element | null
|
|
34
|
+
/** 宿主转换后的 hooks 舱(`inject.hooks.tabs` → `useTabs`)。 */
|
|
35
|
+
useTabs?: <S>(selector: (value: readonly FamilyTabEntry[]) => S) => S
|
|
36
|
+
/** 未被宿主转换时的原始账本投影(防御位,实测各代都会转换走 useTabs)。 */
|
|
37
|
+
hooks?: { tabs: HostObservable<readonly FamilyTabEntry[]> }
|
|
38
|
+
/** 直接挂载面(≤0.1.5 壳无 renderSlot 时):注册侧从账本原始条目
|
|
39
|
+
* (entries 自带 component + inject)现挂激活卡。失败一律 null。 */
|
|
40
|
+
renderContributor?: (id: string) => JSX.Element | null
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
const NO_TABS: readonly FamilyTabEntry[] = []
|
|
44
|
+
|
|
35
45
|
export function GuardFamilySection(props: GuardFamilySectionProps): JSX.Element {
|
|
36
|
-
const
|
|
46
|
+
const raw = props.hooks?.tabs
|
|
47
|
+
// 账本读取:useTabs 舱优先;raw 投影走 React 订阅。face 选择每代固定,
|
|
48
|
+
// 两种路径的 hook 数不同但同实例内次序恒定。
|
|
49
|
+
const [polled, setPolled] = useState<readonly FamilyTabEntry[]>(() => raw?.getSnapshot() ?? NO_TABS)
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (props.useTabs !== undefined || raw === undefined) return
|
|
52
|
+
return raw.subscribe(() => setPolled(raw.getSnapshot()))
|
|
53
|
+
})
|
|
54
|
+
const tabs = props.useTabs !== undefined ? props.useTabs(value => value) : polled
|
|
37
55
|
return (
|
|
38
56
|
<div style={{ display: 'grid', gap: '12px' }}>
|
|
39
57
|
{tabs.map(row => (
|
|
40
58
|
<div key={row.id}>
|
|
41
|
-
{props.renderSlot
|
|
59
|
+
{props.renderSlot
|
|
60
|
+
? props.renderSlot('dsh-family.tab', {}, { only: row.id, fallback: null })
|
|
61
|
+
: props.renderContributor?.(row.id) ?? null}
|
|
42
62
|
</div>
|
|
43
63
|
))}
|
|
44
64
|
</div>
|
package/src/client/index.ts
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
* 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。
|
|
9
9
|
* - 0.1.7:旧的插件设置卡已随席位删除 —— 设置表单由 host
|
|
10
10
|
* 侧 Config 的 `.volatile()` 字段自动生成。
|
|
11
|
+
* - 跨版本(universal-rc):持久设置经 configForms(0.1.7+)/settingsScope(≤0.1.5)
|
|
12
|
+
* 双代腰产出同形 scope;无设置页宿主(0.1.0/0.1.1)经 shell.overlay 浮窗
|
|
13
|
+
* 齿轮提供配置入口(配置存储全在 host 侧,浮层与宿主设置页解耦)。
|
|
11
14
|
*
|
|
12
15
|
* 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。
|
|
13
16
|
*/
|
|
17
|
+
import { createElement } from 'react'
|
|
18
|
+
import type { JSX } from 'react'
|
|
14
19
|
import { StatusBadge } from './status-badge'
|
|
15
20
|
import { PauseButton } from './pause-button'
|
|
16
21
|
import { SessionGuardCard } from './settings-card'
|
|
22
|
+
import { ShellSettingsEntry } from './shell-overlay'
|
|
17
23
|
import { GuardFamilySection, type FamilyTabEntry } from './family-section'
|
|
24
|
+
import type { SessionGuardConfig } from './settings-card'
|
|
25
|
+
import type { SettingsScope } from './scope-face'
|
|
18
26
|
|
|
19
27
|
// 起子插件族共用设置节:宿主 dsh-thinking-levels 注册顶级 `settings.section`
|
|
20
28
|
// (id `dsh-family`)并声明 `dsh-family.tab` 子席位,本插件贡献卡片。宿主缺席
|
|
@@ -33,6 +41,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
33
41
|
* 本卡片改经 inject 工厂拿自己的 scope(与 dsh-family.tab 注册同源)。
|
|
34
42
|
*/
|
|
35
43
|
'plugins.bundle.config': { kind: 'keyed'; scope: 'root'; owner: Record<string, unknown> }
|
|
44
|
+
/** 无设置页宿主(0.1.0/0.1.1)的兜底配置入口(壳级 overlay)。 */
|
|
45
|
+
'shell.overlay': { kind: 'list'; scope: 'root' }
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -40,91 +50,204 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
40
50
|
const FAMILY_SECTION_ID = 'dsh-family'
|
|
41
51
|
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
42
52
|
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
53
|
+
/** 让位宽限:声明批内 TL 的注册微秒级落账,此延迟后查账本足以分辨「TL 在场」。 */
|
|
54
|
+
const FAMILY_YIELD_MS = 500
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
57
|
+
* 客户端所需服务:slots(状态徽标)+ locale。持久设置走双代腰
|
|
58
|
+
* (registerSettingsFaces):configForms 为 0.1.7+ 独有,顶层注入会在
|
|
59
|
+
* 0.1.0/0.1.1(无此服务)上让整段 apply 静默死。
|
|
47
60
|
*/
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
/** 客户端所需服务:slots(状态徽标)+ locale + configForms(共用 tab 设置卡)。 */
|
|
51
|
-
export const inject = ['slots', 'locale', 'configForms']
|
|
61
|
+
export const inject = ['slots', 'locale']
|
|
52
62
|
|
|
53
63
|
/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
|
|
54
64
|
interface SlotsFace {
|
|
55
65
|
inject: (_name: string, _fn: () => unknown) => () => void
|
|
56
66
|
register: (_options: Record<string, unknown>, _component: unknown) => () => void
|
|
57
|
-
entries: (_name: string) => Array<{
|
|
67
|
+
entries: (_name: string) => Array<{
|
|
68
|
+
options: { id?: string; order?: number; label?: unknown }
|
|
69
|
+
component?: unknown
|
|
70
|
+
inject?: () => unknown
|
|
71
|
+
}>
|
|
58
72
|
getVersion: (_name: string) => number
|
|
59
73
|
subscribe: (_name: string, _listener: () => void) => () => void
|
|
60
74
|
}
|
|
61
|
-
interface
|
|
62
|
-
get: <T>(_entryId: string) =>
|
|
75
|
+
interface ConfigFormsFactoryFace {
|
|
76
|
+
get: <T>(_entryId: string) => SettingsScope<T>
|
|
77
|
+
}
|
|
78
|
+
interface SettingsScopeFactoryFace {
|
|
79
|
+
bind: <T>(_opts: { namespace: string }) => SettingsScope<T>
|
|
63
80
|
}
|
|
64
81
|
interface ClientCtx {
|
|
65
82
|
slots: SlotsFace
|
|
66
|
-
|
|
83
|
+
inject: (
|
|
84
|
+
_names: string[],
|
|
85
|
+
_fn: (sctx: { configForms?: ConfigFormsFactoryFace; settingsScope?: SettingsScopeFactoryFace }) => void,
|
|
86
|
+
) => void
|
|
67
87
|
}
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}, PauseButton))
|
|
89
|
+
/** 单面注册护栏:某条宿主线上未知槽名/缺服务的同步异常不得带走其它面。 */
|
|
90
|
+
function safe(tag: string, fn: () => void): void {
|
|
91
|
+
try {
|
|
92
|
+
fn()
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(`dsh-session-guard: client surface '${tag}' failed:`, error)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
/**
|
|
99
|
+
* 双代持久设置腰(移植 input-traffic registerSettingsFaces):两个 scoped
|
|
100
|
+
* 子注入各自产出同形 scope 后回调;0.2.0 两代并存时以 configForms 先到为准
|
|
101
|
+
* (mounted 位防双挂载,ui-slots 同 id 仲裁兜底)。
|
|
102
|
+
*/
|
|
103
|
+
function registerSettingsFaces(
|
|
104
|
+
ctx: ClientCtx,
|
|
105
|
+
onScope: (scope: SettingsScope<SessionGuardConfig>, generation: 'configForms' | 'settingsScope') => void,
|
|
106
|
+
): void {
|
|
107
|
+
let mounted = false
|
|
108
|
+
ctx.inject(['configForms'], (sctx) => {
|
|
109
|
+
if (mounted) return
|
|
110
|
+
mounted = true
|
|
111
|
+
onScope(sctx.configForms!.get<SessionGuardConfig>('session-guard'), 'configForms')
|
|
112
|
+
})
|
|
113
|
+
ctx.inject(['settingsScope'], (sctx) => {
|
|
114
|
+
if (mounted) return
|
|
115
|
+
mounted = true
|
|
116
|
+
onScope(sctx.settingsScope!.bind<SessionGuardConfig>({ namespace: 'session-guard' }), 'settingsScope')
|
|
117
|
+
})
|
|
118
|
+
}
|
|
86
119
|
|
|
87
|
-
|
|
120
|
+
/** 两张配置卡(family tab + bundle 详情页)挂载;scope 由双代腰产出。 */
|
|
121
|
+
function mountConfigCards(ctx: ClientCtx, scope: SettingsScope<SessionGuardConfig>): void {
|
|
122
|
+
// 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的持久配置。
|
|
88
123
|
ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({
|
|
89
124
|
name: 'dsh-family.tab',
|
|
90
125
|
id: 'session-guard',
|
|
91
126
|
order: 20,
|
|
92
127
|
label: '会话守护门禁',
|
|
93
128
|
locale: 'session-guard',
|
|
94
|
-
inject: () => ({ scope
|
|
129
|
+
inject: () => ({ scope }),
|
|
95
130
|
}, SessionGuardCard))
|
|
96
131
|
|
|
97
132
|
// 插件页配置卡(DSH 0.2.0):Plugins 页不自动渲染 volatile 配置表单,须注册
|
|
98
133
|
// `plugins.bundle.config` 席位(key = 包名),配置卡才渲染在 bundle 详情页。
|
|
99
|
-
// 卡片组件与 props(scope)与上方 dsh-family.tab
|
|
100
|
-
// SessionGuardCard、同一 inject 工厂,配置仍写入 entry `session-guard` 的
|
|
101
|
-
// volatile config;页面注入的 owner props(form 等)本卡不消费。
|
|
134
|
+
// 卡片组件与 props(scope)与上方 dsh-family.tab 注册完全同源。
|
|
102
135
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
103
136
|
name: 'plugins.bundle.config',
|
|
104
137
|
key: 'dsh-session-guard',
|
|
105
|
-
inject: () => ({ scope
|
|
138
|
+
inject: () => ({ scope }),
|
|
106
139
|
}, SessionGuardCard))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function apply(ctx: ClientCtx) {
|
|
143
|
+
// 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。
|
|
144
|
+
// order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。
|
|
145
|
+
safe('conversation.input.right/pause', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
146
|
+
name: 'conversation.input.right',
|
|
147
|
+
id: 'session-guard-pause',
|
|
148
|
+
order: 20,
|
|
149
|
+
locale: 'session-guard',
|
|
150
|
+
}, PauseButton)))
|
|
107
151
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
// 状态徽标:输入区右侧,纯展示。
|
|
153
|
+
safe('conversation.input.right/status', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
154
|
+
name: 'conversation.input.right',
|
|
155
|
+
id: 'session-guard-status',
|
|
156
|
+
order: 40,
|
|
157
|
+
locale: 'session-guard',
|
|
158
|
+
}, StatusBadge)))
|
|
159
|
+
|
|
160
|
+
// 双代腰:scope 就绪后按代挂载(每条宿主线恰好一代服务)。
|
|
161
|
+
registerSettingsFaces(ctx, (scope, generation) => {
|
|
162
|
+
safe(`config-cards/${generation}`, () => {
|
|
163
|
+
// 两代共有:family tab(0.1.7+ 由 thinking-levels 容器渲染)与
|
|
164
|
+
// bundle 详情页平铺卡(0.1.7+ 的 plugin-manager 槽)。
|
|
165
|
+
mountConfigCards(ctx, scope)
|
|
166
|
+
// 老设置页(≤0.1.5)插件列表配置卡;0.1.7+ 无此槽,注册静默闲置。
|
|
167
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
168
|
+
name: 'settings.plugin.item',
|
|
169
|
+
id: 'session-guard',
|
|
170
|
+
key: 'session-guard',
|
|
171
|
+
order: 60,
|
|
172
|
+
locale: 'session-guard',
|
|
173
|
+
inject: () => ({ scope }),
|
|
174
|
+
}, SessionGuardCard))
|
|
175
|
+
})
|
|
176
|
+
// 无设置页宿主(0.1.0/0.1.1)的兜底配置入口:壳级 overlay 浮窗齿轮。
|
|
177
|
+
safe('shell.overlay', () => ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
178
|
+
{ name: 'shell.overlay', id: 'session-guard-settings' },
|
|
179
|
+
() => ShellSettingsEntry({ scope }),
|
|
180
|
+
)))
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// 家族节接管(2026-10 对齐 TL route A + bindInjectHooks 实证):让位选举。
|
|
184
|
+
// 2026-10-08 T20-a 实证修正:≤0.1.7 壳的设置导航对同 id 异 priority 的遮蔽位
|
|
185
|
+
// 也各列一行——「TL(p0) 恒头位、本插件 p10 恒阴影不占导航」假设不成立,双注册
|
|
186
|
+
// = 双「起子插件设置」导航行(0.1.0~0.1.7 五格全现;0.1.0 摘除 guard 实验:
|
|
187
|
+
// 单行归位、TL 节独座满血)。改为延迟让位:声明批内 TL 同批落账(工厂 fire
|
|
188
|
+
// 顺序=inject 注册顺序,本插件的 inject 先注册先 fire,TL 同批紧随),延迟
|
|
189
|
+
// FAMILY_YIELD_MS 再查账本——已有 dsh-family(TL 头位落座)→ 让位退场;仍空
|
|
190
|
+
// (TL 缺席/落账失败)→ 接管。inject 仍必须 apply 期注册——slots 核只在「声明
|
|
191
|
+
// 事件」触发工厂,宽限期后注册的 inject 等不到下一次声明(0.1.0 实机日志定案:
|
|
192
|
+
// 4.1.7/4.1.8 的 grace 后注册工厂零执行);register 本就是指令式调用,移入
|
|
193
|
+
// 定时器不改变机制。子席位声明带冲突兜底:TL 先声明 → 去 children 重试(接管
|
|
194
|
+
// 者不重复声明);同 id 同 priority 竞争 → 双抛,放弃。register 走本工厂触发
|
|
195
|
+
// 管道(裸注册的 entry.inject 在 ≤0.1.5 壳上不解,4.1.5「半补位」真相)。
|
|
196
|
+
safe('family-section-election', () => {
|
|
197
|
+
console.log('[dsh-session-guard] election: registering settings.section inject (apply-time)')
|
|
198
|
+
ctx.slots.inject('settings.section', () => {
|
|
199
|
+
console.log('[dsh-session-guard] election: factory fired (settings.section declared)')
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) {
|
|
202
|
+
console.log('[dsh-session-guard] election: dsh-family already seated — stand down')
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const base = {
|
|
206
|
+
name: 'settings.section',
|
|
207
|
+
id: FAMILY_SECTION_ID,
|
|
208
|
+
order: 40,
|
|
209
|
+
priority: 10,
|
|
210
|
+
label: () => '起子插件设置',
|
|
211
|
+
inject: () => ({
|
|
212
|
+
hooks: makeGuardFamilyTabsHooks(ctx.slots),
|
|
213
|
+
renderContributor,
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
ctx.slots.register({
|
|
218
|
+
...base,
|
|
219
|
+
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
220
|
+
}, GuardFamilySection)
|
|
221
|
+
console.log('[dsh-session-guard] family takeover registered (children declared)')
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.log(`[dsh-session-guard] children decl conflict (${error instanceof Error ? error.message : error}); registering without children`)
|
|
224
|
+
try {
|
|
225
|
+
ctx.slots.register(base, GuardFamilySection)
|
|
226
|
+
console.log('[dsh-session-guard] family takeover registered (no children)')
|
|
227
|
+
} catch (secondary) {
|
|
228
|
+
console.log(`[dsh-session-guard] family takeover abandoned: ${secondary instanceof Error ? secondary.message : secondary}`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}, FAMILY_YIELD_MS)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 直接挂载面(≤0.1.5 壳无 renderSlot 的降级):从账本原始条目现挂激活卡。
|
|
237
|
+
* entries 每代都带 component + inject(ui-slots SlotCore.entries 原始记录);
|
|
238
|
+
* 任何失败(条目消失/抛错 inject)降级为 null,不炸接管节。 */
|
|
239
|
+
function renderContributor(id: string): JSX.Element | null {
|
|
240
|
+
try {
|
|
241
|
+
const entry = ctx.slots.entries(FAMILY_CHILD_KEY).find(e => e.options.id === id)
|
|
242
|
+
if (entry === undefined || typeof entry.component !== 'function') return null
|
|
243
|
+
const injected = typeof entry.inject === 'function' ? entry.inject() : undefined
|
|
244
|
+
return createElement(
|
|
245
|
+
entry.component as (props: Record<string, unknown>) => JSX.Element | null,
|
|
246
|
+
(injected ?? {}) as Record<string, unknown>,
|
|
247
|
+
)
|
|
248
|
+
} catch {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
128
251
|
}
|
|
129
252
|
|
|
130
253
|
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|