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.
- package/CHANGELOG.ja.md +154 -143
- package/CHANGELOG.ko.md +154 -143
- package/CHANGELOG.md +227 -216
- package/LICENSE +21 -21
- package/README.md +14 -5
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +16 -16
- package/lib/client.js.map +1 -1
- package/package.json +8 -8
- package/src/bridge.js +136 -136
- package/src/client/family-section.tsx +46 -46
- package/src/client/index.ts +135 -135
- 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
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 家族节接管组件(fallback host section)。
|
|
3
|
-
*
|
|
4
|
-
* 仅当宿主 dsh-thinking-levels 缺席、且本插件在接管选举中胜出时,由
|
|
5
|
-
* index.ts 注册为顶级 `settings.section`(id 固定 `dsh-family`,节名仍为
|
|
6
|
-
* 「起子插件设置」)。组件是纯通用渲染:把 `dsh-family.tab` 账本里的每张
|
|
7
|
-
* 贡献卡(含本插件自己的,因为接管时本插件也声明了该子席位)按 order
|
|
8
|
-
* 依次 renderSlot —— 卡片本身是默认展开的抽屉,整节即一页抽屉。
|
|
9
|
-
*
|
|
10
|
-
* 与 thinking-levels 的 FamilySettingsSection 的差别仅在不需要 thinking-
|
|
11
|
-
* levels 自己的卡(宿主不在了,它的卡也不在):渲染循环没有 own-tab 特例。
|
|
12
|
-
*/
|
|
13
|
-
import { useSyncExternalStore } from 'react'
|
|
14
|
-
import type { JSX } from 'react'
|
|
15
|
-
|
|
16
|
-
/** 与 FamilySectionInjected.hooks.tabs 同形的账本投影(结构化镜像)。 */
|
|
17
|
-
type HostObservable<T> = {
|
|
18
|
-
getSnapshot(): T
|
|
19
|
-
subscribe(listener: () => void): () => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 账本条目(id/order/label,label 已解析为字符串)。 */
|
|
23
|
-
export interface FamilyTabEntry {
|
|
24
|
-
id: string
|
|
25
|
-
order: number
|
|
26
|
-
label: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 组件 props:框架注入的子席位 dispatcher + 注册侧 inject 返回的 hooks。 */
|
|
30
|
-
export interface GuardFamilySectionProps {
|
|
31
|
-
renderSlot: (key: 'dsh-family.tab', owner?: object, opts?: { only?: string; fallback?: JSX.Element | null }) => JSX.Element | null
|
|
32
|
-
hooks: { tabs: HostObservable<readonly FamilyTabEntry[]> }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function GuardFamilySection(props: GuardFamilySectionProps): JSX.Element {
|
|
36
|
-
const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
|
|
37
|
-
return (
|
|
38
|
-
<div style={{ display: 'grid', gap: '12px' }}>
|
|
39
|
-
{tabs.map(row => (
|
|
40
|
-
<div key={row.id}>
|
|
41
|
-
{props.renderSlot('dsh-family.tab', {}, { only: row.id, fallback: null })}
|
|
42
|
-
</div>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 家族节接管组件(fallback host section)。
|
|
3
|
+
*
|
|
4
|
+
* 仅当宿主 dsh-thinking-levels 缺席、且本插件在接管选举中胜出时,由
|
|
5
|
+
* index.ts 注册为顶级 `settings.section`(id 固定 `dsh-family`,节名仍为
|
|
6
|
+
* 「起子插件设置」)。组件是纯通用渲染:把 `dsh-family.tab` 账本里的每张
|
|
7
|
+
* 贡献卡(含本插件自己的,因为接管时本插件也声明了该子席位)按 order
|
|
8
|
+
* 依次 renderSlot —— 卡片本身是默认展开的抽屉,整节即一页抽屉。
|
|
9
|
+
*
|
|
10
|
+
* 与 thinking-levels 的 FamilySettingsSection 的差别仅在不需要 thinking-
|
|
11
|
+
* levels 自己的卡(宿主不在了,它的卡也不在):渲染循环没有 own-tab 特例。
|
|
12
|
+
*/
|
|
13
|
+
import { useSyncExternalStore } from 'react'
|
|
14
|
+
import type { JSX } from 'react'
|
|
15
|
+
|
|
16
|
+
/** 与 FamilySectionInjected.hooks.tabs 同形的账本投影(结构化镜像)。 */
|
|
17
|
+
type HostObservable<T> = {
|
|
18
|
+
getSnapshot(): T
|
|
19
|
+
subscribe(listener: () => void): () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 账本条目(id/order/label,label 已解析为字符串)。 */
|
|
23
|
+
export interface FamilyTabEntry {
|
|
24
|
+
id: string
|
|
25
|
+
order: number
|
|
26
|
+
label: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 组件 props:框架注入的子席位 dispatcher + 注册侧 inject 返回的 hooks。 */
|
|
30
|
+
export interface GuardFamilySectionProps {
|
|
31
|
+
renderSlot: (key: 'dsh-family.tab', owner?: object, opts?: { only?: string; fallback?: JSX.Element | null }) => JSX.Element | null
|
|
32
|
+
hooks: { tabs: HostObservable<readonly FamilyTabEntry[]> }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function GuardFamilySection(props: GuardFamilySectionProps): JSX.Element {
|
|
36
|
+
const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ display: 'grid', gap: '12px' }}>
|
|
39
|
+
{tabs.map(row => (
|
|
40
|
+
<div key={row.id}>
|
|
41
|
+
{props.renderSlot('dsh-family.tab', {}, { only: row.id, fallback: null })}
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
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
|
-
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
24
|
-
interface SlotMap {
|
|
25
|
-
'dsh-family.tab': { kind: 'list'; scope: 'root' }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 家族节固定 id(与 thinking-levels 的注册严格一致)。 */
|
|
30
|
-
const FAMILY_SECTION_ID = 'dsh-family'
|
|
31
|
-
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
32
|
-
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
33
|
-
/**
|
|
34
|
-
* 接管宽限期:等宿主(以及排名更前的贡献方)先落座。下载量排名 guard(1522)
|
|
35
|
-
* 高于 session-steward(1090) → guard 宽限期更短、先尝试接管;并发冲突由
|
|
36
|
-
* ui-slots 的「同 id 同 priority 重复注册抛错」仲裁,后到者放弃即可。
|
|
37
|
-
*/
|
|
38
|
-
const FAMILY_HOST_GRACE_MS = 2000
|
|
39
|
-
|
|
40
|
-
/** 客户端所需服务:slots(状态徽标)+ locale + configForms(共用 tab 设置卡)。 */
|
|
41
|
-
export const inject = ['slots', 'locale', 'configForms']
|
|
42
|
-
|
|
43
|
-
/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
|
|
44
|
-
interface SlotsFace {
|
|
45
|
-
inject: (_name: string, _fn: () => unknown) => () => void
|
|
46
|
-
register: (_options: Record<string, unknown>, _component: unknown) => () => void
|
|
47
|
-
entries: (_name: string) => Array<{ options: { id?: string; order?: number; label?: unknown } }>
|
|
48
|
-
getVersion: (_name: string) => number
|
|
49
|
-
subscribe: (_name: string, _listener: () => void) => () => void
|
|
50
|
-
}
|
|
51
|
-
interface ConfigFormsFace {
|
|
52
|
-
get: <T>(_entryId: string) => import('./scope-face').SettingsScope<T>
|
|
53
|
-
}
|
|
54
|
-
interface ClientCtx {
|
|
55
|
-
slots: SlotsFace
|
|
56
|
-
configForms: ConfigFormsFace
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function apply(ctx: ClientCtx) {
|
|
60
|
-
// 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。
|
|
61
|
-
// order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。
|
|
62
|
-
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
63
|
-
name: 'conversation.input.right',
|
|
64
|
-
id: 'session-guard-pause',
|
|
65
|
-
order: 20,
|
|
66
|
-
locale: 'session-guard',
|
|
67
|
-
}, PauseButton))
|
|
68
|
-
|
|
69
|
-
// 状态徽标:输入区右侧,纯展示。
|
|
70
|
-
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
71
|
-
name: 'conversation.input.right',
|
|
72
|
-
id: 'session-guard-status',
|
|
73
|
-
order: 40,
|
|
74
|
-
locale: 'session-guard',
|
|
75
|
-
}, StatusBadge))
|
|
76
|
-
|
|
77
|
-
// 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的 volatile 配置。
|
|
78
|
-
ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({
|
|
79
|
-
name: 'dsh-family.tab',
|
|
80
|
-
id: 'session-guard',
|
|
81
|
-
order: 20,
|
|
82
|
-
label: '会话守护门禁',
|
|
83
|
-
locale: 'session-guard',
|
|
84
|
-
inject: () => ({ scope: ctx.configForms.get<Record<string, unknown>>('session-guard') }),
|
|
85
|
-
}, SessionGuardCard))
|
|
86
|
-
|
|
87
|
-
// 家族节宿主选举:宿主 dsh-thinking-levels 缺席时顶上「起子插件设置」节。
|
|
88
|
-
// 宽限期后若 `settings.section` 里没有 id `dsh-family` 的条目,则按同一 id
|
|
89
|
-
// 接管(声明同一个 dsh-family.tab 子席位,节内通用渲染全部贡献卡)。与
|
|
90
|
-
// session-steward 的并发竞争由 ui-slots 同 id 重复注册抛错仲裁,先到者胜。
|
|
91
|
-
// 节内各卡(含本插件自己的贡献卡)经子席位 renderSlot 渲染,抽屉默认展开。
|
|
92
|
-
setTimeout(() => {
|
|
93
|
-
if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
|
|
94
|
-
try {
|
|
95
|
-
ctx.slots.register({
|
|
96
|
-
name: 'settings.section',
|
|
97
|
-
id: FAMILY_SECTION_ID,
|
|
98
|
-
order: 40,
|
|
99
|
-
label: () => '起子插件设置',
|
|
100
|
-
inject: () => makeGuardFamilyTabsHooks(ctx.slots),
|
|
101
|
-
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
102
|
-
}, GuardFamilySection)
|
|
103
|
-
} catch {
|
|
104
|
-
// 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
|
|
105
|
-
}
|
|
106
|
-
}, FAMILY_HOST_GRACE_MS)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|
|
110
|
-
function makeGuardFamilyTabsHooks(slots: SlotsFace): {
|
|
111
|
-
getSnapshot: () => readonly FamilyTabEntry[]
|
|
112
|
-
subscribe: (listener: () => void) => () => void
|
|
113
|
-
} {
|
|
114
|
-
let version = -1
|
|
115
|
-
let tabs: readonly FamilyTabEntry[] = []
|
|
116
|
-
return {
|
|
117
|
-
getSnapshot: () => {
|
|
118
|
-
const next = slots.getVersion(FAMILY_CHILD_KEY)
|
|
119
|
-
if (next !== version) {
|
|
120
|
-
version = next
|
|
121
|
-
tabs = slots.entries(FAMILY_CHILD_KEY)
|
|
122
|
-
.map(entry => ({
|
|
123
|
-
id: entry.options.id ?? '',
|
|
124
|
-
order: entry.options.order ?? 0,
|
|
125
|
-
label: typeof entry.options.label === 'function'
|
|
126
|
-
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
127
|
-
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
128
|
-
}))
|
|
129
|
-
.sort((a, b) => a.order - b.order)
|
|
130
|
-
}
|
|
131
|
-
return tabs
|
|
132
|
-
},
|
|
133
|
-
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
134
|
-
}
|
|
135
|
-
}
|
|
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
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
24
|
+
interface SlotMap {
|
|
25
|
+
'dsh-family.tab': { kind: 'list'; scope: 'root' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 家族节固定 id(与 thinking-levels 的注册严格一致)。 */
|
|
30
|
+
const FAMILY_SECTION_ID = 'dsh-family'
|
|
31
|
+
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
32
|
+
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
33
|
+
/**
|
|
34
|
+
* 接管宽限期:等宿主(以及排名更前的贡献方)先落座。下载量排名 guard(1522)
|
|
35
|
+
* 高于 session-steward(1090) → guard 宽限期更短、先尝试接管;并发冲突由
|
|
36
|
+
* ui-slots 的「同 id 同 priority 重复注册抛错」仲裁,后到者放弃即可。
|
|
37
|
+
*/
|
|
38
|
+
const FAMILY_HOST_GRACE_MS = 2000
|
|
39
|
+
|
|
40
|
+
/** 客户端所需服务:slots(状态徽标)+ locale + configForms(共用 tab 设置卡)。 */
|
|
41
|
+
export const inject = ['slots', 'locale', 'configForms']
|
|
42
|
+
|
|
43
|
+
/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
|
|
44
|
+
interface SlotsFace {
|
|
45
|
+
inject: (_name: string, _fn: () => unknown) => () => void
|
|
46
|
+
register: (_options: Record<string, unknown>, _component: unknown) => () => void
|
|
47
|
+
entries: (_name: string) => Array<{ options: { id?: string; order?: number; label?: unknown } }>
|
|
48
|
+
getVersion: (_name: string) => number
|
|
49
|
+
subscribe: (_name: string, _listener: () => void) => () => void
|
|
50
|
+
}
|
|
51
|
+
interface ConfigFormsFace {
|
|
52
|
+
get: <T>(_entryId: string) => import('./scope-face').SettingsScope<T>
|
|
53
|
+
}
|
|
54
|
+
interface ClientCtx {
|
|
55
|
+
slots: SlotsFace
|
|
56
|
+
configForms: ConfigFormsFace
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function apply(ctx: ClientCtx) {
|
|
60
|
+
// 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。
|
|
61
|
+
// order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。
|
|
62
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
63
|
+
name: 'conversation.input.right',
|
|
64
|
+
id: 'session-guard-pause',
|
|
65
|
+
order: 20,
|
|
66
|
+
locale: 'session-guard',
|
|
67
|
+
}, PauseButton))
|
|
68
|
+
|
|
69
|
+
// 状态徽标:输入区右侧,纯展示。
|
|
70
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
71
|
+
name: 'conversation.input.right',
|
|
72
|
+
id: 'session-guard-status',
|
|
73
|
+
order: 40,
|
|
74
|
+
locale: 'session-guard',
|
|
75
|
+
}, StatusBadge))
|
|
76
|
+
|
|
77
|
+
// 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的 volatile 配置。
|
|
78
|
+
ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({
|
|
79
|
+
name: 'dsh-family.tab',
|
|
80
|
+
id: 'session-guard',
|
|
81
|
+
order: 20,
|
|
82
|
+
label: '会话守护门禁',
|
|
83
|
+
locale: 'session-guard',
|
|
84
|
+
inject: () => ({ scope: ctx.configForms.get<Record<string, unknown>>('session-guard') }),
|
|
85
|
+
}, SessionGuardCard))
|
|
86
|
+
|
|
87
|
+
// 家族节宿主选举:宿主 dsh-thinking-levels 缺席时顶上「起子插件设置」节。
|
|
88
|
+
// 宽限期后若 `settings.section` 里没有 id `dsh-family` 的条目,则按同一 id
|
|
89
|
+
// 接管(声明同一个 dsh-family.tab 子席位,节内通用渲染全部贡献卡)。与
|
|
90
|
+
// session-steward 的并发竞争由 ui-slots 同 id 重复注册抛错仲裁,先到者胜。
|
|
91
|
+
// 节内各卡(含本插件自己的贡献卡)经子席位 renderSlot 渲染,抽屉默认展开。
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
|
|
94
|
+
try {
|
|
95
|
+
ctx.slots.register({
|
|
96
|
+
name: 'settings.section',
|
|
97
|
+
id: FAMILY_SECTION_ID,
|
|
98
|
+
order: 40,
|
|
99
|
+
label: () => '起子插件设置',
|
|
100
|
+
inject: () => makeGuardFamilyTabsHooks(ctx.slots),
|
|
101
|
+
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
102
|
+
}, GuardFamilySection)
|
|
103
|
+
} catch {
|
|
104
|
+
// 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
|
|
105
|
+
}
|
|
106
|
+
}, FAMILY_HOST_GRACE_MS)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|
|
110
|
+
function makeGuardFamilyTabsHooks(slots: SlotsFace): {
|
|
111
|
+
getSnapshot: () => readonly FamilyTabEntry[]
|
|
112
|
+
subscribe: (listener: () => void) => () => void
|
|
113
|
+
} {
|
|
114
|
+
let version = -1
|
|
115
|
+
let tabs: readonly FamilyTabEntry[] = []
|
|
116
|
+
return {
|
|
117
|
+
getSnapshot: () => {
|
|
118
|
+
const next = slots.getVersion(FAMILY_CHILD_KEY)
|
|
119
|
+
if (next !== version) {
|
|
120
|
+
version = next
|
|
121
|
+
tabs = slots.entries(FAMILY_CHILD_KEY)
|
|
122
|
+
.map(entry => ({
|
|
123
|
+
id: entry.options.id ?? '',
|
|
124
|
+
order: entry.options.order ?? 0,
|
|
125
|
+
label: typeof entry.options.label === 'function'
|
|
126
|
+
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
127
|
+
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
128
|
+
}))
|
|
129
|
+
.sort((a, b) => a.order - b.order)
|
|
130
|
+
}
|
|
131
|
+
return tabs
|
|
132
|
+
},
|
|
133
|
+
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 暂停/继续按钮文案与状态投影(纯函数,零依赖,可单测)。
|
|
3
|
-
*
|
|
4
|
-
* 交互(v0.2.0 修订):
|
|
5
|
-
* - 未暂停 → 「暂停会话」(**可点**:点击 = 在下一次 step 边界手动暂停该会话);
|
|
6
|
-
* - 已暂停(step 门已拉 / 已请求但未到边界)→ 「继续会话」(点击 = 放行并本峰不再拦)。
|
|
7
|
-
* 按钮不置灰:step 暂停虽然会自动发生,但按钮同时是**手动暂停**入口。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** 按钮状态。 */
|
|
11
|
-
export interface PauseButtonState {
|
|
12
|
-
/** 是否处于暂停态(step 门已挂起,或已请求暂停但还没到边界) */
|
|
13
|
-
paused: boolean
|
|
14
|
-
/** 挂起起始时间戳(毫秒);未知为 null */
|
|
15
|
-
heldSince?: number | null
|
|
16
|
-
/** 暂停是否来自用户手动请求(而非高峰自动拉门) */
|
|
17
|
-
manual?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 按钮文案。 */
|
|
21
|
-
export function pauseButtonLabel(state: PauseButtonState): string {
|
|
22
|
-
return state.paused === true ? '继续会话' : '暂停会话'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 悬浮说明:写清当前状态与点击后的语义。 */
|
|
26
|
-
export function pauseButtonTitle(state: PauseButtonState): string {
|
|
27
|
-
if (state.paused !== true) {
|
|
28
|
-
return '暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)'
|
|
29
|
-
}
|
|
30
|
-
const since = typeof state.heldSince === 'number' && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null
|
|
31
|
-
const at = since === null ? null : `${String(since.getHours()).padStart(2, '0')}:${String(since.getMinutes()).padStart(2, '0')}`
|
|
32
|
-
const why = state.manual === true ? '手动暂停' : '高峰自动暂停'
|
|
33
|
-
const base = at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`
|
|
34
|
-
return `${base} · 点击继续:放行当前 step,且本高峰内不再拦该会话`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */
|
|
38
|
-
export function readPauseState(body: unknown): PauseButtonState {
|
|
39
|
-
const b = (body ?? {}) as {
|
|
40
|
-
ok?: unknown
|
|
41
|
-
paused?: { step?: unknown; manual?: unknown }
|
|
42
|
-
stepGate?: { since?: unknown; manual?: unknown }
|
|
43
|
-
state?: { pausedStep?: unknown; stepHeldSince?: unknown; stepManual?: unknown }
|
|
44
|
-
}
|
|
45
|
-
if (b.ok !== true) return { paused: false, heldSince: null, manual: false }
|
|
46
|
-
const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true
|
|
47
|
-
const paused = b.paused?.step === true || b.state?.pausedStep === true || manual
|
|
48
|
-
const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null
|
|
49
|
-
const heldSince = typeof raw === 'number' && Number.isFinite(raw) ? raw : null
|
|
50
|
-
return { paused, heldSince, manual }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */
|
|
54
|
-
export function readStepSnapshot(snapshot: unknown): PauseButtonState {
|
|
55
|
-
const s = (snapshot ?? {}) as { held?: unknown; since?: unknown; manual?: unknown }
|
|
56
|
-
const manual = s.manual === true
|
|
57
|
-
const paused = s.held === true || manual
|
|
58
|
-
const heldSince = typeof s.since === 'number' && Number.isFinite(s.since) ? s.since : null
|
|
59
|
-
return { paused, heldSince, manual }
|
|
60
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 暂停/继续按钮文案与状态投影(纯函数,零依赖,可单测)。
|
|
3
|
+
*
|
|
4
|
+
* 交互(v0.2.0 修订):
|
|
5
|
+
* - 未暂停 → 「暂停会话」(**可点**:点击 = 在下一次 step 边界手动暂停该会话);
|
|
6
|
+
* - 已暂停(step 门已拉 / 已请求但未到边界)→ 「继续会话」(点击 = 放行并本峰不再拦)。
|
|
7
|
+
* 按钮不置灰:step 暂停虽然会自动发生,但按钮同时是**手动暂停**入口。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 按钮状态。 */
|
|
11
|
+
export interface PauseButtonState {
|
|
12
|
+
/** 是否处于暂停态(step 门已挂起,或已请求暂停但还没到边界) */
|
|
13
|
+
paused: boolean
|
|
14
|
+
/** 挂起起始时间戳(毫秒);未知为 null */
|
|
15
|
+
heldSince?: number | null
|
|
16
|
+
/** 暂停是否来自用户手动请求(而非高峰自动拉门) */
|
|
17
|
+
manual?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 按钮文案。 */
|
|
21
|
+
export function pauseButtonLabel(state: PauseButtonState): string {
|
|
22
|
+
return state.paused === true ? '继续会话' : '暂停会话'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 悬浮说明:写清当前状态与点击后的语义。 */
|
|
26
|
+
export function pauseButtonTitle(state: PauseButtonState): string {
|
|
27
|
+
if (state.paused !== true) {
|
|
28
|
+
return '暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)'
|
|
29
|
+
}
|
|
30
|
+
const since = typeof state.heldSince === 'number' && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null
|
|
31
|
+
const at = since === null ? null : `${String(since.getHours()).padStart(2, '0')}:${String(since.getMinutes()).padStart(2, '0')}`
|
|
32
|
+
const why = state.manual === true ? '手动暂停' : '高峰自动暂停'
|
|
33
|
+
const base = at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`
|
|
34
|
+
return `${base} · 点击继续:放行当前 step,且本高峰内不再拦该会话`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */
|
|
38
|
+
export function readPauseState(body: unknown): PauseButtonState {
|
|
39
|
+
const b = (body ?? {}) as {
|
|
40
|
+
ok?: unknown
|
|
41
|
+
paused?: { step?: unknown; manual?: unknown }
|
|
42
|
+
stepGate?: { since?: unknown; manual?: unknown }
|
|
43
|
+
state?: { pausedStep?: unknown; stepHeldSince?: unknown; stepManual?: unknown }
|
|
44
|
+
}
|
|
45
|
+
if (b.ok !== true) return { paused: false, heldSince: null, manual: false }
|
|
46
|
+
const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true
|
|
47
|
+
const paused = b.paused?.step === true || b.state?.pausedStep === true || manual
|
|
48
|
+
const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null
|
|
49
|
+
const heldSince = typeof raw === 'number' && Number.isFinite(raw) ? raw : null
|
|
50
|
+
return { paused, heldSince, manual }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */
|
|
54
|
+
export function readStepSnapshot(snapshot: unknown): PauseButtonState {
|
|
55
|
+
const s = (snapshot ?? {}) as { held?: unknown; since?: unknown; manual?: unknown }
|
|
56
|
+
const manual = s.manual === true
|
|
57
|
+
const paused = s.held === true || manual
|
|
58
|
+
const heldSince = typeof s.since === 'number' && Number.isFinite(s.since) ? s.since : null
|
|
59
|
+
return { paused, heldSince, manual }
|
|
60
|
+
}
|