@goodandready/dsh-russian-lang 0.2.20 → 0.2.21
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 +12 -2
- package/README.ru.md +12 -2
- package/lib/client.js +127 -67
- package/lib/index.js +10 -2
- package/lib/translator.js +303 -0
- package/package.json +1 -2
- package/README.zh.md +0 -102
package/README.md
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
+
> [!NOTE]
|
|
44
|
+
> **Языковая политика проекта**: `dsh-russian-lang` — специализированный плагин русской локализации интерфейса DSH. Каноническая документация ведётся исключительно на русском языке (`README.md`, `README.ru.md`). Общее требование трёхъязычной документации к нему не применяется.
|
|
45
|
+
|
|
43
46
|
## ⚡ Обзор
|
|
44
47
|
|
|
45
48
|
**`dsh-russian-lang`** — официальный, всеобъемлющий пакет русской локализации для веб-интерфейса [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
|
|
@@ -217,5 +220,12 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
217
220
|
- Мгновенная конвертация набранного транслита в кириллицу и обратно прямо в поле ввода (`privet` ⇄ `привет`).
|
|
218
221
|
- *(Deprecated since v0.2.15: экспериментальный UI-перехватчик удалён для обеспечения беспрепятственного нативного ввода Lexical; программная функция `phoneticTranslit` сохранена в `lib/pure.js`).*
|
|
219
222
|
|
|
220
|
-
7. **Инлайн-перевод реплик ассистента (`RU ↗`)
|
|
221
|
-
- Кнопка `RU ↗` в панели действий ответа ассистента
|
|
223
|
+
7. **Инлайн-перевод реплик ассистента (`RU ↗`) и приватность**:
|
|
224
|
+
- Кнопка `RU ↗` в панели действий ответа ассистента позволяет переводить ответы модели на русский язык с сохранением блоков кода (`📋 Копировать`).
|
|
225
|
+
- 🔒 **Конфиденциальность и выбор движка**:
|
|
226
|
+
- **По умолчанию (Выключен)**: Никакие данные сообщений никуда не отправляются. Клик по кнопке информирует о необходимости выбрать движок.
|
|
227
|
+
- **Локальный LibreTranslate** *(Рекомендуется)*: Работает на 100% автономно на вашем сервере в Docker-контейнере (`~500–700 МБ RAM`). Запускается в 1 клик прямо из карточки настроек DSH или вручную:
|
|
228
|
+
```bash
|
|
229
|
+
docker run -d --name libretranslate --restart unless-stopped -p 127.0.0.1:5000:5000 libretranslate/libretranslate --load-only en,ru,zh
|
|
230
|
+
```
|
|
231
|
+
- **Google Translate (Онлайн)**: Для пользователей, которым удобен онлайн-перевод без выделения ресурсов RAM. В настройках отображается явное предупреждение о передаче текста на публичные серверы Google.
|
package/README.ru.md
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
+
> [!NOTE]
|
|
44
|
+
> **Языковая политика проекта**: `dsh-russian-lang` — специализированный плагин русской локализации интерфейса DSH. Каноническая документация ведётся исключительно на русском языке (`README.md`, `README.ru.md`). Общее требование трёхъязычной документации к нему не применяется.
|
|
45
|
+
|
|
43
46
|
## ⚡ Обзор
|
|
44
47
|
|
|
45
48
|
**`dsh-russian-lang`** — официальный, всеобъемлющий пакет русской локализации для веб-интерфейса [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
|
|
@@ -217,5 +220,12 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
217
220
|
- Мгновенная конвертация набранного транслита в кириллицу и обратно прямо в поле ввода (`privet` ⇄ `привет`).
|
|
218
221
|
- *(Deprecated since v0.2.14: экспериментальный UI-перехватчик удалён для обеспечения беспрепятственного нативного ввода Lexical; программная функция `phoneticTranslit` сохранена в `lib/pure.js`).*
|
|
219
222
|
|
|
220
|
-
7. **Инлайн-перевод реплик ассистента (`RU ↗`)
|
|
221
|
-
- Кнопка `RU ↗` в панели действий ответа ассистента
|
|
223
|
+
7. **Инлайн-перевод реплик ассистента (`RU ↗`) и приватность**:
|
|
224
|
+
- Кнопка `RU ↗` в панели действий ответа ассистента позволяет переводить ответы модели на русский язык с сохранением блоков кода (`📋 Копировать`).
|
|
225
|
+
- 🔒 **Конфиденциальность и выбор движка**:
|
|
226
|
+
- **По умолчанию (Выключен)**: Никакие данные сообщений никуда не отправляются. Клик по кнопке информирует о необходимости выбрать движок.
|
|
227
|
+
- **Локальный LibreTranslate** *(Рекомендуется)*: Работает на 100% автономно на вашем сервере в Docker-контейнере (`~500–700 МБ RAM`). Запускается в 1 клик прямо из карточки настроек DSH или вручную:
|
|
228
|
+
```bash
|
|
229
|
+
docker run -d --name libretranslate --restart unless-stopped -p 127.0.0.1:5000:5000 libretranslate/libretranslate --load-only en,ru,zh
|
|
230
|
+
```
|
|
231
|
+
- **Google Translate (Онлайн)**: Для пользователей, которым удобен онлайн-перевод без выделения ресурсов RAM. В настройках отображается явное предупреждение о передаче текста на публичные серверы Google.
|
package/lib/client.js
CHANGED
|
@@ -522,7 +522,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
522
522
|
/** zh-строка -> ru-строка для DOM-перевода панелей вне locale-ядра. */
|
|
523
523
|
const ZH_RU = {}
|
|
524
524
|
|
|
525
|
-
RU['russian-lang'] = {"cardTitle": "Русская локализация", "cardSub": "Язык интерфейса, типографика, раскладка", "badgeRu": "🟢 RU активен", "badgeEn": "⚪ EN активен", "badgeCoverage": "🟢 100% (7,902 ключа)", "badgeSmartUx": "⚡ Smart UX активен", "secLanguage": "🌐 Язык интерфейса", "secLanguageDesc": "Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.", "enabled": "Русский язык включён", "enabledDesc": "Переключает язык интерфейса DeepSeek Harness на русский.", "quickSwitchNote": "Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.", "secTypography": "✍️ Умная типографика и ввод (Smart UX)", "secTypographyDesc": "Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.", "typography": "Типографика вывода", "typographyDesc": "Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.", "yo": "Буква «ё»", "yoDesc": "Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.", "liveInput": "Живая типографика инпута", "liveInputDesc": "Автоматически заменять \"\" на «» и -- на — прямо во время набора промпта (код в бэктиках игнорируется).", "slashAliases": "Русские алиасы команд", "slashAliasesDesc": "Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.", "altLHintText": "Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.", "secAgentPrompt": "🤖 Системный промпт агента", "secAgentPromptDesc": "Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.", "agentPrompt": "Русский промпт агента", "agentPromptDesc": "Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.", "agentPromptPreset": "Стиль ответов агента", "presetExpert": "Технический эксперт (строгая терминология, чистый код)", "presetWriter": "Технический писатель (Markdown, таблицы, ГОСТ)", "presetConcise": "Лаконичный режим (кратко, без лишней воды)", "secUpdater": "🔄 Обновление языкового пакета", "secUpdaterDesc": "Проверка наличия новых релизов в реестре npm и обновление в один клик.", "updaterCurrent": "Текущая версия: v{version}", "updaterLatest": "Доступна новая версия: v{version}", "updaterUpToDate": "Установлена актуальная версия", "updaterChecking": "Проверка…", "updaterCheckBtn": "Проверить обновления", "updaterBtn": "Обновить до v{version} в 1 клик", "updaterUpdating": "Установка обновления…", "updaterSuccess": "✅ Плагин успешно обновлён! Перезапустите DSH для применения.", "updaterFailed": "❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.", "badgeUpdateAvailable": "Доступно обновление", "badgeUpToDate": "Актуальная версия", "secSupport": "📊 Покрытие экосистемы и поддержка", "secSupportDesc": "Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.", "statNamespaces": "Пространств имён", "statCoreKeys": "Ключей ядра", "statPluginKeys": "Ключей плагинов", "overridesCount": "Своих переопределений", "statusLoading": "Настройки загружаются…", "statusUnavailable": "Настройки недоступны на этом хосте", "translateTurn": "Перевести на русский", "reportIssue": "Сообщить о неточности перевода", "exportMdHint": "Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.", "translateTurnHint": "Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
|
|
525
|
+
RU['russian-lang'] = {"cardTitle": "Русская локализация", "cardSub": "Язык интерфейса, типографика, раскладка", "badgeRu": "🟢 RU активен", "badgeEn": "⚪ EN активен", "badgeCoverage": "🟢 100% (7,902 ключа)", "badgeSmartUx": "⚡ Smart UX активен", "secLanguage": "🌐 Язык интерфейса", "secLanguageDesc": "Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.", "enabled": "Русский язык включён", "enabledDesc": "Переключает язык интерфейса DeepSeek Harness на русский.", "quickSwitchNote": "Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.", "secTypography": "✍️ Умная типографика и ввод (Smart UX)", "secTypographyDesc": "Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.", "typography": "Типографика вывода", "typographyDesc": "Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.", "yo": "Буква «ё»", "yoDesc": "Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.", "liveInput": "Живая типографика инпута", "liveInputDesc": "Автоматически заменять \"\" на «» и -- на — прямо во время набора промпта (код в бэктиках игнорируется).", "slashAliases": "Русские алиасы команд", "slashAliasesDesc": "Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.", "altLHintText": "Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.", "secAgentPrompt": "🤖 Системный промпт агента", "secAgentPromptDesc": "Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.", "agentPrompt": "Русский промпт агента", "agentPromptDesc": "Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.", "agentPromptPreset": "Стиль ответов агента", "presetExpert": "Технический эксперт (строгая терминология, чистый код)", "presetWriter": "Технический писатель (Markdown, таблицы, ГОСТ)", "presetConcise": "Лаконичный режим (кратко, без лишней воды)", "secUpdater": "🔄 Обновление языкового пакета", "secUpdaterDesc": "Проверка наличия новых релизов в реестре npm и обновление в один клик.", "updaterCurrent": "Текущая версия: v{version}", "updaterLatest": "Доступна новая версия: v{version}", "updaterUpToDate": "Установлена актуальная версия", "updaterChecking": "Проверка…", "updaterCheckBtn": "Проверить обновления", "updaterBtn": "Обновить до v{version} в 1 клик", "updaterUpdating": "Установка обновления…", "updaterSuccess": "✅ Плагин успешно обновлён! Перезапустите DSH для применения.", "updaterFailed": "❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.", "badgeUpdateAvailable": "Доступно обновление", "badgeUpToDate": "Актуальная версия", "secTranslator": "🌐 Перевод сообщений ассистента", "secTranslatorDesc": "Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).", "translateEngine": "Движок перевода", "engineOff": "Выключен (по умолчанию, без сетевых вызовов)", "engineLocal": "Локальный LibreTranslate (приватно, ~600 МБ RAM)", "engineGoogle": "Google Translate (онлайн)", "googleWarn": "⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.", "localStatusRunning": "🟢 Контейнер LibreTranslate активен", "localStatusStopped": "⚪ Контейнер LibreTranslate не запущен", "localStartBtn": "Запустить LibreTranslate в Docker (~600 МБ RAM)", "localStarting": "Запуск контейнера…", "localInfo": "LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.", "secSupport": "📊 Покрытие экосистемы и поддержка", "secSupportDesc": "Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.", "statNamespaces": "Пространств имён", "statCoreKeys": "Ключей ядра", "statPluginKeys": "Ключей плагинов", "overridesCount": "Своих переопределений", "statusLoading": "Настройки загружаются…", "statusUnavailable": "Настройки недоступны на этом хосте", "translateTurn": "Перевести на русский", "reportIssue": "Сообщить о неточности перевода", "exportMdHint": "Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.", "translateTurnHint": "Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
|
|
526
526
|
|
|
527
527
|
const SETTINGS_NS_NAME = 'russian-lang'
|
|
528
528
|
|
|
@@ -1505,62 +1505,21 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1505
1505
|
}
|
|
1506
1506
|
|
|
1507
1507
|
async function translateTurnContent(text) {
|
|
1508
|
-
if (!text || typeof text !== 'string') return ''
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
const
|
|
1516
|
-
if (!
|
|
1517
|
-
|
|
1518
|
-
const translatedParas = []
|
|
1519
|
-
for (const para of paragraphs) {
|
|
1520
|
-
const pTrim = para.trim()
|
|
1521
|
-
if (!pTrim) continue
|
|
1522
|
-
|
|
1523
|
-
const sentences = pTrim.match(/[^.!?\n]+[.!?\n]*/g) || [pTrim]
|
|
1524
|
-
const translatedSentences = []
|
|
1525
|
-
|
|
1526
|
-
for (const s of sentences) {
|
|
1527
|
-
let translated = ''
|
|
1528
|
-
|
|
1529
|
-
try {
|
|
1530
|
-
const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ru&dt=t&q=' + encodeURIComponent(s)
|
|
1531
|
-
const gres = await fetch(gurl)
|
|
1532
|
-
if (gres.ok) {
|
|
1533
|
-
const gdata = await gres.json()
|
|
1534
|
-
if (Array.isArray(gdata) && Array.isArray(gdata[0])) {
|
|
1535
|
-
translated = gdata[0].map((it) => it[0]).join('')
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
} catch (e1) {}
|
|
1539
|
-
|
|
1540
|
-
if (!translated) {
|
|
1541
|
-
try {
|
|
1542
|
-
const isZh = /[\u3400-\u9fff]/.test(s)
|
|
1543
|
-
const langPair = isZh ? 'zh-CN|ru' : 'en|ru'
|
|
1544
|
-
const murl = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(s.slice(0, 450)) + '&langpair=' + langPair
|
|
1545
|
-
const mres = await fetch(murl)
|
|
1546
|
-
if (mres.ok) {
|
|
1547
|
-
const mdata = await mres.json()
|
|
1548
|
-
if (mdata && mdata.responseData && mdata.responseData.translatedText) {
|
|
1549
|
-
const txtEl = document.createElement('textarea')
|
|
1550
|
-
txtEl.innerHTML = mdata.responseData.translatedText
|
|
1551
|
-
translated = txtEl.value
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
} catch (e2) {}
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
translatedSentences.push(translated || s)
|
|
1558
|
-
}
|
|
1559
|
-
translatedParas.push(translatedSentences.join(' '))
|
|
1508
|
+
if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
|
|
1509
|
+
try {
|
|
1510
|
+
const res = await fetch('/api/dsh-russian-lang/translate', {
|
|
1511
|
+
method: 'POST',
|
|
1512
|
+
headers: { 'content-type': 'application/json' },
|
|
1513
|
+
body: JSON.stringify({ text })
|
|
1514
|
+
})
|
|
1515
|
+
const data = await res.json().catch(() => ({}))
|
|
1516
|
+
if (!res.ok) {
|
|
1517
|
+
return { error: data.error || ('Ошибка перевода (HTTP ' + res.status + ')') }
|
|
1560
1518
|
}
|
|
1561
|
-
|
|
1519
|
+
return { translatedText: data.translatedText || text }
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
|
|
1562
1522
|
}
|
|
1563
|
-
return parts.join('\n\n')
|
|
1564
1523
|
}
|
|
1565
1524
|
|
|
1566
1525
|
function TranslateTurnAction(props) {
|
|
@@ -1613,8 +1572,42 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1613
1572
|
})
|
|
1614
1573
|
}
|
|
1615
1574
|
|
|
1575
|
+
const [transStatus, setTransStatus] = React.useState(null)
|
|
1576
|
+
const [transStarting, setTransStarting] = React.useState(false)
|
|
1577
|
+
const [transMsg, setTransMsg] = React.useState(null)
|
|
1578
|
+
|
|
1579
|
+
const fetchTransStatus = () => {
|
|
1580
|
+
fetch('/api/dsh-russian-lang/translator/status')
|
|
1581
|
+
.then((r) => r.json())
|
|
1582
|
+
.then((data) => setTransStatus(data))
|
|
1583
|
+
.catch(() => setTransStatus(null))
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const startLibreTranslate = () => {
|
|
1587
|
+
setTransStarting(true)
|
|
1588
|
+
setTransMsg(null)
|
|
1589
|
+
fetch('/api/dsh-russian-lang/translator/setup', { method: 'POST' })
|
|
1590
|
+
.then((r) => r.json())
|
|
1591
|
+
.then((data) => {
|
|
1592
|
+
setTransStarting(false)
|
|
1593
|
+
if (data.ok) {
|
|
1594
|
+
setTransMsg({ type: 'ok', text: data.message || 'Контейнер LibreTranslate запущен.' })
|
|
1595
|
+
fetchTransStatus()
|
|
1596
|
+
} else {
|
|
1597
|
+
setTransMsg({ type: 'err', text: data.error || 'Ошибка запуска контейнера.' })
|
|
1598
|
+
}
|
|
1599
|
+
})
|
|
1600
|
+
.catch((err) => {
|
|
1601
|
+
setTransStarting(false)
|
|
1602
|
+
setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
|
|
1603
|
+
})
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1616
1606
|
React.useEffect(() => {
|
|
1617
|
-
if (open)
|
|
1607
|
+
if (open) {
|
|
1608
|
+
checkUpdate()
|
|
1609
|
+
fetchTransStatus()
|
|
1610
|
+
}
|
|
1618
1611
|
}, [open])
|
|
1619
1612
|
|
|
1620
1613
|
|
|
@@ -1676,29 +1669,36 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1676
1669
|
if (prose) rawText = prose.innerText.trim()
|
|
1677
1670
|
}
|
|
1678
1671
|
|
|
1679
|
-
|
|
1672
|
+
let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
|
|
1673
|
+
if (rawText) {
|
|
1674
|
+
transResult = await translateTurnContent(rawText)
|
|
1675
|
+
}
|
|
1676
|
+
const isErr = !!transResult.error
|
|
1677
|
+
const displayText = transResult.translatedText || transResult.error || ''
|
|
1680
1678
|
|
|
1681
1679
|
box = document.createElement('div')
|
|
1682
1680
|
box.className = 'rl-turn-translation'
|
|
1683
1681
|
const key = tailFlowItem.getAttribute('data-chat-flow-key')
|
|
1684
1682
|
if (key) box.dataset.tailKey = key
|
|
1685
1683
|
box.innerHTML = '<div class="rl-trans-head">' +
|
|
1686
|
-
'<span class="rl-trans-title"
|
|
1684
|
+
'<span class="rl-trans-title">' + (isErr ? '⚠️ Машинный перевод' : '🌐 Перевод на русский') + '</span>' +
|
|
1687
1685
|
'<div class="rl-trans-tools">' +
|
|
1688
|
-
'<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' +
|
|
1686
|
+
(!isErr ? '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' : '') +
|
|
1689
1687
|
'<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
|
|
1690
1688
|
'</div>' +
|
|
1691
1689
|
'</div>' +
|
|
1692
|
-
'<div class="rl-trans-body"></div>'
|
|
1693
|
-
box.querySelector('.rl-trans-body').textContent =
|
|
1690
|
+
'<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-color-warning, #eab308); font-size: 13px;"' : '') + '></div>'
|
|
1691
|
+
box.querySelector('.rl-trans-body').textContent = displayText
|
|
1694
1692
|
|
|
1695
1693
|
const copyBtn = box.querySelector('.rl-btn-copy')
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1694
|
+
if (copyBtn) {
|
|
1695
|
+
copyBtn.addEventListener('click', (e) => {
|
|
1696
|
+
e.stopPropagation()
|
|
1697
|
+
try { navigator.clipboard.writeText(displayText) } catch (err) {}
|
|
1698
|
+
copyBtn.textContent = '✓ Скопировано'
|
|
1699
|
+
setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
|
|
1700
|
+
})
|
|
1701
|
+
}
|
|
1702
1702
|
|
|
1703
1703
|
const closeBtn = box.querySelector('.rl-btn-close')
|
|
1704
1704
|
closeBtn.addEventListener('click', (e) => {
|
|
@@ -1999,6 +1999,66 @@ const exportSessionToMarkdown = (session, options = {}) => {
|
|
|
1999
1999
|
)
|
|
2000
2000
|
),
|
|
2001
2001
|
|
|
2002
|
+
React.createElement('div', { className: 'rl-section-card' },
|
|
2003
|
+
React.createElement('div', { className: 'rl-section-title' },
|
|
2004
|
+
React.createElement('span', null, t('secTranslator')),
|
|
2005
|
+
React.createElement('span', {
|
|
2006
|
+
className: 'rl-badge ' + (value.translateEngine === 'local' ? 'rl-badge-ok' : (value.translateEngine === 'google' ? 'rl-badge-warn' : 'rl-badge-dim'))
|
|
2007
|
+
}, value.translateEngine === 'local' ? 'Локально' : (value.translateEngine === 'google' ? 'Google' : 'Выключен'))
|
|
2008
|
+
),
|
|
2009
|
+
React.createElement('div', { className: 'rl-section-desc' }, t('secTranslatorDesc')),
|
|
2010
|
+
React.createElement('div', { className: 'rl-item-card' },
|
|
2011
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
2012
|
+
React.createElement('label', { className: 'rl-item-label', style: { fontWeight: 500 } }, t('translateEngine')),
|
|
2013
|
+
React.createElement('select', {
|
|
2014
|
+
className: 'rl-select',
|
|
2015
|
+
value: value.translateEngine || 'off',
|
|
2016
|
+
onChange: (ev) => {
|
|
2017
|
+
try {
|
|
2018
|
+
const val = ev.target.value
|
|
2019
|
+
scope.set('translateEngine', val)
|
|
2020
|
+
if (val === 'local') fetchTransStatus()
|
|
2021
|
+
} catch (err) { console.warn('dsh-russian-lang: set translateEngine failed', err) }
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
2024
|
+
React.createElement('option', { value: 'off' }, t('engineOff')),
|
|
2025
|
+
React.createElement('option', { value: 'local' }, t('engineLocal')),
|
|
2026
|
+
React.createElement('option', { value: 'google' }, t('engineGoogle'))
|
|
2027
|
+
)
|
|
2028
|
+
),
|
|
2029
|
+
value.translateEngine === 'google' ? React.createElement('div', {
|
|
2030
|
+
style: {
|
|
2031
|
+
marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
|
|
2032
|
+
background: 'rgba(234, 179, 8, 0.12)', border: '1px solid rgba(234, 179, 8, 0.3)',
|
|
2033
|
+
color: 'var(--dsw-alias-color-warning, #eab308)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
|
|
2034
|
+
}
|
|
2035
|
+
}, t('googleWarn')) : null,
|
|
2036
|
+
value.translateEngine === 'local' ? React.createElement('div', {
|
|
2037
|
+
style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }
|
|
2038
|
+
},
|
|
2039
|
+
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('localInfo')),
|
|
2040
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
|
|
2041
|
+
React.createElement('span', {
|
|
2042
|
+
className: 'rl-badge ' + (transStatus && transStatus.running ? 'rl-badge-ok' : 'rl-badge-dim')
|
|
2043
|
+
}, transStatus && transStatus.running ? t('localStatusRunning') : t('localStatusStopped')),
|
|
2044
|
+
(!transStatus || !transStatus.running) ? React.createElement('button', {
|
|
2045
|
+
type: 'button',
|
|
2046
|
+
className: 'rl-btn rl-btn-primary',
|
|
2047
|
+
disabled: transStarting,
|
|
2048
|
+
onClick: startLibreTranslate
|
|
2049
|
+
}, transStarting ? t('localStarting') : t('localStartBtn')) : null
|
|
2050
|
+
),
|
|
2051
|
+
transMsg ? React.createElement('div', {
|
|
2052
|
+
style: {
|
|
2053
|
+
marginTop: '6px', padding: '8px 12px', borderRadius: '8px',
|
|
2054
|
+
background: transMsg.type === 'ok' ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
|
|
2055
|
+
color: transMsg.type === 'ok' ? '#16a34a' : '#dc2626', fontSize: '12px', fontWeight: 500
|
|
2056
|
+
}
|
|
2057
|
+
}, transMsg.text) : null
|
|
2058
|
+
) : null
|
|
2059
|
+
)
|
|
2060
|
+
),
|
|
2061
|
+
|
|
2002
2062
|
React.createElement('div', { className: 'rl-section-card' },
|
|
2003
2063
|
React.createElement('div', { className: 'rl-section-title' },
|
|
2004
2064
|
React.createElement('span', null, t('secSupport')),
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerPluginUpdater } from './plugin-updater.js'
|
|
2
|
+
import { registerTranslator } from './translator.js'
|
|
2
3
|
import { getPluginDictionaries, getZhRuMap } from './locales.js'
|
|
3
4
|
// dsh-russian-lang — серверная половина.
|
|
4
5
|
//
|
|
@@ -22,11 +23,15 @@ const RussianLangSettingsSchema = z.object({
|
|
|
22
23
|
agentPrompt: z.boolean().required(false),
|
|
23
24
|
agentPromptPreset: z.string().required(false),
|
|
24
25
|
slashAliases: z.boolean().required(false),
|
|
25
|
-
quickSwitch: z.boolean().required(false)
|
|
26
|
+
quickSwitch: z.boolean().required(false),
|
|
27
|
+
translateEngine: z.string().required(false),
|
|
28
|
+
localApiUrl: z.string().required(false)
|
|
26
29
|
})
|
|
27
30
|
|
|
28
31
|
export function apply(ctx) {
|
|
32
|
+
let globalSettingsCtx = null
|
|
29
33
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
34
|
+
globalSettingsCtx = settingsCtx
|
|
30
35
|
settingsCtx.settings.register('russian-lang', RussianLangSettingsSchema)
|
|
31
36
|
})
|
|
32
37
|
|
|
@@ -52,7 +57,7 @@ export function apply(ctx) {
|
|
|
52
57
|
sync()
|
|
53
58
|
})
|
|
54
59
|
|
|
55
|
-
// Web endpoints: One-click plugin updater
|
|
60
|
+
// Web endpoints: One-click plugin updater, modular plugin dictionary serving, & translator gateway
|
|
56
61
|
if (typeof ctx.inject === 'function') {
|
|
57
62
|
ctx.inject(['webServer'], (wctx) => {
|
|
58
63
|
try {
|
|
@@ -63,6 +68,8 @@ export function apply(ctx) {
|
|
|
63
68
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
64
69
|
})
|
|
65
70
|
|
|
71
|
+
const unregTranslator = registerTranslator(wctx, globalSettingsCtx)
|
|
72
|
+
|
|
66
73
|
let ws = null
|
|
67
74
|
try { ws = wctx?.webServer } catch (_) { ws = null }
|
|
68
75
|
|
|
@@ -90,6 +97,7 @@ export function apply(ctx) {
|
|
|
90
97
|
|
|
91
98
|
return () => {
|
|
92
99
|
if (typeof unregUpdater === 'function') unregUpdater()
|
|
100
|
+
if (typeof unregTranslator === 'function') unregTranslator()
|
|
93
101
|
if (typeof unregDict === 'function') unregDict()
|
|
94
102
|
}
|
|
95
103
|
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host-side translation services & LibreTranslate integration
|
|
5
|
+
* for @goodandready/dsh-russian-lang.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function execPromise(cmd, args, timeoutMs = 15000) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
11
|
+
if (err) return reject(new Error(stderr?.trim() || err.message))
|
|
12
|
+
resolve(stdout?.trim() || '')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJsonBody(request, limit = 500_000) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let body = ''
|
|
20
|
+
request.on('data', (chunk) => {
|
|
21
|
+
body += chunk
|
|
22
|
+
if (body.length > limit) {
|
|
23
|
+
reject(new Error('Payload too large'))
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
request.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
resolve(body ? JSON.parse(body) : {})
|
|
29
|
+
} catch (err) {
|
|
30
|
+
reject(err)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
request.on('error', reject)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getTranslatorStatus(settingsCtx) {
|
|
38
|
+
const currentSettings = settingsCtx?.settings?.get('russian-lang') || {}
|
|
39
|
+
const engine = currentSettings.translateEngine || 'off'
|
|
40
|
+
const localUrl = (currentSettings.localApiUrl || 'http://127.0.0.1:5000').replace(/\/+$/, '')
|
|
41
|
+
|
|
42
|
+
let dockerAvailable = false
|
|
43
|
+
let installed = false
|
|
44
|
+
let running = false
|
|
45
|
+
let responsive = false
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const statusOut = await execPromise('docker', [
|
|
49
|
+
'ps', '-a', '--filter', 'name=^libretranslate$', '--format', '{{.Status}}'
|
|
50
|
+
], 3000)
|
|
51
|
+
dockerAvailable = true
|
|
52
|
+
if (statusOut) {
|
|
53
|
+
installed = true
|
|
54
|
+
running = statusOut.toLowerCase().startsWith('up')
|
|
55
|
+
}
|
|
56
|
+
} catch (_) {
|
|
57
|
+
dockerAvailable = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Ping HTTP endpoint if running or local
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${localUrl}/languages`, { signal: AbortSignal.timeout(1500) })
|
|
63
|
+
if (res.ok) responsive = true
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
engine,
|
|
68
|
+
localUrl,
|
|
69
|
+
dockerAvailable,
|
|
70
|
+
installed,
|
|
71
|
+
running,
|
|
72
|
+
responsive,
|
|
73
|
+
memory: '~500–700 МБ RAM',
|
|
74
|
+
recommended: 'libretranslate (CTranslate2 / int8)'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function setupLibreTranslateContainer() {
|
|
79
|
+
// Check if container exists
|
|
80
|
+
try {
|
|
81
|
+
const nameOut = await execPromise('docker', [
|
|
82
|
+
'ps', '-a', '--filter', 'name=^libretranslate$', '--format', '{{.Names}}'
|
|
83
|
+
], 3000)
|
|
84
|
+
if (nameOut && nameOut.trim() === 'libretranslate') {
|
|
85
|
+
// Start stopped container
|
|
86
|
+
await execPromise('docker', ['start', 'libretranslate'], 10000)
|
|
87
|
+
return { ok: true, action: 'started', message: 'Контейнер libretranslate запущен.' }
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error('Docker недоступен или нет прав на выполнение: ' + err.message)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run new container
|
|
94
|
+
try {
|
|
95
|
+
await execPromise('docker', [
|
|
96
|
+
'run', '-d',
|
|
97
|
+
'--name', 'libretranslate',
|
|
98
|
+
'--restart', 'unless-stopped',
|
|
99
|
+
'-p', '127.0.0.1:5000:5000',
|
|
100
|
+
'libretranslate/libretranslate',
|
|
101
|
+
'--load-only', 'en,ru,zh'
|
|
102
|
+
], 30000)
|
|
103
|
+
return { ok: true, action: 'created', message: 'Контейнер libretranslate создан и запускается (~600 МБ RAM).' }
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error('Ошибка запуска контейнера libretranslate: ' + err.message)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function translateGoogle(text, targetLang = 'ru') {
|
|
110
|
+
const parts = text.split(/(```[\s\S]*?```)/g)
|
|
111
|
+
const resultParts = []
|
|
112
|
+
for (let i = 0; i < parts.length; i++) {
|
|
113
|
+
if (i % 2 === 1) {
|
|
114
|
+
resultParts.push(parts[i])
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const chunk = parts[i]
|
|
118
|
+
if (!chunk.trim()) {
|
|
119
|
+
resultParts.push(chunk)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const paragraphs = chunk.split(/\n\n+/)
|
|
123
|
+
const transParas = []
|
|
124
|
+
for (const para of paragraphs) {
|
|
125
|
+
const pTrim = para.trim()
|
|
126
|
+
if (!pTrim) continue
|
|
127
|
+
const sentences = pTrim.match(/[^.!?\n]+[.!?\n]*/g) || [pTrim]
|
|
128
|
+
const transSents = []
|
|
129
|
+
for (const s of sentences) {
|
|
130
|
+
if (!s.trim()) continue
|
|
131
|
+
const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + encodeURIComponent(targetLang) + '&dt=t&q=' + encodeURIComponent(s)
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(gurl, { signal: AbortSignal.timeout(6000) })
|
|
134
|
+
if (res.ok) {
|
|
135
|
+
const data = await res.json()
|
|
136
|
+
if (Array.isArray(data) && Array.isArray(data[0])) {
|
|
137
|
+
transSents.push(data[0].map((it) => it[0]).join(''))
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
transSents.push(s)
|
|
143
|
+
}
|
|
144
|
+
transParas.push(transSents.join(''))
|
|
145
|
+
}
|
|
146
|
+
resultParts.push(transParas.join('\n\n'))
|
|
147
|
+
}
|
|
148
|
+
return resultParts.join('')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function translateLibre(text, targetLang = 'ru', localUrl = 'http://127.0.0.1:5000') {
|
|
152
|
+
const endpoint = localUrl.replace(/\/+$/, '') + '/translate'
|
|
153
|
+
const res = await fetch(endpoint, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
q: text,
|
|
158
|
+
source: 'auto',
|
|
159
|
+
target: targetLang,
|
|
160
|
+
format: 'text'
|
|
161
|
+
}),
|
|
162
|
+
signal: AbortSignal.timeout(15000)
|
|
163
|
+
})
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const errText = await res.text().catch(() => '')
|
|
166
|
+
throw new Error(`LibreTranslate HTTP ${res.status}: ${errText}`)
|
|
167
|
+
}
|
|
168
|
+
const data = await res.json()
|
|
169
|
+
return data.translatedText || text
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function registerTranslator(wctx, settingsCtx) {
|
|
173
|
+
let ws = null
|
|
174
|
+
try { ws = wctx?.webServer } catch (_) { ws = null }
|
|
175
|
+
if (!ws || typeof ws.register !== 'function') return () => {}
|
|
176
|
+
|
|
177
|
+
const unregs = []
|
|
178
|
+
|
|
179
|
+
// 1. Status endpoint: GET /api/dsh-russian-lang/translator/status
|
|
180
|
+
unregs.push(
|
|
181
|
+
ws.register({
|
|
182
|
+
kind: 'exact',
|
|
183
|
+
path: '/api/dsh-russian-lang/translator/status',
|
|
184
|
+
handler: async (request, response) => {
|
|
185
|
+
try {
|
|
186
|
+
if (request.method !== 'GET') {
|
|
187
|
+
response.writeHead(405, { 'content-type': 'application/json' })
|
|
188
|
+
response.end(JSON.stringify({ error: 'Method not allowed' }))
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
const status = await getTranslatorStatus(settingsCtx)
|
|
192
|
+
response.writeHead(200, {
|
|
193
|
+
'content-type': 'application/json; charset=utf-8',
|
|
194
|
+
'cache-control': 'no-store'
|
|
195
|
+
})
|
|
196
|
+
response.end(JSON.stringify(status))
|
|
197
|
+
} catch (err) {
|
|
198
|
+
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
199
|
+
response.end(JSON.stringify({ error: String(err) }))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// 2. Setup endpoint: POST /api/dsh-russian-lang/translator/setup
|
|
206
|
+
unregs.push(
|
|
207
|
+
ws.register({
|
|
208
|
+
kind: 'exact',
|
|
209
|
+
path: '/api/dsh-russian-lang/translator/setup',
|
|
210
|
+
handler: async (request, response) => {
|
|
211
|
+
try {
|
|
212
|
+
if (request.method !== 'POST') {
|
|
213
|
+
response.writeHead(405, { 'content-type': 'application/json' })
|
|
214
|
+
response.end(JSON.stringify({ error: 'Method not allowed' }))
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const result = await setupLibreTranslateContainer()
|
|
218
|
+
response.writeHead(200, {
|
|
219
|
+
'content-type': 'application/json; charset=utf-8',
|
|
220
|
+
'cache-control': 'no-store'
|
|
221
|
+
})
|
|
222
|
+
response.end(JSON.stringify(result))
|
|
223
|
+
} catch (err) {
|
|
224
|
+
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
225
|
+
response.end(JSON.stringify({ error: String(err) }))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// 3. Translation gateway: POST /api/dsh-russian-lang/translate
|
|
232
|
+
unregs.push(
|
|
233
|
+
ws.register({
|
|
234
|
+
kind: 'exact',
|
|
235
|
+
path: '/api/dsh-russian-lang/translate',
|
|
236
|
+
handler: async (request, response) => {
|
|
237
|
+
try {
|
|
238
|
+
if (request.method !== 'POST') {
|
|
239
|
+
response.writeHead(405, { 'content-type': 'application/json' })
|
|
240
|
+
response.end(JSON.stringify({ error: 'Method not allowed' }))
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const body = await readJsonBody(request)
|
|
245
|
+
const text = body.text
|
|
246
|
+
const target = body.target || 'ru'
|
|
247
|
+
|
|
248
|
+
if (!text || typeof text !== 'string') {
|
|
249
|
+
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
|
|
250
|
+
response.end(JSON.stringify({ error: 'Параметр "text" обязателен для перевода.' }))
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const settings = settingsCtx?.settings?.get('russian-lang') || {}
|
|
255
|
+
const engine = settings.translateEngine || 'off'
|
|
256
|
+
const localUrl = settings.localApiUrl || 'http://127.0.0.1:5000'
|
|
257
|
+
|
|
258
|
+
if (engine === 'off') {
|
|
259
|
+
response.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
|
|
260
|
+
response.end(JSON.stringify({
|
|
261
|
+
error: 'Машинный перевод отключен в настройках плагина. Включите локальный сервер (LibreTranslate) или Google Translate в настройках dsh-russian-lang.'
|
|
262
|
+
}))
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let translatedText = ''
|
|
267
|
+
if (engine === 'local') {
|
|
268
|
+
try {
|
|
269
|
+
translatedText = await translateLibre(text, target, localUrl)
|
|
270
|
+
} catch (err) {
|
|
271
|
+
response.writeHead(503, { 'content-type': 'application/json; charset=utf-8' })
|
|
272
|
+
response.end(JSON.stringify({
|
|
273
|
+
error: 'Локальный переводчик LibreTranslate недоступен (' + err.message + '). Проверьте состояние контейнера в настройках плагина.'
|
|
274
|
+
}))
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
} else if (engine === 'google') {
|
|
278
|
+
translatedText = await translateGoogle(text, target)
|
|
279
|
+
} else {
|
|
280
|
+
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
|
|
281
|
+
response.end(JSON.stringify({ error: 'Неизвестный движок перевода: ' + engine }))
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
response.writeHead(200, {
|
|
286
|
+
'content-type': 'application/json; charset=utf-8',
|
|
287
|
+
'cache-control': 'no-store'
|
|
288
|
+
})
|
|
289
|
+
response.end(JSON.stringify({ translatedText, engine }))
|
|
290
|
+
} catch (err) {
|
|
291
|
+
response.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
292
|
+
response.end(JSON.stringify({ error: String(err) }))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
for (const unreg of unregs) {
|
|
300
|
+
try { unreg() } catch (_) {}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-russian-lang",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Russian localization for DeepSeek Harness (DSH v0.1.6-alpha.1+) web UI: adds Русский as native language option. 100.0% coverage across 11,295 keys (127 namespaces). Features full DSH v0.1.6-alpha.1 core support (terminal, archive, command palette, Auto review, trajectory, PTC/Creator presets) and complete GoodAndReady plugin catalog (subscriptions, tts, image-gen, voice, key-limits, notify, commandcode, plugin-console).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"cordis.patch.yml",
|
|
16
16
|
"README.md",
|
|
17
17
|
"README.ru.md",
|
|
18
|
-
"README.zh.md",
|
|
19
18
|
"LICENSE"
|
|
20
19
|
],
|
|
21
20
|
"dsh": {
|
package/README.zh.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# 📦 @goodandready/dsh-russian-lang
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
<h3>DeepSeek Harness 完整俄语本地化、智能排版与键盘布局自动修复插件</h3>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.20-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
-
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
-
</p>
|
|
13
|
-
|
|
14
|
-
<p align="center">
|
|
15
|
-
<img src="https://img.shields.io/badge/DSH-v0.1.5--rc.2%2B-blue.svg?style=for-the-badge&labelColor=1e1b4b" alt="DSH v0.1.6-alpha.1+">
|
|
16
|
-
<img src="https://img.shields.io/badge/覆盖率-100%25-10b981.svg?style=for-the-badge&labelColor=064e3b" alt="覆盖率 100%">
|
|
17
|
-
<img src="https://img.shields.io/badge/UI词条-11295-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="11295 词条">
|
|
18
|
-
<img src="https://img.shields.io/badge/人工校验-100%25-0ea5e9.svg?style=for-the-badge&labelColor=082f49" alt="100% 人工校验">
|
|
19
|
-
</p>
|
|
20
|
-
|
|
21
|
-
<p align="center">
|
|
22
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者所有项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者所有项目"></a>
|
|
23
|
-
</p>
|
|
24
|
-
|
|
25
|
-
<table align="center">
|
|
26
|
-
<tr>
|
|
27
|
-
<td align="center">
|
|
28
|
-
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上点个 Star</strong> —— 这能让我知道该插件对您有所帮助,并激励我持续维护和演进。
|
|
29
|
-
<br><br>
|
|
30
|
-
🐛 <strong>如果您发现了 Bug 或有任何建议</strong>,欢迎随时在 GitHub 提交 Issue(支持中文、俄文或英文)。
|
|
31
|
-
</td>
|
|
32
|
-
</tr>
|
|
33
|
-
</table>
|
|
34
|
-
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## ⚡ 概述
|
|
40
|
-
|
|
41
|
-
**`dsh-russian-lang`** 是面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) Web 界面的全功能俄语本地化增强插件。
|
|
42
|
-
|
|
43
|
-
本插件不仅覆盖 DSH 核心及社区 70+ 个主流扩展插件的俄语翻译,更针对俄语用户深度定制了智能化人机交互特性:**100% 专家人工审校的技术术语**、**俄语复数语法形式(Plural)**、**屏幕排版自动规范化**(书名号《》、长破折号、不换行空格),以及通过快捷键 <kbd>Alt+L</kbd> 实现的**错误键盘布局文本一键纠正**。
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## ✨ 核心特性
|
|
48
|
-
|
|
49
|
-
### 1. 🌐 100% 覆盖 DSH 核心 (45+ 命名空间,1780+ 词条)
|
|
50
|
-
完全覆盖终端、会话归档、命令面板、工作区、右侧边栏、模型设置等所有 DSH 原生功能模块。
|
|
51
|
-
|
|
52
|
-
### 2. 🧩 内置 70+ 社区插件模块化翻译 (9500+ 词条)
|
|
53
|
-
采用按需模块化分发架构(单文件 < 50 KiB),包含市场、看板、定时任务、语音 TTS、模型限额、会话控制等全部热门扩展插件。
|
|
54
|
-
|
|
55
|
-
### 3. 🔢 俄语复数语法精确匹配 (Plural)
|
|
56
|
-
自动计算俄语特有的数词变格规则(`one`、`few`、`many`),彻底避免机械英汉翻译中的语法违和感。
|
|
57
|
-
|
|
58
|
-
### 4. ✍️ 智能输入排版增强 (`russian-lang.typography`)
|
|
59
|
-
在输入和渲染时规范排版格式,智能识别单字母连词并在其后插入不换行空格,且绝对保留 Markdown 代码块和行内代码语法不变。
|
|
60
|
-
|
|
61
|
-
### 5. ⌨️ 误打布局实时纠正 (<kbd>Alt+L</kbd>)
|
|
62
|
-
实时检测英俄误打输入(例如错输入 `ghbdtn` 自动识别为 `привет`),按快捷键即可就地无缝纠正。
|
|
63
|
-
|
|
64
|
-
### 6. ⚙️ 自定义键值覆写 (`russian-lang.overrides`)
|
|
65
|
-
支持在 **设置 → 插件 → 俄语本地化** 中自定义特定术语翻译,无需修改任何源代码。
|
|
66
|
-
|
|
67
|
-
### 7. 🔤 DSH 原生架构集成
|
|
68
|
-
原生注册至 **设置 → 通用 → 语言** 菜单,无缝切换 `<html lang="ru-RU">` 并自动激活浏览器拼写检查。
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 📦 安装方法
|
|
73
|
-
|
|
74
|
-
在终端运行以下命令:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
dsh plugin --profile web add @goodandready/dsh-russian-lang
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
安装完成后,打开 DSH 网页端,前往 **设置 (Settings) → 通用 (General) → 语言 (Language)**,选择 **«Русский»** 即可。
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## ⚙️ 配置说明 (`settings.yaml`)
|
|
85
|
-
|
|
86
|
-
配置命名空间为 `russian-lang`,所有选项均为可选字段:
|
|
87
|
-
|
|
88
|
-
```yaml
|
|
89
|
-
russian-lang:
|
|
90
|
-
enabled: true # 启用俄语界面
|
|
91
|
-
typography:
|
|
92
|
-
enabled: true # 启用智能排版(引号、破折号、不换行空格)
|
|
93
|
-
yo: false # 自动还原字母 «ё»(默认关闭)
|
|
94
|
-
agentPrompt: false # 提示智能体优先使用俄语回复
|
|
95
|
-
overrides: {} # 针对任意词条的自定义翻译映射
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## 📄 开源许可
|
|
101
|
-
|
|
102
|
-
MIT License © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|