dsh-session-guard 3.2.5 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +159 -143
- package/CHANGELOG.ko.md +159 -143
- package/CHANGELOG.md +232 -216
- package/INSTALL.ja.md +5 -0
- package/INSTALL.ko.md +5 -0
- package/INSTALL.md +5 -0
- package/INSTALL.zh.md +5 -0
- package/LICENSE +21 -21
- package/README.de.md +375 -0
- package/README.en.md +16 -1
- package/README.es.md +374 -0
- package/README.fr.md +373 -0
- package/README.it.md +374 -0
- package/README.ja.md +16 -1
- package/README.ko.md +16 -1
- package/README.md +30 -6
- package/README.ru.md +374 -0
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +16 -16
- package/lib/client.js +0 -345
- package/lib/client.js.map +1 -1
- package/package.json +8 -10
- package/src/bridge.js +136 -136
- package/src/client/index.ts +156 -168
- package/src/client/locales.ts +1 -199
- 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/styles.ts +30 -30
- package/src/detect.js +27 -27
- package/src/gate.js +93 -93
- 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/locale/de.json +0 -6
- package/locale/en.json +0 -6
- package/locale/es.json +0 -6
- package/locale/fr.json +0 -6
- package/locale/it.json +0 -6
- package/locale/ja.json +0 -6
- package/locale/ko.json +0 -6
- package/locale/ru.json +0 -6
- package/locale/zh.json +0 -6
- package/src/client/shims.d.ts +0 -21
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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,168 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 浏览器 half。
|
|
3
|
-
*
|
|
4
|
-
* 职责(全部 fail-open,D8):
|
|
5
|
-
* - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询
|
|
6
|
-
* /session-guard/status;
|
|
7
|
-
* - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc
|
|
8
|
-
* 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。
|
|
9
|
-
* - 0.1.7:旧的插件设置卡已随席位删除 —— 设置表单由 host
|
|
10
|
-
* 侧 Config 的 `.volatile()` 字段自动生成。
|
|
11
|
-
*
|
|
12
|
-
* 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。
|
|
13
|
-
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
order: entry.options.order ?? 0,
|
|
158
|
-
label: typeof entry.options.label === 'function'
|
|
159
|
-
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
160
|
-
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
161
|
-
}))
|
|
162
|
-
.sort((a, b) => a.order - b.order)
|
|
163
|
-
}
|
|
164
|
-
return tabs
|
|
165
|
-
},
|
|
166
|
-
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 浏览器 half。
|
|
3
|
+
*
|
|
4
|
+
* 职责(全部 fail-open,D8):
|
|
5
|
+
* - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询
|
|
6
|
+
* /session-guard/status;
|
|
7
|
+
* - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc
|
|
8
|
+
* 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。
|
|
9
|
+
* - 0.1.7:旧的插件设置卡已随席位删除 —— 设置表单由 host
|
|
10
|
+
* 侧 Config 的 `.volatile()` 字段自动生成。
|
|
11
|
+
*
|
|
12
|
+
* 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。
|
|
13
|
+
*/
|
|
14
|
+
import { StatusBadge } from './status-badge'
|
|
15
|
+
import { PauseButton } from './pause-button'
|
|
16
|
+
import { SessionGuardCard } from './settings-card'
|
|
17
|
+
import { GuardFamilySection, type FamilyTabEntry } from './family-section'
|
|
18
|
+
|
|
19
|
+
// 起子插件族共用设置节:宿主 dsh-thinking-levels 注册顶级 `settings.section`
|
|
20
|
+
// (id `dsh-family`)并声明 `dsh-family.tab` 子席位,本插件贡献卡片。宿主缺席
|
|
21
|
+
// 时本插件按下载量排名接管该节(见 apply 内的选举逻辑),节名仍为
|
|
22
|
+
// 「起子插件设置」;宿主在场时本插件只贡献卡片。
|
|
23
|
+
// 类型导入(构建时擦除,bundle 无 value-import 平台包):让下方 `declare
|
|
24
|
+
// module` 增补能解析到 @deepseek-ai/dsh-client-ui-slots 的 SlotMap。
|
|
25
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
26
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
27
|
+
interface SlotMap {
|
|
28
|
+
'dsh-family.tab': { kind: 'list'; scope: 'root' }
|
|
29
|
+
/**
|
|
30
|
+
* 插件页配置卡(官方 ui-plugin-manager 契约,key = 包名):DSH 0.2.0 的
|
|
31
|
+
* Plugins 页不自动渲染 volatile 配置表单,bundle 详情页只渲染注册在本
|
|
32
|
+
* keyed 席位下的卡片;owner props 由页面给(`{ view: 'page', form }`),
|
|
33
|
+
* 本卡片改经 inject 工厂拿自己的 scope(与 dsh-family.tab 注册同源)。
|
|
34
|
+
*/
|
|
35
|
+
'plugins.bundle.config': { kind: 'keyed'; scope: 'root'; owner: Record<string, unknown> }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 家族节固定 id(与 thinking-levels 的注册严格一致)。 */
|
|
40
|
+
const FAMILY_SECTION_ID = 'dsh-family'
|
|
41
|
+
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
42
|
+
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
43
|
+
/**
|
|
44
|
+
* 接管宽限期:等宿主(以及排名更前的贡献方)先落座。下载量排名 guard(1522)
|
|
45
|
+
* 高于 session-steward(1090) → guard 宽限期更短、先尝试接管;并发冲突由
|
|
46
|
+
* ui-slots 的「同 id 同 priority 重复注册抛错」仲裁,后到者放弃即可。
|
|
47
|
+
*/
|
|
48
|
+
const FAMILY_HOST_GRACE_MS = 2000
|
|
49
|
+
|
|
50
|
+
/** 客户端所需服务:slots(状态徽标)+ locale + configForms(共用 tab 设置卡)。 */
|
|
51
|
+
export const inject = ['slots', 'locale', 'configForms']
|
|
52
|
+
|
|
53
|
+
/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
|
|
54
|
+
interface SlotsFace {
|
|
55
|
+
inject: (_name: string, _fn: () => unknown) => () => void
|
|
56
|
+
register: (_options: Record<string, unknown>, _component: unknown) => () => void
|
|
57
|
+
entries: (_name: string) => Array<{ options: { id?: string; order?: number; label?: unknown } }>
|
|
58
|
+
getVersion: (_name: string) => number
|
|
59
|
+
subscribe: (_name: string, _listener: () => void) => () => void
|
|
60
|
+
}
|
|
61
|
+
interface ConfigFormsFace {
|
|
62
|
+
get: <T>(_entryId: string) => import('./scope-face').SettingsScope<T>
|
|
63
|
+
}
|
|
64
|
+
interface ClientCtx {
|
|
65
|
+
slots: SlotsFace
|
|
66
|
+
configForms: ConfigFormsFace
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function apply(ctx: ClientCtx) {
|
|
70
|
+
// 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。
|
|
71
|
+
// order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。
|
|
72
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
73
|
+
name: 'conversation.input.right',
|
|
74
|
+
id: 'session-guard-pause',
|
|
75
|
+
order: 20,
|
|
76
|
+
locale: 'session-guard',
|
|
77
|
+
}, PauseButton))
|
|
78
|
+
|
|
79
|
+
// 状态徽标:输入区右侧,纯展示。
|
|
80
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
81
|
+
name: 'conversation.input.right',
|
|
82
|
+
id: 'session-guard-status',
|
|
83
|
+
order: 40,
|
|
84
|
+
locale: 'session-guard',
|
|
85
|
+
}, StatusBadge))
|
|
86
|
+
|
|
87
|
+
// 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的 volatile 配置。
|
|
88
|
+
ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({
|
|
89
|
+
name: 'dsh-family.tab',
|
|
90
|
+
id: 'session-guard',
|
|
91
|
+
order: 20,
|
|
92
|
+
label: '会话守护门禁',
|
|
93
|
+
locale: 'session-guard',
|
|
94
|
+
inject: () => ({ scope: ctx.configForms.get<Record<string, unknown>>('session-guard') }),
|
|
95
|
+
}, SessionGuardCard))
|
|
96
|
+
|
|
97
|
+
// 插件页配置卡(DSH 0.2.0):Plugins 页不自动渲染 volatile 配置表单,须注册
|
|
98
|
+
// `plugins.bundle.config` 席位(key = 包名),配置卡才渲染在 bundle 详情页。
|
|
99
|
+
// 卡片组件与 props(scope)与上方 dsh-family.tab 注册完全同源:同一张
|
|
100
|
+
// SessionGuardCard、同一 inject 工厂,配置仍写入 entry `session-guard` 的
|
|
101
|
+
// volatile config;页面注入的 owner props(form 等)本卡不消费。
|
|
102
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
103
|
+
name: 'plugins.bundle.config',
|
|
104
|
+
key: 'dsh-session-guard',
|
|
105
|
+
inject: () => ({ scope: ctx.configForms.get<Record<string, unknown>>('session-guard') }),
|
|
106
|
+
}, SessionGuardCard))
|
|
107
|
+
|
|
108
|
+
// 家族节宿主选举:宿主 dsh-thinking-levels 缺席时顶上「起子插件设置」节。
|
|
109
|
+
// 宽限期后若 `settings.section` 里没有 id `dsh-family` 的条目,则按同一 id
|
|
110
|
+
// 接管(声明同一个 dsh-family.tab 子席位,节内通用渲染全部贡献卡)。与
|
|
111
|
+
// session-steward 的并发竞争由 ui-slots 同 id 重复注册抛错仲裁,先到者胜。
|
|
112
|
+
// 节内各卡(含本插件自己的贡献卡)经子席位 renderSlot 渲染,抽屉默认展开。
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
|
|
115
|
+
try {
|
|
116
|
+
ctx.slots.register({
|
|
117
|
+
name: 'settings.section',
|
|
118
|
+
id: FAMILY_SECTION_ID,
|
|
119
|
+
order: 40,
|
|
120
|
+
label: () => '起子插件设置',
|
|
121
|
+
inject: () => makeGuardFamilyTabsHooks(ctx.slots),
|
|
122
|
+
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
123
|
+
}, GuardFamilySection)
|
|
124
|
+
} catch {
|
|
125
|
+
// 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
|
|
126
|
+
}
|
|
127
|
+
}, FAMILY_HOST_GRACE_MS)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|
|
131
|
+
function makeGuardFamilyTabsHooks(slots: SlotsFace): {
|
|
132
|
+
getSnapshot: () => readonly FamilyTabEntry[]
|
|
133
|
+
subscribe: (listener: () => void) => () => void
|
|
134
|
+
} {
|
|
135
|
+
let version = -1
|
|
136
|
+
let tabs: readonly FamilyTabEntry[] = []
|
|
137
|
+
return {
|
|
138
|
+
getSnapshot: () => {
|
|
139
|
+
const next = slots.getVersion(FAMILY_CHILD_KEY)
|
|
140
|
+
if (next !== version) {
|
|
141
|
+
version = next
|
|
142
|
+
tabs = slots.entries(FAMILY_CHILD_KEY)
|
|
143
|
+
.map(entry => ({
|
|
144
|
+
id: entry.options.id ?? '',
|
|
145
|
+
order: entry.options.order ?? 0,
|
|
146
|
+
label: typeof entry.options.label === 'function'
|
|
147
|
+
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
148
|
+
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) => a.order - b.order)
|
|
151
|
+
}
|
|
152
|
+
return tabs
|
|
153
|
+
},
|
|
154
|
+
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
155
|
+
}
|
|
156
|
+
}
|