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,129 +1,129 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。
|
|
3
|
-
*
|
|
4
|
-
* 交互(v0.2.0 修订):
|
|
5
|
-
* - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停
|
|
6
|
-
* (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);
|
|
7
|
-
* - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;
|
|
8
|
-
* - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底
|
|
9
|
-
* (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。
|
|
10
|
-
*
|
|
11
|
-
* 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:
|
|
12
|
-
* 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。
|
|
13
|
-
*/
|
|
14
|
-
import { useEffect, useState } from 'react'
|
|
15
|
-
import {
|
|
16
|
-
pauseButtonLabel,
|
|
17
|
-
pauseButtonTitle,
|
|
18
|
-
readPauseState,
|
|
19
|
-
readStepSnapshot,
|
|
20
|
-
type PauseButtonState,
|
|
21
|
-
} from './pause-button-text'
|
|
22
|
-
import { injectClientCss } from './styles'
|
|
23
|
-
|
|
24
|
-
/** slot 运行时注入的会话级 props。 */
|
|
25
|
-
export interface PauseButtonProps {
|
|
26
|
-
sessionId?: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */
|
|
30
|
-
const POLL_MS = 10_000
|
|
31
|
-
|
|
32
|
-
const IDLE: PauseButtonState = { paused: false, heldSince: null, manual: false }
|
|
33
|
-
|
|
34
|
-
/** 暂停 / 继续会话按钮。 */
|
|
35
|
-
export function PauseButton({ sessionId }: PauseButtonProps) {
|
|
36
|
-
const [state, setState] = useState<PauseButtonState>(IDLE)
|
|
37
|
-
const [busy, setBusy] = useState(false)
|
|
38
|
-
|
|
39
|
-
injectClientCss()
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (sessionId === undefined || sessionId === '') return
|
|
43
|
-
let cancelled = false
|
|
44
|
-
|
|
45
|
-
const poll = async (): Promise<void> => {
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)
|
|
48
|
-
const body = await res.json().catch(() => null)
|
|
49
|
-
if (cancelled) return
|
|
50
|
-
setState(readPauseState(body))
|
|
51
|
-
} catch {
|
|
52
|
-
// fail-open:路由不存在 / 网络失败 → 保持上一次状态。
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
void poll()
|
|
56
|
-
const timer = setInterval(() => {
|
|
57
|
-
void poll()
|
|
58
|
-
}, POLL_MS)
|
|
59
|
-
|
|
60
|
-
let source: EventSource | undefined
|
|
61
|
-
try {
|
|
62
|
-
source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`)
|
|
63
|
-
source.onmessage = (ev: MessageEvent<string>) => {
|
|
64
|
-
try {
|
|
65
|
-
const msg = JSON.parse(ev.data) as { type?: string; state?: unknown }
|
|
66
|
-
if (cancelled || msg?.type !== 'step') return
|
|
67
|
-
setState(readStepSnapshot(msg.state))
|
|
68
|
-
} catch {
|
|
69
|
-
/* 非法负载:忽略,等下一次推送或轮询 */
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// onerror:EventSource 会自行重连;轮询同时兜底。
|
|
73
|
-
source.onerror = () => undefined
|
|
74
|
-
} catch {
|
|
75
|
-
// 浏览器不支持 EventSource → 只靠轮询。
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true
|
|
80
|
-
clearInterval(timer)
|
|
81
|
-
try {
|
|
82
|
-
source?.close()
|
|
83
|
-
} catch {
|
|
84
|
-
/* ignore */
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}, [sessionId])
|
|
88
|
-
|
|
89
|
-
if (sessionId === undefined || sessionId === '') return null
|
|
90
|
-
|
|
91
|
-
const paused = state.paused === true
|
|
92
|
-
const onClick = async (): Promise<void> => {
|
|
93
|
-
if (busy) return
|
|
94
|
-
setBusy(true)
|
|
95
|
-
try {
|
|
96
|
-
const action = paused ? 'stepResume' : 'stepPause'
|
|
97
|
-
const res = await fetch('/session-guard/rpc', {
|
|
98
|
-
method: 'POST',
|
|
99
|
-
headers: { 'content-type': 'application/json' },
|
|
100
|
-
body: JSON.stringify({ sessionId, action }),
|
|
101
|
-
})
|
|
102
|
-
const body = await res.json().catch(() => null)
|
|
103
|
-
if (body?.ok === true) {
|
|
104
|
-
// 立即乐观更新;SSE / 轮询随后校正。
|
|
105
|
-
setState(paused ? IDLE : { paused: true, heldSince: null, manual: true })
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// fail-open:下一轮轮询会纠正显示。
|
|
109
|
-
} finally {
|
|
110
|
-
setBusy(false)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<button
|
|
116
|
-
type="button"
|
|
117
|
-
className="sg-pause"
|
|
118
|
-
disabled={busy}
|
|
119
|
-
aria-pressed={paused || undefined}
|
|
120
|
-
title={pauseButtonTitle(state)}
|
|
121
|
-
data-sg-step-paused={paused ? 'on' : 'off'}
|
|
122
|
-
onClick={() => {
|
|
123
|
-
void onClick()
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
{pauseButtonLabel(state)}
|
|
127
|
-
</button>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。
|
|
3
|
+
*
|
|
4
|
+
* 交互(v0.2.0 修订):
|
|
5
|
+
* - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停
|
|
6
|
+
* (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);
|
|
7
|
+
* - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;
|
|
8
|
+
* - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底
|
|
9
|
+
* (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。
|
|
10
|
+
*
|
|
11
|
+
* 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:
|
|
12
|
+
* 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useState } from 'react'
|
|
15
|
+
import {
|
|
16
|
+
pauseButtonLabel,
|
|
17
|
+
pauseButtonTitle,
|
|
18
|
+
readPauseState,
|
|
19
|
+
readStepSnapshot,
|
|
20
|
+
type PauseButtonState,
|
|
21
|
+
} from './pause-button-text'
|
|
22
|
+
import { injectClientCss } from './styles'
|
|
23
|
+
|
|
24
|
+
/** slot 运行时注入的会话级 props。 */
|
|
25
|
+
export interface PauseButtonProps {
|
|
26
|
+
sessionId?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */
|
|
30
|
+
const POLL_MS = 10_000
|
|
31
|
+
|
|
32
|
+
const IDLE: PauseButtonState = { paused: false, heldSince: null, manual: false }
|
|
33
|
+
|
|
34
|
+
/** 暂停 / 继续会话按钮。 */
|
|
35
|
+
export function PauseButton({ sessionId }: PauseButtonProps) {
|
|
36
|
+
const [state, setState] = useState<PauseButtonState>(IDLE)
|
|
37
|
+
const [busy, setBusy] = useState(false)
|
|
38
|
+
|
|
39
|
+
injectClientCss()
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (sessionId === undefined || sessionId === '') return
|
|
43
|
+
let cancelled = false
|
|
44
|
+
|
|
45
|
+
const poll = async (): Promise<void> => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)
|
|
48
|
+
const body = await res.json().catch(() => null)
|
|
49
|
+
if (cancelled) return
|
|
50
|
+
setState(readPauseState(body))
|
|
51
|
+
} catch {
|
|
52
|
+
// fail-open:路由不存在 / 网络失败 → 保持上一次状态。
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
void poll()
|
|
56
|
+
const timer = setInterval(() => {
|
|
57
|
+
void poll()
|
|
58
|
+
}, POLL_MS)
|
|
59
|
+
|
|
60
|
+
let source: EventSource | undefined
|
|
61
|
+
try {
|
|
62
|
+
source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`)
|
|
63
|
+
source.onmessage = (ev: MessageEvent<string>) => {
|
|
64
|
+
try {
|
|
65
|
+
const msg = JSON.parse(ev.data) as { type?: string; state?: unknown }
|
|
66
|
+
if (cancelled || msg?.type !== 'step') return
|
|
67
|
+
setState(readStepSnapshot(msg.state))
|
|
68
|
+
} catch {
|
|
69
|
+
/* 非法负载:忽略,等下一次推送或轮询 */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// onerror:EventSource 会自行重连;轮询同时兜底。
|
|
73
|
+
source.onerror = () => undefined
|
|
74
|
+
} catch {
|
|
75
|
+
// 浏览器不支持 EventSource → 只靠轮询。
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true
|
|
80
|
+
clearInterval(timer)
|
|
81
|
+
try {
|
|
82
|
+
source?.close()
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, [sessionId])
|
|
88
|
+
|
|
89
|
+
if (sessionId === undefined || sessionId === '') return null
|
|
90
|
+
|
|
91
|
+
const paused = state.paused === true
|
|
92
|
+
const onClick = async (): Promise<void> => {
|
|
93
|
+
if (busy) return
|
|
94
|
+
setBusy(true)
|
|
95
|
+
try {
|
|
96
|
+
const action = paused ? 'stepResume' : 'stepPause'
|
|
97
|
+
const res = await fetch('/session-guard/rpc', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'content-type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ sessionId, action }),
|
|
101
|
+
})
|
|
102
|
+
const body = await res.json().catch(() => null)
|
|
103
|
+
if (body?.ok === true) {
|
|
104
|
+
// 立即乐观更新;SSE / 轮询随后校正。
|
|
105
|
+
setState(paused ? IDLE : { paused: true, heldSince: null, manual: true })
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// fail-open:下一轮轮询会纠正显示。
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy(false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className="sg-pause"
|
|
118
|
+
disabled={busy}
|
|
119
|
+
aria-pressed={paused || undefined}
|
|
120
|
+
title={pauseButtonTitle(state)}
|
|
121
|
+
data-sg-step-paused={paused ? 'on' : 'off'}
|
|
122
|
+
onClick={() => {
|
|
123
|
+
void onClick()
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{pauseButtonLabel(state)}
|
|
127
|
+
</button>
|
|
128
|
+
)
|
|
129
|
+
}
|
package/src/client/scope-face.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 0.1.7-rc.2 停止从 `@deepseek-ai/dsh-client-ui-settings/client` 导出
|
|
3
|
-
* `SettingsScope`;卡片消费的结构化面移到此处,字段逐一镜像
|
|
4
|
-
* `configForms.get(entryId)` 返回的 ConfigForm。
|
|
5
|
-
*/
|
|
6
|
-
export interface SettingsScope<T> {
|
|
7
|
-
getSnapshot(): {
|
|
8
|
-
status: 'loading' | 'ready' | 'unavailable'
|
|
9
|
-
value: T | undefined
|
|
10
|
-
revision: number | undefined
|
|
11
|
-
writable: boolean
|
|
12
|
-
base: unknown
|
|
13
|
-
user: unknown
|
|
14
|
-
mode: 'host' | 'memory'
|
|
15
|
-
}
|
|
16
|
-
subscribe(listener: () => void): () => void
|
|
17
|
-
set(field: string, value: unknown): Promise<boolean>
|
|
18
|
-
unset(field: string): Promise<boolean>
|
|
19
|
-
mutate?(ops: readonly { path: readonly string[]; op: string; value?: unknown }[]): Promise<boolean>
|
|
20
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 0.1.7-rc.2 停止从 `@deepseek-ai/dsh-client-ui-settings/client` 导出
|
|
3
|
+
* `SettingsScope`;卡片消费的结构化面移到此处,字段逐一镜像
|
|
4
|
+
* `configForms.get(entryId)` 返回的 ConfigForm。
|
|
5
|
+
*/
|
|
6
|
+
export interface SettingsScope<T> {
|
|
7
|
+
getSnapshot(): {
|
|
8
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
9
|
+
value: T | undefined
|
|
10
|
+
revision: number | undefined
|
|
11
|
+
writable: boolean
|
|
12
|
+
base: unknown
|
|
13
|
+
user: unknown
|
|
14
|
+
mode: 'host' | 'memory'
|
|
15
|
+
}
|
|
16
|
+
subscribe(listener: () => void): () => void
|
|
17
|
+
set(field: string, value: unknown): Promise<boolean>
|
|
18
|
+
unset(field: string): Promise<boolean>
|
|
19
|
+
mutate?(ops: readonly { path: readonly string[]; op: string; value?: unknown }[]): Promise<boolean>
|
|
20
|
+
}
|