dsh-session-guard 0.1.2 → 0.2.0-beta.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 +75 -29
- package/CHANGELOG.ko.md +75 -29
- package/CHANGELOG.md +146 -29
- package/INSTALL.ja.md +83 -42
- package/INSTALL.ko.md +83 -42
- package/INSTALL.md +86 -44
- package/INSTALL.zh.md +82 -42
- package/README.en.md +303 -170
- package/README.ja.md +177 -106
- package/README.ko.md +177 -106
- package/README.md +159 -20
- package/dsh.plugin.json +13 -0
- package/lib/client.js +397 -11
- package/lib/client.js.map +1 -1
- package/package.json +101 -78
- package/src/bridge.js +61 -1
- package/src/client/badge-text.ts +49 -0
- package/src/client/index.ts +11 -1
- package/src/client/locales.ts +163 -95
- package/src/client/pause-button-text.ts +60 -0
- package/src/client/pause-button.tsx +129 -0
- package/src/client/settings-card.tsx +391 -191
- package/src/client/status-badge.tsx +59 -66
- package/src/client/styles.ts +30 -0
- package/src/deferrals.js +197 -0
- package/src/index.js +405 -293
- package/src/pause-gate.js +472 -416
- package/src/provider-directory.js +101 -0
- package/src/provider.js +139 -0
- package/src/request-guard.js +191 -0
- package/src/retry.js +227 -202
- package/src/settings.js +118 -97
- package/src/step-gate.js +399 -0
- package/src/targets.js +70 -0
- package/src/time.js +195 -104
- package/src/tool-call-id.js +44 -0
- package/src/wiring.js +305 -0
package/README.ko.md
CHANGED
|
@@ -1,106 +1,177 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<strong>피크 자동 세션 게이트: 주말 모드 + 피크 자동 일시정지 + 세션급 동결 + 백엔드 자동 재시도</strong>
|
|
3
|
-
</p>
|
|
4
|
-
<p align="center">
|
|
5
|
-
<a href="README.en.md">English</a> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <strong>한국어</strong>
|
|
6
|
-
</p>
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
9
|
-
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" style="max-width: 100%;">
|
|
10
|
-
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
11
|
-
</p>
|
|
12
|
-
|
|
13
|
-
# dsh-session-guard
|
|
14
|
-
|
|
15
|
-
- [English README](./README.en.md)
|
|
16
|
-
- [中文 README](./README.md)
|
|
17
|
-
- [日本語 README](./README.ja.md)
|
|
18
|
-
- [한국어 README](./README.ko.md)
|
|
19
|
-
- [Installation guide](./INSTALL.md)
|
|
20
|
-
- [中文安装指南](./INSTALL.zh.md)
|
|
21
|
-
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
22
|
-
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
23
|
-
- [Changelog](./CHANGELOG.md)
|
|
24
|
-
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
25
|
-
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
26
|
-
|
|
27
|
-
> **호환성 참고:** v0.1.1에는 일본어(`ja`)와 한국어(`ko`) 사전이 포함되어 있지만, 현재 공식 DSH 릴리스는 `LocaleRuntime`을 통해 `zh`와 `en`만 제공합니다. 순정 DSH에서 `ja` 또는 `ko`를 선택하면 `locale "<id>" is not registered` 오류가 발생합니다. 공식 DSH가 해당 locale ID를 추가할 때까지 사용할 수 없습니다. 고급 사용자는 DSH 포크를 유지하면서 업데이트하세요.
|
|
28
|
-
|
|
29
|
-
> 피크 과금 시간대에 실행 중인 세션을 자동 일시정지하고 오피크/주말에 자동 재개; input-traffic의 동결 버튼과 페어링하여 **세션급** 잠금 구현; 백엔드 **자동 재시도**는 동결/게이트 기간 중 양보. 커스텀 세션 게이트(`agent.cancel keepInbox + goals.pause + session/event 안전 경계 + followup 재개`) 기반, dsh-task-control 의존성 제거.
|
|
30
|
-
|
|
31
|
-
`dsh plugin` 명령으로 조립 + 번들 패치로 장착하는 cordis 플러그인. dsh 소스 변경이나 PR 필요 없음.
|
|
32
|
-
|
|
33
|
-
> 💡 **권장 이유**: DeepSeek는 2026-08-17부터 **피크/오피크 과금**을 시행. 피크 시간대 단가 2배. 본 플러그인이 피크 시 실행 세션을 자동 일시정지하고 오피크에 자동 재개하여 장시간 세션 비용을 최대 **50%** 절감. 수동 동결(input-traffic 버튼 경유)로 세션별 정밀 제어 가능.
|
|
34
|
-
|
|
35
|
-
## 기능
|
|
36
|
-
|
|
37
|
-
- **주말 모드**: `Intl.DateTimeFormat`으로 주말을 정확히 인식(타임존 정확) → 주말은 피크/오피크 무시하고 자유 실행.
|
|
38
|
-
- **피크 자동 일시정지(글로벌)**: 피크 진입 시(그리고 주말이 아닌 경우) 모든 running 루트 세션을 자동 일시정지; 이탈 시 자동 재개 — **글로벌 스위치, 수동 불필요**.
|
|
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
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>피크 자동 세션 게이트: 주말 모드 + 피크 자동 일시정지 + 세션급 동결 + 백엔드 자동 재시도</strong>
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="README.en.md">English</a> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <strong>한국어</strong>
|
|
6
|
+
</p>
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
9
|
+
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" style="max-width: 100%;">
|
|
10
|
+
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
# dsh-session-guard
|
|
14
|
+
|
|
15
|
+
- [English README](./README.en.md)
|
|
16
|
+
- [中文 README](./README.md)
|
|
17
|
+
- [日本語 README](./README.ja.md)
|
|
18
|
+
- [한국어 README](./README.ko.md)
|
|
19
|
+
- [Installation guide](./INSTALL.md)
|
|
20
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
21
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
22
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
23
|
+
- [Changelog](./CHANGELOG.md)
|
|
24
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
25
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
26
|
+
|
|
27
|
+
> **호환성 참고:** v0.1.1에는 일본어(`ja`)와 한국어(`ko`) 사전이 포함되어 있지만, 현재 공식 DSH 릴리스는 `LocaleRuntime`을 통해 `zh`와 `en`만 제공합니다. 순정 DSH에서 `ja` 또는 `ko`를 선택하면 `locale "<id>" is not registered` 오류가 발생합니다. 공식 DSH가 해당 locale ID를 추가할 때까지 사용할 수 없습니다. 고급 사용자는 DSH 포크를 유지하면서 업데이트하세요.
|
|
28
|
+
|
|
29
|
+
> 피크 과금 시간대에 실행 중인 세션을 자동 일시정지하고 오피크/주말에 자동 재개; input-traffic의 동결 버튼과 페어링하여 **세션급** 잠금 구현; 백엔드 **자동 재시도**는 동결/게이트 기간 중 양보. 커스텀 세션 게이트(`agent.cancel keepInbox + goals.pause + session/event 안전 경계 + followup 재개`) 기반, dsh-task-control 의존성 제거.
|
|
30
|
+
|
|
31
|
+
`dsh plugin` 명령으로 조립 + 번들 패치로 장착하는 cordis 플러그인. dsh 소스 변경이나 PR 필요 없음.
|
|
32
|
+
|
|
33
|
+
> 💡 **권장 이유**: DeepSeek는 2026-08-17부터 **피크/오피크 과금**을 시행. 피크 시간대 단가 2배. 본 플러그인이 피크 시 실행 세션을 자동 일시정지하고 오피크에 자동 재개하여 장시간 세션 비용을 최대 **50%** 절감. 수동 동결(input-traffic 버튼 경유)로 세션별 정밀 제어 가능.
|
|
34
|
+
|
|
35
|
+
## 기능
|
|
36
|
+
|
|
37
|
+
- **주말 모드**: `Intl.DateTimeFormat`으로 주말을 정확히 인식(타임존 정확) → 주말은 피크/오피크 무시하고 자유 실행.
|
|
38
|
+
- **피크 자동 일시정지(글로벌)**: 피크 진입 시(그리고 주말이 아닌 경우) 모든 running 루트 세션을 자동 일시정지; 이탈 시 자동 재개 — **글로벌 스위치, 수동 불필요**.
|
|
39
|
+
- **공식 소스 2차 판정(`providerGuard`)**: 피크 시간에는 **요청 대상이 DeepSeek 공식 소스일 때만** 차단. 로컬/서드파티 provider(예: `local-35b`)는 정상 실행. 판정 순서 = 명시 id 목록 → `baseURL` 엔드포인트 → catalog 기본 엔드포인트 → 내장 id.
|
|
40
|
+
- **요청급 백스톱 + 연기 큐**: 피크 진입 후 시작된 세션, 도중에 공식 소스로 전환된 세션은 `agent/request` 가드가 포착(기본 `hold`: 요청을 보류하고 오류 없이, 피크 종료 시 자동 해제).
|
|
41
|
+
- **세션급 동결/재개**: `sessionGuard` 중복 포트 + `POST /session-guard/rpc`, input-traffic 동결 버튼으로 세션별 패스스루. `/pause /resume /cancel` 수동 명령도 제공.
|
|
42
|
+
- **백엔드 자동 재시도(D9)**: turn/end 일시적 실패(error/429/max-tokens)는 적응형 백오프로 자동 재시도; 영구 실패는 중지; **동결/게이트 기간 중 양보**, 세션 게이트를 우회하지 않음.
|
|
43
|
+
- **fail-open**: 커스텀 세션 게이트 사용 불가, session-guard 미설치, 설정 서비스 누락 — 모두 조용히 성능 저하, 의존성으로 크래시하지 않음.
|
|
44
|
+
|
|
45
|
+
## 설치
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
설치 후 dsh web을 재시작하고 페이지를 새로고침.
|
|
52
|
+
|
|
53
|
+
## 설정 (설정 → 플러그인 → session-guard)
|
|
54
|
+
|
|
55
|
+
| 스위치 | 기본값 | 설명 |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `enabled` | on | **피크 자동 일시정지**: 피크 시간대에 실행 세션을 자동 일시정지 |
|
|
58
|
+
| `stepLevelPause` | on | **step급 게이트**: 피크에 다음 step의 모델 요청 **전에** 게이트를 닫음(턴급보다 이르고 더 절약). 끄면 턴급 일시정지로 폴백 |
|
|
59
|
+
| `providerGuard` | on | **공식 소스 2차 판정**: 피크에는 DeepSeek 공식 소스만 차단, 로컬/서드파티 provider는 정상 실행 |
|
|
60
|
+
| `guardSubagents` | on | **서브에이전트 포함**: 서브에이전트 요청도 과금 대상, 기본 포함 |
|
|
61
|
+
| `offPeakAutoResume` | on | **오피크 자동 재개**: 오피크에 일시정지 세션을 자동 재개 |
|
|
62
|
+
| `weekendMode` | on | **주말 모드**: 주말 인식 → 주말 자동 일시정지 안 함 |
|
|
63
|
+
| `deferredResume` | on | **피크 후 자동 재개**: 끄면 연기된 요청/세션은 수동 `/resume` 전까지 멈춤 |
|
|
64
|
+
| `queueFallback` | on | 커스텀 세션 게이트 사용 불가 시 락 대기 큐로 폴백 (fail-open) |
|
|
65
|
+
| `retryEnabled` | off | **자동 재시도 (백엔드)**: 일시적 실패 자동 재시도 (기본값 off, 보수적) |
|
|
66
|
+
|
|
67
|
+
추가 설정:
|
|
68
|
+
|
|
69
|
+
- `timezone` (기본값 Asia/Shanghai) — **주말 판정**과 배지 표시에 사용. **피크/오피크 판정에는 영향 없음** (피크는 항상 북경 시간);
|
|
70
|
+
- `peakWindows` (기본값 09:00–12:00 / 14:00–18:00) — 북경 시간(UTC+8) 기준 피크 윈도우. DeepSeek 공식 과금과 일치;
|
|
71
|
+
- `pauseMode` (`safe`/`force`), `pauseReason` (`wait`/`stop`);
|
|
72
|
+
- `stepGateTimeoutMs` (기본값 300000) — step 게이트 보류 타임아웃. 만료 시 게이트를 해제하고 **턴급 일시정지로 승격**(교착 방지, "5분마다 1 step" 토큰 누수도 방지);
|
|
73
|
+
- 공식 소스 판정: `officialProviders`(공식 provider id 추가, 쉼표 구분, 최우선), `officialBaseURLs`(공식 엔드포인트 host, 기본 `api.deepseek.com`);
|
|
74
|
+
- 연기 큐: `deferredMode`(`hold`/`error`), `deferredResumeText`, `deferredMaxHoldMs`(보류 상한, 기본 6h, 초과 시 error);
|
|
75
|
+
- 재시도 매개변수: `retryText`, `retryGraceMs`, `retryCooldownMs`, `retryBackoffFactor`, `retryBackoffMaxMs`, `retryMaxConsecutive`.
|
|
76
|
+
|
|
77
|
+
## 동작
|
|
78
|
+
|
|
79
|
+
### 피크 자동 게이트 (글로벌)
|
|
80
|
+
|
|
81
|
+
- **피크 진입** (그리고 주말 아님): `stepLevelPause`가 켜져 있으면 **턴을 즉시 중단하지 않음** — 세션은 다음 `agent/pre-step` 경계까지 진행하고 거기서 step 게이트가 닫힘(아래). 꺼져 있으면 모든 running 루트 세션에 `gate.stopNextTurn` 호출(커스텀 세션 게이트, `queueFallback`으로 락 대기 큐 폴백);
|
|
82
|
+
- **오피크 / 주말**: 먼저 보류 중인 step을 `releaseAll`로 해제(턴은 그 자리에서 계속), 그다음 `gate.resume` **모든** 세션 — `offPeakAutoResume` 스위치로 제어;
|
|
83
|
+
- **피크 타임존**: 하드코딩된 북경 시간 (`Asia/Shanghai`), DeepSeek 공식 과금 기준과 일치 — `timezone` 설정의 영향을 받지 않음;
|
|
84
|
+
- 상태 머신: 단일 인스턴스 `NORMAL ↔ PAUSED_PEAK` (`scheduler.js`), 단일 30s tick으로 구동.
|
|
85
|
+
|
|
86
|
+
### step급 게이트 (v0.2.0, 절약의 핵심)
|
|
87
|
+
|
|
88
|
+
`agent/pre-step` waterfall에 연결되어 **다음 step의 모델 요청이 발생하기 전에** 턴을 보류합니다.
|
|
89
|
+
|
|
90
|
+
- **게이트 조건** (모두 충족): `enabled` + `stepLevelPause` + `step > 1` + 피크(북경 시간, 주말 아님) + 대상 provider가 공식(`providerGuard`, 끄면 전부) + 요청급 hold 아님 + 이번 피크 구간에서 수동 스킵 아님;
|
|
91
|
+
- **`step > 1`인 이유**: 턴의 첫 step은 요청급 가드가 담당하므로 두 게이트가 겹치지 않음;
|
|
92
|
+
- **해제 경로**: ① "⏸ 일시정지 중(재개)" 버튼 / `POST /session-guard/rpc {action:'stepResume'}` / `/resume` → 현재 step을 통과시키고 **이번 피크 구간 동안 더 이상 게이트하지 않음**;② 오피크 → 전부 해제, 턴은 그 자리에서 계속(**followup 불필요**);③ 동결 버튼 / `/pause` / `/cancel` → 게이트 해제 후 턴급 일시정지로;④ `signal` abort → 해제;
|
|
93
|
+
- **타임아웃 승격**: `stepGateTimeoutMs`(기본 5분) 초과 시 게이트 해제 + **턴급 force 일시정지로 승격**, 오피크에 복귀(교착 없음·토큰 누수 없음);
|
|
94
|
+
- **상태**: `GET /session-guard/state?session=<id>`가 `paused: { step, turn }`과 `stepGate: { held, since, bypass }` 반환. 서비스 포트 `state().paused`는 **호환을 위해 불리언 유지**, step 상태는 `pausedStep`;
|
|
95
|
+
- **영속화 안 함**: 보류는 프로세스 내 Promise, 재시작 시 소멸(유령 상태 방지).
|
|
96
|
+
|
|
97
|
+
#### "일시정지 / 재개" 버튼 (session-guard 제공)
|
|
98
|
+
|
|
99
|
+
컴포저 오른쪽의 "일시정지" 버튼 (slot `conversation.input.right`, id `session-guard-pause`, order 20 — input-traffic "❄ 동결 후 추가" 왼쪽):
|
|
100
|
+
|
|
101
|
+
- 일시정지 아님 → "일시정지", **클릭 가능**: `stepPause`를 호출해 **다음 step의 모델 요청 전에** 세션을 정지(현재 step은 중단하지 않음. step 1도 대상이며 피크/provider 제한을 받지 않음);
|
|
102
|
+
- 일시정지 중 → "재개", `stepResume`을 호출해 현재 step을 통과시키고 이번 피크 구간 동안 더 이상 게이트하지 않음;
|
|
103
|
+
- **이벤트 push**: `GET /session-guard/events?session=<id>`(SSE)가 step 게이트 상태 변화를 **즉시** 전달 — 피크에서 자동으로 닫히면 버튼이 바로 "재개"로 바뀝니다. 10초 주기 `/session-guard/state` 폴링은 폴백(SSE 불통이어도 수렴);
|
|
104
|
+
- 같은 줄의 input-traffic 버튼과 모양을 맞췄습니다(높이 24px / 반경 6px / 12px 글꼴 / 동일 CSS 토큰).
|
|
105
|
+
|
|
106
|
+
### 공식 소스 판정 (`providerGuard`)
|
|
107
|
+
|
|
108
|
+
피크 시간에 무차별 정지하지 않고, 먼저 "이 요청이 실제로 향하는 라우트가 DeepSeek 공식 소스인가"를 판정합니다.
|
|
109
|
+
|
|
110
|
+
| 우선순위 | 근거 | `matchedBy` | 예 |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| 1 | `officialProviders` 명시 id 목록 | `explicit` | 자체 게이트웨이를 공식으로 선언 |
|
|
113
|
+
| 2 | 실시간 `baseURL` 정규화 host | `endpoint` | `deepseek-official`을 중계로 변경 → **차단 안 함** |
|
|
114
|
+
| 3 | catalog 내장 기본 엔드포인트 | `endpoint-default` | pi-ai의 `deepseek` 라우트는 기본이 공식 API → **차단함** |
|
|
115
|
+
| 4 | 내장 id 목록 (`deepseek-official`) | `route-id` | 엔드포인트를 읽을 수 없을 때 폴백 |
|
|
116
|
+
| 5 | 그 외 | `unknown` | 비공식, 통과 |
|
|
117
|
+
|
|
118
|
+
- **엔드포인트가 id보다 우선**: `deepseek-official`의 `baseURL`을 중계로 돌려도 오차단하지 않습니다. 반대로 pi-ai 내장 `deepseek` 라우트는 누락되지 않습니다.
|
|
119
|
+
- **엔드포인트 출처**: `ctx.get('llm').listConfigurableProviders()` → 디렉터리 항목 → `ctx.settings.get(settingsNs)` → `settingsPath`로 `baseURL`(비밀 아닌 필드만, `apiKeyEnv` 값은 읽지 않음). 요청마다 재계산·캐시 없음 → provider 설정 변경이 즉시 반영.
|
|
120
|
+
- **비공식으로 전환 시 자동 복귀**: 피크 진입으로 정지된 세션을 로컬/서드파티 provider로 바꾸면(0.1.2+의 `model/selection` 이벤트) 자동으로 재개합니다(`deferredResume` 제약). 본 플러그인이 피크 진입 시 정지한 세션만 대상이며 수동 `/pause`는 덮어쓰지 않습니다. 0.1.1에는 이 이벤트가 없어 "다음 요청 또는 수동 `/resume`"으로 강등.
|
|
121
|
+
- **읽을 수 없을 때**: `llm` 서비스 부재, 네임스페이스 구조 변경, 비문자열 필드 — 모두 id / 내장 엔드포인트 판정으로 강등하고 `matchedBy`를 기록, **예외를 던지지 않습니다**.
|
|
122
|
+
- **오판정 조사**: `GET /session-guard/provider?provider=<id>`가 `{ official, matchedBy, endpoint }`를 반환.
|
|
123
|
+
|
|
124
|
+
### 요청급 가드와 연기 큐
|
|
125
|
+
|
|
126
|
+
- **왜 요청급인가**: 30s tick은 `NORMAL → PAUSED_PEAK` 전환 시점에 `running`이던 세션만 처리합니다. 피크 진입 후 시작된 세션, 도중에 공식 소스로 바뀐 세션은 누락됩니다. `agent/request` waterfall은 **모든 요청**을 지납니다.
|
|
127
|
+
- **`next()` 반환값으로 판정**: 모델 선택 미들웨어가 waterfall 내부에서 provider/model을 덮어쓰므로 `await next()` 후에 판정합니다.
|
|
128
|
+
- **`hold` 모드(기본)**: 요청을 보류 — **전송도 오류도 없음** — 피크 종료 순간 자동 해제(`msUntilOffPeak` 정밀 타이머, 30s tick은 백스톱); abort 시 정상 중단.
|
|
129
|
+
- **`error` 모드**: 식별 가능한 `PEAK_DEFERRED` 오류를 던지고 연기 큐에 기록, 피크 종료 시 `deferredResumeText`로 재개(`deferredResume` 끄면 자동 재개 안 함).
|
|
130
|
+
- **상한**: `deferredMaxHoldMs`(기본 6h)로 무한 보류를 error로 전환.
|
|
131
|
+
- **상호 배타**: 보류 중에는 세션 게이트 일시정지를 **함께 쓰지 않습니다**(일시정지는 안전 경계를 기다리지만 보류된 요청은 도달할 수 없음). 피크 진입 시 이미 보류 중인 세션은 건너뜁니다.
|
|
132
|
+
- **영속화 없음**: 연기 큐는 프로세스 내 promise, 재시작 시 사라집니다.
|
|
133
|
+
|
|
134
|
+
### 경계 (대상 아님)
|
|
135
|
+
|
|
136
|
+
- **provider 재라우팅 없음**(차단만).
|
|
137
|
+
- **compaction은 `agent/request`를 지나지 않음**: 세션이 정지된 동안에는 발생하지 않습니다. 피크 중 수동 압축은 공식 소스로 갈 수 있습니다(`ctx.llm.stream` 계층은 개입하지 않음).
|
|
138
|
+
- **0.1.1에는 `model/selection` 이벤트가 없음**: 비공식 소스로 전환 후 자동 복귀는 "다음 요청 또는 수동 `/resume`"으로 강등(0.1.2+는 즉시).
|
|
139
|
+
- **npm 의존성 추가 없음**, 자격 증명 읽기 없음, `dsh-llm-retry`의 429 / 전송 재시도는 그대로.
|
|
140
|
+
|
|
141
|
+
### 타임존 처리
|
|
142
|
+
|
|
143
|
+
- **피크/오피크判定**: 항상 **북경 시간(UTC+8)** 사용 (`BILLING_TIMEZONE = 'Asia/Shanghai'`). DeepSeek 공식 과금 기준. `timezone` 설정으로 변경 불가 (하드코딩);
|
|
144
|
+
- **주말判定**: 사용자 설정 `timezone` (예: `Asia/Tokyo`, `Asia/Seoul`) 사용. "주말"은 로컬 개념이므로;
|
|
145
|
+
- `Intl.DateTimeFormat`으로 타임존 투영. 잘못된 IANA 타임존 이름은 `RangeError`로 fail-open하여 `Asia/Shanghai`로 폴백;
|
|
146
|
+
- 피크 윈도우는 **좌폐우개** `[start, end)`. 자정 횡단 윈도우 (예: `22:00–06:00`) 지원.
|
|
147
|
+
|
|
148
|
+
### 상태 배지 (프론트엔드 표시)
|
|
149
|
+
|
|
150
|
+
컴포저 입력 영역 오른쪽에 **읽기 전용** 상태 배지 표시:
|
|
151
|
+
|
|
152
|
+
| 단계 | 라벨 | CSS 클래스 | 의미 |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `peak` (2차 판정 on) | 高峰·拦官方 | `sg-peak` | 피크 시간대, DeepSeek 공식 소스만 차단 |
|
|
155
|
+
| `peak` (2차 판정 off) | 高峰·全部暂停 | `sg-peak` | 피크 시간대, 모든 세션 정지 |
|
|
156
|
+
| `off-peak` | 谷时 | `sg-off` | 오피크 시간대, 세션 정상 실행 |
|
|
157
|
+
| `weekend` | 週末 | `sg-weekend` | 주말 (주말 모드 활성화 시), 피크/오피크 무시 |
|
|
158
|
+
|
|
159
|
+
- 15초마다 `GET /session-guard/status` 폴링(`phase` / `providerGuard` / `held` / `deferred` / `stepHeld`);
|
|
160
|
+
- fail-open: 라우트 도달 불가·네트워크 오류·`enabled` OFF → 배지 숨김;
|
|
161
|
+
- **input-traffic에 의존하지 않음**: session-guard 클라이언트 코드가 단독으로 렌더링. input-traffic는 동결 버튼만 담당;
|
|
162
|
+
|
|
163
|
+
### input-traffic와의 협업
|
|
164
|
+
|
|
165
|
+
- input-traffic의 **"❄ 동결 후 추가" 버튼**은 `sessionGuard.stopNextTurn` (RPC, 세션별)로 전달하며 **먼저 step 게이트를 해제**합니다 (그렇지 않으면 턴급 일시정지가 영원히 오지 않는 안전 경계를 기다려 상호 대기가 됨);
|
|
166
|
+
### input-traffic와의 역할 분담: "멈추는" 쪽과 "줄 세우는" 쪽
|
|
167
|
+
|
|
168
|
+
**DSH 큐 의미론(두 개의 큐)**: `next-step` = "다음 step 경계를 기다리는 입력"(다음 `agent/pre-step`에서 **도구 결과와 같은 레벨**의 step으로 같은 turn 안에서 소비), `next-turn` = "독립 턴을 기다리는 프롬프트"(현재 턴 종료 후 **새 turn**으로 소비). `Inbox.claim()`은 **항상 `next-step`을 먼저 전부** 가져가고, 새 턴을 여는 경계에서만 `next-turn`을 **1건** 추가로 가져갑니다.
|
|
169
|
+
|
|
170
|
+
- **session-guard = 멈춤**: 언제 진행 가능한지만 결정하며 **큐 내용·순서는 건드리지 않습니다**. step 게이트(`agent/pre-step`, 다음 step의 모델 요청 전), 턴급 일시정지(`agent.cancel({keepInbox:true})` + `goals.pause`, **큐는 그대로 보존**), 요청급 hold(`agent/request`).
|
|
171
|
+
- **input-traffic = 줄 세움**: 사용자 입력이 어느 큐에, 어떤 단계로 들어가 언제 소비될지만 결정합니다(3단계: 빨강=interrupt는 `cancel()` 후 `steer`, 노랑=`steer`(→ `next-step`), 초록=`next-turn` 대기). 동결 = `queued`+`steering` 행을 단계째로 분리 + composer 차단 + `sessionGuard.stopNextTurn`; 재개 = 차단 해제 → `sessionGuard.resume` → 단계 순 재투입.
|
|
172
|
+
- **접점의 두 불변식**: ① 동결은 본 플러그인이 **먼저 step 게이트를 해제**하게 해야 합니다(아니면 턴급 일시정지가 영원히 오지 않는 안전 경계를 기다려 상호 대기). ② step 게이트 보류 중에는 `preStep()`이 waterfall 전에 `inbox.claim()`을 끝냈으므로 새 입력은 이미 가져간 배치 뒤에 줄을 섭니다(`keepInbox`는 턴급에만 적용).
|
|
173
|
+
- 버튼: 본 플러그인의 "일시정지 / 재개"(order 20)와 input-traffic의 "❄ 동결 후 추가 / 재개 후 추가"(order 30)는 **병렬 표시·상호 대체 없음**.
|
|
174
|
+
|
|
175
|
+
## 라이선스
|
|
176
|
+
|
|
177
|
+
MIT — [LICENSE](LICENSE) 참조.
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<strong>高峰自动会话门:周末模式 + 高峰自动暂停 + 会话级冻结 + 后端自动重试</strong>
|
|
2
|
+
<strong>高峰自动会话门:周末模式 + 高峰自动暂停 + 官方源二维判定 + 会话级冻结 + 后端自动重试</strong>
|
|
3
3
|
</p>
|
|
4
4
|
<img width="832" height="182" alt="00c4b89a-b026-4bf1-a358-a068e80d2da7" src="https://github.com/user-attachments/assets/31a8836f-0fe0-4043-948a-f0865bb1b3bb" />
|
|
5
5
|
|
|
@@ -28,6 +28,24 @@
|
|
|
28
28
|
|
|
29
29
|
> **兼容性说明:** v0.1.1 已包含日语(`ja`)和韩语(`ko`)字典,但当前官方 DSH 只通过 `LocaleRuntime` 提供 `zh` 和 `en`。在原版 DSH 中选择 `ja` 或 `ko` 会失败,并提示 `locale "<id>" is not registered`。需要等待官方 DSH 增加对应 locale ID 后才能正常使用。高级用户可以维护 DSH fork 进行扩展。
|
|
30
30
|
|
|
31
|
+
> **▼ DSH 版本适配**
|
|
32
|
+
>
|
|
33
|
+
> | DSH 版本 | 加载 | 设置注册 | 会话事件 / 会话门 | 客户端半 |
|
|
34
|
+
> | --- | --- | --- | --- | --- |
|
|
35
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `ctx.settings.register(ns, schema, { base })` | ✅ 形状一致 | ✅ 无平台值导入 |
|
|
36
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | `register` 仍保留(另加 `installSection`) | ✅ 形状一致 | ✅ 无平台值导入 |
|
|
37
|
+
> | 0.1.3+ / 0.1.5-alpha.1 | 接口仍在(未验证) | `register` 仍在(行号未变) | ✅ | ✅ |
|
|
38
|
+
>
|
|
39
|
+
> 一份产物同时支持两版本。`session/event`、`agent.cancel`、`goals.pause`、
|
|
40
|
+
> `agent.followup`、`commands.register`、`timer.interval`、`webServer.register`、
|
|
41
|
+
> `agent/request`、`llm.listConfigurableProviders`、`settings.register/get` 在
|
|
42
|
+
> `dsh-v0.1.1-rc.2` 与 `dsh-v0.1.2-rc.1` 之间签名一致(并已核到 `0.1.5-alpha.1`);
|
|
43
|
+
> 唯一需要双读的是 `tool/result` 记录的调用 id 形态(`content[].toolCallId` 优先、
|
|
44
|
+
> `source.callId` 回退),已抽到 `src/tool-call-id.js` 并配单测——两版本的回放日志都可能出现这两种形态。
|
|
45
|
+
> `model/selection` 事件**仅 0.1.2+**,只做切模型加速且必须特性探测;设置面只用
|
|
46
|
+
> `register` + `get` 交集(不碰 `installSection` / 已移除的 `installSettingsSection`)。
|
|
47
|
+
> 漂移守卫脚本:`tools/check-api-drift.ps1`(对四个 tag 断言必需接口存在)。
|
|
48
|
+
|
|
31
49
|
> 高峰时段自动暂停运行中的会话、低峰/周末自动续跑;配合 input-traffic 的冻结按钮做到**会话级**锁定;后端**自动重试**在冻结/门控期间让路。核心基于**自研会话门**(`agent.cancel keepInbox + goals.pause + session/event 安全边界 + followup 续跑`),不再依赖 dsh-task-control。
|
|
32
50
|
|
|
33
51
|
无需修改 dsh 源码、无需提 PR:`dsh plugin` 命令组装 + bundle patch 装配的 cordis 插件。
|
|
@@ -38,10 +56,21 @@
|
|
|
38
56
|
|
|
39
57
|
- **周末模式**:识别周末(基于配置时区 `Intl.DateTimeFormat`,不踩裸 `getUTCDay()` 的北京边界 8 小时 bug)→ 周末无视峰谷、畅快跑。
|
|
40
58
|
- **高峰自动暂停(全局)**:进入高峰(且非周末)时,对所有 running root session 自动暂停;退峰自动恢复全部——**全局开关,无需手动**。
|
|
59
|
+
- **官方源二维判定(providerGuard)**:高峰期**只在请求目标是 DeepSeek 官方源时**才拦;用本地/第三方 provider(如 `local-35b`)照常跑,不受高峰门影响。判定口径 = 显式 id 名单 → `baseURL` 端点 → catalog 默认端点 → 内置 id。
|
|
60
|
+
- **请求级兜底 + 延后队列**:入峰后才启动的会话、会话中途被切到官方源的情况,由 `agent/request` 请求级守卫拦住(默认 `hold`:请求挂起不报错,退峰自动放行)。
|
|
41
61
|
- **会话级冻结 / 恢复**:`sessionGuard` 冗余端口 + `POST /session-guard/rpc`,input-traffic 冻结按钮逐会话透传接入;也提供 `/pause /resume /cancel` 手动命令。
|
|
42
62
|
- **后端自动重试(D9)**:turn/end 瞬时失败(error/429/max-tokens)自适应退避自动续跑;永久失败停止;**冻结/门控期间让路**,绝不绕过会话门。
|
|
43
63
|
- **fail-open**:自研会话门不可用、session-guard 未装、设置服务缺失——均静默降级,绝不因依赖而崩。
|
|
44
64
|
|
|
65
|
+
## 界面预览
|
|
66
|
+
|
|
67
|
+
实际运行截屏(Windows,dsh web)——周末模式激活状态:
|
|
68
|
+
|
|
69
|
+
<figure>
|
|
70
|
+
<img style="max-width:100%" alt="输入区状态控制条:处于激活态的「周末」按钮高亮(周末模式开启时无视峰谷畅快跑),相邻「冻结会话」按钮(配 input-traffic)、DeepSeek-V4-Flash 思维档位与发送控件,底部为轮次/步数、LLM 耗时、缓存命中率等状态条" src="assets/高峰低峰周末提醒-周末状态.png" />
|
|
71
|
+
<figcaption>周末模式激活:输入区「周末」徽标高亮,与「冻结会话」并列;周末无视峰谷、会话自动畅跑。</figcaption>
|
|
72
|
+
</figure>
|
|
73
|
+
|
|
45
74
|
## 安装
|
|
46
75
|
|
|
47
76
|
```bash
|
|
@@ -55,8 +84,12 @@ dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
|
55
84
|
| 开关 | 默认 | 说明 |
|
|
56
85
|
|---|---|---|
|
|
57
86
|
| `enabled` | on | **高峰自动暂停冻结会话**:高峰时段自动暂停运行会话 |
|
|
87
|
+
| `stepLevelPause` | on | **step 级门控**:高峰在下一个 step 的模型请求**之前**拉门(比回合级暂停更早、更省);关掉则回退为回合级暂停 |
|
|
88
|
+
| `providerGuard` | on | **官方源二维判定**:高峰期只拦 DeepSeek 官方源,本地/第三方 provider 照常跑 |
|
|
89
|
+
| `guardSubagents` | on | **纳入子代理请求**:子代理请求同样计费,默认一并拦截 |
|
|
58
90
|
| `offPeakAutoResume` | on | **低谷自动恢复**:低峰时段自动恢复被暂停的会话;关掉则退峰不自动恢复(需手动) |
|
|
59
91
|
| `weekendMode` | on | **周末模式**:识别周末 → 周末不自动暂停(周末本无高峰,畅快跑) |
|
|
92
|
+
| `deferredResume` | on | **退峰自动继续**:关闭后延后的请求/会话不自动续跑,需手动 `/resume` |
|
|
60
93
|
| `queueFallback` | on | 自研会话门不可用时回退锁等待队列(fail-open) |
|
|
61
94
|
| `retryEnabled` | off | **自动重试(后端)**:瞬时失败自动续跑(默认关,保守) |
|
|
62
95
|
|
|
@@ -65,17 +98,40 @@ dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
|
65
98
|
- `timezone`(默认 Asia/Shanghai)——**周末判定**和徽标显示用的时区;**不影响峰谷判定**(峰谷固定按北京时间);
|
|
66
99
|
- `peakWindows`(默认 09:00–12:00 / 14:00–18:00)——按北京时间(UTC+8)的峰谷窗口,与 DeepSeek 官方计费一致;
|
|
67
100
|
- `pauseMode`(`safe`/`force`)、`pauseReason`(`wait`/`stop`)——暂停推进方式;
|
|
101
|
+
- `stepGateTimeoutMs`(默认 300000)——step 门挂起超时;到期释放门并**升级为回合级暂停**(防死锁,不会形成「每 5 分钟一个 step」的 token 滴漏);
|
|
102
|
+
- 官方源判定:`officialProviders`(追加官方 provider id,逗号分隔,优先级最高)、`officialBaseURLs`(官方端点 host 名单,默认 `api.deepseek.com`);
|
|
103
|
+
- 延后队列:`deferredMode`(`hold` 挂起等待 / `error` 报错并延后)、`deferredResumeText`(退峰续跑文案)、`deferredMaxHoldMs`(挂起上限,默认 6 小时,到期转 error);
|
|
68
104
|
- 重试参数:`retryText`、`retryGraceMs`、`retryCooldownMs`、`retryBackoffFactor`、`retryBackoffMaxMs`、`retryMaxConsecutive`。
|
|
69
105
|
|
|
70
106
|
## 行为
|
|
71
107
|
|
|
72
108
|
### 高峰自动门(全局)
|
|
73
109
|
|
|
74
|
-
-
|
|
75
|
-
- **退峰 /
|
|
110
|
+
- **入峰**(且非周末):`stepLevelPause` 开启时**不再立即掐断回合**——会话自然跑到下一个 `agent/pre-step` 边界由 step 门拉门(见下节);关掉则对所有 running root session 调 `gate.stopNextTurn`(自研会话门真暂停,或按 `queueFallback` 回退锁等待队列);
|
|
111
|
+
- **退峰 / 周末**:先 `releaseAll` 放行被挂起的 step(回合原地续跑),再 `gate.resume` **全部**会话——受 `offPeakAutoResume` 开关控制,关掉则退峰不自动恢复;
|
|
76
112
|
- **峰谷时区**:固定使用北京时间(`Asia/Shanghai`),与 DeepSeek 官方计费基准一致,不受 `timezone` 配置影响;
|
|
77
113
|
- 状态机:单实例 `NORMAL ↔ PAUSED_PEAK`(`scheduler.js`),由单一 30s tick 驱动。
|
|
78
114
|
|
|
115
|
+
### step 级门控(v0.2.0,省 token 的关键)
|
|
116
|
+
|
|
117
|
+
挂在 `agent/pre-step` waterfall 上:**在下一个 step 的模型请求发生之前**把回合挂起。
|
|
118
|
+
|
|
119
|
+
- **拉门条件**(全部满足):`enabled` + `stepLevelPause` + `step > 1` + 高峰(北京时间,非周末)+ 目标 provider 属官方(`providerGuard`,关闭时全部拦)+ 该会话未被请求级 hold + 本峰内未被手动跳过;
|
|
120
|
+
- **为什么 `step > 1`**:一个回合的第 1 个 step 由请求级守卫覆盖,两道门不重叠;
|
|
121
|
+
- **释放路径**:①「⏸ 暂停中(继续)」按钮 / `POST /session-guard/rpc {action:'stepResume'}` / `/resume` → 放行当前 step,且**本高峰内不再拦该会话**;② 退峰 → 全部放行,回合原地续跑(**不需要 followup**);③ 冻结按钮 / `/pause` / `/cancel` → 释放门并转入回合级暂停;④ `signal` abort(用户取消)→ 释放门;
|
|
122
|
+
- **超时升级**:挂起超过 `stepGateTimeoutMs`(默认 5 分钟)→ 释放门并**升级为回合级 force 暂停**,退峰统一恢复(不会卡死,也不会在高峰形成 token 滴漏);
|
|
123
|
+
- **状态**:`GET /session-guard/state?session=<id>` 返回 `paused: { step, turn }` 与 `stepGate: { held, since, bypass }`;服务端口 `state()` 的 `paused` **仍是布尔**(向后兼容),step 态用 `pausedStep`;
|
|
124
|
+
- **不落盘**:挂起的是进程内 Promise,重启即失效(避免幽灵状态)。
|
|
125
|
+
|
|
126
|
+
#### 暂停会话 / 继续会话按钮(session-guard 提供)
|
|
127
|
+
|
|
128
|
+
输入区右侧的「暂停会话」按钮(slot `conversation.input.right`,id `session-guard-pause`,order 20,排在 input-traffic「❄ 冻结追加」左侧):
|
|
129
|
+
|
|
130
|
+
- 未暂停 → 「暂停会话」,**可点**:点击调 `stepPause`,在**下一次 step 的模型请求之前**暂停该会话(不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);
|
|
131
|
+
- 已暂停 → 「继续会话」,点击调 `stepResume`:放行当前 step,且本高峰内不再拦该会话;
|
|
132
|
+
- **事件推送**:`GET /session-guard/events?session=<id>`(SSE)在 step 门状态变化时**即时**推送——高峰期自动拉门后按钮立刻变「继续会话」,无需等轮询;另每 10 秒轮询 `/session-guard/state` 兜底(SSE 不可用 / 断线时仍能收敛);
|
|
133
|
+
- 样式与同一行的 input-traffic 按钮对齐(24px 高 / 6px 圆角 / 12px 字号 / 同一套 CSS 令牌),悬停与暂停态都有对应视觉反馈。
|
|
134
|
+
|
|
79
135
|
### 会话锁定(冻结)
|
|
80
136
|
|
|
81
137
|
- **冗余端口**:`ctx.provide('sessionGuard', service)`——`stopNextTurn(sessionId)` / `resume(sessionId)` / `lockQueue(sessionId)` / `unlockQueue(sessionId)` / `state(sessionId)`;
|
|
@@ -97,14 +153,50 @@ dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
|
97
153
|
|
|
98
154
|
| 阶段 | 徽标文案 | CSS 类 | 含义 |
|
|
99
155
|
|---|---|---|---|
|
|
100
|
-
| `peak
|
|
156
|
+
| `peak`(二维判定开) | 高峰·拦官方 | `sg-peak` | 高峰期,只拦 DeepSeek 官方源请求 |
|
|
157
|
+
| `peak`(二维判定关) | 高峰·全部暂停 | `sg-peak` | 高峰期,全部会话暂停 |
|
|
101
158
|
| `off-peak` | 谷时 | `sg-off` | 非高峰时段,会话正常运行 |
|
|
102
159
|
| `weekend` | 周末 | `sg-weekend` | 周末(周末模式开启时),无视峰谷畅快跑 |
|
|
103
160
|
|
|
104
|
-
- **轮询**:每 15 秒请求 `GET /session-guard/status`,获取全局 `phase`;
|
|
161
|
+
- **轮询**:每 15 秒请求 `GET /session-guard/status`,获取全局 `phase`、`providerGuard`、`held`、`deferred`、`stepHeld`;
|
|
105
162
|
- **fail-open**:路由不可达、网络错误、或 `enabled` 关闭时→ 徽标静默隐藏,不影响任何会话;
|
|
106
163
|
- **独立于 input-traffic**:徽标由 session-guard 客户端独立渲染,**不需要安装 input-traffic 插件**即可显示。input-traffic 只负责冻结按钮,与徽标无依赖关系;
|
|
107
|
-
- **tooltip**:悬停显示 `阶段 · 时区 ·
|
|
164
|
+
- **tooltip**:悬停显示 `阶段 · 时区 · 周末模式 · 判定口径 · 挂起/延后/step 挂起数量`。
|
|
165
|
+
|
|
166
|
+
### 官方源判定口径(providerGuard)
|
|
167
|
+
|
|
168
|
+
高峰期**不是无差别停会话**,而是先判断「这次请求真正要去的路由是不是 DeepSeek 官方源」:
|
|
169
|
+
|
|
170
|
+
| 优先级 | 依据 | `matchedBy` | 例子 |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| 1 | `officialProviders` 显式 id 名单 | `explicit` | 用户把自建网关声明为官方 |
|
|
173
|
+
| 2 | 实时 `baseURL` 归一化后的 host | `endpoint` | `deepseek-official` 改到中转 → **不拦** |
|
|
174
|
+
| 3 | catalog 内置默认端点 | `endpoint-default` | pi-ai 的 `deepseek` 路由默认就打官方 API → **拦** |
|
|
175
|
+
| 4 | 内置 id 名单(`deepseek-official`) | `route-id` | 读不到端点时的兜底 |
|
|
176
|
+
| 5 | 其他 | `unknown` | 非官方,放行 |
|
|
177
|
+
|
|
178
|
+
- **端点优先于 id**:同名 `deepseek-official` 但把 `baseURL` 指向中转的配置**不会**被误拦;反过来,pi-ai 内置 `deepseek` 路由的默认端点就是官方 API,**不会**被漏拦。
|
|
179
|
+
- **端点来源**:`ctx.get('llm').listConfigurableProviders()` 找目录条目 → `ctx.settings.get(settingsNs)` 按 `settingsPath` 读 `baseURL`(只读非密字段,绝不读 `apiKeyEnv` 的值)。每次请求实时算、不缓存 → provider 配置热改立即生效。
|
|
180
|
+
- **目标转非官方即恢复**:高峰暂停后把该会话切到本地/第三方 provider(0.1.2+ 的 `model/selection` 事件)→ 自动恢复该会话(受 `deferredResume` 约束);只恢复本插件因入峰暂停的会话,**不会**碰用户手动 `/pause` 的会话。0.1.1 无该事件 → 退化为「下次请求或手动 `/resume`」。
|
|
181
|
+
- **取不到端点**:`llm` 服务缺失、命名空间结构变化、字段非字符串——一律降级为 id / 内置端点判定并记 `matchedBy`,**绝不抛出**。
|
|
182
|
+
- **排查误判**:`GET /session-guard/provider?provider=<id>` 返回 `{ official, matchedBy, endpoint }`。
|
|
183
|
+
|
|
184
|
+
### 请求级守卫与延后队列
|
|
185
|
+
|
|
186
|
+
- **为什么要请求级**:30s tick 只在 `NORMAL → PAUSED_PEAK` 跳变时处理当时 `running` 的会话;入峰后新启动的会话、会话中途切到官方源的情况都会漏。`agent/request` waterfall 是**每次请求都过**的兜底。
|
|
187
|
+
- **判定基于 `next()` 的返回值**:模型选择中间件会在 waterfall 内把 provider/model 覆盖成 UI 里选的值,所以必须先 `await next()` 再判定。
|
|
188
|
+
- **hold 模式(默认)**:请求挂起、**不发出也不报错**,退峰瞬间自动放行(`msUntilOffPeak` 精确定时,30s tick 兜底);用户取消(abort)则正常中断。
|
|
189
|
+
- **error 模式**:抛可识别的 `PEAK_DEFERRED` 错误 + 记入延后队列,退峰按 `deferredResumeText` 续跑(`deferredResume` 关闭则不自动继续)。
|
|
190
|
+
- **上限保护**:`deferredMaxHoldMs`(默认 6h)到期仍未退峰 → 转 error,避免无限挂起。
|
|
191
|
+
- **互斥铁律**:hold 期间**不会**再调会话门暂停(暂停要等安全边界,而请求被挂住就永远到不了安全边界 → 双方互等)。入峰时已挂起的会话会被跳过。
|
|
192
|
+
- **不持久化**:延后队列是进程内 promise,重启即消失。
|
|
193
|
+
|
|
194
|
+
### 边界(明确不做)
|
|
195
|
+
|
|
196
|
+
- **不换 provider / 不做转接**:只拦不路由;
|
|
197
|
+
- **compaction 不走 `agent/request`**:会话被暂停时不会发生压缩;高峰期间若手动触发压缩仍可能打官方源(本插件不拦 `ctx.llm.stream` 层);
|
|
198
|
+
- **0.1.1 没有 `model/selection` 事件**:切到非官方源后的自动恢复退化为「等下一次请求或手动 `/resume`」(0.1.2+ 立即生效);
|
|
199
|
+
- **不新增 npm 依赖**、不读写凭据、不动 `dsh-llm-retry` 的 429 / 传输层重试。
|
|
108
200
|
|
|
109
201
|
### 时区处理与校验
|
|
110
202
|
|
|
@@ -113,11 +205,47 @@ dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
|
113
205
|
- 峰谷窗口为**左闭右开** `[start, end)`,支持跨午夜窗口(如 `22:00–06:00`);
|
|
114
206
|
- `timezone` 配置项对所有语言(中/英/日/韩)通用——`Intl.DateTimeFormat` 的 IANA 时区名不依赖 locale,日文/韩文界面下时区行为与中文完全一致。
|
|
115
207
|
|
|
116
|
-
### 与 input-traffic
|
|
208
|
+
### 与 input-traffic 的分工:一个「停」,一个「排」
|
|
209
|
+
|
|
210
|
+
两者作用在**同一条链**的不同环节,边界由 DSH 自身的 inbox 模型决定:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
用户输入 ──(input-traffic 定档)──▶ next-step / next-turn 两条待处理队列
|
|
214
|
+
│
|
|
215
|
+
agent/pre-step ──(本插件 step 门)──▶ 放行 / 挂起
|
|
216
|
+
│
|
|
217
|
+
agent/request ──(本插件请求级 hold)──▶ 放行 / 挂起
|
|
218
|
+
│
|
|
219
|
+
模型调用
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**DSH 的队列语义(两条队列,别记混)**
|
|
223
|
+
|
|
224
|
+
| 队列 | 含义 | 消费时机 |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `next-step` | 「等下一个 step 边界的输入」 | 下一个 `agent/pre-step`:与**工具返回同级**,在同一次 turn 里再走一个 step |
|
|
227
|
+
| `next-turn` | 「等待独立回合的提示」 | 当前回合结束后,作为**新的 turn** 开跑 |
|
|
228
|
+
|
|
229
|
+
`Inbox.claim()` **永远先取光 `next-step`**,只有该次边界要开新回合时再额外取 **1 条** `next-turn`;一个 turn 的第 1 个 step 取 next-turn,之后都取 next-step。
|
|
230
|
+
|
|
231
|
+
**职责划分**
|
|
232
|
+
|
|
233
|
+
- **session-guard = 停**:只决定「何时可以推进」,**不碰队列内容与顺序**。
|
|
234
|
+
- step 门(`agent/pre-step`):在下一个 step 的模型请求**之前**挂起;
|
|
235
|
+
- 回合级暂停(`agent.cancel({keepInbox:true})` + `goals.pause` + 安全边界):停掉当前回合,**队列原样保留**;
|
|
236
|
+
- 请求级守卫(`agent/request` hold):挂起**这一次模型请求**。
|
|
237
|
+
- **input-traffic = 排**:只决定「用户输入进哪条队列、什么档位、何时被消费」。
|
|
238
|
+
- 三档 = 往哪条队列放:红「打断」先 `cancel()` 再 `steer`;黄「插话」`steer`(→ `next-step`,同 turn 的下一步);绿「排队」留在 `next-turn`;
|
|
239
|
+
- 冻结 = 把 `queued` + `steering` 行整体摘出(保留档位)+ composer block + 调 `sessionGuard.stopNextTurn`;恢复 = 清 block → 先 `sessionGuard.resume` → 按档位重投。
|
|
240
|
+
|
|
241
|
+
**相遇点上的两条铁律**
|
|
242
|
+
|
|
243
|
+
1. **冻结必须让本插件先释放 step 门**:step 门挂在 `agent/pre-step`,而回合级暂停在等安全边界事件——两者互等(本插件 `pauseTask` / `cancelTask` 已先 `release`);
|
|
244
|
+
2. **step 门挂起时消息已被取走**:`preStep()` 先 `inbox.claim()` 再派发 waterfall,所以挂起期间新输入排在被取走的那批之后;`keepInbox` 只作用于回合级暂停。
|
|
245
|
+
|
|
246
|
+
**不会互相越界**:input-traffic 不监听 `agent/pre-step` / `agent/request`(唯一例外是「打断」档显式 `cancel()`,那是用户主动要求打断);本插件也不改写 `next-step` / `next-turn` 的内容与顺序。
|
|
117
247
|
|
|
118
|
-
|
|
119
|
-
- input-traffic **只做冻结增强**(队列冻结/解冻 + composer block),重试归本插件后端;
|
|
120
|
-
- 两者共享「会话隔离」语义:input-traffic 冻结队列按 sessionId 隔离,session-guard RPC 同样按 sessionId 锁。
|
|
248
|
+
按钮上:本插件的「暂停会话 / 继续会话」(order 20)与 input-traffic 的「❄ 冻结追加 / 恢复追加」(order 30)并列显示、互不取代——前者控 step 门,后者控队列摘除 + 回合级冻结。
|
|
121
249
|
|
|
122
250
|
## 冗余端口 `sessionGuard`
|
|
123
251
|
|
|
@@ -127,17 +255,21 @@ dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
|
127
255
|
resume(sessionId, opts), // 恢复(confirm + choice: rerun|skip)
|
|
128
256
|
lockQueue(sessionId, reason), // 显式锁队列
|
|
129
257
|
unlockQueue(sessionId), // 显式解锁
|
|
130
|
-
|
|
258
|
+
stepPause(sessionId), // 手动请求 step 级暂停(下一次 pre-step 边界拉门,step 1 也拦)
|
|
259
|
+
stepResume(sessionId, opts), // 解开 step 门(v0.2.0);opts.bypass=false 时不置本峰跳过
|
|
260
|
+
state(sessionId), // { queueLocked, lockReason, paused, pausedStep, stepHeldSince, stepBypass, taskControlAvailable, taskControl }
|
|
131
261
|
}
|
|
132
262
|
```
|
|
133
263
|
|
|
134
264
|
## HTTP 路由
|
|
135
265
|
|
|
136
|
-
- `GET /session-guard/state?session=<id>` —
|
|
266
|
+
- `GET /session-guard/state?session=<id>` — 会话状态(含 `paused: { step, turn, manual }` / `stepGate` / 最近目标 / 是否挂起 / 是否延后)
|
|
267
|
+
- `GET /session-guard/events?session=<id>` — **SSE**:step 门状态变化即时推送(按钮据此更新)
|
|
137
268
|
- `GET /session-guard/settings` — 设置 + taskControl 可用性
|
|
138
|
-
- `GET /session-guard/status` —
|
|
139
|
-
- `GET /session-guard/
|
|
140
|
-
- `
|
|
269
|
+
- `GET /session-guard/status` — 全局当前阶段(状态徽标轮询;含 `stepHeld`)
|
|
270
|
+
- `GET /session-guard/provider?provider=<id>` — 官方源判定诊断(`official` / `matchedBy` / `endpoint`)
|
|
271
|
+
- `GET /session-guard/diag` — 运行时诊断(含 `stepGate`)
|
|
272
|
+
- `POST /session-guard/rpc` — `{ action: stopNextTurn|resume|lockQueue|unlockQueue|stepPause|stepResume|state, sessionId }`
|
|
141
273
|
|
|
142
274
|
## 状态存储
|
|
143
275
|
|
|
@@ -153,18 +285,25 @@ npm test # node --test tests/*.test.mjs(时区/周末/状态机/会话门/
|
|
|
153
285
|
|
|
154
286
|
| 文件 | 职责 |
|
|
155
287
|
|---|---|
|
|
156
|
-
| `src/time.js` |
|
|
288
|
+
| `src/time.js` | 高峰/周末判定(时区正确)+ `msUntilOffPeak`(退峰精确定时) |
|
|
157
289
|
| `src/scheduler.js` | 纯状态机 NORMAL ↔ PAUSED_PEAK |
|
|
158
|
-
| `src/
|
|
290
|
+
| `src/provider.js` | 官方源五级判定(纯函数:端点归一化 + 判定矩阵) |
|
|
291
|
+
| `src/provider-directory.js` | 端点目录(`llm.listConfigurableProviders` + `settings.get`,全链路降级) |
|
|
292
|
+
| `src/deferrals.js` | 延后登记表(hold 挂起 / 释放 / 超限 / `PeakDeferredError`) |
|
|
293
|
+
| `src/request-guard.js` | `agent/request` 请求级守卫(hold / error 两模式) |
|
|
294
|
+
| `src/step-gate.js` | **`agent/pre-step` step 级门控**(v0.2.0:拉门 / 释放 / 超时升级 / bypass,纯判定 `decideStepHold` 可单测) |
|
|
295
|
+
| `src/targets.js` | 会话「最近真实目标」追踪(`request/header` + `model/selection`) |
|
|
296
|
+
| `src/wiring.js` | 接线编排(入峰过滤 / step 门接线 / 退峰释放 / 精确定时 / 卸载清理) |
|
|
297
|
+
| `src/pause-gate.js` | 自研会话门引擎(agent.cancel keepInbox + goals.pause + 安全边界 + followup 续跑;暂停前先释放 step 门) |
|
|
159
298
|
| `src/pause-store.js` | 自研暂停状态持久化 |
|
|
160
299
|
| `src/gate.js` | 会话门驱动(自研真暂停 / 回退锁队列,fail-open) |
|
|
161
300
|
| `src/bridge.js` | `sessionGuard` 冗余端口 |
|
|
162
|
-
| `src/retry.js` |
|
|
301
|
+
| `src/retry.js` | 后端自动重试(失败分类/退避/冻结让路;只按精确码 `PEAK_DEFERRED` 短路) |
|
|
163
302
|
| `src/detect.js` | 自动检测(host taskControl / client input-traffic 桥) |
|
|
164
303
|
| `src/store.js` | 每会话持久化状态 |
|
|
165
304
|
| `src/settings.js` | 设置子板块(schemastery schema + fail-open 注册) |
|
|
166
|
-
| `src/index.js` | host apply(设置/路由/tick
|
|
167
|
-
| `src/client/` | 浏览器 half
|
|
305
|
+
| `src/index.js` | host apply(设置/路由/tick/提供服务/重试接线/请求守卫) |
|
|
306
|
+
| `src/client/` | 浏览器 half(**暂停会话按钮** + 状态徽标 + 设置卡片) |
|
|
168
307
|
|
|
169
308
|
## License
|
|
170
309
|
|
package/dsh.plugin.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "dsh-external/dsh-session-guard",
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"main": "./src/index.js",
|
|
5
|
+
"description": "高峰自动会话门:step 级门控 + 周末模式 + 高峰自动暂停 + 官方源二维判定 + 会话级冻结 + 后端自动重试。",
|
|
6
|
+
"contributes": {
|
|
7
|
+
"tools": [],
|
|
8
|
+
"skills": []
|
|
9
|
+
},
|
|
10
|
+
"client": {
|
|
11
|
+
"main": "./lib/client.js"
|
|
12
|
+
}
|
|
13
|
+
}
|