@goodandready/dsh-model-sync 0.3.8 → 0.3.10
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 +25 -0
- package/README.ru.md +25 -0
- package/lib/client.js +14 -4
- package/lib/history.js +14 -3
- package/lib/http.js +17 -5
- package/lib/index.js +9 -4
- package/lib/scheduler.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,31 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
142
142
|
|
|
143
143
|
---
|
|
144
144
|
|
|
145
|
+
## 🚀 Enhancements in v0.3.10
|
|
146
|
+
|
|
147
|
+
* 📦 **Compacted History Storage**:
|
|
148
|
+
* 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`).
|
|
149
|
+
* Optimized default snapshot retention (`DEFAULT_SNAPSHOT_RETENTION = 3`) to keep configuration files lean while preserving full rollback functionality.
|
|
150
|
+
* ⚡ **Immediate Scheduler First Tick**:
|
|
151
|
+
* Scheduler now triggers an initial discovery run immediately upon startup when enabled without requiring an initial 60-minute wait.
|
|
152
|
+
* 🛡️ **Client-side Policy Regex Validation**:
|
|
153
|
+
* Added instant regex pattern verification in the Web UI before submitting `/policy` requests with localized error feedback.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 🚀 Enhancements in v0.3.9
|
|
158
|
+
|
|
159
|
+
* 🔒 **Serialized Settings Mutations (`saveConfig`)**:
|
|
160
|
+
* Chained internal settings persistence via an async promise mutex (`saveQueue`).
|
|
161
|
+
* Eliminates race conditions between simultaneous sync runs and user UI interactions, ensuring clean atomic writes to `settings.yaml`.
|
|
162
|
+
* 🛡️ **Hardened Cross-Origin & CSRF Protections**:
|
|
163
|
+
* Enhanced `trusted(req)` checking to evaluate `sec-fetch-site`, `origin`, and `referer` against the HTTP `Host`.
|
|
164
|
+
* Blocks unauthorized cross-origin mutations while preserving legitimate local and reverse-proxy requests.
|
|
165
|
+
* 🎨 **Standardized Plugin Style Discovery**:
|
|
166
|
+
* Added `data-dsh-plugin="dsh-model-sync"` attribute to injected UI style tags in compliance with the DeepSeek Harness plugin authoring standard.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
145
170
|
## 🚀 Enhancements in v0.3.5
|
|
146
171
|
|
|
147
172
|
* ⏱️ **Adaptive Rate Limit & Retry-After Handling**:
|
package/README.ru.md
CHANGED
|
@@ -119,6 +119,31 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
+
## 🚀 Новые возможности в v0.3.10
|
|
123
|
+
|
|
124
|
+
* 📦 **Компактизация хранения истории**:
|
|
125
|
+
* Очистка некритичных метаданных (`description`, `tags`, `aliases`) из снимков моделей (`before`/`after`) в `settings.yaml`, с сохранением ключевых свойств (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
126
|
+
* Оптимизирована глубина хранения снимков (`DEFAULT_SNAPSHOT_RETENTION = 3`) для предотвращения разрастания конфигурационного файла при сохранении возможности безопасного отката.
|
|
127
|
+
* ⚡ **Мгновенный первый запуск планировщика**:
|
|
128
|
+
* Планировщик автоматически запускает первичный синк сразу при старте, если расписание активно и предыдущие запуски отсутствуют.
|
|
129
|
+
* 🛡️ **Клиентская валидация regex-политик**:
|
|
130
|
+
* Добавлена проверка синтаксиса регулярных выражений в интерфейсе настроек перед отправкой запроса на `/policy` с локализованными подсказками.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 🚀 Новые возможности в v0.3.9
|
|
135
|
+
|
|
136
|
+
* 🔒 **Атомарная очередь сохранения настроек (`saveConfig`)**:
|
|
137
|
+
* Сериализация внутренних мутаций конфигурации через цепочку промисов (`saveQueue`).
|
|
138
|
+
* Предотвращает состояние гонки между фоновой синхронизацией и действиями пользователя в интерфейсе, гарантируя целостность `settings.yaml`.
|
|
139
|
+
* 🛡️ **Усиленная защита от CSRF и cross-origin атак**:
|
|
140
|
+
* Расширенная валидация запросов `trusted(req)` с проверкой заголовков `sec-fetch-site`, `origin` и `referer` против `Host`.
|
|
141
|
+
* Блокировка межсайтовых вызовов мутаций без нарушения локальных обращений и проксирования.
|
|
142
|
+
* 🎨 **Стандартизация инжекции стилей интерфейса**:
|
|
143
|
+
* Добавлен атрибут `data-dsh-plugin="dsh-model-sync"` для тега стилей в полном соответствии со стандартом разработки плагинов DeepSeek Harness.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
122
147
|
## 🚀 Новые возможности в v0.3.5
|
|
123
148
|
|
|
124
149
|
* ⏱️ **Адаптивная обработка Rate Limit и Retry-After**:
|
package/lib/client.js
CHANGED
|
@@ -33,18 +33,19 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
function ensureStyles() {
|
|
34
34
|
if (typeof document === 'undefined' || !document.head) return
|
|
35
35
|
const cssId = 'dsh-model-sync/settings.module.css'
|
|
36
|
-
if (document.querySelector('style[data-plugin-css="' + cssId + '"]')) return
|
|
36
|
+
if (document.querySelector('style[data-plugin-css="' + cssId + '"]') || document.querySelector('style[data-dsh-plugin="dsh-model-sync"]')) return
|
|
37
37
|
const tag = document.createElement('style')
|
|
38
38
|
tag.textContent = CSS
|
|
39
39
|
tag.setAttribute('data-plugin', 'dsh-model-sync')
|
|
40
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-model-sync')
|
|
40
41
|
tag.setAttribute('data-plugin-css', cssId)
|
|
41
42
|
document.head.appendChild(tag)
|
|
42
43
|
}
|
|
43
44
|
const en = {
|
|
44
|
-
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago', noHistory: 'No synchronization history.', searchPlaceholder: 'Search id/name/tag', sortName: 'Name A→Z', sortPrice: 'Price ↑', sortContext: 'Context ↓', sortDate: 'Newest ↓', schedulerSettings: 'Scheduler configuration', scheduleEnabled: 'Enable background schedule', intervalMinutes: 'Interval (minutes)', autoApplySchedule: 'Auto-apply changes', saveScheduler: 'Save scheduler', schedulerSaved: 'Saved', scopeUnavailable: 'Settings service unavailable.',
|
|
45
|
+
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago', noHistory: 'No synchronization history.', searchPlaceholder: 'Search id/name/tag', sortName: 'Name A→Z', sortPrice: 'Price ↑', sortContext: 'Context ↓', sortDate: 'Newest ↓', schedulerSettings: 'Scheduler configuration', scheduleEnabled: 'Enable background schedule', intervalMinutes: 'Interval (minutes)', autoApplySchedule: 'Auto-apply changes', saveScheduler: 'Save scheduler', schedulerSaved: 'Saved', scopeUnavailable: 'Settings service unavailable.', invalidPattern: 'Invalid regex pattern: %s',
|
|
45
46
|
}
|
|
46
47
|
const ru = {
|
|
47
|
-
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад', noHistory: 'Истории синхронизаций нет.', searchPlaceholder: 'Поиск id/name/tag', sortName: 'Имя A→Z', sortPrice: 'Цена ↑', sortContext: 'Контекст ↓', sortDate: 'Новые ↓', schedulerSettings: 'Настройка планировщика', scheduleEnabled: 'Включить фоновое расписание', intervalMinutes: 'Интервал (минуты)', autoApplySchedule: 'Автоматически применять изменения', saveScheduler: 'Сохранить расписание', schedulerSaved: 'Сохранено', scopeUnavailable: 'Служба настроек недоступна.',
|
|
48
|
+
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад', noHistory: 'Истории синхронизаций нет.', searchPlaceholder: 'Поиск id/name/tag', sortName: 'Имя A→Z', sortPrice: 'Цена ↑', sortContext: 'Контекст ↓', sortDate: 'Новые ↓', schedulerSettings: 'Настройка планировщика', scheduleEnabled: 'Включить фоновое расписание', intervalMinutes: 'Интервал (минуты)', autoApplySchedule: 'Автоматически применять изменения', saveScheduler: 'Сохранить расписание', schedulerSaved: 'Сохранено', scopeUnavailable: 'Служба настроек недоступна.', invalidPattern: 'Некорректный regex-шаблон: %s',
|
|
48
49
|
}
|
|
49
50
|
const API = '/dsh-model-sync'
|
|
50
51
|
const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
|
|
@@ -96,7 +97,16 @@ window.__ModuleLoader__.load({
|
|
|
96
97
|
const closePolicy = () => { setPolicyProvider(''); setPolicyDraft({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' }) }
|
|
97
98
|
const splitValues = (value) => [...new Set(String(value ?? '').split(/[,\n]+/).map((item) => item.trim()).filter(Boolean))]
|
|
98
99
|
const capabilityValues = (value) => Object.fromEntries(splitValues(value).map((key) => [key, true]))
|
|
99
|
-
const
|
|
100
|
+
const validatePatterns = (patterns) => { for (const p of patterns) { try { new RegExp(p, 'i') } catch { return p } } return null }
|
|
101
|
+
const savePolicy = () => {
|
|
102
|
+
const includeList = splitValues(policyDraft.include)
|
|
103
|
+
const excludeList = splitValues(policyDraft.exclude)
|
|
104
|
+
const badInclude = validatePatterns(includeList)
|
|
105
|
+
if (badInclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badInclude)); return }
|
|
106
|
+
const badExclude = validatePatterns(excludeList)
|
|
107
|
+
if (badExclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badExclude)); return }
|
|
108
|
+
setBusy(true); setError(''); fetch(`${API}/policy`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: policyProvider, include: includeList, exclude: excludeList, requireCapabilities: capabilityValues(policyDraft.requireCapabilities), denyCapabilities: capabilityValues(policyDraft.denyCapabilities) }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => { closePolicy(); return load() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false))
|
|
109
|
+
}
|
|
100
110
|
const saveSelection = () => { const row = rows.find((item) => item.provider === selectionProvider); const available = row?.availableModels ?? row?.models ?? []; if (available.length > 0 && selectionDraft.length === 0) { setError(t.selectAtLeastOne); return } const models = selectionDraft.length === available.length ? [] : selectionDraft; setBusy(true); setError(''); fetch(`${API}/selection`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: selectionProvider, models }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); closeSelection() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
101
111
|
const rollback = (entry, targetProvider) => { if (!window.confirm(t.rollbackConfirm)) return; setBusy(true); setError(''); fetch(`${API}/history/rollback`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ historyId: entry.id, provider: targetProvider }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => load()).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
102
112
|
const isNewModel = (model, row) => { const added = status?.lastRun?.results?.find(r=>r.provider===row.provider)?.diff?.added||[]; const isAdded = added.some(m=>m.id===model.id); if(!isAdded) return false; const finishedAt = status?.lastRun?.finishedAt; if(!finishedAt) return false; return (Date.now()-new Date(finishedAt).getTime()) < 7*24*60*60*1000; };
|
package/lib/history.js
CHANGED
|
@@ -16,11 +16,22 @@ export function historyLimit(value) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function cloneModel(provider, model) {
|
|
19
|
-
|
|
19
|
+
const normalized = normalizeModels(provider, [model])[0]
|
|
20
|
+
if (!normalized) return null
|
|
21
|
+
const compact = {
|
|
22
|
+
provider: normalized.provider,
|
|
23
|
+
id: normalized.id,
|
|
24
|
+
name: normalized.name,
|
|
25
|
+
}
|
|
26
|
+
if (normalized.capabilities) compact.capabilities = normalized.capabilities
|
|
27
|
+
if (normalized.pricing) compact.pricing = normalized.pricing
|
|
28
|
+
if (normalized.contextWindow !== undefined) compact.contextWindow = normalized.contextWindow
|
|
29
|
+
if (normalized.maxTokens !== undefined) compact.maxTokens = normalized.maxTokens
|
|
30
|
+
return compact
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
function catalog(provider, models) {
|
|
23
|
-
return normalizeModels(provider, models).slice(0, MAX_HISTORY_MODELS)
|
|
34
|
+
return (normalizeModels(provider, models) ?? []).map((model) => cloneModel(provider, model)).filter(Boolean).slice(0, MAX_HISTORY_MODELS)
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function message(value) {
|
|
@@ -80,7 +91,7 @@ export function createHistoryEntry(result, { version = 1, maxModels = MAX_HISTOR
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
export const DEFAULT_SNAPSHOT_RETENTION =
|
|
94
|
+
export const DEFAULT_SNAPSHOT_RETENTION = 3
|
|
84
95
|
|
|
85
96
|
export function compactHistoryEntry(entry) {
|
|
86
97
|
if (!entry || typeof entry !== 'object') return entry
|
package/lib/http.js
CHANGED
|
@@ -8,12 +8,24 @@ function json(res, status, body) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function trusted(req) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!origin) return true
|
|
11
|
+
const fetchSite = req.headers?.['sec-fetch-site']
|
|
12
|
+
if (fetchSite === 'cross-site') return false
|
|
14
13
|
const host = req.headers?.host
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const origin = req.headers?.origin
|
|
15
|
+
if (origin) {
|
|
16
|
+
if (!host) return false
|
|
17
|
+
try { return new URL(origin).host === host } catch { return false }
|
|
18
|
+
}
|
|
19
|
+
const referer = req.headers?.referer
|
|
20
|
+
if (referer) {
|
|
21
|
+
if (!host) return false
|
|
22
|
+
try { return new URL(referer).host === host } catch { return false }
|
|
23
|
+
}
|
|
24
|
+
// If sec-fetch-site is same-origin or same-site, allow
|
|
25
|
+
if (fetchSite === 'same-origin' || fetchSite === 'same-site') return true
|
|
26
|
+
// If sec-fetch-mode is present but origin/referer missing on a cross-site request, reject
|
|
27
|
+
if (req.headers?.['sec-fetch-mode'] && !host) return false
|
|
28
|
+
return true
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
function readJson(req) {
|
package/lib/index.js
CHANGED
|
@@ -115,10 +115,15 @@ export function apply(ctx, config) {
|
|
|
115
115
|
liveConfig = resolveConfig(next || baseConfig)
|
|
116
116
|
reconfigure()
|
|
117
117
|
})
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
let saveQueue = Promise.resolve()
|
|
119
|
+
saveConfigImpl = (patch) => {
|
|
120
|
+
const nextTask = saveQueue.then(async () => {
|
|
121
|
+
const result = await scope.update(patch)
|
|
122
|
+
liveConfig = resolveConfig(scope.get() || baseConfig)
|
|
123
|
+
return result
|
|
124
|
+
})
|
|
125
|
+
saveQueue = nextTask.catch(() => {})
|
|
126
|
+
return nextTask
|
|
122
127
|
}
|
|
123
128
|
ownerCtx.effect(() => () => {
|
|
124
129
|
settingsScope = undefined
|
package/lib/scheduler.js
CHANGED
|
@@ -117,10 +117,16 @@ export function createSyncScheduler(sync, {
|
|
|
117
117
|
if (config.enabled === false || config.scheduleEnabled === false) return false
|
|
118
118
|
const rows = scheduleRows(config)
|
|
119
119
|
if (inventory(config) !== null && rows.length === 0) return false
|
|
120
|
+
const isFirstRun = state.size === 0 || [...state.values()].every((row) => row.lastRunAt === null)
|
|
120
121
|
resetStates(config)
|
|
121
122
|
// One-minute polling is the lower safety bound. Per-provider intervals,
|
|
122
123
|
// TTLs, and jitter are enforced by tick without multiplying timers.
|
|
123
124
|
timer = setIntervalFn(() => tick(), 60_000)
|
|
125
|
+
if (isFirstRun) {
|
|
126
|
+
Promise.resolve().then(() => tick()).catch((err) => {
|
|
127
|
+
logger.warn?.("[dsh-model-sync] initial tick failed: " + errorMessage(err))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
124
130
|
return true
|
|
125
131
|
}
|
|
126
132
|
|