@goodandready/dsh-russian-lang 0.2.16 → 0.2.17

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/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { registerPluginUpdater } from './plugin-updater.js'
1
2
  // dsh-russian-lang — серверная половина.
2
3
  //
3
4
  // Словари и переключатель живут в браузере (lib/client.js). Хосту переводить
@@ -49,4 +50,17 @@ export function apply(ctx) {
49
50
  ctx.on('settings/updated', (ns) => { if (ns === NS) sync() })
50
51
  sync()
51
52
  })
53
+
54
+ // One-click plugin updater per DSH standard
55
+ const host = ctx
56
+ if (typeof host.webServer?.register === 'function') {
57
+ const mount = () => registerPluginUpdater(ctx, {
58
+ endpoint: '/api/dsh-russian-lang/update',
59
+ packageName: '@goodandready/dsh-russian-lang',
60
+ manifestUrl: new URL('../package.json', import.meta.url),
61
+ })
62
+ if (typeof ctx.effect === 'function') ctx.effect(mount, 'russian-lang: plugin updater')
63
+ else mount()
64
+ }
65
+
52
66
  }
@@ -0,0 +1,333 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ /**
9
+ * Host-side one-click updater for @goodandready/dsh-russian-lang.
10
+ * Implements the unified DSH plugin updater standard.
11
+ */
12
+
13
+ const UPDATE_HEADER = 'x-dsh-plugin-update'
14
+ const UPDATE_TIMEOUT_MS = 10 * 60_000
15
+ const VERSION_CACHE_MS = 5 * 60_000
16
+ let latestCache = undefined
17
+
18
+ function header(request, name) {
19
+ const value = request.headers?.[name]
20
+ return Array.isArray(value) ? value[0] : value
21
+ }
22
+
23
+ function isLoopback(value) {
24
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
25
+ return (
26
+ address === 'localhost' ||
27
+ address === 'localhost.' ||
28
+ address === '::1' ||
29
+ address?.startsWith('127.') === true ||
30
+ address?.startsWith('::ffff:127.') === true
31
+ )
32
+ }
33
+
34
+ export function isTrustedUpdateRequest(request) {
35
+ if (header(request, UPDATE_HEADER) !== '1') return false
36
+ if (!isLoopback(request.socket?.remoteAddress)) return false
37
+ const site = header(request, 'sec-fetch-site')
38
+ if (site !== undefined && site !== 'same-origin') return false
39
+ const origin = header(request, 'origin')
40
+ const host = header(request, 'host')
41
+ if (origin === undefined || host === undefined) return false
42
+ try {
43
+ const url = new URL(origin)
44
+ return (
45
+ (url.protocol === 'http:' || url.protocol === 'https:') &&
46
+ isLoopback(url.hostname) &&
47
+ url.host === host
48
+ )
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ function validProfileName(value) {
55
+ return (
56
+ typeof value === 'string' &&
57
+ value !== '' &&
58
+ value !== '.' &&
59
+ value !== '..' &&
60
+ !value.includes('/') &&
61
+ !value.includes('\\') &&
62
+ !/[\0-\x1f\x7f]/.test(value)
63
+ )
64
+ }
65
+
66
+ function profileNameFromArgv(argv) {
67
+ for (let index = 2; index < argv.length; index += 1) {
68
+ if (argv[index] === '--profile') return argv[index + 1]
69
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
70
+ }
71
+ return argv[2] === 'web' ? 'web' : undefined
72
+ }
73
+
74
+ function findDshCliEntry() {
75
+ const value = process.argv[1]
76
+ if (value === undefined || value === '') return undefined
77
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
78
+ if (!existsSync(entry)) return undefined
79
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
80
+ const manifestPath = resolve(directory, 'package.json')
81
+ if (existsSync(manifestPath)) {
82
+ try {
83
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
84
+ const bin =
85
+ typeof manifest.bin === 'string'
86
+ ? manifest.bin
87
+ : typeof manifest.bin === 'object' && manifest.bin !== null
88
+ ? manifest.bin.dsh
89
+ : undefined
90
+ if (
91
+ manifest.name === '@deepseek-ai/dsh' &&
92
+ typeof bin === 'string' &&
93
+ !isAbsolute(bin) &&
94
+ resolve(directory, bin) === resolve(entry)
95
+ ) {
96
+ return entry
97
+ }
98
+ } catch {
99
+ // Continue searching parent directories
100
+ }
101
+ }
102
+ const parent = dirname(directory)
103
+ if (parent === directory) return undefined
104
+ }
105
+ }
106
+
107
+ function runtime() {
108
+ const profileDir = resolve(
109
+ process.env.DSH_PROFILE_DIR ?? resolve(homedir(), '.dsh', 'profiles', 'web')
110
+ )
111
+ const selected = profileNameFromArgv(process.argv)
112
+ const profileName = validProfileName(selected)
113
+ ? selected
114
+ : validProfileName(basename(profileDir))
115
+ ? basename(profileDir)
116
+ : 'web'
117
+ const cliEntry = findDshCliEntry()
118
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
119
+ }
120
+
121
+ function parseSemver(value) {
122
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
123
+ if (match === null) return undefined
124
+ return {
125
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
126
+ prerelease: match[4]?.split('.') ?? [],
127
+ }
128
+ }
129
+
130
+ function comparePrerelease(left, right) {
131
+ if (left.length === 0 || right.length === 0) {
132
+ return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
133
+ }
134
+ const length = Math.max(left.length, right.length)
135
+ for (let index = 0; index < length; index += 1) {
136
+ const a = left[index]
137
+ const b = right[index]
138
+ if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
139
+ if (a === b) continue
140
+ const aNumeric = /^\d+$/.test(a)
141
+ const bNumeric = /^\d+$/.test(b)
142
+ if (aNumeric && bNumeric) {
143
+ const aNumber = BigInt(a)
144
+ const bNumber = BigInt(b)
145
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
146
+ continue
147
+ }
148
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
149
+ return a > b ? 1 : -1
150
+ }
151
+ return 0
152
+ }
153
+
154
+ export function isNewerVersion(currentValue, candidateValue) {
155
+ const current = parseSemver(currentValue)
156
+ const candidate = parseSemver(candidateValue)
157
+ if (current === undefined || candidate === undefined) return false
158
+ for (let index = 0; index < 3; index += 1) {
159
+ if (candidate.core[index] !== current.core[index]) {
160
+ return candidate.core[index] > current.core[index]
161
+ }
162
+ }
163
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0
164
+ }
165
+
166
+ async function latestVersion(packageName, registry) {
167
+ if (
168
+ latestCache?.packageName === packageName &&
169
+ latestCache.registry === registry &&
170
+ Date.now() < latestCache.expiresAt
171
+ ) {
172
+ return latestCache.version
173
+ }
174
+ try {
175
+ const response = await fetch(
176
+ `${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`,
177
+ { signal: AbortSignal.timeout(8_000) }
178
+ )
179
+ if (!response.ok) return undefined
180
+ const value = await response.json()
181
+ if (typeof value.version !== 'string' || value.version === '') return undefined
182
+ latestCache = {
183
+ packageName,
184
+ registry,
185
+ version: value.version,
186
+ expiresAt: Date.now() + VERSION_CACHE_MS,
187
+ }
188
+ return value.version
189
+ } catch {
190
+ return undefined
191
+ }
192
+ }
193
+
194
+ async function currentVersion(manifestUrl) {
195
+ const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
196
+ if (typeof value.version !== 'string' || value.version === '') {
197
+ throw new Error('Cannot read current plugin version.')
198
+ }
199
+ return value.version
200
+ }
201
+
202
+ async function status(options, target) {
203
+ const current = await currentVersion(options.manifestUrl)
204
+ const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
205
+ return {
206
+ packageName: options.packageName,
207
+ currentVersion: current,
208
+ ...(latest === undefined ? {} : { latestVersion: latest }),
209
+ latestCheckFailed: latest === undefined,
210
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
211
+ profileName: target.profileName,
212
+ canAutoUpdate: target.cliEntry !== undefined,
213
+ }
214
+ }
215
+
216
+ async function installExact(target, packageSpec, options) {
217
+ if (target.cliEntry === undefined) {
218
+ throw new Error('Automatic update is unavailable in this runtime.')
219
+ }
220
+ await new Promise((resolvePromise, reject) => {
221
+ const child = spawn(
222
+ process.execPath,
223
+ [
224
+ target.cliEntry,
225
+ 'plugin',
226
+ '--profile',
227
+ target.profileName,
228
+ 'add',
229
+ '--config.minimumReleaseAge=0',
230
+ packageSpec,
231
+ `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
232
+ ],
233
+ {
234
+ cwd: target.profileDir,
235
+ windowsHide: true,
236
+ stdio: ['ignore', 'pipe', 'pipe'],
237
+ env: { ...process.env, NO_COLOR: '1' },
238
+ }
239
+ )
240
+ let detail = ''
241
+ child.stdout?.on('data', (chunk) => {
242
+ detail = (detail + String(chunk)).slice(-4_000)
243
+ })
244
+ child.stderr?.on('data', (chunk) => {
245
+ detail = (detail + String(chunk)).slice(-4_000)
246
+ })
247
+ const timer = setTimeout(() => {
248
+ child.kill()
249
+ reject(new Error('Update timed out; use the normal DSH update flow.'))
250
+ }, UPDATE_TIMEOUT_MS)
251
+ child.once('error', (error) => {
252
+ clearTimeout(timer)
253
+ reject(error)
254
+ })
255
+ child.once('exit', (code) => {
256
+ clearTimeout(timer)
257
+ if (code === 0) resolvePromise()
258
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
259
+ })
260
+ })
261
+ }
262
+
263
+ function json(response, statusCode, value) {
264
+ response.writeHead(statusCode, {
265
+ 'content-type': 'application/json; charset=utf-8',
266
+ 'cache-control': 'no-store',
267
+ })
268
+ response.end(JSON.stringify(value))
269
+ }
270
+
271
+ export function registerPluginUpdater(ctx, options) {
272
+ const host = ctx
273
+ if (!host.webServer || typeof host.webServer.register !== 'function') {
274
+ return () => {}
275
+ }
276
+ let installing = false
277
+ return host.webServer.register({
278
+ kind: 'exact',
279
+ path: options.endpoint,
280
+ handler: async (request, response) => {
281
+ try {
282
+ const target = runtime()
283
+ if (request.method === 'GET' || request.method === 'HEAD') {
284
+ const payload = await status(options, target)
285
+ response.writeHead(200, {
286
+ 'content-type': 'application/json; charset=utf-8',
287
+ 'cache-control': 'no-store',
288
+ })
289
+ response.end(request.method === 'HEAD' ? undefined : JSON.stringify(payload))
290
+ return
291
+ }
292
+ if (request.method !== 'POST') {
293
+ response.writeHead(405, { allow: 'GET, HEAD, POST' })
294
+ response.end()
295
+ return
296
+ }
297
+ if (!isTrustedUpdateRequest(request)) {
298
+ json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
299
+ return
300
+ }
301
+ if (installing) {
302
+ json(response, 409, { error: 'This plugin is already updating.' })
303
+ return
304
+ }
305
+ installing = true
306
+ try {
307
+ const before = await status(options, target)
308
+ if (before.latestVersion === undefined) {
309
+ json(response, 503, { error: 'The latest version is temporarily unavailable.' })
310
+ return
311
+ }
312
+ if (!before.updateAvailable) {
313
+ json(response, 200, before)
314
+ return
315
+ }
316
+ await installExact(target, `${options.packageName}@${before.latestVersion}`, options)
317
+ json(response, 200, {
318
+ ...before,
319
+ updatedVersion: before.latestVersion,
320
+ restartRequired: true,
321
+ })
322
+ } finally {
323
+ installing = false
324
+ }
325
+ } catch (error) {
326
+ if (ctx.logger && typeof ctx.logger.warn === 'function') {
327
+ ctx.logger.warn(`plugin updater failed: ${String(error)}`)
328
+ }
329
+ json(response, 503, { error: 'Plugin update failed; see server logs.' })
330
+ }
331
+ },
332
+ })
333
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.16",
4
- "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-rc.2+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 6,808 keys (1,225 core UI keys + 5,583 keys across 40+ ecosystem plugins) with zero machine drafts. Features unified dsh-clinebot visual design standard for Settings Card, full translations for updated ecosystem plugins (dsh-model-sync, dsh-issue-reporter, dsh-model-search, dsh-session-control, dsh-grok-xsearch, dsh-cron, Live Canvas, dsh-goal, Skills Manager, Shadow Auditor, Kanban Epics, Key Rotation, Loop Guard, Remote Workspace, Cost Meter, Vision Bridge, Context, Time Machine), Smart Russian UX (localized Markdown export, turn translation, live typography, slash command aliases, Alt+L layout fix), expanded error humanizer (POSIX, HTTP 4xx/5xx), Skin Center & Wallpaper Engine support, Workshop market integration, sidebar previewers, and runtime snapshot extension without modifying core files.",
3
+ "version": "0.2.17",
4
+ "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-rc.2+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 7,149 keys (1,225 core UI keys + 5,924 keys across 45+ ecosystem plugins) with zero machine drafts. Features unified dsh-clinebot visual design standard for Settings Card, official One-click plugin updater (/api/dsh-russian-lang/update), full translations for updated ecosystem plugins (dsh-lanmode, dsh-moa, dsh-gitea, dsh-issue-reporter, dsh-messenger-gateway, dsh-usage-guard, dsh-cron, dsh-model-search, dsh-agent-orchestrator, dsh-model-sync, Live Canvas, dsh-goal, Skills Manager, Shadow Auditor, Kanban Epics, Key Rotation, Remote Workspace, Cost Meter, Vision Bridge, Time Machine), Smart Russian UX (localized Markdown export, turn translation, live typography, slash command aliases, Alt+L layout fix), expanded error humanizer (POSIX, HTTP 4xx/5xx), and runtime snapshot extension without modifying core files.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -38,6 +38,177 @@
38
38
  "clear": "Очистить",
39
39
  "noModelsMatch": "Моделей по запросу не найдено.",
40
40
  "useCustom": "Использовать свою",
41
- "selectModel": "Выберите модель..."
41
+ "selectModel": "Выберите модель...",
42
+ "header.title": "Оркестрация Mixture of Agents (MoA)",
43
+ "header.sub": "Параллельная генерация несколькими моделями-кандидатами с финальной оценкой и объединением ведущей моделью-судьёй. Автоматически возвращает результат в модель текущей сессии.",
44
+ "badge.online": "MoA активен",
45
+ "badge.offline": "Хост недоступен",
46
+ "badge.preset": "Пресет: {name}",
47
+ "badge.models": "Доступно моделей: {count}",
48
+ "badge.candidates": "Кандидатов: {count}",
49
+ "tab.orchestration": "Оркестрация",
50
+ "tab.critique": "Консилиум и критика",
51
+ "tab.stats": "Лидерборд и винрейт",
52
+ "tab.advanced": "Дополнительно",
53
+ "sec.roles_title": "Назначение ролей моделей",
54
+ "sec.roles_desc": "Выберите пул моделей-кандидатов (proposers) и синтезирующую модель-судью (judge)",
55
+ "sec.critique_title": "Режим взаимной критики (Peer Review)",
56
+ "sec.critique_desc": "Кандидаты анонимно рецензируют ответы друг друга перед финальным решением судьи",
57
+ "sec.stats_title": "Метрики и победы моделей",
58
+ "sec.stats_desc": "Статистика качества ответов и частота выбора модели судьёй",
59
+ "field.judge_model": "Модель-судья (Judge / Synthesizer)",
60
+ "field.judge_model_hint": "Финальная модель, которая оценивает ответы кандидатов и формирует итоговый ответ",
61
+ "field.proposer_models": "Модели-кандидаты (Proposers)",
62
+ "field.proposer_models_hint": "Модели, генерирующие независимые варианты ответа в параллельном режиме",
63
+ "field.rounds": "Количество раундов синтеза",
64
+ "field.rounds_hint": "Число итераций доработки и взаимной критики (рекомендуется 1–2)",
65
+ "field.temperature": "Температура генерации",
66
+ "field.temperature_hint": "Степень креативности для моделей-кандидатов (0.0 — детерминировано, 1.0 — вариативно)",
67
+ "field.max_tokens": "Лимит токенов на раунд",
68
+ "field.max_tokens_hint": "Максимальное число токенов для ответов каждого кандидата",
69
+ "field.peer_critique": "Включить взаимную критику (Peer Critique)",
70
+ "field.peer_critique_hint": "Перед передачей судье кандидаты выставляют оценки и пишут замечания к чужим ответам",
71
+ "field.blind_review": "Слепое рецензирование (Blind Review)",
72
+ "field.blind_review_hint": "Скрывать имена моделей при взаимной оценке для исключения предвзятости",
73
+ "field.curator_advice": "Совет куратора (Curator Guidance)",
74
+ "field.curator_advice_hint": "Инструкция судье, на какие аспекты обращать внимание (точность, лаконичность, код)",
75
+ "field.fallback_judges": "Резервные судьи (Fallback Judges)",
76
+ "field.fallback_judges_hint": "Цепочка моделей на случай превышения лимитов или недоступности основного судьи",
77
+ "field.timeout_sec": "Таймаут раунда (сек)",
78
+ "field.timeout_sec_hint": "Максимальное время ожидания ответа самого медленного кандидата",
79
+ "stat.total_runs": "Всего раундов MoA",
80
+ "stat.successful_runs": "Успешных раундов",
81
+ "stat.win_rate": "Доля побед моделей (Win Rate)",
82
+ "stat.avg_latency": "Среднее время раунда",
83
+ "stat.tokens_saved": "Экономия токенов",
84
+ "stat.no_data": "Статистика раундов пока пуста. Запустите MoA через /moa или кнопку выше.",
85
+ "col.model": "Модель",
86
+ "col.wins": "Побед",
87
+ "col.win_rate": "Винрейт",
88
+ "col.critique_score": "Оценка консилиума",
89
+ "col.avg_latency": "Средняя задержка",
90
+ "status.idle": "Готов к работе",
91
+ "status.proposing": "Генерация ответов кандидатами ({current}/{total})…",
92
+ "status.critique": "Взаимная критика и оценка ответов…",
93
+ "status.judging": "Судья {judge} синтезирует финальный ответ…",
94
+ "status.completed": "Раунд успешно завершён",
95
+ "status.error": "Ошибка раунда: {error}",
96
+ "error.no_proposers": "Необходимо выбрать хотя бы одну модель-кандидата",
97
+ "error.no_judge": "Необходимо указать модель-судью",
98
+ "error.timeout": "Превышено время ожидания ответа моделей ({timeout}с)",
99
+ "msg.saved": "Настройки MoA успешно сохранены",
100
+ "msg.reset": "Настройки сброшены к значениям по умолчанию",
101
+ "msg.stats_cleared": "История побед и статистика очищены",
102
+ "btn.run": "Запустить раунд MoA",
103
+ "btn.running": "Генерация ответов…",
104
+ "btn.save": "Сохранить конфигурацию",
105
+ "btn.saving": "Сохранение…",
106
+ "btn.reset": "Сбросить к умолчаниям",
107
+ "btn.refresh": "Обновить статус",
108
+ "btn.export_stats": "Экспорт статистики (CSV/JSON)",
109
+ "btn.clear_stats": "Очистить статистику",
110
+ "card.critique_title": "Карточка взаимной критики",
111
+ "card.critique_score": "Средний балл: {score}/10",
112
+ "card.strengths": "Сильные стороны:",
113
+ "card.weaknesses": "Замечания:",
114
+ "card.verdict": "Вердикт судьи:",
115
+ "card.winner": "Выбранный кандидат: {model}",
116
+ "card.consensus": "Степень согласия консилиума: {consensus}%",
117
+ "export.json": "Экспорт JSON",
118
+ "export.csv": "Экспорт CSV",
119
+ "export.copied": "Данные скопированы в буфер обмена",
120
+ "help.title": "Справка по Mixture of Agents",
121
+ "help.desc": "MoA комбинирует сильные стороны разных моделей (код, фактология, логика) для получения результата, превосходящего любую одиночную модель.",
122
+ "help.slash_command": "Слэш-команда:",
123
+ "help.slash_example": "/moa Сравни подходы к кэшированию и составь архитектуру",
124
+ "help.doc_link": "Документация и лучшие практики",
125
+ "slash.desc": "Выполнить запрос через оркестрацию нескольких моделей (MoA)",
126
+ "toast.copied": "Скопировано в буфер обмена",
127
+ "toast.cleared": "История очищена",
128
+ "empty.models": "Нет доступных моделей для выбора",
129
+ "empty.proposers": "Выберите хотя бы одну модель-кандидата в списке выше",
130
+ "tag.recommended": "Рекомендуется",
131
+ "tag.fast": "Быстрая",
132
+ "tag.reasoning": "Рассуждающая",
133
+ "tag.coder": "Для кода",
134
+ "presets.title": "Управление пресетами MoA",
135
+ "presets.desc": "Готовые наборы ролей, моделей-кандидатов и параметров судейства для различных типов задач",
136
+ "presets.active_badge": "Активный пресет",
137
+ "presets.set_default": "Сделать по умолчанию",
138
+ "presets.new_btn": "Создать пресет…",
139
+ "presets.del_btn": "Удалить пресет",
140
+ "presets.del_confirm": "Вы уверены, что хотите удалить этот пресет?",
141
+ "presets.create": "Создать",
142
+ "presets.cancel": "Отмена",
143
+ "presets.name_placeholder": "Имя пресета (например: Code Review, Deep Debate)",
144
+ "aggregator.title": "Модель-судья и агрегация (Judge / Aggregator)",
145
+ "aggregator.desc": "Модель, которая оценивает ответы кандидатов, применяет критерии и формирует итоговый результат",
146
+ "aggregator.temp_label": "Температура судьи",
147
+ "aggregator.temp_hint": "Степень вариативности синтеза (обычно 0.1–0.3 для строгой оценки)",
148
+ "aggregator.criteria_label": "Критерии оценки судьи",
149
+ "aggregator.criteria_placeholder": "Оценивай корректность кода, отсутствие галлюцинаций, следование стилю...",
150
+ "aggregator.questions_label": "Контрольные вопросы для критики",
151
+ "aggregator.questions_hint": "Список контрольных проверок при сопоставлении ответов кандидатов",
152
+ "aggregator.curator_label": "Инструкция куратора",
153
+ "aggregator.curator_hint": "Специальные указания по синтезу ответов",
154
+ "aggregator.stream_label": "Потоковый вывод агрегатора",
155
+ "aggregator.stream_hint": "Отображать финальный синтез в реальном времени",
156
+ "aggregator.peer_label": "Режим взаимного консилиума",
157
+ "aggregator.peer_hint": "Включить раунд критики между кандидатами перед вынесением вердикта",
158
+ "aggregator.override_label": "Переопределение системного промпта",
159
+ "aggregator.override_hint": "Заменить системную инструкцию судьи на собственную",
160
+ "proposers.role_label": "Специализация кандидата",
161
+ "proposers.role_general": "Общий ассистент (General)",
162
+ "proposers.role_minimalist": "Минималист (Ponytail/KISS)",
163
+ "proposers.role_robustness": "Надёжность и безопасность",
164
+ "proposers.role_performance": "Производительность и оптимизация",
165
+ "proposers.role_tester": "Тестирование и валидация",
166
+ "leaderboard.filter_all": "Все модели",
167
+ "leaderboard.export_csv": "Экспорт CSV",
168
+ "leaderboard.export_json": "Экспорт JSON",
169
+ "aggregator.blind_label": "Слепое судейство (Blind Evaluation)",
170
+ "aggregator.blind_hint": "Анонимизировать ответы кандидатов, чтобы исключить предвзятость к конкретной модели",
171
+ "aggregator.timeout_label": "Таймаут агрегатора (сек)",
172
+ "proposers.timeout_label": "Таймаут кандидатов (сек)",
173
+ "leaderboard.title": "Таблица побед моделей (Leaderboard)",
174
+ "leaderboard.desc": "Рейтинг эффективности кандидатов по оценкам судьи за все проведённые раунды",
175
+ "leaderboard.model": "Модель",
176
+ "leaderboard.runs": "Раундов",
177
+ "leaderboard.wins": "Побед",
178
+ "leaderboard.winrate": "Винрейт",
179
+ "leaderboard.cost": "Расход токенов",
180
+ "leaderboard.empty": "История раундов пока пуста",
181
+ "aggregator.fallbacks_title": "Резервные модели-судьи",
182
+ "aggregator.fallbacks_desc": "Цепочка запасных моделей при отказе или превышении квот основного судьи",
183
+ "aggregator.add_fallback_btn": "Добавить резервную модель…",
184
+ "proposers.quorum_label": "Кворум ответов",
185
+ "proposers.quorum_hint": "Минимальное количество успешных ответов кандидатов для продолжения раунда",
186
+ "proposers.grace_label": "Льготное время ожидания (сек)",
187
+ "proposers.title": "Пул моделей-кандидатов (Proposers)",
188
+ "proposers.desc": "Список независимых моделей, параллельно генерирующих варианты ответа",
189
+ "proposers.parallel_count": "Параллельных потоков: {count}",
190
+ "proposers.candidate": "Кандидат #{index}",
191
+ "proposers.add_btn": "Добавить модель-кандидата…",
192
+ "proposers.temp_label": "Температура кандидата",
193
+ "proposers.temp_hint": "Степень вариативности ответа кандидата",
194
+ "stats.title": "Сводная телеметрия MoA",
195
+ "stats.desc": "Общая статистика генераций, экономия и средняя стоимость",
196
+ "stats.total_runs": "Всего раундов",
197
+ "stats.avg_cost": "Средняя стоимость",
198
+ "stats.active_models": "Активных моделей",
199
+ "stats.preset_count": "Пресетов",
200
+ "picker.select": "Выбрать модель",
201
+ "picker.search_placeholder": "Поиск модели по имени или провайдеру…",
202
+ "picker.found": "Найдено:",
203
+ "picker.of": "из",
204
+ "picker.clear": "Очистить",
205
+ "picker.none": "Модель не выбрана",
206
+ "picker.custom": "Указать вручную…",
207
+ "actions.save": "Сохранить конфигурацию",
208
+ "actions.saving": "Сохранение…",
209
+ "actions.saved": "Сохранено!",
210
+ "actions.retry": "Повторить",
211
+ "badge.disabled": "Отключено",
212
+ "config.enabled_label": "Включить модуль Mixture of Agents"
42
213
  }
43
214
  }
@@ -176,6 +176,61 @@
176
176
  "voiceModeMirror": "В ответ на входящие голосовые",
177
177
  "voiceModeOff": "Выключено (если не включён /voice)",
178
178
  "webhookSecretHint": "Оставьте пустым для сохранения текущего",
179
- "webhookUrlHint": "Полный HTTPS URL, на который Telegram отправляет POST-запросы"
179
+ "webhookUrlHint": "Полный HTTPS URL, на который Telegram отправляет POST-запросы",
180
+ "settings.loading": "Загрузка настроек шлюза мессенджеров…",
181
+ "settings.retry": "Повторить",
182
+ "settings.unavailable": "Настройки недоступны (хостовый модуль не готов).",
183
+ "header.title": "Шлюз Telegram и мессенджеров",
184
+ "header.sub": "Мост между DeepSeek Harness и Telegram: сессии, управление, уведомления и доступ с мобильных",
185
+ "badge.online": "Telegram онлайн (@{username})",
186
+ "badge.offline": "Telegram отключен",
187
+ "badge.pending": "Ожидание подключения бота…",
188
+ "sec.bot_title": "Параметры Telegram-бота",
189
+ "sec.bot_desc": "Укажите токен бота от @BotFather для приёма сообщений и запуска сессий DSH",
190
+ "sec.access_title": "Контроль доступа и белый список",
191
+ "sec.access_desc": "Ограничение доступа по Telegram User ID или разовым кодам сопряжения",
192
+ "sec.notify_title": "Уведомления о событиях",
193
+ "sec.notify_desc": "Оповещения в Telegram о завершении ответов и ошибках",
194
+ "field.token": "Bot Token (Telegram)",
195
+ "field.token_hint": "Токен бота от @BotFather (хранится безопасно в DSH Credentials)",
196
+ "field.allowed_users": "Разрешённые ID пользователей",
197
+ "field.allowed_users_hint": "Список Telegram ID через запятую (оставьте пустым для сопряжения по коду)",
198
+ "field.pairing_code": "Код сопряжения (Pairing Code)",
199
+ "field.pairing_code_hint": "Введите этот 6-значный код боту для авторизации нового устройства",
200
+ "field.notify_on_turn": "Уведомлять о завершении хода",
201
+ "field.notify_on_turn_hint": "Отправлять сообщение в Telegram, когда модель закончила длинный ответ",
202
+ "field.notify_on_error": "Оповещать об ошибках выполнения",
203
+ "field.notify_on_error_hint": "Срочные уведомления о падении агента или превышении лимитов",
204
+ "stat.active_chats": "Активных чатов",
205
+ "stat.messages_relayed": "Передано сообщений",
206
+ "stat.avg_latency": "Пинг до Telegram API",
207
+ "btn.ping": "Проверить связь (Ping)",
208
+ "btn.regenerate_code": "Новый код сопряжения",
209
+ "btn.disconnect": "Отключить бота",
210
+ "msg.ping_ok": "Связь с Telegram API установлена ({ms} мс)",
211
+ "msg.ping_failed": "Не удалось связаться с Telegram API: {error}",
212
+ "badge.running": "Шлюз запущен",
213
+ "badge.stopped": "Шлюз остановлен",
214
+ "badge.token_ok": "Токен настроен",
215
+ "badge.token_missing": "Токен отсутствует",
216
+ "badge.pairing_count": "Сопряжено устройств: {count}",
217
+ "badge.transport": "Транспорт: Long Polling",
218
+ "telegramSub": "Настройки Telegram Bot API и интеграции",
219
+ "agentSub": "Назначение модели и системных инструкций для Telegram-сессий",
220
+ "mediaSub": "Приём и отправка голосовых сообщений, фото и файлов",
221
+ "ttsSub": "Синтез речи (TTS) для голосовых ответов бота",
222
+ "pairingSub": "Управление авторизованными пользователями и разовыми кодами",
223
+ "diag.title": "Диагностика связи и Smoke-тест",
224
+ "diag.desc": "Проверка доступности Telegram Bot API, пинга и прав бота",
225
+ "diag.smoke_btn": "Запустить Smoke-тест",
226
+ "diag.smoke_testing": "Тестирование шлюза…",
227
+ "diag.smoke_ok": "Smoke-тест пройден успешно! Бот готов к работе.",
228
+ "diag.smoke_err": "Smoke-тест завершился с ошибкой: {error}",
229
+ "stats.title": "Метрики шлюза",
230
+ "stats.desc": "Текущая статистика передаваемых сообщений и времени аптайма",
231
+ "stats.sent": "Отправлено сообщений",
232
+ "stats.errors": "Ошибок шлюза",
233
+ "stats.uptime": "Время работы",
234
+ "stats.active_chats": "Активных чатов"
180
235
  }
181
236
  }