@goodandready/dsh-russian-lang 0.2.20 → 0.2.22

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.
@@ -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.20",
3
+ "version": "0.2.22",
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)