@goodandready/dsh-model-sync 0.3.14 → 0.4.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/README.md +2 -141
- package/README.ru.md +8 -125
- package/README.zh.md +8 -10
- package/lib/client.js +121 -13
- package/lib/http.js +30 -4
- package/lib/index.js +9 -1
- package/lib/models.js +84 -2
- package/lib/reconcile.js +14 -2
- package/lib/synchronizer-helpers.js +113 -0
- package/lib/synchronizer-prober.js +50 -0
- package/lib/synchronizer-transfer.js +118 -0
- package/lib/synchronizer.js +31 -264
- package/lib/updater.js +362 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ graph LR
|
|
|
75
75
|
## ✨ Key Features
|
|
76
76
|
|
|
77
77
|
* 🔄 **Automated Catalog Discovery**: Syncs model lists, aliases, context windows, and capability flags (`vision`, `tools`, `reasoning`, `embeddings`) across 25+ providers.
|
|
78
|
+
* 🛡️ **Minijinja Chat Template & Role Compatibility**: Automatically sets `compat.supportsDeveloperRole: false` for Groq open-source models (Qwen, Llama, Mistral, Gemma, DeepSeek) and DeepSeek API, preventing 400 `Unexpected message role` errors with reasoning models while preserving user overrides. Syncs model lists, aliases, context windows, and capability flags (`vision`, `tools`, `reasoning`, `embeddings`) across 25+ providers.
|
|
78
79
|
* 🌐 **25+ Built-in Provider Adapters**: Pre-configured discovery for OpenAI, Anthropic, Google, DeepSeek, xAI, OpenRouter, Groq, Mistral, Cerebras, Fireworks, HuggingFace, Moonshot, NVIDIA, Qwen, Together, Xiaomi MiMo, SiliconFlow, and local Ollama.
|
|
79
80
|
* 🔌 **Generic / Custom Adapter Support**: Connect arbitrary OpenAI-compatible `/v1/models` endpoints with configurable auth (`bearer`, `x-api-key`, `query-key`, `none`).
|
|
80
81
|
* 📊 **Balance & Quota Monitoring**: Queries upstream billing endpoints (where supported) to report account credit balances and prevent unexpected exhaustion.
|
|
@@ -96,7 +97,7 @@ graph LR
|
|
|
96
97
|
| `deepseek` | Bearer Token | `tools`, `reasoning` |
|
|
97
98
|
| `xai` | Bearer Token | `vision`, `tools`, `reasoning` |
|
|
98
99
|
| `openrouter` | Bearer Token | Multi-vendor model catalog with pricing & context limits |
|
|
99
|
-
| `groq` | Bearer Token |
|
|
100
|
+
| `groq` | Bearer Token | `tools`, `reasoning`, `vision`, `supportsDeveloperRole: false` |
|
|
100
101
|
| `mistral` | Bearer Token | `tools`, `reasoning`, `vision` |
|
|
101
102
|
| `fireworks` | Bearer Token | Open-weights inference endpoints |
|
|
102
103
|
| `huggingface` | Bearer Token | Serverless Inference API catalog |
|
|
@@ -116,143 +117,3 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
116
117
|
> Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) to activate background catalog synchronization.
|
|
117
118
|
|
|
118
119
|
---
|
|
119
|
-
|
|
120
|
-
## 🚀 Enhancements in v0.3.14
|
|
121
|
-
|
|
122
|
-
- **Language Standard**: Canonical plugin in `en` and `zh`. Runtime Russian translations decoupled into `goodandready/dsh-russian-lang` (#191).
|
|
123
|
-
- **Packaging Isolation**: Dedicated `.npmignore` excludes tests, plans, and documentation to maintain a minimal < 256 KiB npm release bundle.
|
|
124
|
-
- **Batch Health Check**: Probe multiple selected models in parallel via `POST /dsh-model-sync/batch-try` with latency badges and one-click removal of unreachable models.
|
|
125
|
-
- **Cost & Context Policies**: Set maximum price per million tokens and minimum context window thresholds in provider policies.
|
|
126
|
-
- **Catalog Export & Import**: Backup and restore full plugin configuration (policies, selections, aliases, scheduler) via `GET /export` and `POST /import`.
|
|
127
|
-
- **Model Alias Mapping**: Assign friendly aliases to provider/model pairs with `GET /aliases` and `POST /aliases` with interactive UI card.
|
|
128
|
-
|
|
129
|
-
## 🚀 Enhancements in v0.3.13
|
|
130
|
-
|
|
131
|
-
* ⚡ **Zero-Overhead Polling via HTTP ETag / 304 Not Modified**:
|
|
132
|
-
* Implemented weak ETag generation for `GET /dsh-model-sync/status`. When the Web UI polls every 15 seconds, unchanged catalogs receive an empty `304 Not Modified` response, eliminating redundant JSON serialization and network traffic.
|
|
133
|
-
* 🌐 **Upstream Conditional HTTP Requests**:
|
|
134
|
-
* Added `If-None-Match` and `If-Modified-Since` headers to upstream provider discovery. Catalogs that return `304 Not Modified` resolve instantly from memory cache without re-parsing or diff re-computation.
|
|
135
|
-
* 🎯 **Debounced Search & Memoized Sorting in UI**:
|
|
136
|
-
* Introduced 150ms input debouncing and `React.useMemo` for model filtering/sorting in the model picker, ensuring fluid search responsiveness on catalogs with hundreds of models.
|
|
137
|
-
* 🧠 **Expanded Capability Detection**:
|
|
138
|
-
* Added detection for modern reasoning models (`DeepSeek-R1`, `o1`, `o3-mini`, `thinking`) and specialized code generation models (`code`), plus support for the `code` capability filter in policy matching and UI picker.
|
|
139
|
-
* 🛡️ **Exponential Retry Jitter**:
|
|
140
|
-
* Enhanced `retryWithBackoff` with randomized jitter (`0.5 - 1.0` multiplier) to prevent thundering-herd effects on provider APIs during network hiccups or rate limits.
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 🛠️ Enhancements & Fixes in v0.3.11
|
|
145
|
-
|
|
146
|
-
* 🗂️ **Streamlined Settings UI Surface**:
|
|
147
|
-
* Removed the redundant top-level settings sidebar section fallback (`settings.section`), keeping configuration strictly inside the standard DSH plugin settings card (`settings.plugin.item`).
|
|
148
|
-
* 🛡️ **Hardened Cordis Service Access**:
|
|
149
|
-
* Service access for `settings` and `llm` is now performed via `ctx.get(...)` with safe fallback to direct property inspection, preventing silent resolution failures on Cordis proxy implementations.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## 🛠️ Enhancements & Fixes in v0.3.8
|
|
154
|
-
|
|
155
|
-
* ⚙️ **Reactive Settings Card Binding (`settingsScope`)**:
|
|
156
|
-
* Integrated modern DSH kernel `settingsScope` service (`ctx.settingsScope.bind({ namespace: 'dsh-model-sync' })`) with `useSyncExternalStore`.
|
|
157
|
-
* Added direct configuration controls for the background scheduler in the Web UI: enable/disable background sync (`scheduleEnabled`), customize interval (`intervalMinutes`), and toggle automatic application of new models (`autoApply`).
|
|
158
|
-
* Gracefully handles all snapshot lifecycle states (`ready`, `loading`, `unavailable`) and collects field validation errors without dropping form drafts.
|
|
159
|
-
* 🌐 **Full Model Picker Localization**:
|
|
160
|
-
* Translated all previously hardcoded strings in the model selector, including search input placeholder and sorting options (`Name A→Z`, `Price ↑`, `Context ↓`, `Newest ↓`), dynamically reacting to DSH locale switching (`en` / `ru`).
|
|
161
|
-
* 📜 **Synchronization History Localization**:
|
|
162
|
-
* Added the missing `noHistory` key to English and Russian localization dictionaries, ensuring informative feedback when sync history is empty.
|
|
163
|
-
* 📋 **Project Design Contract**:
|
|
164
|
-
* Added authoritative `docs/design/DESIGN.md` defining UI surfaces, styling foundations, token conventions, and state contracts.
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## 🛠️ Enhancements & Fixes in v0.3.6
|
|
169
|
-
|
|
170
|
-
* 🔑 **Universal Credential Resolution (`credentialRef` & `apiKeyRef`)**:
|
|
171
|
-
* Added full support for modern DSH `credentialRef` and `apiKeyRef` references across inventory detection, provider adapters, and OpenAI-compatible generic routes.
|
|
172
|
-
* Providers configured via DSH Credentials Service are now accurately discovered and authenticated without requiring legacy `apiKeyEnv`.
|
|
173
|
-
* ♻️ **Lifecycle Cleanup for Deprecated Models**:
|
|
174
|
-
* Fixed an issue where models previously marked as `deprecated` would remain indefinitely in settings when removed by the upstream provider. When `removeMissing: true` is enabled, missing deprecated models now cleanly transition to `removed` status once the grace period expires.
|
|
175
|
-
* ⏰ **Accurate Scheduler Inactive Status**:
|
|
176
|
-
* Corrected `scheduler.status()` to return `nextRunAt: null` when background synchronization is disabled, avoiding misleading run times in the Web UI.
|
|
177
|
-
* 🛡️ **Web UI Resilience & Big Catalog Scalability**:
|
|
178
|
-
* Eliminated potential UI crashes during model sorting by safely handling missing model display names.
|
|
179
|
-
* Replaced spread operations on model arrays with iterative aggregations, preventing call stack overflow (`RangeError`) on massive catalogs (10,000+ models).
|
|
180
|
-
* Surfaced detailed probe error messages in the UI when testing individual models via the `▶` button.
|
|
181
|
-
* Localized cache expiration and offline status strings in Russian and English.
|
|
182
|
-
* ⚡ **Deterministic Model Diffing & HTTP Stream Safety**:
|
|
183
|
-
* Implemented stable key-sorted serialization in `diffModels` and `sameModel` to eliminate false-positive diffs caused by non-deterministic JSON key order.
|
|
184
|
-
* Added early stream destruction on oversized HTTP request bodies to prevent memory bloat.
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## 🚀 Enhancements in v0.3.10
|
|
189
|
-
|
|
190
|
-
* 📦 **Compacted History Storage**:
|
|
191
|
-
* Stripped non-essential metadata (`description`, `tags`, `aliases`) from catalog snapshots (`before`/`after`) in `settings.yaml`, retaining only essential properties (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
192
|
-
* Optimized default snapshot retention (`DEFAULT_SNAPSHOT_RETENTION = 3`) to keep configuration files lean while preserving full rollback functionality.
|
|
193
|
-
* ⚡ **Immediate Scheduler First Tick**:
|
|
194
|
-
* Scheduler now triggers an initial discovery run immediately upon startup when enabled without requiring an initial 60-minute wait.
|
|
195
|
-
* 🛡️ **Client-side Policy Regex Validation**:
|
|
196
|
-
* Added instant regex pattern verification in the Web UI before submitting `/policy` requests with localized error feedback.
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## 🚀 Enhancements in v0.3.9
|
|
201
|
-
|
|
202
|
-
* 🔒 **Serialized Settings Mutations (`saveConfig`)**:
|
|
203
|
-
* Chained internal settings persistence via an async promise mutex (`saveQueue`).
|
|
204
|
-
* Eliminates race conditions between simultaneous sync runs and user UI interactions, ensuring clean atomic writes to `settings.yaml`.
|
|
205
|
-
* 🛡️ **Hardened Cross-Origin & CSRF Protections**:
|
|
206
|
-
* Enhanced `trusted(req)` checking to evaluate `sec-fetch-site`, `origin`, and `referer` against the HTTP `Host`.
|
|
207
|
-
* Blocks unauthorized cross-origin mutations while preserving legitimate local and reverse-proxy requests.
|
|
208
|
-
* 🎨 **Standardized Plugin Style Discovery**:
|
|
209
|
-
* Added `data-dsh-plugin="dsh-model-sync"` attribute to injected UI style tags in compliance with the DeepSeek Harness plugin authoring standard.
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## 🚀 Enhancements in v0.3.5
|
|
214
|
-
|
|
215
|
-
* ⏱️ **Adaptive Rate Limit & Retry-After Handling**:
|
|
216
|
-
* Automatically parses HTTP headers `Retry-After` (in seconds or RFC dates) and `x-ratelimit-reset`.
|
|
217
|
-
* Exponential backoff prioritizes the exact delay requested by upstream providers, eliminating unnecessary throttling.
|
|
218
|
-
* Re-classified HTTP 429 errors under a dedicated `rate-limit` diagnostic label.
|
|
219
|
-
* 🔄 **Optimistic Concurrency & Conflict Auto-Recovery**:
|
|
220
|
-
* Added automated retry loop for `SETTINGS_CONFLICT` (HTTP 409). When concurrent settings updates occur in DSH, the plugin seamlessly re-fetches the latest revision and reapplies the catalog mutations.
|
|
221
|
-
* 🎯 **Advanced Web UI Picker & Filtering**:
|
|
222
|
-
* **Tokenized Smart Search**: Multi-word queries (`claude 3.5 sonnet`) match across model IDs, display names, tags, and descriptions simultaneously.
|
|
223
|
-
* **Quick Filter Chips**: One-click toggles for `Context ≥ 100k` and `Cheap ≤ $1/1M` in addition to capability toggles (`Vision`, `Tools`, `Reasoning`, `Embeddings`).
|
|
224
|
-
* **Batch Selection Controls**: One-click buttons to *Select all shown*, *Deselect all shown*, and *Invert selection*.
|
|
225
|
-
* **Chunked Rendering / Pagination**: Renders models in batches of 50 to maintain smooth 60fps UI performance even with catalogs containing 1,000+ models.
|
|
226
|
-
* **Deprecation Badges**: Models with scheduled retirement dates display an explicit countdown badge (`Deprecates in Nd`).
|
|
227
|
-
* 💾 **Storage & Metadata Optimizations**:
|
|
228
|
-
* Robust context limit parser supporting suffix multipliers (`128k`, `1M`, `128,000`).
|
|
229
|
-
* Automatic history compaction: historical snapshots older than the 5 latest sync runs have heavy before/after dumps pruned while retaining summary diffs, keeping `settings.yaml` ultra-compact.
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
## ⚙️ Configuration Reference (`settings.yaml`)
|
|
234
|
-
|
|
235
|
-
```yaml
|
|
236
|
-
dsh-model-sync:
|
|
237
|
-
enabled: true
|
|
238
|
-
syncIntervalMinutes: 60
|
|
239
|
-
autoReconcile: true
|
|
240
|
-
providers:
|
|
241
|
-
openrouter:
|
|
242
|
-
enabled: true
|
|
243
|
-
keyEnv: OPENROUTER_API_KEY
|
|
244
|
-
deepseek:
|
|
245
|
-
enabled: true
|
|
246
|
-
keyEnv: DEEPSEEK_API_KEY
|
|
247
|
-
custom:
|
|
248
|
-
enabled: false
|
|
249
|
-
baseURL: http://127.0.0.1:8000/v1
|
|
250
|
-
authType: bearer
|
|
251
|
-
keyEnv: CUSTOM_API_KEY
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## 📄 License
|
|
257
|
-
|
|
258
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.ru.md
CHANGED
|
@@ -94,6 +94,14 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
|
+
## 🚀 Улучшения в версии 0.4.0
|
|
98
|
+
|
|
99
|
+
- **One-Click обновление плагина из настроек**: В карточку настроек плагина добавлен блок обновления с проверкой актуальной версии в npm, цветовым бейджем статуса и кнопкой обновления в один клик.
|
|
100
|
+
- **Защита write-эндпоинтов по Loopback**: Все мутирующие HTTP-маршруты (`/apply`, `/policy`, `/clear-cache`, `/updater/update`) строго проверяют адрес источника (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) и заголовок Origin/Host для предотвращения несанкционированного доступа.
|
|
101
|
+
- **Полная поддержка темы DSH (0 rgba / 0 hex)**: Все стили интерфейса переведены на официальные токены дизайн-системы DSH (`--dsw-alias-*`), обеспечивая нативную интеграцию со светлой и тёмной темами оформления.
|
|
102
|
+
- **Модульная архитектура синхронизатора**: Монолитная логика синхронизации разделена на специализированные модули (`synchronizer-helpers.js`, `synchronizer-transfer.js`, `synchronizer-prober.js`), что упрощает поддержку и снижает размер основного файла ниже 600 строк.
|
|
103
|
+
- **Чистый дистрибутив пакета**: Исключены внутренние файлы планирования и отладки; размер сжатого npm-пакета составляет менее 70 KiB, все файлы укладываются в лимит 256 KiB.
|
|
104
|
+
|
|
97
105
|
## 🚀 Улучшения в v0.3.13
|
|
98
106
|
|
|
99
107
|
* ⚡ **ETag / 304 Not Modified для фонового опроса UI**:
|
|
@@ -108,128 +116,3 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
108
116
|
* Защита от thundering herd с помощью случайного коэффициента джиттера (`0.5 - 1.0`) при повторных запросах к API провайдеров.
|
|
109
117
|
|
|
110
118
|
---
|
|
111
|
-
|
|
112
|
-
## 🛠️ Улучшения и исправления в v0.3.11
|
|
113
|
-
|
|
114
|
-
* 🗂️ **Чистый интерфейс настроек (DSH Design Contract)**:
|
|
115
|
-
* Удалена избыточная регистрация fallback-раздела верхнего уровня бокового меню (`settings.section`), настройки доступны только через стандартную карточку в «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`).
|
|
116
|
-
* 🛡️ **Надёжный доступ к сервисам Cordis**:
|
|
117
|
-
* Доступ к сервисам ядра (`settings`, `llm`) переведён на `ctx.get(...)` с безопасным fallback к прямым свойствам контекста для исключения ошибок проксирования.
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## 🛠️ Улучшения и исправления в v0.3.8
|
|
122
|
-
|
|
123
|
-
* ⚙️ **Реактивное управление настройками через `settingsScope`**:
|
|
124
|
-
* Интегрирована встроенная служба ядра DSH `settingsScope` (`ctx.settingsScope.bind({ namespace: 'dsh-model-sync' })`) с использованием `useSyncExternalStore`.
|
|
125
|
-
* В карточку настроек плагина добавлены элементы прямого управления планировщиком: включение фонового расписания (`scheduleEnabled`), настройка интервала синхронизации (`intervalMinutes`) и флаг автоприменения изменений (`autoApply`).
|
|
126
|
-
* Корректная обработка всех статусов снимка (`ready`, `loading`, `unavailable`) с валидацией и сохранением полей без прерывания на первой ошибке.
|
|
127
|
-
* 🌐 **Полная локализация пикера моделей**:
|
|
128
|
-
* Заменены захардкоженные русские строки в селекторе моделей: плейсхолдер поиска и варианты сортировки (`Имя A→Z`, `Цена ↑`, `Контекст ↓`, `Новые ↓`) теперь динамически переключаются при смене языка интерфейса (`en` / `ru`).
|
|
129
|
-
* 📜 **Локализация пустой истории синхронизаций**:
|
|
130
|
-
* Добавлен отсутствовавший ключ `noHistory` в словари английского и русского языков («No synchronization history.» / «Истории синхронизаций нет.»).
|
|
131
|
-
* 📋 **Дизайн-контракт проекта**:
|
|
132
|
-
* Создан обязательный документ `docs/design/DESIGN.md`, фиксирующий пользовательские поверхности, токены `--dsw-*` и контракты компонентов.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## 🛠️ Улучшения и исправления в v0.3.6
|
|
137
|
-
|
|
138
|
-
* 🔑 **Универсальное разрешение учетных данных (`credentialRef` и `apiKeyRef`)**:
|
|
139
|
-
* Добавлена полная поддержка ссылок `credentialRef` и `apiKeyRef` в модулях инвентаря, реестре адаптеров и generic-маршрутах OpenAI-совместимых провайдеров.
|
|
140
|
-
* Провайдеры, настроенные через DSH Credentials Service, теперь корректно обнаруживаются и аутентифицируются без необходимости обязательного наличия `apiKeyEnv`.
|
|
141
|
-
* ♻️ **Очистка устаревших моделей в жизненном цикле**:
|
|
142
|
-
* Исправлена ошибка, из-за которой модели в статусе `deprecated` оставались в конфигурации бессрочно после их удаления провайдером. При флаге `removeMissing: true` такие модели теперь гарантированно переводятся в статус `removed` по истечении grace-периода.
|
|
143
|
-
* ⏰ **Точный статус неактивного планировщика**:
|
|
144
|
-
* Исправлен `scheduler.status()`: при отключённой фоновой синхронизации `nextRunAt` теперь возвращает `null`, исключая вводящие в заблуждение даты следующего запуска в Web UI.
|
|
145
|
-
* 🛡️ **Надежность и масштабируемость Web UI**:
|
|
146
|
-
* Защита от сбоев сортировки при отсутствии поля `name` у модели.
|
|
147
|
-
* Замена spread-операций на итеративную агрегацию при вычислении экстремумов контекста и цены, что предотвращает переполнение стека (`RangeError`) на огромных каталогах (10 000+ моделей).
|
|
148
|
-
* Детальный вывод текста ошибки при тестировании модели по кнопке `▶`.
|
|
149
|
-
* Локализация срока давности кеша в карточке провайдера.
|
|
150
|
-
* ⚡ **Детерминированное сравнение каталогов и безопасность потоков**:
|
|
151
|
-
* Реализована стабильная сортировка ключей перед сравнением моделей, исключающая фиктивные diff-записи из-за произвольного порядка ключей в JSON.
|
|
152
|
-
* Добавлено прерывание сетевого потока при превышении лимита размера тела HTTP-запроса для защиты от утечек памяти.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## 🚀 Новые возможности в v0.3.10
|
|
157
|
-
|
|
158
|
-
* 📦 **Компактизация хранения истории**:
|
|
159
|
-
* Очистка некритичных метаданных (`description`, `tags`, `aliases`) из снимков моделей (`before`/`after`) в `settings.yaml`, с сохранением ключевых свойств (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
160
|
-
* Оптимизирована глубина хранения снимков (`DEFAULT_SNAPSHOT_RETENTION = 3`) для предотвращения разрастания конфигурационного файла при сохранении возможности безопасного отката.
|
|
161
|
-
* ⚡ **Мгновенный первый запуск планировщика**:
|
|
162
|
-
* Планировщик автоматически запускает первичный синк сразу при старте, если расписание активно и предыдущие запуски отсутствуют.
|
|
163
|
-
* 🛡️ **Клиентская валидация regex-политик**:
|
|
164
|
-
* Добавлена проверка синтаксиса регулярных выражений в интерфейсе настроек перед отправкой запроса на `/policy` с локализованными подсказками.
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## 🚀 Новые возможности в v0.3.9
|
|
169
|
-
|
|
170
|
-
* 🔒 **Атомарная очередь сохранения настроек (`saveConfig`)**:
|
|
171
|
-
* Сериализация внутренних мутаций конфигурации через цепочку промисов (`saveQueue`).
|
|
172
|
-
* Предотвращает состояние гонки между фоновой синхронизацией и действиями пользователя в интерфейсе, гарантируя целостность `settings.yaml`.
|
|
173
|
-
* 🛡️ **Усиленная защита от CSRF и cross-origin атак**:
|
|
174
|
-
* Расширенная валидация запросов `trusted(req)` с проверкой заголовков `sec-fetch-site`, `origin` и `referer` против `Host`.
|
|
175
|
-
* Блокировка межсайтовых вызовов мутаций без нарушения локальных обращений и проксирования.
|
|
176
|
-
* 🎨 **Стандартизация инжекции стилей интерфейса**:
|
|
177
|
-
* Добавлен атрибут `data-dsh-plugin="dsh-model-sync"` для тега стилей в полном соответствии со стандартом разработки плагинов DeepSeek Harness.
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## 🚀 Новые возможности в v0.3.5
|
|
182
|
-
|
|
183
|
-
* ⏱️ **Адаптивная обработка Rate Limit и Retry-After**:
|
|
184
|
-
* Автоматический парсинг HTTP-заголовков `Retry-After` (в секундах и формате RFC-дат) и `x-ratelimit-reset`.
|
|
185
|
-
* При повторах задержка выставляется строго по требованию провайдера.
|
|
186
|
-
* Ошибки 429 классифицируются в отдельную категорию `rate-limit`.
|
|
187
|
-
* 🔄 **Автоматическое разрешение конфликтов настроек**:
|
|
188
|
-
* Добавлен цикл повторов для `SETTINGS_CONFLICT` (HTTP 409). При параллельных правках конфигурации DSH плагин автоматически перечитывает ревизию и накладывает изменения без ошибок.
|
|
189
|
-
* 🎯 **Оптимизация панели выбора моделей в Web UI**:
|
|
190
|
-
* **Токенизированный умный поиск**: Слова поискового запроса (`claude 3.5 sonnet`) сопоставляются независимо по ID, названиям, тегам и описаниям моделей.
|
|
191
|
-
* **Быстрые фильтр-чипы**: Фильтры `Контекст ≥ 100k` и `Дешевые ≤ $1/1M` в дополнение к фильтрам возможностей (`Зрение`, `Инструменты`, `Рассуждение`, `Эмбеддинги`).
|
|
192
|
-
* **Массовый выбор**: Кнопки *Выбрать все показанные*, *Снять выбор с показанных*, *Инвертировать выбор*.
|
|
193
|
-
* **Порционный рендеринг (пагинация)**: Отображение порциями по 50 моделей с кнопкой «Показать ещё» для плавной работы интерфейса в каталогах с 1000+ моделями.
|
|
194
|
-
* **Бейджи устаревания моделей**: Для моделей с датой вывода из эксплуатации выводится обратный отсчет (`Устареет через N дн.`).
|
|
195
|
-
* 💾 **Хранение и парсинг метаданных**:
|
|
196
|
-
* Корректный разбор числовых значений с запятыми и суффиксами (`128k`, `1M`, `128,000`).
|
|
197
|
-
* Автоматическая компактификация: снимки каталогов старше 5 последних запусков очищаются от тяжелых дампов, сохраняя diff-аудит и компактность `settings.yaml`.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## ⚙️ Пример конфигурации (`settings.yaml`)
|
|
202
|
-
|
|
203
|
-
```yaml
|
|
204
|
-
dsh-model-sync:
|
|
205
|
-
enabled: true
|
|
206
|
-
syncIntervalMinutes: 60
|
|
207
|
-
autoReconcile: true
|
|
208
|
-
providers:
|
|
209
|
-
openrouter:
|
|
210
|
-
enabled: true
|
|
211
|
-
keyEnv: OPENROUTER_API_KEY
|
|
212
|
-
deepseek:
|
|
213
|
-
enabled: true
|
|
214
|
-
keyEnv: DEEPSEEK_API_KEY
|
|
215
|
-
custom:
|
|
216
|
-
enabled: false
|
|
217
|
-
baseURL: http://127.0.0.1:8000/v1
|
|
218
|
-
authType: bearer
|
|
219
|
-
keyEnv: CUSTOM_API_KEY
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## 🚀 Улучшения в версии 0.3.14
|
|
225
|
-
|
|
226
|
-
- **Языковой стандарт**: Каноническая поддержка `en` и `zh` в кодовой базе плагина; русскоязычная локализация вынесена в пакет `goodandready/dsh-russian-lang` (#191).
|
|
227
|
-
- **Изоляция релиза**: Настроен `.npmignore` для исключения тестов, планов и документации из npm-пакета.
|
|
228
|
-
- **Пакетная проверка моделей (Batch Try)**: Эндпоинт `POST /dsh-model-sync/batch-try`, бейджи задержки и мгновенная деселекция недоступных моделей.
|
|
229
|
-
- **Политики по цене и контексту**: Опциональные фильтры `maxPricePerMillion` и `minContextTokens` в настройках провайдера.
|
|
230
|
-
- **Экспорт и импорт конфигурации**: JSON-бэкап и перенос настроек через `GET /export` и `POST /import`.
|
|
231
|
-
- **Пользовательские алиасы моделей**: Управление алиасами моделей через `GET/POST /aliases` и UI-карточку.
|
|
232
|
-
|
|
233
|
-
## 📄 Лицензия
|
|
234
|
-
|
|
235
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.zh.md
CHANGED
|
@@ -95,6 +95,14 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
+
## 🚀 v0.4.0 新特性与增强
|
|
99
|
+
|
|
100
|
+
- **设置卡片一键更新**: 在 DSH 设置卡片中集成自动版本检查,展示当前与最新版本对比徽标,支持一键安全升级并在更新后提示重启。
|
|
101
|
+
- **严格本地环回 (Loopback) 安全保护**: 所有修改状态的 HTTP 路由 (`/apply`, `/policy`, `/clear-cache`, `/updater/update`) 均强制校验本地环回 IP (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) 及请求来源,杜绝外部恶意调用。
|
|
102
|
+
- **100% 原生主题变量适配**: 客户端 UI 全面移除硬编码颜色与 `rgba()` 样式,完全切换至 DSH 官方 CSS 设计令牌 (`--dsw-alias-*`),无缝适配深色与浅色主题。
|
|
103
|
+
- **同步引擎模块化拆分**: 核心同步逻辑已解耦拆分为专项子模块 (`synchronizer-helpers.js`, `synchronizer-transfer.js`, `synchronizer-prober.js`),大幅提升代码可读性与维护性。
|
|
104
|
+
- **轻量分发包清理**: 彻底排除内部计划与调试文件,精简 npm 发布体积至 70 KiB 以下,所有文件均完全符合 256 KiB 上限规范。
|
|
105
|
+
|
|
98
106
|
## 🚀 v0.3.14 新特性与增强
|
|
99
107
|
|
|
100
108
|
- **语言标准**: 插件原生支持 `en` 与 `zh` 双语,俄语本地化已独立解耦至 `goodandready/dsh-russian-lang` 翻译包 (#191)。
|
|
@@ -107,13 +115,3 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
107
115
|
## 📄 开源协议
|
|
108
116
|
|
|
109
117
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
110
|
-
|
|
111
|
-
## 🛠️ v0.3.11 改进与修复
|
|
112
|
-
|
|
113
|
-
* 🗂️ **规范化设置界面**:
|
|
114
|
-
* 移除侧边栏顶层多余的 `settings.section` fallback,统一使用标准插件设置卡片 `settings.plugin.item`。
|
|
115
|
-
* 🛡️ **增强 Cordis 服务访问稳定性**:
|
|
116
|
-
* 使用 `ctx.get(...)` 安全获取 `settings` 与 `llm` 服务,避免代理对象属性解析异常。
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
package/lib/client.js
CHANGED
|
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
const CSS = '.dms-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;margin:0;padding:0;overflow:hidden;box-sizing:border-box}' +
|
|
9
9
|
'.dms-card-open{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-border-l2)}' +
|
|
10
10
|
'.dms-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
11
|
-
'.dms-card-header:hover{background:var(--dsw-alias-interactive-bg-hover,
|
|
11
|
+
'.dms-card-header:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}' +
|
|
12
12
|
'.dms-card-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' +
|
|
13
13
|
'.dms-card-head-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
|
|
14
14
|
'.dms-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
@@ -17,16 +17,16 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
'.dms-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:10px;box-sizing:border-box}' +
|
|
18
18
|
'.dms-wrap{display:flex;flex-direction:column;gap:14px;max-width:760px;box-sizing:border-box;color:var(--dsw-alias-label-primary,inherit)}' +
|
|
19
19
|
'.dms-wrap>p{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}' +
|
|
20
|
-
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary
|
|
20
|
+
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box}' +
|
|
21
21
|
'.dms-toolbar>select{flex:1 1 190px;min-width:190px}' +
|
|
22
22
|
'.dms-toolbar>.dms-actions{flex-basis:100%}' +
|
|
23
23
|
'.dms-toolbar>label{display:inline-flex;align-items:flex-start;gap:7px;font-size:12px;line-height:1.35;color:var(--dsw-alias-label-secondary)}' +
|
|
24
24
|
'.dms-toolbar>select,.dms-editor input,.dms-editor textarea,.dms-input{box-sizing:border-box;min-width:0;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2,transparent));border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2));border-radius:6px;color:var(--dsw-alias-label-primary);font:inherit;padding:7px 9px}' +
|
|
25
25
|
'.dms-toolbar>label input{accent-color:var(--dsw-alias-brand-primary,var(--dsw-alias-button-info-fill))}' +
|
|
26
26
|
'.dms-actions{display:flex;align-items:center;flex-wrap:wrap;gap:7px}' +
|
|
27
|
-
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary
|
|
27
|
+
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent)}' +
|
|
28
28
|
'.dms-btn{margin:0!important;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1.35;padding:6px 10px;cursor:pointer;white-space:normal}' +
|
|
29
|
-
'.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2
|
|
29
|
+
'.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2)}' +
|
|
30
30
|
'.dms-btn:disabled{opacity:.45;cursor:default}' +
|
|
31
31
|
'.dms-section>strong{display:block;font-size:13px;font-weight:650}' +
|
|
32
32
|
'.dms-provider-wrap{margin:0 0 7px}' +
|
|
@@ -44,18 +44,18 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
'.dms-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}' +
|
|
45
45
|
'.dms-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}' +
|
|
46
46
|
'.dms-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
47
|
-
'.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:
|
|
48
|
-
'.dms-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
49
|
-
'.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
47
|
+
'.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
48
|
+
'.dms-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-state-warning-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
49
|
+
'.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2))}' +
|
|
50
50
|
'.dms-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
51
51
|
'.dms-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
52
52
|
'.dms-btn-primary{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' +
|
|
53
53
|
'.dms-btn-primary:hover:not(:disabled){opacity:0.88}' +
|
|
54
|
-
'.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:
|
|
55
|
-
'.dms-btn-danger:hover:not(:disabled){background:
|
|
54
|
+
'.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:var(--dsw-alias-state-error-primary)!important}' +
|
|
55
|
+
'.dms-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2))!important;border-color:var(--dsw-alias-state-error-primary)!important}' +
|
|
56
56
|
'.dms-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
57
|
-
'.dms-alert-ok{background:
|
|
58
|
-
'.dms-alert-bad{background:
|
|
57
|
+
'.dms-alert-ok{background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary)}' +
|
|
58
|
+
'.dms-alert-bad{background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary)}' +
|
|
59
59
|
'.dms-probe-result{padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px}' +
|
|
60
60
|
'@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-section-card,.dms-editor{padding:11px 12px}}'
|
|
61
61
|
|
|
@@ -227,6 +227,17 @@ window.__ModuleLoader__.load({
|
|
|
227
227
|
addAlias: 'Save alias',
|
|
228
228
|
removeAlias: 'Delete alias',
|
|
229
229
|
noAliases: 'No model aliases defined.',
|
|
230
|
+
updaterTitle: 'Plugin Updates',
|
|
231
|
+
checkForUpdates: 'Check for updates',
|
|
232
|
+
checkingUpdates: 'Checking…',
|
|
233
|
+
updateAvailable: 'Update available: ',
|
|
234
|
+
upToDate: 'Up to date',
|
|
235
|
+
updateNow: 'Update now',
|
|
236
|
+
updating: 'Updating…',
|
|
237
|
+
updateSuccess: 'Updated to ',
|
|
238
|
+
restartRequired: ' (restart required)',
|
|
239
|
+
updateFailed: 'Update failed: ',
|
|
240
|
+
checkUpdateFailed: 'Failed to check for updates',
|
|
230
241
|
targetProvider: 'Provider',
|
|
231
242
|
}
|
|
232
243
|
|
|
@@ -347,6 +358,17 @@ window.__ModuleLoader__.load({
|
|
|
347
358
|
addAlias: '保存别名',
|
|
348
359
|
removeAlias: '删除别名',
|
|
349
360
|
noAliases: '未定义任何模型别名。',
|
|
361
|
+
updaterTitle: '插件更新',
|
|
362
|
+
checkForUpdates: '检查更新',
|
|
363
|
+
checkingUpdates: '正在检查…',
|
|
364
|
+
updateAvailable: '发现新版本: ',
|
|
365
|
+
upToDate: '已是最新版本',
|
|
366
|
+
updateNow: '立即更新',
|
|
367
|
+
updating: '正在更新…',
|
|
368
|
+
updateSuccess: '已成功更新至 ',
|
|
369
|
+
restartRequired: ' (需重启 DSH)',
|
|
370
|
+
updateFailed: '更新失败: ',
|
|
371
|
+
checkUpdateFailed: '检查更新失败',
|
|
350
372
|
targetProvider: '服务商',
|
|
351
373
|
}
|
|
352
374
|
|
|
@@ -372,6 +394,84 @@ window.__ModuleLoader__.load({
|
|
|
372
394
|
)
|
|
373
395
|
}
|
|
374
396
|
|
|
397
|
+
|
|
398
|
+
function UpdaterSection({ t, api }) {
|
|
399
|
+
const [updaterStatus, setUpdaterStatus] = React.useState(null)
|
|
400
|
+
const [updaterChecking, setUpdaterChecking] = React.useState(false)
|
|
401
|
+
const [updaterUpdating, setUpdaterUpdating] = React.useState(false)
|
|
402
|
+
const [updaterNotice, setUpdaterNotice] = React.useState('')
|
|
403
|
+
const [updaterError, setUpdaterError] = React.useState('')
|
|
404
|
+
|
|
405
|
+
const checkUpdates = () => {
|
|
406
|
+
setUpdaterChecking(true)
|
|
407
|
+
setUpdaterError('')
|
|
408
|
+
setUpdaterNotice('')
|
|
409
|
+
fetch(`${api}/update`)
|
|
410
|
+
.then((res) => {
|
|
411
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
412
|
+
return res.json()
|
|
413
|
+
})
|
|
414
|
+
.then((data) => {
|
|
415
|
+
setUpdaterStatus(data)
|
|
416
|
+
setUpdaterChecking(false)
|
|
417
|
+
})
|
|
418
|
+
.catch(() => {
|
|
419
|
+
setUpdaterChecking(false)
|
|
420
|
+
setUpdaterError(t.checkUpdateFailed)
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const runUpdate = () => {
|
|
425
|
+
if (updaterUpdating) return
|
|
426
|
+
setUpdaterUpdating(true)
|
|
427
|
+
setUpdaterError('')
|
|
428
|
+
setUpdaterNotice('')
|
|
429
|
+
fetch(`${api}/update`, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: { 'x-dsh-plugin-update': '1', 'content-type': 'application/json' },
|
|
432
|
+
})
|
|
433
|
+
.then((res) => res.json())
|
|
434
|
+
.then((resData) => {
|
|
435
|
+
if (resData.error) throw new Error(resData.error.message || resData.error)
|
|
436
|
+
const cur = resData.updatedVersion || (updaterStatus && updaterStatus.latestVersion) || ''
|
|
437
|
+
const nextData = Object.assign({}, updaterStatus, { currentVersion: cur, updateAvailable: false })
|
|
438
|
+
setUpdaterUpdating(false)
|
|
439
|
+
setUpdaterStatus(nextData)
|
|
440
|
+
setUpdaterNotice(t.updateSuccess + cur + (resData.restartRequired ? t.restartRequired : ''))
|
|
441
|
+
})
|
|
442
|
+
.catch((err) => {
|
|
443
|
+
setUpdaterUpdating(false)
|
|
444
|
+
setUpdaterError(t.updateFailed + (err.message || String(err)))
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return React.createElement('div', { style: { marginTop: 8 } },
|
|
449
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
450
|
+
React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } },
|
|
451
|
+
updaterStatus ? `v${updaterStatus.currentVersion}` : '',
|
|
452
|
+
updaterStatus && updaterStatus.updateAvailable ? ` → v${updaterStatus.latestVersion}` : ''
|
|
453
|
+
),
|
|
454
|
+
React.createElement('div', null,
|
|
455
|
+
updaterStatus && updaterStatus.updateAvailable
|
|
456
|
+
? React.createElement('button', {
|
|
457
|
+
type: 'button',
|
|
458
|
+
className: 'dms-btn dms-btn-primary',
|
|
459
|
+
disabled: updaterUpdating,
|
|
460
|
+
onClick: runUpdate,
|
|
461
|
+
}, updaterUpdating ? t.updating : t.updateNow)
|
|
462
|
+
: React.createElement('button', {
|
|
463
|
+
type: 'button',
|
|
464
|
+
className: 'dms-btn',
|
|
465
|
+
disabled: updaterChecking,
|
|
466
|
+
onClick: checkUpdates,
|
|
467
|
+
}, updaterChecking ? t.checkingUpdates : (updaterStatus ? t.upToDate : t.checkForUpdates))
|
|
468
|
+
)
|
|
469
|
+
),
|
|
470
|
+
updaterNotice ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-success)', marginTop: 8, fontSize: 12 } }, updaterNotice) : null,
|
|
471
|
+
updaterError ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary)', marginTop: 8, fontSize: 12 } }, updaterError) : null
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
375
475
|
function ModelSyncSection({ ctx, includeIntro = true }) {
|
|
376
476
|
const locale = useLocale(ctx)
|
|
377
477
|
const t = locale === 'zh' ? zh : en
|
|
@@ -1119,6 +1219,10 @@ window.__ModuleLoader__.load({
|
|
|
1119
1219
|
React.createElement('div', { className: 'dms-section-card dms-section' },
|
|
1120
1220
|
React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.history)),
|
|
1121
1221
|
renderHistory()
|
|
1222
|
+
),
|
|
1223
|
+
React.createElement('div', { className: 'dms-section-card dms-section' },
|
|
1224
|
+
React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.updaterTitle)),
|
|
1225
|
+
React.createElement(UpdaterSection, { t, api: API })
|
|
1122
1226
|
)
|
|
1123
1227
|
)
|
|
1124
1228
|
}
|
|
@@ -1157,12 +1261,16 @@ window.__ModuleLoader__.load({
|
|
|
1157
1261
|
|
|
1158
1262
|
function apply(ctx) {
|
|
1159
1263
|
ctx.effect(() => {
|
|
1160
|
-
try { ctx.locale?.register?.(NS, { en, zh }) } catch {
|
|
1264
|
+
try { ctx.locale?.register?.(NS, { en, zh }) } catch (err) {
|
|
1265
|
+
try { console.debug('[dsh-model-sync] locale register best-effort:', err) } catch { /* console unavailable */ }
|
|
1266
|
+
}
|
|
1161
1267
|
return () => {}
|
|
1162
1268
|
}, 'dsh-model-sync: dictionaries')
|
|
1163
1269
|
try {
|
|
1164
1270
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ErrorBoundary, null, React.createElement(ModelSyncCard, { ...props, ctx }))))
|
|
1165
|
-
} catch {
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
try { console.debug('[dsh-model-sync] slots inject best-effort:', err) } catch { /* console unavailable */ }
|
|
1273
|
+
}
|
|
1166
1274
|
}
|
|
1167
1275
|
|
|
1168
1276
|
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }
|
package/lib/http.js
CHANGED
|
@@ -8,24 +8,50 @@ function json(res, status, body) {
|
|
|
8
8
|
res.end(JSON.stringify(body))
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
export function isLoopbackAddress(ip) {
|
|
12
|
+
if (!ip || typeof ip !== 'string') return false
|
|
13
|
+
const clean = ip.toLowerCase().replace(/^\[|\]$/g, '')
|
|
14
|
+
if (clean === '127.0.0.1' || clean === '::1' || clean === '::ffff:127.0.0.1' || clean === 'localhost') return true
|
|
15
|
+
if (clean.startsWith('127.') || clean.startsWith('::ffff:127.')) return true
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function trusted(req, { requireLoopback = false } = {}) {
|
|
20
|
+
const remoteAddress = req?.socket?.remoteAddress
|
|
21
|
+
if (requireLoopback || req?.method === 'POST') {
|
|
22
|
+
if (!isLoopbackAddress(remoteAddress)) return false
|
|
23
|
+
}
|
|
12
24
|
const fetchSite = req.headers?.['sec-fetch-site']
|
|
13
25
|
if (fetchSite === 'cross-site') return false
|
|
14
26
|
const host = req.headers?.host
|
|
15
27
|
const origin = req.headers?.origin
|
|
16
28
|
if (origin) {
|
|
17
29
|
if (!host) return false
|
|
18
|
-
try {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(origin)
|
|
32
|
+
if (url.host !== host) return false
|
|
33
|
+
if (requireLoopback || req?.method === 'POST') {
|
|
34
|
+
if (!isLoopbackAddress(url.hostname) && url.hostname !== 'localhost') return false
|
|
35
|
+
}
|
|
36
|
+
} catch { return false }
|
|
19
37
|
}
|
|
20
38
|
const referer = req.headers?.referer
|
|
21
39
|
if (referer) {
|
|
22
40
|
if (!host) return false
|
|
23
|
-
try {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(referer)
|
|
43
|
+
if (url.host !== host) return false
|
|
44
|
+
if (requireLoopback || req?.method === 'POST') {
|
|
45
|
+
if (!isLoopbackAddress(url.hostname) && url.hostname !== 'localhost') return false
|
|
46
|
+
}
|
|
47
|
+
} catch { return false }
|
|
24
48
|
}
|
|
25
49
|
// If sec-fetch-site is same-origin or same-site, allow
|
|
26
50
|
if (fetchSite === 'same-origin' || fetchSite === 'same-site') return true
|
|
27
51
|
// If sec-fetch-mode is present but origin/referer missing on a cross-site request, reject
|
|
28
52
|
if (req.headers?.['sec-fetch-mode'] && !host) return false
|
|
53
|
+
// For POST write requests without any browser origin/referer headers, require explicit loopback remoteAddress
|
|
54
|
+
if (req?.method === 'POST' && !origin && !referer && !isLoopbackAddress(remoteAddress)) return false
|
|
29
55
|
return true
|
|
30
56
|
}
|
|
31
57
|
|
|
@@ -188,7 +214,7 @@ export function normalizeSelectionRequest(body) {
|
|
|
188
214
|
|
|
189
215
|
export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () => null } = {}) {
|
|
190
216
|
const makeHandler = (route) => async (req, res) => {
|
|
191
|
-
if (!trusted(req)) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin request rejected' } })
|
|
217
|
+
if (!trusted(req, { requireLoopback: req.method === 'POST' })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
|
|
192
218
|
if (route === 'status') {
|
|
193
219
|
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
194
220
|
const payload = { ...synchronizer.status(), scheduler: getSchedulerStatus(), providers: synchronizer.listProviders() }
|