@goodandready/dsh-model-sync 0.3.9 → 0.3.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/README.md +21 -0
- package/README.ru.md +21 -0
- package/README.zh.md +10 -0
- package/lib/client.js +17 -15
- package/lib/history.js +14 -3
- package/lib/index.js +1 -1
- package/lib/provider-service.js +12 -3
- package/lib/scheduler.js +6 -0
- package/lib/synchronizer.js +645 -636
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,15 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
+
## 🛠️ Enhancements & Fixes in v0.3.11
|
|
111
|
+
|
|
112
|
+
* 🗂️ **Streamlined Settings UI Surface**:
|
|
113
|
+
* Removed the redundant top-level settings sidebar section fallback (`settings.section`), keeping configuration strictly inside the standard DSH plugin settings card (`settings.plugin.item`).
|
|
114
|
+
* 🛡️ **Hardened Cordis Service Access**:
|
|
115
|
+
* 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.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
110
119
|
## 🛠️ Enhancements & Fixes in v0.3.8
|
|
111
120
|
|
|
112
121
|
* ⚙️ **Reactive Settings Card Binding (`settingsScope`)**:
|
|
@@ -142,6 +151,18 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
142
151
|
|
|
143
152
|
---
|
|
144
153
|
|
|
154
|
+
## 🚀 Enhancements in v0.3.10
|
|
155
|
+
|
|
156
|
+
* 📦 **Compacted History Storage**:
|
|
157
|
+
* 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`).
|
|
158
|
+
* Optimized default snapshot retention (`DEFAULT_SNAPSHOT_RETENTION = 3`) to keep configuration files lean while preserving full rollback functionality.
|
|
159
|
+
* ⚡ **Immediate Scheduler First Tick**:
|
|
160
|
+
* Scheduler now triggers an initial discovery run immediately upon startup when enabled without requiring an initial 60-minute wait.
|
|
161
|
+
* 🛡️ **Client-side Policy Regex Validation**:
|
|
162
|
+
* Added instant regex pattern verification in the Web UI before submitting `/policy` requests with localized error feedback.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
145
166
|
## 🚀 Enhancements in v0.3.9
|
|
146
167
|
|
|
147
168
|
* 🔒 **Serialized Settings Mutations (`saveConfig`)**:
|
package/README.ru.md
CHANGED
|
@@ -84,6 +84,15 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
+
## 🛠️ Улучшения и исправления в v0.3.11
|
|
88
|
+
|
|
89
|
+
* 🗂️ **Чистый интерфейс настроек (DSH Design Contract)**:
|
|
90
|
+
* Удалена избыточная регистрация fallback-раздела верхнего уровня бокового меню (`settings.section`), настройки доступны только через стандартную карточку в «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`).
|
|
91
|
+
* 🛡️ **Надёжный доступ к сервисам Cordis**:
|
|
92
|
+
* Доступ к сервисам ядра (`settings`, `llm`) переведён на `ctx.get(...)` с безопасным fallback к прямым свойствам контекста для исключения ошибок проксирования.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
87
96
|
## 🛠️ Улучшения и исправления в v0.3.8
|
|
88
97
|
|
|
89
98
|
* ⚙️ **Реактивное управление настройками через `settingsScope`**:
|
|
@@ -119,6 +128,18 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
119
128
|
|
|
120
129
|
---
|
|
121
130
|
|
|
131
|
+
## 🚀 Новые возможности в v0.3.10
|
|
132
|
+
|
|
133
|
+
* 📦 **Компактизация хранения истории**:
|
|
134
|
+
* Очистка некритичных метаданных (`description`, `tags`, `aliases`) из снимков моделей (`before`/`after`) в `settings.yaml`, с сохранением ключевых свойств (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
135
|
+
* Оптимизирована глубина хранения снимков (`DEFAULT_SNAPSHOT_RETENTION = 3`) для предотвращения разрастания конфигурационного файла при сохранении возможности безопасного отката.
|
|
136
|
+
* ⚡ **Мгновенный первый запуск планировщика**:
|
|
137
|
+
* Планировщик автоматически запускает первичный синк сразу при старте, если расписание активно и предыдущие запуски отсутствуют.
|
|
138
|
+
* 🛡️ **Клиентская валидация regex-политик**:
|
|
139
|
+
* Добавлена проверка синтаксиса регулярных выражений в интерфейсе настроек перед отправкой запроса на `/policy` с локализованными подсказками.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
122
143
|
## 🚀 Новые возможности в v0.3.9
|
|
123
144
|
|
|
124
145
|
* 🔒 **Атомарная очередь сохранения настроек (`saveConfig`)**:
|
package/README.zh.md
CHANGED
|
@@ -73,3 +73,13 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
73
73
|
## 📄 开源协议
|
|
74
74
|
|
|
75
75
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
76
|
+
|
|
77
|
+
## 🛠️ v0.3.11 改进与修复
|
|
78
|
+
|
|
79
|
+
* 🗂️ **规范化设置界面**:
|
|
80
|
+
* 移除侧边栏顶层多余的 `settings.section` fallback,统一使用标准插件设置卡片 `settings.plugin.item`。
|
|
81
|
+
* 🛡️ **增强 Cordis 服务访问稳定性**:
|
|
82
|
+
* 使用 `ctx.get(...)` 安全获取 `settings` 与 `llm` 服务,避免代理对象属性解析异常。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
package/lib/client.js
CHANGED
|
@@ -42,14 +42,13 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
document.head.appendChild(tag)
|
|
43
43
|
}
|
|
44
44
|
const en = {
|
|
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.',
|
|
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',
|
|
46
46
|
}
|
|
47
47
|
const ru = {
|
|
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: 'Служба настроек недоступна.',
|
|
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',
|
|
49
49
|
}
|
|
50
50
|
const API = '/dsh-model-sync'
|
|
51
51
|
const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
|
|
52
|
-
function capabilityKeys(models) { return [...new Set((models ?? []).flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort() }
|
|
53
52
|
function filterModelsByCapabilities(models, selected) { const required = [...new Set((selected ?? []).filter((key) => typeof key === 'string' && key))]; return (models ?? []).filter((model) => required.every((key) => model?.capabilities?.[key] === true)) }
|
|
54
53
|
const NS = 'dsh-model-sync'
|
|
55
54
|
function useLocale(ctx) {
|
|
@@ -86,7 +85,7 @@ window.__ModuleLoader__.load({
|
|
|
86
85
|
const doTry = (provider, modelId) => { setTryState((s)=>({ ...s, [provider+':'+modelId]: { loading: true } })); fetch(`${API}/try`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider, model: modelId }) }).then(async r=>{ const b=await r.json(); if(!r.ok) throw new Error(b?.error?.message||`HTTP ${r.status}`); return b }).then(b=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { ok: b.ok, latency: b.latencyMs, status: b.status, error: b.error } }))).catch(e=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { error: String(e.message||e) } }))) }
|
|
87
86
|
const load = React.useCallback(() => { setError(''); return fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
|
|
88
87
|
const loadCredentials = React.useCallback(() => { setError(''); return fetch(`${API}/credentials`).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setCredentialStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
|
|
89
|
-
React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(load, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
|
|
88
|
+
React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(() => { if (typeof document !== 'undefined' && document.hidden) return; load() }, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
|
|
90
89
|
const execute = (targetProvider, dryRunOverride = dryRun) => { if (!dryRunOverride && removeMissing && !window.confirm(t.removeMissing)) return; setBusy(true); setError(''); fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider || undefined, dryRun: dryRunOverride, removeMissing }) }).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); if (dryRunOverride) setPreview({ ...body, provider: targetProvider || '' }); else setPreview(null) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
91
90
|
const checkHealth = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/health`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { 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(setStatus).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
92
91
|
const checkCredentials = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/credentials/check`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { 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((body) => { setCredentialStatus(body.credentials); setStatus((current) => ({ ...(current ?? {}), lastHealth: body.health })) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
@@ -97,7 +96,16 @@ window.__ModuleLoader__.load({
|
|
|
97
96
|
const closePolicy = () => { setPolicyProvider(''); setPolicyDraft({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' }) }
|
|
98
97
|
const splitValues = (value) => [...new Set(String(value ?? '').split(/[,\n]+/).map((item) => item.trim()).filter(Boolean))]
|
|
99
98
|
const capabilityValues = (value) => Object.fromEntries(splitValues(value).map((key) => [key, true]))
|
|
100
|
-
const
|
|
99
|
+
const validatePatterns = (patterns) => { for (const p of patterns) { try { new RegExp(p, 'i') } catch { return p } } return null }
|
|
100
|
+
const savePolicy = () => {
|
|
101
|
+
const includeList = splitValues(policyDraft.include)
|
|
102
|
+
const excludeList = splitValues(policyDraft.exclude)
|
|
103
|
+
const badInclude = validatePatterns(includeList)
|
|
104
|
+
if (badInclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badInclude)); return }
|
|
105
|
+
const badExclude = validatePatterns(excludeList)
|
|
106
|
+
if (badExclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badExclude)); return }
|
|
107
|
+
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))
|
|
108
|
+
}
|
|
101
109
|
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)) }
|
|
102
110
|
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)) }
|
|
103
111
|
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; };
|
|
@@ -175,7 +183,7 @@ window.__ModuleLoader__.load({
|
|
|
175
183
|
function ModelSyncCard({ ctx }) {
|
|
176
184
|
const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en; const [open, setOpen] = React.useState(false)
|
|
177
185
|
const [cardStatus, setCardStatus] = React.useState(null)
|
|
178
|
-
React.useEffect(() => { ensureStyles(); const load = () => fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}); load(); const id=setInterval(load,15000); return ()=>clearInterval(id)}, [])
|
|
186
|
+
React.useEffect(() => { ensureStyles(); const load = () => { if (typeof document !== 'undefined' && document.hidden) return; fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}) }; load(); if (open) return; const id = setInterval(load, 15000); return () => clearInterval(id) }, [open])
|
|
179
187
|
const cardBadge = (cardStatus?.lastRun?.results?.reduce((s,r)=>s+(r.diff?.added?.length||0),0) || 0)
|
|
180
188
|
return React.createElement('li', { className: 'dms-card' + (open ? ' dms-card-open' : '') },
|
|
181
189
|
React.createElement('button', { type: 'button', className: 'dms-card-header', 'aria-expanded': open, 'aria-label': `${t[open ? 'hideSettings' : 'showSettings']}: ${t.title}`, onClick: () => setOpen((value) => !value) },
|
|
@@ -189,15 +197,9 @@ window.__ModuleLoader__.load({
|
|
|
189
197
|
try { ctx.locale?.register?.(NS, { en, ru }) } catch {}
|
|
190
198
|
return () => {}
|
|
191
199
|
}, 'dsh-model-sync: dictionaries')
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return true
|
|
196
|
-
} catch { return false }
|
|
197
|
-
}
|
|
198
|
-
if (!registerPluginCard()) {
|
|
199
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-model-sync', order: 25, label: () => 'Model Sync' }, (props) => React.createElement(ModelSyncSection, { ...props, ctx })))
|
|
200
|
-
}
|
|
200
|
+
try {
|
|
201
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ModelSyncCard, { ...props, ctx })))
|
|
202
|
+
} catch {}
|
|
201
203
|
}
|
|
202
204
|
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }; return module.exports
|
|
203
205
|
},
|
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/index.js
CHANGED
|
@@ -137,7 +137,7 @@ export function apply(ctx, config) {
|
|
|
137
137
|
try { settings = ctx.get('settings') } catch {}
|
|
138
138
|
if (!bindSettings(settings, ctx)) {
|
|
139
139
|
ctx.inject(['settings'], (sctx) => {
|
|
140
|
-
bindSettings(sctx.settings, sctx)
|
|
140
|
+
bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
|
|
141
141
|
})
|
|
142
142
|
}
|
|
143
143
|
|
package/lib/provider-service.js
CHANGED
|
@@ -3,13 +3,22 @@ import { listApiKeyProviders } from './inventory.js'
|
|
|
3
3
|
export function createProviderService(ctx) {
|
|
4
4
|
const readSettings = () => {
|
|
5
5
|
try {
|
|
6
|
-
return ctx.get('settings')
|
|
6
|
+
return (typeof ctx.get === 'function' ? (ctx.get('settings') ?? ctx.settings) : ctx.settings)
|
|
7
7
|
} catch {
|
|
8
|
-
return
|
|
8
|
+
return ctx.settings
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const readLlm = () => {
|
|
13
|
+
try {
|
|
14
|
+
const llm = typeof ctx.get === 'function' ? ctx.get('llm') : null
|
|
15
|
+
return (llm && typeof llm === 'object' && (llm.listConfigurableProviders || llm.listProviders)) ? llm : ctx.llm
|
|
16
|
+
} catch {
|
|
17
|
+
return ctx.llm
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const listProviders = () => listApiKeyProviders(readLlm(), readSettings())
|
|
13
22
|
|
|
14
23
|
return Object.freeze({
|
|
15
24
|
listProviders,
|
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
|
|