dsh-session-guard 4.0.2 → 4.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -21
- package/README.de.md +375 -375
- package/README.es.md +374 -374
- package/README.fr.md +373 -373
- package/README.it.md +374 -374
- package/README.ru.md +374 -374
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +1 -1
- package/lib/client.js +222 -44
- package/lib/client.js.map +1 -1
- package/package.json +10 -10
- package/src/bridge.js +136 -136
- package/src/client/family-section.tsx +26 -6
- package/src/client/index.ts +176 -53
- package/src/client/pause-button-text.ts +60 -60
- package/src/client/pause-button.tsx +129 -129
- package/src/client/scope-face.ts +20 -20
- package/src/client/settings-card.tsx +391 -391
- package/src/client/shell-overlay.tsx +69 -0
- package/src/client/status-badge.tsx +17 -0
- package/src/client/styles.ts +30 -30
- package/src/detect.js +27 -27
- package/src/gate.js +93 -93
- package/src/index.js +36 -1
- package/src/pause-gate.js +544 -544
- package/src/pause-store.js +101 -101
- package/src/retry.js +227 -227
- package/src/scheduler.js +42 -42
- package/src/step-gate.js +399 -399
- package/src/store.js +77 -77
- package/src/wiring.js +305 -305
package/README.ru.md
CHANGED
|
@@ -1,374 +1,374 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<strong>Автоматические сессионные ворота в часы пик: режим выходных + автопауза в пик + двухмерное определение официальных источников + заморозка на уровне сессии + автоматический повтор на стороне бэкенда</strong>
|
|
3
|
-
</p>
|
|
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
|
-
|
|
6
|
-
<p align="center">
|
|
7
|
-
<a href="README.en.md">English</a> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.fr.md">Français</a> · <a href="README.de.md">Deutsch</a> · <a href="README.it.md">Italiano</a> · <strong>Русский</strong> · <a href="README.es.md">Español</a>
|
|
8
|
-
</p>
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
11
|
-
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" data-canonical-src="https://img.shields.io/badge/dsh--plugin-ready-478CBF?logo=deepseek&logoColor=white" style="max-width: 100%;">
|
|
12
|
-
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
# dsh-session-guard
|
|
16
|
-
|
|
17
|
-
- [English README](./README.en.md)
|
|
18
|
-
- [中文 README](./README.md)
|
|
19
|
-
- [日本語 README](./README.ja.md)
|
|
20
|
-
- [한국어 README](./README.ko.md)
|
|
21
|
-
- [README en français](./README.fr.md)
|
|
22
|
-
- [README auf Deutsch](./README.de.md)
|
|
23
|
-
- [README in italiano](./README.it.md)
|
|
24
|
-
- [README на русском](./README.ru.md)
|
|
25
|
-
- [README en español](./README.es.md)
|
|
26
|
-
- [Installation guide](./INSTALL.md)
|
|
27
|
-
- [中文安装指南](./INSTALL.zh.md)
|
|
28
|
-
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
29
|
-
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
30
|
-
- [Guide d'installation en français](./INSTALL.fr.md)
|
|
31
|
-
- [Installationsanleitung auf Deutsch](./INSTALL.de.md)
|
|
32
|
-
- [Guida all'installazione in italiano](./INSTALL.it.md)
|
|
33
|
-
- [Руководство по установке на русском](./INSTALL.ru.md)
|
|
34
|
-
- [Guía de instalación en español](./INSTALL.es.md)
|
|
35
|
-
- [Changelog](./CHANGELOG.md)
|
|
36
|
-
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
37
|
-
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
38
|
-
- [Changelog en français](./CHANGELOG.fr.md)
|
|
39
|
-
- [Changelog auf Deutsch](./CHANGELOG.de.md)
|
|
40
|
-
- [Changelog in italiano](./CHANGELOG.it.md)
|
|
41
|
-
- [Changelog на русском](./CHANGELOG.ru.md)
|
|
42
|
-
- [Changelog en español](./CHANGELOG.es.md)
|
|
43
|
-
|
|
44
|
-
> **Замечание о совместимости:** в v0.1.1 уже входят словари японского (`ja`) и корейского (`ko`), но текущие официальные сборки DSH предоставляют через `LocaleRuntime` только `zh` и `en`. В оригинальном DSH выбор `ja` или `ko` завершается ошибкой `locale "<id>" is not registered`. Придётся дождаться, пока официальный DSH добавит соответствующие идентификаторы локалей. Опытные пользователи могут поддерживать форк DSH, чтобы расширить его.
|
|
45
|
-
|
|
46
|
-
> **▼ Совместимость версий DSH**
|
|
47
|
-
>
|
|
48
|
-
> | Версия DSH | Загрузка | Регистрация настроек | События сессии / ворота | Клиентская половина |
|
|
49
|
-
> | --- | --- | --- | --- | --- |
|
|
50
|
-
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ➖ вне этой линии (исторические версии до `legacy/0.1.2`) | `ctx.settings.register(ns, schema, { base })` | ✅ форма совпадает | ✅ без импорта платформенных значений |
|
|
51
|
-
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ➖ вне этой линии → используйте `legacy/0.1.2` (npm `@dsh-0.1.2`) | `register` сохранён (добавлен `installSection`) | ✅ форма совпадает | ✅ без импорта платформенных значений |
|
|
52
|
-
> | **0.2.0-rc.1+** | ✅ (**эта линия**, dist-tag `dsh-0.2.0`, npm `4.0.0`) | декларативно: поля `.volatile()` в Config хост проецирует в формы, `register` удалён | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ карточка настроек клиента переведена на `configForms` |
|
|
53
|
-
> | 0.1.7-rc.1+ | ✅ (ветка `compat/0.1.7`, dist-tag `dsh-0.1.7`) | декларативно: поля `.volatile()` в Config хост проецирует в формы, `register` удалён | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ карточка настроек клиента переведена на `configForms` |
|
|
54
|
-
> | 0.1.5-rc.2 | ✅ (ветка `compat/0.1.5`, dist-tag `dsh-0.1.5`) | `register` на месте (строковые пространства имён) | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ |
|
|
55
|
-
>
|
|
56
|
-
> **Идентификация этой линии**: ветка `compat/0.2.0`, номер версии npm **`4.0.0`** (semver), dist-tag **`dsh-0.2.0`**.
|
|
57
|
-
> `engines.dsh` в `package.json` и `dsh.plugin.json`, а также четыре peer-зависимости `@deepseek-ai/dsh-client-*`
|
|
58
|
-
> единообразно приведены к `>=0.2.0-rc.1 <0.2.1-0`; нижняя граница peer-зависимости `@deepseek-ai/cordis` выровнена по `^4.0.4` хостовой линии.
|
|
59
|
-
> Раньше README использовало «2.x / 3.x» как **кодовые имена линий** — это была манера изложения, а **не номера версий, которые можно
|
|
60
|
-
> получить из реестра**, — ориентируйтесь только на npm-версии `0.3.1` (линия 0.1.2), `3.0.0`/`3.0.1` (линия 0.1.5),
|
|
61
|
-
> `3.2.4` (линия 0.1.7) и `4.0.0` (линия 0.2.0).
|
|
62
|
-
>
|
|
63
|
-
> **Адаптация 0.2.0 (ветка compat/0.2.0, npm 4.0.0)**:
|
|
64
|
-
> API плагинов, которую использует этот плагин (manifest/settings/HMR/slot/сессии V4), в 0.2.0-rc.1 полностью совместима с 0.1.7;
|
|
65
|
-
> адаптация сводится к смене поколения метаданных (peer/engines/dist-tag/версия 4.0.0). Попутно с базой перенесены инженерные исправления:
|
|
66
|
-
> ① `src/client/family-section.tsx` добавлен под контроль версий (раньше существовал только в локальном рабочем дереве, чистый checkout не мог собрать проект);
|
|
67
|
-
> ② из конфигурации eslint убрано ядрёное `no-unused-vars`, ошибочно помечавшее параметры в сигнатурах типов TS, — lint-базовая линия снова полностью зелёная;
|
|
68
|
-
> ③ статические шлюзы подписей путей записи v4 (`tests/source-kind.test.mjs`) переносятся в эту линию вместе с базой;
|
|
69
|
-
> ④ peer-зависимость `@deepseek-ai/cordis` выровнена по `^4.0.4` (пакеты UI хоста 0.2.0-rc.1 объявляют `~4.0.4`; прежний `^4.0.1` не конфликтовал, но его нижняя граница была слишком стара).
|
|
70
|
-
>
|
|
71
|
-
> **Где оказываются другие линии**: для хостов DSH `0.1.2-rc.x` используйте ветку **`legacy/0.1.2`** (npm dist-tag
|
|
72
|
-
> `dsh-0.1.2`, версия `0.3.1`). **`main` заморожен на `0.2.0-beta.1` и не является веткой релизов линии 0.1.2.**
|
|
73
|
-
> Для хостов DSH `0.1.0-rc.7` ~ `0.1.1-rc.x` используйте исторические версии ≤ `0.1.2`.
|
|
74
|
-
> Канонические определения источников полей:
|
|
75
|
-
> `mine-dsh-plugins/improve-dsh-plugins/DSH-PLUGIN-VERSION-DISTRIBUTION-STRATEGY.md` §2.2.
|
|
76
|
-
>
|
|
77
|
-
> **Адаптация 0.1.5 (ветка compat/0.1.5, npm 3.0.0)**:
|
|
78
|
-
> ① двойной путь для `agent.followup` — 0.1.5 превращает Inbox в проекцию agent-loop только для чтения; если `followup`
|
|
79
|
-
> больше не существует, происходит откат к `agent.send`, а если недоступно ни то ни другое — понижение до warn без исключений (страховка возобновления);
|
|
80
|
-
> ② возобновляющие сообщения добавляют в `source` поле `form: 'instructions'` (контракт ContextFormed из 0.1.5, старые версии его игнорируют);
|
|
81
|
-
> ③ `webServer.register` обёрнут в try/catch: неудачная регистрация лишь пишется в журнал и не рушит загрузку плагинов хоста;
|
|
82
|
-
> ④ 0.1.5 удаляет доступ к массиву `session.events`; чтение событий идёт по основному пути `snapshotEvents()` +
|
|
83
|
-
> с откатом к старому массиву (затрагивает только два вспомогательных пути `findToolOutcome` / `lastUserPrompt`, сопоставление типов событий не меняется).
|
|
84
|
-
> Проверено, что контракт `WebRoute` 0.1.5 (exact/prefix + SSE) не изменился: клиентскому
|
|
85
|
-
> `fetch('/session-guard/...')` префикс `/api` не нужен.
|
|
86
|
-
> Приведённая таблица совместимости охватывает два поколения API (0.1.1 / 0.1.2); эта линия претендует только на строку 0.1.5.
|
|
87
|
-
> `session/event`, `agent.cancel`, `goals.pause`,
|
|
88
|
-
> `agent.followup`, `commands.register`, `timer.interval`, `webServer.register`,
|
|
89
|
-
> `agent/request`, `llm.listConfigurableProviders`, `settings.register/get` имеют одинаковые подписи
|
|
90
|
-
> между `dsh-v0.1.1-rc.2` и `dsh-v0.1.2-rc.1` (эта линия проверена вплоть до `dsh-v0.1.5-rc.2`);
|
|
91
|
-
> единственное место, требующее двойного чтения, — форма id вызова, записываемого `tool/result` (приоритет у `content[].toolCallId`,
|
|
92
|
-
> откат к `source.callId`), вынесена в `src/tool-call-id.js` с модульными тестами — в журналах воспроизведения обеих версий могут встречаться обе формы.
|
|
93
|
-
> С 0.1.5 доступ к массиву `session.events` удалён; эта линия читает через `snapshotEvents()` (с сохранением отката к старому массиву),
|
|
94
|
-
> затрагивая только два вспомогательных пути `findToolOutcome` / `lastUserPrompt`.
|
|
95
|
-
> Событие `model/selection` есть **только с 0.1.2**, служит лишь для ускорения смены модели и обязательно требует проверки доступности функции; поверхность настроек использует только
|
|
96
|
-
> пересечение `register` + `get` (никогда `installSection` / удалённый `installSettingsSection`).
|
|
97
|
-
> Скрипт-страж дрейфа: `tools/check-api-drift.ps1` (эта линия по умолчанию проверяет наличие нужных интерфейсов относительно `dsh-v0.1.5-rc.2`).
|
|
98
|
-
|
|
99
|
-
> Автоматически приостанавливает запущенные сессии в часы пик и автоматически возобновляет их вне пиковых часов и в выходные; вместе с кнопкой заморозки input-traffic даёт блокировку **на уровне сессии**; **автоматический повтор** на стороне бэкенда уступает дорогу во время заморозки/действия ворот. В основе — **собственные сессионные ворота** (`agent.cancel keepInbox + goals.pause + session/event безопасная граница + возобновление через followup`), зависимость от dsh-task-control устранена.
|
|
100
|
-
|
|
101
|
-
Плагин cordis, собираемый командой `dsh plugin` и патчем бандла — без правки исходников dsh и без PR.
|
|
102
|
-
|
|
103
|
-
> 💡 **Почему стоит установить**: с 2026-08-17 DeepSeek перешёл на **тарификацию по пиковой и непиковой зоне** — в пиковые часы (пекинское время 9:00-12:00, 14:00-18:00) цена единицы **в 2 раза** выше, чем в непиковые (включая полдень, ночь, выходные и праздники). Плагин автоматически останавливает запущенные сессии в пик и автоматически возобновляет их после пика: длинные прогоны со сдвигом по времени экономят до **50 %**; ручная заморозка (через кнопку input-traffic) даёт ещё более точное управление остановкой по каждой сессии.
|
|
104
|
-
|
|
105
|
-
## Обзор возможностей
|
|
106
|
-
|
|
107
|
-
- **Режим выходных**: распознаёт выходные (через `Intl.DateTimeFormat` с настроенным часовым поясом, без классической ошибки в 8 часов на границе UTC+8 из-за голого `getUTCDay()` для пекинского времени) → в выходные пиковая зона игнорируется, можно работать без ограничений.
|
|
108
|
-
- **Автопауза в пик (глобально)**: при входе в пик (и если не выходные) автоматически приостанавливаются все корневые сессии в состоянии `running`; после пика все автоматически возобновляются — **глобальный переключатель, вручную ничего делать не нужно**.
|
|
109
|
-
- **Двухмерное определение официальных источников (providerGuard)**: в пиковые часы блокируется **только если целью запроса является официальный источник DeepSeek**; с локальными/сторонними провайдерами (например `local-35b`) работа идёт как обычно, пиковые ворота её не трогают. Порядок определения = явный список id → эндпоинт `baseURL` → эндпоинт по умолчанию из каталога → встроенный id.
|
|
110
|
-
- **Страховка на уровне запроса + отложенная очередь**: сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, перехватываются стражем уровня запроса `agent/request` (по умолчанию `hold`: запрос приостанавливается без ошибки и автоматически выпускается после пика).
|
|
111
|
-
- **Заморозка / возобновление на уровне сессии**: избыточный порт `sessionGuard` + `POST /session-guard/rpc`, подключается по каждой сессии через кнопку заморозки input-traffic; есть и ручные команды `/pause /resume /cancel`.
|
|
112
|
-
- **Автоматический повтор на стороне бэкенда (D9)**: переходящие сбои turn/end (error/429/max-tokens) автоматически возобновляются с адаптивной задержкой; постоянные сбои останавливают работу; **уступает дорогу во время заморозки/действия ворот**, никогда не обходит сессионные ворота.
|
|
113
|
-
- **fail-open**: собственные сессионные ворота недоступны, session-guard не установлен, сервис настроек отсутствует — во всех случаях тихая деградация, никогда не падает из-за зависимости.
|
|
114
|
-
|
|
115
|
-
## Просмотр интерфейса
|
|
116
|
-
|
|
117
|
-
Скриншот реальной работы (Windows, dsh web) — активный режим выходных:
|
|
118
|
-
|
|
119
|
-
<figure>
|
|
120
|
-
<img style="max-width:100%" alt="Панель управления состоянием в области ввода: активная кнопка «Выходные» подсвечена (при включённом режиме выходных сессии работают свободно, без учёта пика), рядом кнопка «Заморозить сессию» (вместе с input-traffic), режим рассуждений DeepSeek-V4-Flash и элементы отправки, внизу полосы состояния: раунды/шаги, время LLM, доля попаданий в кэш и т. д." src="assets/高峰低峰周末提醒-周末状态.png" />
|
|
121
|
-
<figcaption>Режим выходных активен: бейдж «выходные» в области ввода подсвечен, рядом «Заморозить сессию»; в выходные пиковая зона игнорируется, сессии работают свободно.</figcaption>
|
|
122
|
-
</figure>
|
|
123
|
-
|
|
124
|
-
## Установка
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
# Хосты DSH 0.1.5-rc.x (эта линия, dist-tag dsh-0.1.5)
|
|
128
|
-
dsh plugin --profile web add dsh-session-guard@dsh-0.1.5
|
|
129
|
-
|
|
130
|
-
# или напрямую из ветки git
|
|
131
|
-
dsh plugin --profile web add github:drscrewdriver/dsh-session-guard#compat/0.1.5
|
|
132
|
-
|
|
133
|
-
# Хосты DSH 0.1.2-rc.x должны использовать линию 0.1.2
|
|
134
|
-
dsh plugin --profile web add dsh-session-guard@dsh-0.1.2
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
`compat/0.1.5` — выделенная линия для DSH `0.1.5-rc.x`, номер версии npm **`3.0.0`**; для хостов DSH `0.1.2-rc.x` используйте
|
|
138
|
-
ветку **`legacy/0.1.2`** (npm dist-tag `dsh-0.1.2`, версия `0.3.1`). **`main` заморожен на `0.2.0-beta.1`,
|
|
139
|
-
это не ветка релизов линии 0.1.2.**
|
|
140
|
-
|
|
141
|
-
> ⚠️ Не полагайтесь на «голое» имя пакета `dsh-session-guard`: тег `latest` в npm не может обслуживать сразу две взаимоисключающие
|
|
142
|
-
> линии версий (их диапазоны `engines.dsh` взаимоисключающи по правилам semver для prerelease) — dist-tag нужно указывать явно.
|
|
143
|
-
|
|
144
|
-
После установки перезапустите dsh web и обновите страницу.
|
|
145
|
-
|
|
146
|
-
## Настройки (Настройки → Плагины → session-guard, простые переключатели)
|
|
147
|
-
|
|
148
|
-
| Переключатель | По умолчанию | Описание |
|
|
149
|
-
|---|---|---|
|
|
150
|
-
| `enabled` | on | **Автопауза/заморозка сессий в пик**: автоматически приостанавливает запущенные сессии в пиковые часы |
|
|
151
|
-
| `stepLevelPause` | on | **Ворота на уровне шага**: в пик ворота закрываются **до** запроса к модели следующего шага (раньше и экономнее, чем пауза на уровне раунда); off — откат к паузе на уровне раунда |
|
|
152
|
-
| `providerGuard` | on | **Двухмерное определение официальных источников**: в пик блокируются только официальные источники DeepSeek, локальные/сторонние провайдеры работают как обычно |
|
|
153
|
-
| `guardSubagents` | on | **Включая запросы субагентов**: запросы субагентов тоже тарифицируются, по умолчанию блокируются вместе с остальными |
|
|
154
|
-
| `offPeakAutoResume` | on | **Автовозобновление в непик**: приостановленные сессии автоматически возобновляются в непиковые часы; off — после пика автоматического возобновления нет (нужна ручная команда) |
|
|
155
|
-
| `weekendMode` | on | **Режим выходных**: распознаёт выходные → в выходные автопаузы нет (в выходные нет пикового времени, можно работать свободно) |
|
|
156
|
-
| `deferredResume` | on | **Автовозобновление после пика**: off — отложенные запросы/сессии не возобновляются сами, нужен ручной `/resume` |
|
|
157
|
-
| `queueFallback` | on | Откат к очереди ожидания под блокировкой, когда собственные сессионные ворота недоступны (fail-open) |
|
|
158
|
-
| `retryEnabled` | off | **Автоповтор (бэкенд)**: автоматическое возобновление после переходящих сбоев (по умолчанию off, консервативно) |
|
|
159
|
-
|
|
160
|
-
Дополнительные параметры:
|
|
161
|
-
|
|
162
|
-
- `timezone` (по умолчанию Asia/Shanghai) — часовой пояс для **определения выходных** и отображения бейджа; **на определение пика не влияет** (пик всегда по пекинскому времени);
|
|
163
|
-
- `peakWindows` (по умолчанию 09:00–12:00 / 14:00–18:00) — окна пика/непика по пекинскому времени (UTC+8), совпадают с официальной тарификацией DeepSeek;
|
|
164
|
-
- `pauseMode` (`safe`/`force`), `pauseReason` (`wait`/`stop`) — способ выполнения паузы;
|
|
165
|
-
- `stepGateTimeoutMs` (по умолчанию 300000) — таймаут удержания ворот шага; по истечении ворота открываются с **повышением до паузы на уровне раунда** (защита от дедлока, без «одного шага каждые 5 минут» и утечки токенов);
|
|
166
|
-
- Определение официальных источников: `officialProviders` (дополнительные id официальных провайдеров, через запятую, наивысший приоритет), `officialBaseURLs` (список хостов официальных эндпоинтов, по умолчанию `api.deepseek.com`);
|
|
167
|
-
- Отложенная очередь: `deferredMode` (`hold` удержание / `error` ошибка и отложение), `deferredResumeText` (текст возобновления после пика), `deferredMaxHoldMs` (предел удержания, по умолчанию 6 часов, по истечении переводится в error);
|
|
168
|
-
- Параметры повтора: `retryText`, `retryGraceMs`, `retryCooldownMs`, `retryBackoffFactor`, `retryBackoffMaxMs`, `retryMaxConsecutive`.
|
|
169
|
-
|
|
170
|
-
## Поведение
|
|
171
|
-
|
|
172
|
-
### Автоматические пиковые ворота (глобально)
|
|
173
|
-
|
|
174
|
-
- **Вход в пик** (и не выходные): при включённом `stepLevelPause` текущий раунд **больше не обрывается сразу** — сессия естественно доходит до следующей границы `agent/pre-step`, где закрываются ворота шага (см. следующий раздел); при выключенном для всех корневых сессий `running` вызывается `gate.stopNextTurn` (настоящая пауза через собственные сессионные ворота, либо в зависимости от `queueFallback` откат к очереди ожидания под блокировкой);
|
|
175
|
-
- **Выход из пика / выходные**: сначала `releaseAll` освобождает удерживаемые шаги (раунд продолжается на месте), затем `gate.resume` для **всех** сессий — управляется переключателем `offPeakAutoResume`, при выключенном после пика автоматического возобновления нет;
|
|
176
|
-
- **Часовой пояс пика**: всегда пекинское время (`Asia/Shanghai`), совпадает с базой официальной тарификации DeepSeek и не зависит от настройки `timezone`;
|
|
177
|
-
- Машина состояний: единственный экземпляр `NORMAL ↔ PAUSED_PEAK` (`scheduler.js`), управляется одним тиком в 30 с.
|
|
178
|
-
|
|
179
|
-
### Ворота на уровне шага (v0.2.0, ключ к экономии токенов)
|
|
180
|
-
|
|
181
|
-
Подвешены на waterfall `agent/pre-step`: раунд приостанавливается **до того, как произойдёт запрос к модели следующего шага**.
|
|
182
|
-
|
|
183
|
-
- **Условия закрытия** (все обязательны): `enabled` + `stepLevelPause` + `step > 1` + пик (пекинское время, не выходные) + официальный целевой провайдер (`providerGuard`, при выключенном блокируются все) + сессия не удержана на уровне запроса + не обойдена вручную в этом пиковом окне;
|
|
184
|
-
- **Почему `step > 1`**: первый шаг раунда покрывается стражем уровня запроса, чтобы два gate не пересекались;
|
|
185
|
-
- **Пути освобождения**: ① кнопка «⏸ пауза (продолжить)» / `POST /session-guard/rpc {action:'stepResume'}` / `/resume` → пропускает текущий шаг и **больше не перекрывает эту сессию до конца пика**; ② выход из пика → всё освобождается, раунд продолжается на месте (**followup не нужен**); ③ кнопка заморозки / `/pause` / `/cancel` → ворота открываются и происходит переход к паузе на уровне раунда; ④ abort по `signal` (отмена пользователем) → открытие;
|
|
186
|
-
- **Повышение по таймауту**: удержание дольше `stepGateTimeoutMs` (по умолчанию 5 минут) → ворота открываются с **повышением до принудительной паузы на уровне раунда**, единообразное возобновление после пика (без дедлока и без утечки токенов по капле во время пика);
|
|
187
|
-
- **Состояние**: `GET /session-guard/state?session=<id>` возвращает `paused: { step, turn }` и `stepGate: { held, since, bypass }`; поле `paused` порта сервиса `state()` **остаётся булевым** (для обратной совместимости), состояние шага живёт в `pausedStep`;
|
|
188
|
-
- **Без записи на диск**: удержание — это Promise внутри процесса, при перезапуске исчезает (чтобы не оставались «призрачные» состояния).
|
|
189
|
-
|
|
190
|
-
#### Кнопки «Пауза сессии / Продолжить сессию» (предоставляет session-guard)
|
|
191
|
-
|
|
192
|
-
Кнопка «Пауза сессии» справа в области ввода (slot `conversation.input.right`, id `session-guard-pause`, order 20, слева от «❄ Заморозка и добавить» у input-traffic):
|
|
193
|
-
|
|
194
|
-
- не на паузе → «Пауза сессии», **нажимается**: клик вызывает `stepPause` и приостанавливает сессию **до запроса к модели следующего шага** (текущий шаг не обрывается; шаг 1 тоже перехватывается, независимо от пика и провайдера);
|
|
195
|
-
- на паузе → «Продолжить сессию», клик вызывает `stepResume`: пропускает текущий шаг и больше не перекрывает эту сессию до конца пика;
|
|
196
|
-
- **Отправка событий**: `GET /session-guard/events?session=<id>` (SSE) **мгновенно** доставляет изменения состояния ворот шага — как только пик автоматически закрывает ворота, кнопка тут же становится «Продолжить сессию», без ожидания опроса; вдобавок каждые 10 секунд опрашивается `/session-guard/state` как страховка (сходимость сохраняется и при недоступном/разорванном SSE);
|
|
197
|
-
- стиль выровнен с кнопками input-traffic в той же строке (высота 24 px / радиус 6 px / шрифт 12 px / те же CSS-токены), с визуальным откликом при наведении и в состоянии паузы.
|
|
198
|
-
|
|
199
|
-
### Блокировка сессии (заморозка)
|
|
200
|
-
|
|
201
|
-
- **Избыточный порт**: `ctx.provide('sessionGuard', service)` — `stopNextTurn(sessionId)` / `resume(sessionId)` / `lockQueue(sessionId)` / `unlockQueue(sessionId)` / `state(sessionId)`;
|
|
202
|
-
- **RPC-мост**: `POST /session-guard/rpc { action, sessionId }` — кнопка заморозки input-traffic вызывает `stopNextTurn` / `resume` **по каждой сессии** согласно `sessionId`; молча пропускается, если этот плагин не установлен (fail-open D8);
|
|
203
|
-
- **Ручные команды**: `/pause [force|safe] [stop|wait]`, `/resume [confirm] [rerun|skip]`, `/cancel` — действуют на сессию, из которой вызваны (берётся `invocation.agent.id`).
|
|
204
|
-
|
|
205
|
-
### Автоматический повтор на стороне бэкенда (D9)
|
|
206
|
-
|
|
207
|
-
Слушает `turn/end` и классифицирует сбои:
|
|
208
|
-
|
|
209
|
-
- **Переходящие сбои** (error/429/max-tokens и т. п.) → автоматическое возобновление `followup(retryText)` с адаптивной задержкой;
|
|
210
|
-
- **Постоянные сбои** (аутентификация/баланс/модель/переполнение контекста) → остановка;
|
|
211
|
-
- **Уступает дорогу при заморозке/воротах**: повтор не выполняется, если `isFrozen(sessionId)` истинно (queueLocked / paused / taskControl paused);
|
|
212
|
-
- Вмешательство пользователя или успешный раунд сбрасывают счётчик последовательных сбоев.
|
|
213
|
-
|
|
214
|
-
### Бейдж состояния (отображение на фронтенде)
|
|
215
|
-
|
|
216
|
-
Справа в области ввода показывается **чисто информационный** бейдж состояния, отражающий текущую фазу в реальном времени:
|
|
217
|
-
|
|
218
|
-
| Фаза | Текст бейджа | CSS-класс | Значение |
|
|
219
|
-
|---|---|---|---|
|
|
220
|
-
| `peak` (двухмерное определение включено) | 高峰·拦官方 | `sg-peak` | Пиковые часы, блокируются только запросы к официальным источникам DeepSeek |
|
|
221
|
-
| `peak` (двухмерное определение выключено) | 高峰·全部暂停 | `sg-peak` | Пиковые часы, все сессии на паузе |
|
|
222
|
-
| `off-peak` | 谷时 | `sg-off` | Внепиковые часы, сессии работают нормально |
|
|
223
|
-
| `weekend` | 周末 | `sg-weekend` | Выходные (при включённом режиме выходных), пиковая зона игнорируется |
|
|
224
|
-
|
|
225
|
-
- **Опрос**: каждые 15 секунд запрашивается `GET /session-guard/status`, чтобы получить глобальные `phase`, `providerGuard`, `held`, `deferred`, `stepHeld`;
|
|
226
|
-
- **fail-open**: маршрут недоступен, сетевая ошибка или `enabled` выключен → бейдж тихо скрывается, ни одна сессия не затрагивается;
|
|
227
|
-
- **Независимость от input-traffic**: бейдж рендерится только клиентом session-guard и отображается **без установки плагина input-traffic**. input-traffic отвечает лишь за кнопку заморозки, зависимости с бейджем нет;
|
|
228
|
-
- **Tooltip**: при наведении показывает `фаза · часовой пояс · режим выходных · база определения · числа удержаний/отложений/удержаний шага`.
|
|
229
|
-
|
|
230
|
-
### База определения официальных источников (providerGuard)
|
|
231
|
-
|
|
232
|
-
В пик сессии не останавливаются без разбора: сначала выясняется, «является ли маршрут, куда этот запрос реально пойдёт, официальным источником DeepSeek»:
|
|
233
|
-
|
|
234
|
-
| Приоритет | Основание | `matchedBy` | Пример |
|
|
235
|
-
|---|---|---|---|
|
|
236
|
-
| 1 | явный список id `officialProviders` | `explicit` | пользователь объявил собственный шлюз официальным |
|
|
237
|
-
| 2 | хост нормализованной в реальном времени `baseURL` | `endpoint` | `deepseek-official` переключён на релей → **не блокируется** |
|
|
238
|
-
| 3 | встроенный в каталог эндпоинт по умолчанию | `endpoint-default` | маршрут `deepseek` у pi-ai по умолчанию бьёт в официальный API → **блокируется** |
|
|
239
|
-
| 4 | встроенный список id (`deepseek-official`) | `route-id` | откат, когда эндпоинт прочитать нельзя |
|
|
240
|
-
| 5 | всё остальное | `unknown` | неофициальный, пропускается |
|
|
241
|
-
|
|
242
|
-
- **Эндпоинт важнее id**: конфигурация с именем `deepseek-official`, но с `baseURL` на релей **не** блокируется по ошибке; наоборот, встроенный у pi-ai маршрут `deepseek` с эндпоинтом по умолчанию в официальный API **не** ускользает от блокировки.
|
|
243
|
-
- **Источник эндпоинтов**: `ctx.get('llm').listConfigurableProviders()` находит запись каталога → `ctx.settings.get(settingsNs)` читает `baseURL` по `settingsPath` (читаются только несекретные поля, значение `apiKeyEnv` никогда не читается). Вычисляется заново на каждый запрос, без кэша → горячие правки конфигурации провайдера вступают в силу немедленно.
|
|
244
|
-
- **Переключение на неофициальный источник возвращает сессию**: если после пиковой паузы сессию переключить на локального/стороннего провайдера (событие `model/selection` с 0.1.2+) → эта сессия возобновляется автоматически (под условием `deferredResume`); затрагиваются только сессии, приостановленные этим плагином при входе в пик, сессии, приостановленные вручную через `/pause`, **никогда** не трогаются. Без этого события на 0.1.1 → деградация до «следующий запрос или ручной `/resume`».
|
|
245
|
-
- **Эндпоинт недоступен для чтения**: сервис `llm` отсутствует, структура пространства имён изменилась, поле не строка — всегда деградация к определению по id / встроенному эндпоинту с записью `matchedBy`, **исключения никогда не выбрасываются**.
|
|
246
|
-
- **Разбор ошибочного определения**: `GET /session-guard/provider?provider=<id>` возвращает `{ official, matchedBy, endpoint }`.
|
|
247
|
-
|
|
248
|
-
### Страж уровня запроса и отложенная очередь
|
|
249
|
-
|
|
250
|
-
- **Зачем уровень запроса**: тик в 30 с обрабатывает при скачке `NORMAL → PAUSED_PEAK` только те сессии, что были `running` в тот момент; сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, выпали бы. Waterfall `agent/request` — страховка, которая проходит **с каждым запросом**.
|
|
251
|
-
- **Определение опирается на возвращаемое значение `next()`**: middleware выбора модели внутри waterfall перезаписывает provider/model выбранными в интерфейсе значениями, поэтому сначала нужно `await next()` и только потом выносить вердикт.
|
|
252
|
-
- **Режим hold (по умолчанию)**: запрос удерживается, **не отправляясь и не вызывая ошибки**, и выпускается в точный момент выхода из пика (`msUntilOffPeak` — точный таймер, тик в 30 с как страховка); отмена пользователем (abort) прерывает штатно.
|
|
253
|
-
- **Режим error**: выбрасывается узнаваемая ошибка `PEAK_DEFERRED` + запись в отложенную очередь, после пика возобновление с `deferredResumeText` (без автоматического продолжения, если `deferredResume` выключен).
|
|
254
|
-
- **Защита потолком**: если `deferredMaxHoldMs` (по умолчанию 6 ч) истёк, а пик не кончился → перевод в error, чтобы не зависать бесконечно.
|
|
255
|
-
- **Железное правило взаимного исключения**: во время удержания **никогда** не запрашивается дополнительно пауза через сессионные ворота (пауза ждёт безопасную границу, до которой удержанный запрос никогда не дойдёт → они ожидали бы друг друга). При входе в пик уже удержанные сессии пропускаются.
|
|
256
|
-
- **Без персистентности**: отложенная очередь — promise внутри процесса, при перезапуске исчезает.
|
|
257
|
-
|
|
258
|
-
### Границы (явно вне рамок)
|
|
259
|
-
|
|
260
|
-
- **Никакой смены провайдера / перенаправления**: только блокировка, без маршрутизации;
|
|
261
|
-
- **Компактация не проходит через `agent/request`**: пока сессия на паузе, она происходить не может; вызванная вручную компрессия во время пика всё ещё может попасть в официальный источник (этот плагин не перехватывает уровень `ctx.llm.stream`);
|
|
262
|
-
- **На 0.1.1 нет события `model/selection`**: автоматическое возобновление после переключения на неофициальный источник деградирует до «ждать следующий запрос или ручной `/resume`» (с 0.1.2 — немедленно);
|
|
263
|
-
- **Никаких новых npm-зависимостей**, никаких чтения/записи учётных данных, повторы 429 / транспортного уровня у `dsh-llm-retry` не трогаются.
|
|
264
|
-
|
|
265
|
-
### Работа с часовыми поясами и валидация
|
|
266
|
-
|
|
267
|
-
- Определение часового пояса основано на **именах поясов IANA** (например `Asia/Shanghai`, `Asia/Tokyo`, `Asia/Seoul`), через `Intl.DateTimeFormat` проецируемых на настенные часы настроенного пояса, **без опоры на голый `getUTCDay()`** — это позволяет избежать классической ошибки в 8 часов на границе UTC+8 пекинского времени (суббота 00:30 по Пекину — в UTC ещё пятница);
|
|
268
|
-
- `Intl.DateTimeFormat` сам служит слоем валидации: недопустимое имя пояса (например `Foo/Bar`) выбрасывает `RangeError`, которую внешний try-catch тихо понижает до пояса по умолчанию `Asia/Shanghai` (fail-open);
|
|
269
|
-
- Окна пика **левая граница включена, правая исключена** `[start, end)`, поддерживаются окна через полночь (например `22:00–06:00`);
|
|
270
|
-
- Настройка `timezone` работает одинаково для всех языков интерфейса (zh/en/ja/ko) — имена поясов IANA в `Intl.DateTimeFormat` не зависят от локали: при японском/корейском интерфейсе поведение часового пояса полностью совпадает с китайским.
|
|
271
|
-
|
|
272
|
-
### Разделение труда с input-traffic: один «останавливает», другой «выстраивает очередь»
|
|
273
|
-
|
|
274
|
-
Оба действуют на **разные звенья одной цепи**, границу задаёт сама модель inbox в DSH:
|
|
275
|
-
|
|
276
|
-
```
|
|
277
|
-
Ввод пользователя ──(input-traffic выбирает ярус)──▶ очереди ожидания next-step / next-turn
|
|
278
|
-
│
|
|
279
|
-
agent/pre-step ──(ворота шага этого плагина)──▶ пропустить / удержать
|
|
280
|
-
│
|
|
281
|
-
agent/request ──(удержание уровня запроса этого плагина)──▶ пропустить / удержать
|
|
282
|
-
│
|
|
283
|
-
вызов модели
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Семантика очередей DSH (две очереди, не путать)**
|
|
287
|
-
|
|
288
|
-
| Очередь | Смысл | Когда потребляется |
|
|
289
|
-
|---|---|---|
|
|
290
|
-
| `next-step` | «Ввод, ожидающий следующей границы шага» | На следующем `agent/pre-step`: **на одном уровне с результатом инструмента**, ещё один шаг внутри того же раунда |
|
|
291
|
-
| `next-turn` | «Промпт, ожидающий отдельного раунда» | По завершении текущего раунда, запускается как **новый раунд** |
|
|
292
|
-
|
|
293
|
-
`Inbox.claim()` **всегда сначала выгребает `next-step`** и лишь дополнительно берёт **1** элемент `next-turn`, если на этой границе открывается новый раунд; первый шаг раунда читает next-turn, все последующие читают next-step.
|
|
294
|
-
|
|
295
|
-
**Разделение обязанностей**
|
|
296
|
-
|
|
297
|
-
- **session-guard = останавливать**: решает только, «когда можно продолжать», и **никогда не трогает содержимое и порядок очередей**.
|
|
298
|
-
- ворота шага (`agent/pre-step`): удерживают **до** запроса к модели следующего шага;
|
|
299
|
-
- пауза на уровне раунда (`agent.cancel({keepInbox:true})` + `goals.pause` + безопасная граница): останавливает текущий раунд, **очереди остаются как есть**;
|
|
300
|
-
- страж уровня запроса (удержание на `agent/request`): удерживает **именно этот один запрос к модели**.
|
|
301
|
-
- **input-traffic = выстраивать очередь**: решает только, «в какую очередь попадает ввод пользователя, каким ярусом и когда будет потреблён».
|
|
302
|
-
- три яруса = в какую очередь положить: красный «прервать» делает сначала `cancel()`, затем `steer`; жёлтый «вставить реплику» делает `steer` (→ `next-step`, следующий шаг того же раунда); зелёный «в очередь» остаётся в `next-turn`;
|
|
303
|
-
- заморозка = извлечь строки `queued` + `steering` (ярус сохраняется) + блокировка composer + вызов `sessionGuard.stopNextTurn`; возобновление = снять блокировку → сначала `sessionGuard.resume` → повторная подача по ярусу.
|
|
304
|
-
|
|
305
|
-
**Два железных правила в точке встречи**
|
|
306
|
-
|
|
307
|
-
1. **Заморозка обязана позволить плагину сначала открыть ворота шага**: ворота шага подвешены на `agent/pre-step`, а пауза на уровне раунда ждёт события на безопасной границе — они ожидали бы друг друга (`pauseTask` / `cancelTask` этого плагина сначала делают `release`);
|
|
308
|
-
2. **Пока ворота шага закрыты, сообщения уже забраны**: `preStep()` делает `inbox.claim()` до диспетчеризации waterfall, поэтому новый ввод встаёт в очередь за уже забранной партией; `keepInbox` действует только на паузу уровня раунда.
|
|
309
|
-
|
|
310
|
-
**Никаких взаимных вторжений**: input-traffic не слушает `agent/pre-step` / `agent/request` (единственное исключение — явный `cancel()` яруса «прервать», запрошенный самим пользователем); этот плагин никогда не переписывает содержимое и порядок `next-step` / `next-turn`.
|
|
311
|
-
|
|
312
|
-
Кнопки: кнопки «Пауза сессии / Продолжить сессию» этого плагина (order 20) и кнопки input-traffic «❄ Заморозка и добавить / Возобновить и добавить» (order 30) стоят рядом и не заменяют друг друга — первые управляют воротами шага, вторые извлечением из очереди + заморозкой на уровне раунда.
|
|
313
|
-
|
|
314
|
-
## Избыточный порт `sessionGuard`
|
|
315
|
-
|
|
316
|
-
```js
|
|
317
|
-
{
|
|
318
|
-
stopNextTurn(sessionId, opts), // останавливает следующий раунд сессии (собственные сессионные ворота / откат к очереди с блокировкой)
|
|
319
|
-
resume(sessionId, opts), // возобновляет (confirm + choice: rerun|skip)
|
|
320
|
-
lockQueue(sessionId, reason), // явно блокирует очередь
|
|
321
|
-
unlockQueue(sessionId), // явно разблокирует
|
|
322
|
-
stepPause(sessionId), // вручную запрашивает паузу уровня шага (закрытие на следующей границе pre-step, шаг 1 тоже перехватывается)
|
|
323
|
-
stepResume(sessionId, opts), // открывает ворота шага (v0.2.0); при opts.bypass=false без обхода на этот пик
|
|
324
|
-
state(sessionId), // { queueLocked, lockReason, paused, pausedStep, stepHeldSince, stepBypass, taskControlAvailable, taskControl }
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
## HTTP-маршруты
|
|
329
|
-
|
|
330
|
-
- `GET /session-guard/state?session=<id>` — состояние сессии (включая `paused: { step, turn, manual }` / `stepGate` / последнюю цель / удержание / отложение)
|
|
331
|
-
- `GET /session-guard/events?session=<id>` — **SSE**: мгновенная доставка изменений состояния ворот шага (кнопка обновляется по ним)
|
|
332
|
-
- `GET /session-guard/settings` — настройки + доступность taskControl
|
|
333
|
-
- `GET /session-guard/status` — текущая глобальная фаза (опрос бейджа состояния; включает `stepHeld`)
|
|
334
|
-
- `GET /session-guard/provider?provider=<id>` — диагностика определения официального источника (`official` / `matchedBy` / `endpoint`)
|
|
335
|
-
- `GET /session-guard/diag` — диагностика времени выполнения (включая `stepGate`)
|
|
336
|
-
- `POST /session-guard/rpc` — `{ action: stopNextTurn|resume|lockQueue|unlockQueue|stepPause|stepResume|state, sessionId }`
|
|
337
|
-
|
|
338
|
-
## Хранение состояния
|
|
339
|
-
|
|
340
|
-
JSON на сессию: `$DSH_HOME/.dsh/session-guard/<sessionId>.json` (атомарная запись; можно переопределить через `DSH_SESSION_GUARD_STATE_DIR`).
|
|
341
|
-
|
|
342
|
-
## Тесты
|
|
343
|
-
|
|
344
|
-
```bash
|
|
345
|
-
npm test # node --test tests/*.test.mjs (часовые пояса/выходные/машина состояний/сессионные ворота/мост/повтор)
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
## Модули
|
|
349
|
-
|
|
350
|
-
| Файл | Ответственность |
|
|
351
|
-
|---|---|
|
|
352
|
-
| `src/time.js` | Определение пика/выходных (с правильным поясом) + `msUntilOffPeak` (точный таймер выхода из пика) |
|
|
353
|
-
| `src/scheduler.js` | Чистая машина состояний NORMAL ↔ PAUSED_PEAK |
|
|
354
|
-
| `src/provider.js` | Пятиуровневое определение официальных источников (чистая функция: нормализация эндпоинта + матрица решений) |
|
|
355
|
-
| `src/provider-directory.js` | Каталог эндпоинтов (`llm.listConfigurableProviders` + `settings.get`, деградация по всей цепочке) |
|
|
356
|
-
| `src/deferrals.js` | Реестр отложений (удержание / освобождение / превышение предела / `PeakDeferredError`) |
|
|
357
|
-
| `src/request-guard.js` | Страж уровня запроса на `agent/request` (два режима hold / error) |
|
|
358
|
-
| `src/step-gate.js` | **Ворота уровня шага на `agent/pre-step`** (v0.2.0: закрытие / открытие / повышение по таймауту / bypass, чистый `decideStepHold` покрыт юнит-тестами) |
|
|
359
|
-
| `src/targets.js` | Отслеживание «последней реальной цели» сессии (`request/header` + `model/selection`) |
|
|
360
|
-
| `src/wiring.js` | Оркестрация подключения (фильтр входа в пик / подключение ворот шага / освобождение после пика / точный таймер / очистка при выгрузке) |
|
|
361
|
-
| `src/pause-gate.js` | Движок собственных сессионных ворот (agent.cancel keepInbox + goals.pause + безопасная граница + возобновление followup; перед паузой сначала открывает ворота шага) |
|
|
362
|
-
| `src/pause-store.js` | Персистентность собственного состояния паузы |
|
|
363
|
-
| `src/gate.js` | Драйвер сессионных ворот (собственная настоящая пауза / откат к очереди с блокировкой, fail-open) |
|
|
364
|
-
| `src/bridge.js` | Избыточный порт `sessionGuard` |
|
|
365
|
-
| `src/retry.js` | Автоповтор на бэкенде (классификация сбоев/задержка/уступание при заморозке; короткое замыкание только по точному коду `PEAK_DEFERRED`) |
|
|
366
|
-
| `src/detect.js` | Автоопределение (taskControl хоста / мост клиента input-traffic) |
|
|
367
|
-
| `src/store.js` | Персистентное состояние на сессию |
|
|
368
|
-
| `src/settings.js` | Подраздел настроек (схема schemastery + fail-open регистрация) |
|
|
369
|
-
| `src/index.js` | apply на стороне хоста (настройки/маршруты/тик/предоставление сервиса/подключение повтора/страж запросов) |
|
|
370
|
-
| `src/client/` | Браузерная половина (**кнопка паузы сессии** + бейдж состояния + карточка настроек) |
|
|
371
|
-
|
|
372
|
-
## Лицензия
|
|
373
|
-
|
|
374
|
-
MIT — см. [LICENSE](LICENSE).
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>Автоматические сессионные ворота в часы пик: режим выходных + автопауза в пик + двухмерное определение официальных источников + заморозка на уровне сессии + автоматический повтор на стороне бэкенда</strong>
|
|
3
|
+
</p>
|
|
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
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="README.en.md">English</a> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.fr.md">Français</a> · <a href="README.de.md">Deutsch</a> · <a href="README.it.md">Italiano</a> · <strong>Русский</strong> · <a href="README.es.md">Español</a>
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
11
|
+
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" data-canonical-src="https://img.shields.io/badge/dsh--plugin-ready-478CBF?logo=deepseek&logoColor=white" style="max-width: 100%;">
|
|
12
|
+
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
# dsh-session-guard
|
|
16
|
+
|
|
17
|
+
- [English README](./README.en.md)
|
|
18
|
+
- [中文 README](./README.md)
|
|
19
|
+
- [日本語 README](./README.ja.md)
|
|
20
|
+
- [한국어 README](./README.ko.md)
|
|
21
|
+
- [README en français](./README.fr.md)
|
|
22
|
+
- [README auf Deutsch](./README.de.md)
|
|
23
|
+
- [README in italiano](./README.it.md)
|
|
24
|
+
- [README на русском](./README.ru.md)
|
|
25
|
+
- [README en español](./README.es.md)
|
|
26
|
+
- [Installation guide](./INSTALL.md)
|
|
27
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
28
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
29
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
30
|
+
- [Guide d'installation en français](./INSTALL.fr.md)
|
|
31
|
+
- [Installationsanleitung auf Deutsch](./INSTALL.de.md)
|
|
32
|
+
- [Guida all'installazione in italiano](./INSTALL.it.md)
|
|
33
|
+
- [Руководство по установке на русском](./INSTALL.ru.md)
|
|
34
|
+
- [Guía de instalación en español](./INSTALL.es.md)
|
|
35
|
+
- [Changelog](./CHANGELOG.md)
|
|
36
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
37
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
38
|
+
- [Changelog en français](./CHANGELOG.fr.md)
|
|
39
|
+
- [Changelog auf Deutsch](./CHANGELOG.de.md)
|
|
40
|
+
- [Changelog in italiano](./CHANGELOG.it.md)
|
|
41
|
+
- [Changelog на русском](./CHANGELOG.ru.md)
|
|
42
|
+
- [Changelog en español](./CHANGELOG.es.md)
|
|
43
|
+
|
|
44
|
+
> **Замечание о совместимости:** в v0.1.1 уже входят словари японского (`ja`) и корейского (`ko`), но текущие официальные сборки DSH предоставляют через `LocaleRuntime` только `zh` и `en`. В оригинальном DSH выбор `ja` или `ko` завершается ошибкой `locale "<id>" is not registered`. Придётся дождаться, пока официальный DSH добавит соответствующие идентификаторы локалей. Опытные пользователи могут поддерживать форк DSH, чтобы расширить его.
|
|
45
|
+
|
|
46
|
+
> **▼ Совместимость версий DSH**
|
|
47
|
+
>
|
|
48
|
+
> | Версия DSH | Загрузка | Регистрация настроек | События сессии / ворота | Клиентская половина |
|
|
49
|
+
> | --- | --- | --- | --- | --- |
|
|
50
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ➖ вне этой линии (исторические версии до `legacy/0.1.2`) | `ctx.settings.register(ns, schema, { base })` | ✅ форма совпадает | ✅ без импорта платформенных значений |
|
|
51
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ➖ вне этой линии → используйте `legacy/0.1.2` (npm `@dsh-0.1.2`) | `register` сохранён (добавлен `installSection`) | ✅ форма совпадает | ✅ без импорта платформенных значений |
|
|
52
|
+
> | **0.2.0-rc.1+** | ✅ (**эта линия**, dist-tag `dsh-0.2.0`, npm `4.0.0`) | декларативно: поля `.volatile()` в Config хост проецирует в формы, `register` удалён | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ карточка настроек клиента переведена на `configForms` |
|
|
53
|
+
> | 0.1.7-rc.1+ | ✅ (ветка `compat/0.1.7`, dist-tag `dsh-0.1.7`) | декларативно: поля `.volatile()` в Config хост проецирует в формы, `register` удалён | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ карточка настроек клиента переведена на `configForms` |
|
|
54
|
+
> | 0.1.5-rc.2 | ✅ (ветка `compat/0.1.5`, dist-tag `dsh-0.1.5`) | `register` на месте (строковые пространства имён) | ✅ события читаются по двойному пути `snapshotEvents()` | ✅ |
|
|
55
|
+
>
|
|
56
|
+
> **Идентификация этой линии**: ветка `compat/0.2.0`, номер версии npm **`4.0.0`** (semver), dist-tag **`dsh-0.2.0`**.
|
|
57
|
+
> `engines.dsh` в `package.json` и `dsh.plugin.json`, а также четыре peer-зависимости `@deepseek-ai/dsh-client-*`
|
|
58
|
+
> единообразно приведены к `>=0.2.0-rc.1 <0.2.1-0`; нижняя граница peer-зависимости `@deepseek-ai/cordis` выровнена по `^4.0.4` хостовой линии.
|
|
59
|
+
> Раньше README использовало «2.x / 3.x» как **кодовые имена линий** — это была манера изложения, а **не номера версий, которые можно
|
|
60
|
+
> получить из реестра**, — ориентируйтесь только на npm-версии `0.3.1` (линия 0.1.2), `3.0.0`/`3.0.1` (линия 0.1.5),
|
|
61
|
+
> `3.2.4` (линия 0.1.7) и `4.0.0` (линия 0.2.0).
|
|
62
|
+
>
|
|
63
|
+
> **Адаптация 0.2.0 (ветка compat/0.2.0, npm 4.0.0)**:
|
|
64
|
+
> API плагинов, которую использует этот плагин (manifest/settings/HMR/slot/сессии V4), в 0.2.0-rc.1 полностью совместима с 0.1.7;
|
|
65
|
+
> адаптация сводится к смене поколения метаданных (peer/engines/dist-tag/версия 4.0.0). Попутно с базой перенесены инженерные исправления:
|
|
66
|
+
> ① `src/client/family-section.tsx` добавлен под контроль версий (раньше существовал только в локальном рабочем дереве, чистый checkout не мог собрать проект);
|
|
67
|
+
> ② из конфигурации eslint убрано ядрёное `no-unused-vars`, ошибочно помечавшее параметры в сигнатурах типов TS, — lint-базовая линия снова полностью зелёная;
|
|
68
|
+
> ③ статические шлюзы подписей путей записи v4 (`tests/source-kind.test.mjs`) переносятся в эту линию вместе с базой;
|
|
69
|
+
> ④ peer-зависимость `@deepseek-ai/cordis` выровнена по `^4.0.4` (пакеты UI хоста 0.2.0-rc.1 объявляют `~4.0.4`; прежний `^4.0.1` не конфликтовал, но его нижняя граница была слишком стара).
|
|
70
|
+
>
|
|
71
|
+
> **Где оказываются другие линии**: для хостов DSH `0.1.2-rc.x` используйте ветку **`legacy/0.1.2`** (npm dist-tag
|
|
72
|
+
> `dsh-0.1.2`, версия `0.3.1`). **`main` заморожен на `0.2.0-beta.1` и не является веткой релизов линии 0.1.2.**
|
|
73
|
+
> Для хостов DSH `0.1.0-rc.7` ~ `0.1.1-rc.x` используйте исторические версии ≤ `0.1.2`.
|
|
74
|
+
> Канонические определения источников полей:
|
|
75
|
+
> `mine-dsh-plugins/improve-dsh-plugins/DSH-PLUGIN-VERSION-DISTRIBUTION-STRATEGY.md` §2.2.
|
|
76
|
+
>
|
|
77
|
+
> **Адаптация 0.1.5 (ветка compat/0.1.5, npm 3.0.0)**:
|
|
78
|
+
> ① двойной путь для `agent.followup` — 0.1.5 превращает Inbox в проекцию agent-loop только для чтения; если `followup`
|
|
79
|
+
> больше не существует, происходит откат к `agent.send`, а если недоступно ни то ни другое — понижение до warn без исключений (страховка возобновления);
|
|
80
|
+
> ② возобновляющие сообщения добавляют в `source` поле `form: 'instructions'` (контракт ContextFormed из 0.1.5, старые версии его игнорируют);
|
|
81
|
+
> ③ `webServer.register` обёрнут в try/catch: неудачная регистрация лишь пишется в журнал и не рушит загрузку плагинов хоста;
|
|
82
|
+
> ④ 0.1.5 удаляет доступ к массиву `session.events`; чтение событий идёт по основному пути `snapshotEvents()` +
|
|
83
|
+
> с откатом к старому массиву (затрагивает только два вспомогательных пути `findToolOutcome` / `lastUserPrompt`, сопоставление типов событий не меняется).
|
|
84
|
+
> Проверено, что контракт `WebRoute` 0.1.5 (exact/prefix + SSE) не изменился: клиентскому
|
|
85
|
+
> `fetch('/session-guard/...')` префикс `/api` не нужен.
|
|
86
|
+
> Приведённая таблица совместимости охватывает два поколения API (0.1.1 / 0.1.2); эта линия претендует только на строку 0.1.5.
|
|
87
|
+
> `session/event`, `agent.cancel`, `goals.pause`,
|
|
88
|
+
> `agent.followup`, `commands.register`, `timer.interval`, `webServer.register`,
|
|
89
|
+
> `agent/request`, `llm.listConfigurableProviders`, `settings.register/get` имеют одинаковые подписи
|
|
90
|
+
> между `dsh-v0.1.1-rc.2` и `dsh-v0.1.2-rc.1` (эта линия проверена вплоть до `dsh-v0.1.5-rc.2`);
|
|
91
|
+
> единственное место, требующее двойного чтения, — форма id вызова, записываемого `tool/result` (приоритет у `content[].toolCallId`,
|
|
92
|
+
> откат к `source.callId`), вынесена в `src/tool-call-id.js` с модульными тестами — в журналах воспроизведения обеих версий могут встречаться обе формы.
|
|
93
|
+
> С 0.1.5 доступ к массиву `session.events` удалён; эта линия читает через `snapshotEvents()` (с сохранением отката к старому массиву),
|
|
94
|
+
> затрагивая только два вспомогательных пути `findToolOutcome` / `lastUserPrompt`.
|
|
95
|
+
> Событие `model/selection` есть **только с 0.1.2**, служит лишь для ускорения смены модели и обязательно требует проверки доступности функции; поверхность настроек использует только
|
|
96
|
+
> пересечение `register` + `get` (никогда `installSection` / удалённый `installSettingsSection`).
|
|
97
|
+
> Скрипт-страж дрейфа: `tools/check-api-drift.ps1` (эта линия по умолчанию проверяет наличие нужных интерфейсов относительно `dsh-v0.1.5-rc.2`).
|
|
98
|
+
|
|
99
|
+
> Автоматически приостанавливает запущенные сессии в часы пик и автоматически возобновляет их вне пиковых часов и в выходные; вместе с кнопкой заморозки input-traffic даёт блокировку **на уровне сессии**; **автоматический повтор** на стороне бэкенда уступает дорогу во время заморозки/действия ворот. В основе — **собственные сессионные ворота** (`agent.cancel keepInbox + goals.pause + session/event безопасная граница + возобновление через followup`), зависимость от dsh-task-control устранена.
|
|
100
|
+
|
|
101
|
+
Плагин cordis, собираемый командой `dsh plugin` и патчем бандла — без правки исходников dsh и без PR.
|
|
102
|
+
|
|
103
|
+
> 💡 **Почему стоит установить**: с 2026-08-17 DeepSeek перешёл на **тарификацию по пиковой и непиковой зоне** — в пиковые часы (пекинское время 9:00-12:00, 14:00-18:00) цена единицы **в 2 раза** выше, чем в непиковые (включая полдень, ночь, выходные и праздники). Плагин автоматически останавливает запущенные сессии в пик и автоматически возобновляет их после пика: длинные прогоны со сдвигом по времени экономят до **50 %**; ручная заморозка (через кнопку input-traffic) даёт ещё более точное управление остановкой по каждой сессии.
|
|
104
|
+
|
|
105
|
+
## Обзор возможностей
|
|
106
|
+
|
|
107
|
+
- **Режим выходных**: распознаёт выходные (через `Intl.DateTimeFormat` с настроенным часовым поясом, без классической ошибки в 8 часов на границе UTC+8 из-за голого `getUTCDay()` для пекинского времени) → в выходные пиковая зона игнорируется, можно работать без ограничений.
|
|
108
|
+
- **Автопауза в пик (глобально)**: при входе в пик (и если не выходные) автоматически приостанавливаются все корневые сессии в состоянии `running`; после пика все автоматически возобновляются — **глобальный переключатель, вручную ничего делать не нужно**.
|
|
109
|
+
- **Двухмерное определение официальных источников (providerGuard)**: в пиковые часы блокируется **только если целью запроса является официальный источник DeepSeek**; с локальными/сторонними провайдерами (например `local-35b`) работа идёт как обычно, пиковые ворота её не трогают. Порядок определения = явный список id → эндпоинт `baseURL` → эндпоинт по умолчанию из каталога → встроенный id.
|
|
110
|
+
- **Страховка на уровне запроса + отложенная очередь**: сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, перехватываются стражем уровня запроса `agent/request` (по умолчанию `hold`: запрос приостанавливается без ошибки и автоматически выпускается после пика).
|
|
111
|
+
- **Заморозка / возобновление на уровне сессии**: избыточный порт `sessionGuard` + `POST /session-guard/rpc`, подключается по каждой сессии через кнопку заморозки input-traffic; есть и ручные команды `/pause /resume /cancel`.
|
|
112
|
+
- **Автоматический повтор на стороне бэкенда (D9)**: переходящие сбои turn/end (error/429/max-tokens) автоматически возобновляются с адаптивной задержкой; постоянные сбои останавливают работу; **уступает дорогу во время заморозки/действия ворот**, никогда не обходит сессионные ворота.
|
|
113
|
+
- **fail-open**: собственные сессионные ворота недоступны, session-guard не установлен, сервис настроек отсутствует — во всех случаях тихая деградация, никогда не падает из-за зависимости.
|
|
114
|
+
|
|
115
|
+
## Просмотр интерфейса
|
|
116
|
+
|
|
117
|
+
Скриншот реальной работы (Windows, dsh web) — активный режим выходных:
|
|
118
|
+
|
|
119
|
+
<figure>
|
|
120
|
+
<img style="max-width:100%" alt="Панель управления состоянием в области ввода: активная кнопка «Выходные» подсвечена (при включённом режиме выходных сессии работают свободно, без учёта пика), рядом кнопка «Заморозить сессию» (вместе с input-traffic), режим рассуждений DeepSeek-V4-Flash и элементы отправки, внизу полосы состояния: раунды/шаги, время LLM, доля попаданий в кэш и т. д." src="assets/高峰低峰周末提醒-周末状态.png" />
|
|
121
|
+
<figcaption>Режим выходных активен: бейдж «выходные» в области ввода подсвечен, рядом «Заморозить сессию»; в выходные пиковая зона игнорируется, сессии работают свободно.</figcaption>
|
|
122
|
+
</figure>
|
|
123
|
+
|
|
124
|
+
## Установка
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Хосты DSH 0.1.5-rc.x (эта линия, dist-tag dsh-0.1.5)
|
|
128
|
+
dsh plugin --profile web add dsh-session-guard@dsh-0.1.5
|
|
129
|
+
|
|
130
|
+
# или напрямую из ветки git
|
|
131
|
+
dsh plugin --profile web add github:drscrewdriver/dsh-session-guard#compat/0.1.5
|
|
132
|
+
|
|
133
|
+
# Хосты DSH 0.1.2-rc.x должны использовать линию 0.1.2
|
|
134
|
+
dsh plugin --profile web add dsh-session-guard@dsh-0.1.2
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`compat/0.1.5` — выделенная линия для DSH `0.1.5-rc.x`, номер версии npm **`3.0.0`**; для хостов DSH `0.1.2-rc.x` используйте
|
|
138
|
+
ветку **`legacy/0.1.2`** (npm dist-tag `dsh-0.1.2`, версия `0.3.1`). **`main` заморожен на `0.2.0-beta.1`,
|
|
139
|
+
это не ветка релизов линии 0.1.2.**
|
|
140
|
+
|
|
141
|
+
> ⚠️ Не полагайтесь на «голое» имя пакета `dsh-session-guard`: тег `latest` в npm не может обслуживать сразу две взаимоисключающие
|
|
142
|
+
> линии версий (их диапазоны `engines.dsh` взаимоисключающи по правилам semver для prerelease) — dist-tag нужно указывать явно.
|
|
143
|
+
|
|
144
|
+
После установки перезапустите dsh web и обновите страницу.
|
|
145
|
+
|
|
146
|
+
## Настройки (Настройки → Плагины → session-guard, простые переключатели)
|
|
147
|
+
|
|
148
|
+
| Переключатель | По умолчанию | Описание |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `enabled` | on | **Автопауза/заморозка сессий в пик**: автоматически приостанавливает запущенные сессии в пиковые часы |
|
|
151
|
+
| `stepLevelPause` | on | **Ворота на уровне шага**: в пик ворота закрываются **до** запроса к модели следующего шага (раньше и экономнее, чем пауза на уровне раунда); off — откат к паузе на уровне раунда |
|
|
152
|
+
| `providerGuard` | on | **Двухмерное определение официальных источников**: в пик блокируются только официальные источники DeepSeek, локальные/сторонние провайдеры работают как обычно |
|
|
153
|
+
| `guardSubagents` | on | **Включая запросы субагентов**: запросы субагентов тоже тарифицируются, по умолчанию блокируются вместе с остальными |
|
|
154
|
+
| `offPeakAutoResume` | on | **Автовозобновление в непик**: приостановленные сессии автоматически возобновляются в непиковые часы; off — после пика автоматического возобновления нет (нужна ручная команда) |
|
|
155
|
+
| `weekendMode` | on | **Режим выходных**: распознаёт выходные → в выходные автопаузы нет (в выходные нет пикового времени, можно работать свободно) |
|
|
156
|
+
| `deferredResume` | on | **Автовозобновление после пика**: off — отложенные запросы/сессии не возобновляются сами, нужен ручной `/resume` |
|
|
157
|
+
| `queueFallback` | on | Откат к очереди ожидания под блокировкой, когда собственные сессионные ворота недоступны (fail-open) |
|
|
158
|
+
| `retryEnabled` | off | **Автоповтор (бэкенд)**: автоматическое возобновление после переходящих сбоев (по умолчанию off, консервативно) |
|
|
159
|
+
|
|
160
|
+
Дополнительные параметры:
|
|
161
|
+
|
|
162
|
+
- `timezone` (по умолчанию Asia/Shanghai) — часовой пояс для **определения выходных** и отображения бейджа; **на определение пика не влияет** (пик всегда по пекинскому времени);
|
|
163
|
+
- `peakWindows` (по умолчанию 09:00–12:00 / 14:00–18:00) — окна пика/непика по пекинскому времени (UTC+8), совпадают с официальной тарификацией DeepSeek;
|
|
164
|
+
- `pauseMode` (`safe`/`force`), `pauseReason` (`wait`/`stop`) — способ выполнения паузы;
|
|
165
|
+
- `stepGateTimeoutMs` (по умолчанию 300000) — таймаут удержания ворот шага; по истечении ворота открываются с **повышением до паузы на уровне раунда** (защита от дедлока, без «одного шага каждые 5 минут» и утечки токенов);
|
|
166
|
+
- Определение официальных источников: `officialProviders` (дополнительные id официальных провайдеров, через запятую, наивысший приоритет), `officialBaseURLs` (список хостов официальных эндпоинтов, по умолчанию `api.deepseek.com`);
|
|
167
|
+
- Отложенная очередь: `deferredMode` (`hold` удержание / `error` ошибка и отложение), `deferredResumeText` (текст возобновления после пика), `deferredMaxHoldMs` (предел удержания, по умолчанию 6 часов, по истечении переводится в error);
|
|
168
|
+
- Параметры повтора: `retryText`, `retryGraceMs`, `retryCooldownMs`, `retryBackoffFactor`, `retryBackoffMaxMs`, `retryMaxConsecutive`.
|
|
169
|
+
|
|
170
|
+
## Поведение
|
|
171
|
+
|
|
172
|
+
### Автоматические пиковые ворота (глобально)
|
|
173
|
+
|
|
174
|
+
- **Вход в пик** (и не выходные): при включённом `stepLevelPause` текущий раунд **больше не обрывается сразу** — сессия естественно доходит до следующей границы `agent/pre-step`, где закрываются ворота шага (см. следующий раздел); при выключенном для всех корневых сессий `running` вызывается `gate.stopNextTurn` (настоящая пауза через собственные сессионные ворота, либо в зависимости от `queueFallback` откат к очереди ожидания под блокировкой);
|
|
175
|
+
- **Выход из пика / выходные**: сначала `releaseAll` освобождает удерживаемые шаги (раунд продолжается на месте), затем `gate.resume` для **всех** сессий — управляется переключателем `offPeakAutoResume`, при выключенном после пика автоматического возобновления нет;
|
|
176
|
+
- **Часовой пояс пика**: всегда пекинское время (`Asia/Shanghai`), совпадает с базой официальной тарификации DeepSeek и не зависит от настройки `timezone`;
|
|
177
|
+
- Машина состояний: единственный экземпляр `NORMAL ↔ PAUSED_PEAK` (`scheduler.js`), управляется одним тиком в 30 с.
|
|
178
|
+
|
|
179
|
+
### Ворота на уровне шага (v0.2.0, ключ к экономии токенов)
|
|
180
|
+
|
|
181
|
+
Подвешены на waterfall `agent/pre-step`: раунд приостанавливается **до того, как произойдёт запрос к модели следующего шага**.
|
|
182
|
+
|
|
183
|
+
- **Условия закрытия** (все обязательны): `enabled` + `stepLevelPause` + `step > 1` + пик (пекинское время, не выходные) + официальный целевой провайдер (`providerGuard`, при выключенном блокируются все) + сессия не удержана на уровне запроса + не обойдена вручную в этом пиковом окне;
|
|
184
|
+
- **Почему `step > 1`**: первый шаг раунда покрывается стражем уровня запроса, чтобы два gate не пересекались;
|
|
185
|
+
- **Пути освобождения**: ① кнопка «⏸ пауза (продолжить)» / `POST /session-guard/rpc {action:'stepResume'}` / `/resume` → пропускает текущий шаг и **больше не перекрывает эту сессию до конца пика**; ② выход из пика → всё освобождается, раунд продолжается на месте (**followup не нужен**); ③ кнопка заморозки / `/pause` / `/cancel` → ворота открываются и происходит переход к паузе на уровне раунда; ④ abort по `signal` (отмена пользователем) → открытие;
|
|
186
|
+
- **Повышение по таймауту**: удержание дольше `stepGateTimeoutMs` (по умолчанию 5 минут) → ворота открываются с **повышением до принудительной паузы на уровне раунда**, единообразное возобновление после пика (без дедлока и без утечки токенов по капле во время пика);
|
|
187
|
+
- **Состояние**: `GET /session-guard/state?session=<id>` возвращает `paused: { step, turn }` и `stepGate: { held, since, bypass }`; поле `paused` порта сервиса `state()` **остаётся булевым** (для обратной совместимости), состояние шага живёт в `pausedStep`;
|
|
188
|
+
- **Без записи на диск**: удержание — это Promise внутри процесса, при перезапуске исчезает (чтобы не оставались «призрачные» состояния).
|
|
189
|
+
|
|
190
|
+
#### Кнопки «Пауза сессии / Продолжить сессию» (предоставляет session-guard)
|
|
191
|
+
|
|
192
|
+
Кнопка «Пауза сессии» справа в области ввода (slot `conversation.input.right`, id `session-guard-pause`, order 20, слева от «❄ Заморозка и добавить» у input-traffic):
|
|
193
|
+
|
|
194
|
+
- не на паузе → «Пауза сессии», **нажимается**: клик вызывает `stepPause` и приостанавливает сессию **до запроса к модели следующего шага** (текущий шаг не обрывается; шаг 1 тоже перехватывается, независимо от пика и провайдера);
|
|
195
|
+
- на паузе → «Продолжить сессию», клик вызывает `stepResume`: пропускает текущий шаг и больше не перекрывает эту сессию до конца пика;
|
|
196
|
+
- **Отправка событий**: `GET /session-guard/events?session=<id>` (SSE) **мгновенно** доставляет изменения состояния ворот шага — как только пик автоматически закрывает ворота, кнопка тут же становится «Продолжить сессию», без ожидания опроса; вдобавок каждые 10 секунд опрашивается `/session-guard/state` как страховка (сходимость сохраняется и при недоступном/разорванном SSE);
|
|
197
|
+
- стиль выровнен с кнопками input-traffic в той же строке (высота 24 px / радиус 6 px / шрифт 12 px / те же CSS-токены), с визуальным откликом при наведении и в состоянии паузы.
|
|
198
|
+
|
|
199
|
+
### Блокировка сессии (заморозка)
|
|
200
|
+
|
|
201
|
+
- **Избыточный порт**: `ctx.provide('sessionGuard', service)` — `stopNextTurn(sessionId)` / `resume(sessionId)` / `lockQueue(sessionId)` / `unlockQueue(sessionId)` / `state(sessionId)`;
|
|
202
|
+
- **RPC-мост**: `POST /session-guard/rpc { action, sessionId }` — кнопка заморозки input-traffic вызывает `stopNextTurn` / `resume` **по каждой сессии** согласно `sessionId`; молча пропускается, если этот плагин не установлен (fail-open D8);
|
|
203
|
+
- **Ручные команды**: `/pause [force|safe] [stop|wait]`, `/resume [confirm] [rerun|skip]`, `/cancel` — действуют на сессию, из которой вызваны (берётся `invocation.agent.id`).
|
|
204
|
+
|
|
205
|
+
### Автоматический повтор на стороне бэкенда (D9)
|
|
206
|
+
|
|
207
|
+
Слушает `turn/end` и классифицирует сбои:
|
|
208
|
+
|
|
209
|
+
- **Переходящие сбои** (error/429/max-tokens и т. п.) → автоматическое возобновление `followup(retryText)` с адаптивной задержкой;
|
|
210
|
+
- **Постоянные сбои** (аутентификация/баланс/модель/переполнение контекста) → остановка;
|
|
211
|
+
- **Уступает дорогу при заморозке/воротах**: повтор не выполняется, если `isFrozen(sessionId)` истинно (queueLocked / paused / taskControl paused);
|
|
212
|
+
- Вмешательство пользователя или успешный раунд сбрасывают счётчик последовательных сбоев.
|
|
213
|
+
|
|
214
|
+
### Бейдж состояния (отображение на фронтенде)
|
|
215
|
+
|
|
216
|
+
Справа в области ввода показывается **чисто информационный** бейдж состояния, отражающий текущую фазу в реальном времени:
|
|
217
|
+
|
|
218
|
+
| Фаза | Текст бейджа | CSS-класс | Значение |
|
|
219
|
+
|---|---|---|---|
|
|
220
|
+
| `peak` (двухмерное определение включено) | 高峰·拦官方 | `sg-peak` | Пиковые часы, блокируются только запросы к официальным источникам DeepSeek |
|
|
221
|
+
| `peak` (двухмерное определение выключено) | 高峰·全部暂停 | `sg-peak` | Пиковые часы, все сессии на паузе |
|
|
222
|
+
| `off-peak` | 谷时 | `sg-off` | Внепиковые часы, сессии работают нормально |
|
|
223
|
+
| `weekend` | 周末 | `sg-weekend` | Выходные (при включённом режиме выходных), пиковая зона игнорируется |
|
|
224
|
+
|
|
225
|
+
- **Опрос**: каждые 15 секунд запрашивается `GET /session-guard/status`, чтобы получить глобальные `phase`, `providerGuard`, `held`, `deferred`, `stepHeld`;
|
|
226
|
+
- **fail-open**: маршрут недоступен, сетевая ошибка или `enabled` выключен → бейдж тихо скрывается, ни одна сессия не затрагивается;
|
|
227
|
+
- **Независимость от input-traffic**: бейдж рендерится только клиентом session-guard и отображается **без установки плагина input-traffic**. input-traffic отвечает лишь за кнопку заморозки, зависимости с бейджем нет;
|
|
228
|
+
- **Tooltip**: при наведении показывает `фаза · часовой пояс · режим выходных · база определения · числа удержаний/отложений/удержаний шага`.
|
|
229
|
+
|
|
230
|
+
### База определения официальных источников (providerGuard)
|
|
231
|
+
|
|
232
|
+
В пик сессии не останавливаются без разбора: сначала выясняется, «является ли маршрут, куда этот запрос реально пойдёт, официальным источником DeepSeek»:
|
|
233
|
+
|
|
234
|
+
| Приоритет | Основание | `matchedBy` | Пример |
|
|
235
|
+
|---|---|---|---|
|
|
236
|
+
| 1 | явный список id `officialProviders` | `explicit` | пользователь объявил собственный шлюз официальным |
|
|
237
|
+
| 2 | хост нормализованной в реальном времени `baseURL` | `endpoint` | `deepseek-official` переключён на релей → **не блокируется** |
|
|
238
|
+
| 3 | встроенный в каталог эндпоинт по умолчанию | `endpoint-default` | маршрут `deepseek` у pi-ai по умолчанию бьёт в официальный API → **блокируется** |
|
|
239
|
+
| 4 | встроенный список id (`deepseek-official`) | `route-id` | откат, когда эндпоинт прочитать нельзя |
|
|
240
|
+
| 5 | всё остальное | `unknown` | неофициальный, пропускается |
|
|
241
|
+
|
|
242
|
+
- **Эндпоинт важнее id**: конфигурация с именем `deepseek-official`, но с `baseURL` на релей **не** блокируется по ошибке; наоборот, встроенный у pi-ai маршрут `deepseek` с эндпоинтом по умолчанию в официальный API **не** ускользает от блокировки.
|
|
243
|
+
- **Источник эндпоинтов**: `ctx.get('llm').listConfigurableProviders()` находит запись каталога → `ctx.settings.get(settingsNs)` читает `baseURL` по `settingsPath` (читаются только несекретные поля, значение `apiKeyEnv` никогда не читается). Вычисляется заново на каждый запрос, без кэша → горячие правки конфигурации провайдера вступают в силу немедленно.
|
|
244
|
+
- **Переключение на неофициальный источник возвращает сессию**: если после пиковой паузы сессию переключить на локального/стороннего провайдера (событие `model/selection` с 0.1.2+) → эта сессия возобновляется автоматически (под условием `deferredResume`); затрагиваются только сессии, приостановленные этим плагином при входе в пик, сессии, приостановленные вручную через `/pause`, **никогда** не трогаются. Без этого события на 0.1.1 → деградация до «следующий запрос или ручной `/resume`».
|
|
245
|
+
- **Эндпоинт недоступен для чтения**: сервис `llm` отсутствует, структура пространства имён изменилась, поле не строка — всегда деградация к определению по id / встроенному эндпоинту с записью `matchedBy`, **исключения никогда не выбрасываются**.
|
|
246
|
+
- **Разбор ошибочного определения**: `GET /session-guard/provider?provider=<id>` возвращает `{ official, matchedBy, endpoint }`.
|
|
247
|
+
|
|
248
|
+
### Страж уровня запроса и отложенная очередь
|
|
249
|
+
|
|
250
|
+
- **Зачем уровень запроса**: тик в 30 с обрабатывает при скачке `NORMAL → PAUSED_PEAK` только те сессии, что были `running` в тот момент; сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, выпали бы. Waterfall `agent/request` — страховка, которая проходит **с каждым запросом**.
|
|
251
|
+
- **Определение опирается на возвращаемое значение `next()`**: middleware выбора модели внутри waterfall перезаписывает provider/model выбранными в интерфейсе значениями, поэтому сначала нужно `await next()` и только потом выносить вердикт.
|
|
252
|
+
- **Режим hold (по умолчанию)**: запрос удерживается, **не отправляясь и не вызывая ошибки**, и выпускается в точный момент выхода из пика (`msUntilOffPeak` — точный таймер, тик в 30 с как страховка); отмена пользователем (abort) прерывает штатно.
|
|
253
|
+
- **Режим error**: выбрасывается узнаваемая ошибка `PEAK_DEFERRED` + запись в отложенную очередь, после пика возобновление с `deferredResumeText` (без автоматического продолжения, если `deferredResume` выключен).
|
|
254
|
+
- **Защита потолком**: если `deferredMaxHoldMs` (по умолчанию 6 ч) истёк, а пик не кончился → перевод в error, чтобы не зависать бесконечно.
|
|
255
|
+
- **Железное правило взаимного исключения**: во время удержания **никогда** не запрашивается дополнительно пауза через сессионные ворота (пауза ждёт безопасную границу, до которой удержанный запрос никогда не дойдёт → они ожидали бы друг друга). При входе в пик уже удержанные сессии пропускаются.
|
|
256
|
+
- **Без персистентности**: отложенная очередь — promise внутри процесса, при перезапуске исчезает.
|
|
257
|
+
|
|
258
|
+
### Границы (явно вне рамок)
|
|
259
|
+
|
|
260
|
+
- **Никакой смены провайдера / перенаправления**: только блокировка, без маршрутизации;
|
|
261
|
+
- **Компактация не проходит через `agent/request`**: пока сессия на паузе, она происходить не может; вызванная вручную компрессия во время пика всё ещё может попасть в официальный источник (этот плагин не перехватывает уровень `ctx.llm.stream`);
|
|
262
|
+
- **На 0.1.1 нет события `model/selection`**: автоматическое возобновление после переключения на неофициальный источник деградирует до «ждать следующий запрос или ручной `/resume`» (с 0.1.2 — немедленно);
|
|
263
|
+
- **Никаких новых npm-зависимостей**, никаких чтения/записи учётных данных, повторы 429 / транспортного уровня у `dsh-llm-retry` не трогаются.
|
|
264
|
+
|
|
265
|
+
### Работа с часовыми поясами и валидация
|
|
266
|
+
|
|
267
|
+
- Определение часового пояса основано на **именах поясов IANA** (например `Asia/Shanghai`, `Asia/Tokyo`, `Asia/Seoul`), через `Intl.DateTimeFormat` проецируемых на настенные часы настроенного пояса, **без опоры на голый `getUTCDay()`** — это позволяет избежать классической ошибки в 8 часов на границе UTC+8 пекинского времени (суббота 00:30 по Пекину — в UTC ещё пятница);
|
|
268
|
+
- `Intl.DateTimeFormat` сам служит слоем валидации: недопустимое имя пояса (например `Foo/Bar`) выбрасывает `RangeError`, которую внешний try-catch тихо понижает до пояса по умолчанию `Asia/Shanghai` (fail-open);
|
|
269
|
+
- Окна пика **левая граница включена, правая исключена** `[start, end)`, поддерживаются окна через полночь (например `22:00–06:00`);
|
|
270
|
+
- Настройка `timezone` работает одинаково для всех языков интерфейса (zh/en/ja/ko) — имена поясов IANA в `Intl.DateTimeFormat` не зависят от локали: при японском/корейском интерфейсе поведение часового пояса полностью совпадает с китайским.
|
|
271
|
+
|
|
272
|
+
### Разделение труда с input-traffic: один «останавливает», другой «выстраивает очередь»
|
|
273
|
+
|
|
274
|
+
Оба действуют на **разные звенья одной цепи**, границу задаёт сама модель inbox в DSH:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Ввод пользователя ──(input-traffic выбирает ярус)──▶ очереди ожидания next-step / next-turn
|
|
278
|
+
│
|
|
279
|
+
agent/pre-step ──(ворота шага этого плагина)──▶ пропустить / удержать
|
|
280
|
+
│
|
|
281
|
+
agent/request ──(удержание уровня запроса этого плагина)──▶ пропустить / удержать
|
|
282
|
+
│
|
|
283
|
+
вызов модели
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Семантика очередей DSH (две очереди, не путать)**
|
|
287
|
+
|
|
288
|
+
| Очередь | Смысл | Когда потребляется |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `next-step` | «Ввод, ожидающий следующей границы шага» | На следующем `agent/pre-step`: **на одном уровне с результатом инструмента**, ещё один шаг внутри того же раунда |
|
|
291
|
+
| `next-turn` | «Промпт, ожидающий отдельного раунда» | По завершении текущего раунда, запускается как **новый раунд** |
|
|
292
|
+
|
|
293
|
+
`Inbox.claim()` **всегда сначала выгребает `next-step`** и лишь дополнительно берёт **1** элемент `next-turn`, если на этой границе открывается новый раунд; первый шаг раунда читает next-turn, все последующие читают next-step.
|
|
294
|
+
|
|
295
|
+
**Разделение обязанностей**
|
|
296
|
+
|
|
297
|
+
- **session-guard = останавливать**: решает только, «когда можно продолжать», и **никогда не трогает содержимое и порядок очередей**.
|
|
298
|
+
- ворота шага (`agent/pre-step`): удерживают **до** запроса к модели следующего шага;
|
|
299
|
+
- пауза на уровне раунда (`agent.cancel({keepInbox:true})` + `goals.pause` + безопасная граница): останавливает текущий раунд, **очереди остаются как есть**;
|
|
300
|
+
- страж уровня запроса (удержание на `agent/request`): удерживает **именно этот один запрос к модели**.
|
|
301
|
+
- **input-traffic = выстраивать очередь**: решает только, «в какую очередь попадает ввод пользователя, каким ярусом и когда будет потреблён».
|
|
302
|
+
- три яруса = в какую очередь положить: красный «прервать» делает сначала `cancel()`, затем `steer`; жёлтый «вставить реплику» делает `steer` (→ `next-step`, следующий шаг того же раунда); зелёный «в очередь» остаётся в `next-turn`;
|
|
303
|
+
- заморозка = извлечь строки `queued` + `steering` (ярус сохраняется) + блокировка composer + вызов `sessionGuard.stopNextTurn`; возобновление = снять блокировку → сначала `sessionGuard.resume` → повторная подача по ярусу.
|
|
304
|
+
|
|
305
|
+
**Два железных правила в точке встречи**
|
|
306
|
+
|
|
307
|
+
1. **Заморозка обязана позволить плагину сначала открыть ворота шага**: ворота шага подвешены на `agent/pre-step`, а пауза на уровне раунда ждёт события на безопасной границе — они ожидали бы друг друга (`pauseTask` / `cancelTask` этого плагина сначала делают `release`);
|
|
308
|
+
2. **Пока ворота шага закрыты, сообщения уже забраны**: `preStep()` делает `inbox.claim()` до диспетчеризации waterfall, поэтому новый ввод встаёт в очередь за уже забранной партией; `keepInbox` действует только на паузу уровня раунда.
|
|
309
|
+
|
|
310
|
+
**Никаких взаимных вторжений**: input-traffic не слушает `agent/pre-step` / `agent/request` (единственное исключение — явный `cancel()` яруса «прервать», запрошенный самим пользователем); этот плагин никогда не переписывает содержимое и порядок `next-step` / `next-turn`.
|
|
311
|
+
|
|
312
|
+
Кнопки: кнопки «Пауза сессии / Продолжить сессию» этого плагина (order 20) и кнопки input-traffic «❄ Заморозка и добавить / Возобновить и добавить» (order 30) стоят рядом и не заменяют друг друга — первые управляют воротами шага, вторые извлечением из очереди + заморозкой на уровне раунда.
|
|
313
|
+
|
|
314
|
+
## Избыточный порт `sessionGuard`
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
{
|
|
318
|
+
stopNextTurn(sessionId, opts), // останавливает следующий раунд сессии (собственные сессионные ворота / откат к очереди с блокировкой)
|
|
319
|
+
resume(sessionId, opts), // возобновляет (confirm + choice: rerun|skip)
|
|
320
|
+
lockQueue(sessionId, reason), // явно блокирует очередь
|
|
321
|
+
unlockQueue(sessionId), // явно разблокирует
|
|
322
|
+
stepPause(sessionId), // вручную запрашивает паузу уровня шага (закрытие на следующей границе pre-step, шаг 1 тоже перехватывается)
|
|
323
|
+
stepResume(sessionId, opts), // открывает ворота шага (v0.2.0); при opts.bypass=false без обхода на этот пик
|
|
324
|
+
state(sessionId), // { queueLocked, lockReason, paused, pausedStep, stepHeldSince, stepBypass, taskControlAvailable, taskControl }
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## HTTP-маршруты
|
|
329
|
+
|
|
330
|
+
- `GET /session-guard/state?session=<id>` — состояние сессии (включая `paused: { step, turn, manual }` / `stepGate` / последнюю цель / удержание / отложение)
|
|
331
|
+
- `GET /session-guard/events?session=<id>` — **SSE**: мгновенная доставка изменений состояния ворот шага (кнопка обновляется по ним)
|
|
332
|
+
- `GET /session-guard/settings` — настройки + доступность taskControl
|
|
333
|
+
- `GET /session-guard/status` — текущая глобальная фаза (опрос бейджа состояния; включает `stepHeld`)
|
|
334
|
+
- `GET /session-guard/provider?provider=<id>` — диагностика определения официального источника (`official` / `matchedBy` / `endpoint`)
|
|
335
|
+
- `GET /session-guard/diag` — диагностика времени выполнения (включая `stepGate`)
|
|
336
|
+
- `POST /session-guard/rpc` — `{ action: stopNextTurn|resume|lockQueue|unlockQueue|stepPause|stepResume|state, sessionId }`
|
|
337
|
+
|
|
338
|
+
## Хранение состояния
|
|
339
|
+
|
|
340
|
+
JSON на сессию: `$DSH_HOME/.dsh/session-guard/<sessionId>.json` (атомарная запись; можно переопределить через `DSH_SESSION_GUARD_STATE_DIR`).
|
|
341
|
+
|
|
342
|
+
## Тесты
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm test # node --test tests/*.test.mjs (часовые пояса/выходные/машина состояний/сессионные ворота/мост/повтор)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Модули
|
|
349
|
+
|
|
350
|
+
| Файл | Ответственность |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `src/time.js` | Определение пика/выходных (с правильным поясом) + `msUntilOffPeak` (точный таймер выхода из пика) |
|
|
353
|
+
| `src/scheduler.js` | Чистая машина состояний NORMAL ↔ PAUSED_PEAK |
|
|
354
|
+
| `src/provider.js` | Пятиуровневое определение официальных источников (чистая функция: нормализация эндпоинта + матрица решений) |
|
|
355
|
+
| `src/provider-directory.js` | Каталог эндпоинтов (`llm.listConfigurableProviders` + `settings.get`, деградация по всей цепочке) |
|
|
356
|
+
| `src/deferrals.js` | Реестр отложений (удержание / освобождение / превышение предела / `PeakDeferredError`) |
|
|
357
|
+
| `src/request-guard.js` | Страж уровня запроса на `agent/request` (два режима hold / error) |
|
|
358
|
+
| `src/step-gate.js` | **Ворота уровня шага на `agent/pre-step`** (v0.2.0: закрытие / открытие / повышение по таймауту / bypass, чистый `decideStepHold` покрыт юнит-тестами) |
|
|
359
|
+
| `src/targets.js` | Отслеживание «последней реальной цели» сессии (`request/header` + `model/selection`) |
|
|
360
|
+
| `src/wiring.js` | Оркестрация подключения (фильтр входа в пик / подключение ворот шага / освобождение после пика / точный таймер / очистка при выгрузке) |
|
|
361
|
+
| `src/pause-gate.js` | Движок собственных сессионных ворот (agent.cancel keepInbox + goals.pause + безопасная граница + возобновление followup; перед паузой сначала открывает ворота шага) |
|
|
362
|
+
| `src/pause-store.js` | Персистентность собственного состояния паузы |
|
|
363
|
+
| `src/gate.js` | Драйвер сессионных ворот (собственная настоящая пауза / откат к очереди с блокировкой, fail-open) |
|
|
364
|
+
| `src/bridge.js` | Избыточный порт `sessionGuard` |
|
|
365
|
+
| `src/retry.js` | Автоповтор на бэкенде (классификация сбоев/задержка/уступание при заморозке; короткое замыкание только по точному коду `PEAK_DEFERRED`) |
|
|
366
|
+
| `src/detect.js` | Автоопределение (taskControl хоста / мост клиента input-traffic) |
|
|
367
|
+
| `src/store.js` | Персистентное состояние на сессию |
|
|
368
|
+
| `src/settings.js` | Подраздел настроек (схема schemastery + fail-open регистрация) |
|
|
369
|
+
| `src/index.js` | apply на стороне хоста (настройки/маршруты/тик/предоставление сервиса/подключение повтора/страж запросов) |
|
|
370
|
+
| `src/client/` | Браузерная половина (**кнопка паузы сессии** + бейдж состояния + карточка настроек) |
|
|
371
|
+
|
|
372
|
+
## Лицензия
|
|
373
|
+
|
|
374
|
+
MIT — см. [LICENSE](LICENSE).
|