@goodandready/dsh-russian-lang 0.2.7 → 0.2.9

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
@@ -5,6 +5,7 @@
5
5
  // в собственном namespace, чтобы запись переживала браузеры и машины через
6
6
  // штатный механизм настроек DSH.
7
7
  import z from '@deepseek-ai/schemastery'
8
+ import { SYSTEM_PROMPT_PRESETS } from './pure.js'
8
9
 
9
10
  export const name = '@goodandready/dsh-russian-lang'
10
11
 
@@ -13,9 +14,13 @@ const RussianLangSettingsSchema = z.object({
13
14
  overrides: z.dict(z.string()).required(false),
14
15
  typography: z.object({
15
16
  enabled: z.boolean().required(false),
16
- yo: z.boolean().required(false)
17
+ yo: z.boolean().required(false),
18
+ liveInput: z.boolean().required(false)
17
19
  }).required(false),
18
- agentPrompt: z.boolean().required(false)
20
+ agentPrompt: z.boolean().required(false),
21
+ agentPromptPreset: z.string().required(false),
22
+ slashAliases: z.boolean().required(false),
23
+ quickSwitch: z.boolean().required(false)
19
24
  })
20
25
 
21
26
  export function apply(ctx) {
@@ -24,19 +29,22 @@ export function apply(ctx) {
24
29
  })
25
30
 
26
31
  // #68: русский системный промпт агента (опционально). Регистрируем секцию
27
- // промпта, которая просит отвечать по-русски, когда включён флаг
32
+ // промпта, которая задаёт стиль ответов по-русски, когда включён флаг
28
33
  // russian-lang.agentPrompt. Секция — официальная точка расширения ядра.
29
34
  ctx.inject(['settings', 'systemPrompt'], (settingsCtx, prompt) => {
30
35
  const NS = 'russian-lang'
31
36
  const RU_SECTION = 'dsh-russian-lang-agent-prompt'
32
- const RU_TEXT = 'Отвечай пользователю на русском языке. Если пользователь пишет на другом языке, отвечай на его языке.'
33
- let current = false
37
+ const DEFAULT_RU_TEXT = 'Отвечай пользователю на русском языке. Если пользователь пишет на другом языке, отвечай на его языке.'
38
+ let currentText = ''
34
39
  const sync = () => {
35
40
  const value = settingsCtx.settings.get(NS)
36
41
  const want = !!(value && value.agentPrompt)
37
- if (want === current) return
38
- current = want
39
- prompt.section({ name: RU_SECTION, order: 1000, text: want ? RU_TEXT : '' })
42
+ const presetKey = value && value.agentPromptPreset
43
+ const preset = SYSTEM_PROMPT_PRESETS[presetKey]
44
+ const text = want ? (preset ? preset.text : DEFAULT_RU_TEXT) : ''
45
+ if (text === currentText) return
46
+ currentText = text
47
+ prompt.section({ name: RU_SECTION, order: 1000, text })
40
48
  }
41
49
  ctx.on('settings/updated', (ns) => { if (ns === NS) sync() })
42
50
  sync()
package/lib/pure.js CHANGED
@@ -341,3 +341,206 @@ export const makeLayout = (freq, localDict = new Set()) => {
341
341
  }
342
342
  return { ruWordFraction, candidate, learnWords, localDict }
343
343
  }
344
+
345
+
346
+ // ---------------------------------------------------- живая типографика инпута
347
+
348
+ // Предлоги и союзы, после которых ставится неразрывный пробел (U+00A0)
349
+ const SHORT_WORDS_RE = /(^|[\s(«])([вВнНсСпПоОкКуУиИаА]|из|от|до|за|по|со|во|ко|об|на|под|над|при|про|без|для|не|ни|но)( )/g
350
+
351
+ /**
352
+ * Живая типографика для поля ввода DSH.
353
+ * Заменяет кавычки "" на «», дефисы -- на тире —, многоточие ... на …,
354
+ * и добавляет неразрывные пробелы после коротких предлогов.
355
+ * ИГНОРИРУЕТ код внутри обратных кавычек `...` и блоков ```...```.
356
+ */
357
+ export const formatInputLive = (text) => {
358
+ if (!text || typeof text !== 'string') return text
359
+
360
+ // Разбиваем по блокам кода (inline `...` или fenced ```...```), чтобы не портить синтаксис
361
+ const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
362
+ for (let i = 0; i < parts.length; i += 2) {
363
+ let s = parts[i]
364
+ if (!s) continue
365
+
366
+ // 1. Троеточие
367
+ s = s.replace(/\.{3}/g, '…')
368
+
369
+ // 2. Длинное тире (-- -> —)
370
+ s = s.replace(/(^|\s)--(\s|$)/g, '$1—$2')
371
+
372
+ // 3. Кавычки-ёлочки («...») с учётом открывающейся/закрывающейся
373
+ s = s.replace(/(^|[\s([{-])"/g, '$1«')
374
+ s = s.replace(/"/g, '»')
375
+
376
+ // 4. Неразрывный пробел после коротких слов/предлогов
377
+ s = s.replace(SHORT_WORDS_RE, '$1$2\u00A0')
378
+
379
+ parts[i] = s
380
+ }
381
+ return parts.join('')
382
+ }
383
+
384
+ // ------------------------------------------------------- алиасы слэш-команд
385
+
386
+ export const RUSSIAN_SLASH_ALIASES = {
387
+ '/цель': '/goal',
388
+ '/сжать': '/compact',
389
+ '/план': '/plan',
390
+ '/экспорт': '/export',
391
+ '/отзыв': '/feedback',
392
+ '/разрешение': '/permission',
393
+ '/разрешения': '/permission',
394
+ '/память': '/memory',
395
+ '/помощь': '/help',
396
+ '/очистить': '/clear',
397
+ '/сессия': '/session',
398
+ '/модель': '/model'
399
+ }
400
+
401
+ /**
402
+ * Разворачивает русский алиас слэш-команды в каноническую команду DSH.
403
+ * Например: "/цель сделать тесты" -> "/goal сделать тесты"
404
+ */
405
+ export const expandSlashAlias = (input) => {
406
+ if (!input || typeof input !== 'string' || !input.startsWith('/')) return input
407
+ const match = input.match(/^(\s*\/[^\s]+)(.*)$/)
408
+ if (!match) return input
409
+ const [, cmd, rest] = match
410
+ const lowerCmd = cmd.trim().toLowerCase()
411
+ if (RUSSIAN_SLASH_ALIASES[lowerCmd]) {
412
+ return RUSSIAN_SLASH_ALIASES[lowerCmd] + rest
413
+ }
414
+ return input
415
+ }
416
+
417
+ // --------------------------------------------------- фонетический транслит
418
+
419
+ const PHONETIC_LAT_TO_CYR = [
420
+ ['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],
421
+ ['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],
422
+ ['a', 'а'], ['b', 'б'], ['v', 'в'], ['g', 'г'], ['d', 'д'], ['e', 'е'],
423
+ ['z', 'з'], ['i', 'и'], ['j', 'й'], ['k', 'к'], ['l', 'л'], ['m', 'м'],
424
+ ['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],
425
+ ['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
426
+ ]
427
+
428
+ const PHONETIC_CYR_TO_LAT = [
429
+ ['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],
430
+ ['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],
431
+ ['а', 'a'], ['б', 'b'], ['в', 'v'], ['г', 'g'], ['д', 'd'], ['е', 'e'],
432
+ ['з', 'z'], ['и', 'i'], ['й', 'j'], ['к', 'k'], ['л', 'l'], ['м', 'm'],
433
+ ['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],
434
+ ['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
435
+ ]
436
+
437
+ export const phoneticTranslit = (text, direction = 'lat2cyr') => {
438
+ if (!text || typeof text !== 'string') return text
439
+ let res = text
440
+ const pairs = direction === 'lat2cyr' ? PHONETIC_LAT_TO_CYR : PHONETIC_CYR_TO_LAT
441
+ for (const [from, to] of pairs) {
442
+ const fromUpper = from.toUpperCase()
443
+ const fromTitle = from[0].toUpperCase() + from.slice(1)
444
+ const toUpper = to.toUpperCase()
445
+ const toTitle = to[0] ? to[0].toUpperCase() + to.slice(1) : ''
446
+
447
+ if (from.length > 1) {
448
+ res = res.replaceAll(fromUpper, toUpper)
449
+ res = res.replaceAll(fromTitle, toTitle)
450
+ }
451
+ res = res.replaceAll(from, to)
452
+ if (from.length === 1) {
453
+ res = res.replaceAll(fromUpper, toUpper)
454
+ }
455
+ }
456
+ return res
457
+ }
458
+
459
+ // ---------------------------------------------------- детектор раскладки
460
+
461
+ /**
462
+ * Определяет преобладающий язык ввода в тексте: 'RU', 'EN' или null.
463
+ */
464
+ export const detectInputLayout = (text) => {
465
+ if (!text || typeof text !== 'string') return null
466
+ const clean = text.replace(/`[^`]*`/g, '').replace(/https?:\/\/\S+/g, '')
467
+ let cyr = 0
468
+ let lat = 0
469
+ for (const ch of clean) {
470
+ const code = ch.charCodeAt(0)
471
+ if ((code >= 0x0400 && code <= 0x04FF) || code === 0x0500) cyr++
472
+ else if ((code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)) lat++
473
+ }
474
+ if (cyr > lat && cyr >= 2) return 'RU'
475
+ if (lat > cyr && lat >= 2) return 'EN'
476
+ return null
477
+ }
478
+
479
+ // ------------------------------------------------- пресеты промптов агента
480
+
481
+ export const SYSTEM_PROMPT_PRESETS = {
482
+ technical_expert: {
483
+ id: 'technical_expert',
484
+ label: 'Технический эксперт',
485
+ desc: 'Строгая русская инженерная терминология, чистый код и русские комментарии',
486
+ text: 'Отвечай пользователю на русском языке. Используй точную русскую инженерную терминологию, пиши чистый, идиоматичный код с русскими комментариями и следуй лучшим практикам разработки. Если пользователь пишет на другом языке, отвечай на его языке.'
487
+ },
488
+ tech_writer: {
489
+ id: 'tech_writer',
490
+ label: 'Технический писатель',
491
+ desc: 'Структурированные тексты, Markdown, ГОСТ/RFC и выверенная типографика',
492
+ text: 'Отвечай пользователю на русском языке. Оформляй документацию и пояснения в структурированном виде (Markdown, списки, таблицы), строго соблюдай правила русской типографики («кавычки-ёлочки», длинное тире, буква «ё») и требования к техническим текстам.'
493
+ },
494
+ concise: {
495
+ id: 'concise',
496
+ label: 'Лаконичный режим',
497
+ desc: 'Краткие ёмкие ответы без лишней вводной воды и повторов',
498
+ text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без лишних вводных слов, повторов и пространных рассуждений. Сразу переходи к сути, коду или прямому решению задачи.'
499
+ }
500
+ }
501
+
502
+ // -------------------------------------------------- локализованный экспорт
503
+
504
+ /**
505
+ * Генерирует локализованный Markdown-отчёт по сессии диалога.
506
+ */
507
+ export const exportSessionToMarkdown = (session, options = {}) => {
508
+ if (!session) return ''
509
+ const title = session.title || session.name || 'Диалог DSH'
510
+ const date = session.createdAt ? new Date(session.createdAt) : new Date()
511
+ const dateStr = new Intl.DateTimeFormat('ru-RU', {
512
+ dateStyle: 'full',
513
+ timeStyle: 'medium'
514
+ }).format(date)
515
+
516
+ const messages = Array.isArray(session.messages) ? session.messages : []
517
+ let md = `# 💬 ${title}\n\n`
518
+ md += `> **Дата экспорта:** ${dateStr}\n`
519
+ if (session.model) md += `> **Модель:** \`${session.model}\`\n`
520
+ if (session.workspace) md += `> **Рабочая область:** \`${session.workspace}\`\n`
521
+ md += `> **Всего сообщений:** ${messages.length}\n\n---\n\n`
522
+
523
+ for (let idx = 0; idx < messages.length; idx++) {
524
+ const msg = messages[idx]
525
+ const role = msg.role || 'unknown'
526
+ const roleName = role === 'user' ? '👤 Пользователь' :
527
+ role === 'assistant' ? '🤖 Ассистент' :
528
+ role === 'system' ? '⚙️ Система' : `🔧 Инструмент (${role})`
529
+
530
+ md += `### ${roleName}\n\n`
531
+ if (msg.content) {
532
+ md += `${msg.content}\n\n`
533
+ }
534
+ if (Array.isArray(msg.toolCalls) && msg.toolCalls.length) {
535
+ md += `*Вызовы инструментов:*\n`
536
+ for (const tc of msg.toolCalls) {
537
+ md += `- **${tc.name || 'tool'}**: \`${JSON.stringify(tc.arguments || {})}\`\n`
538
+ }
539
+ md += `\n`
540
+ }
541
+ md += `---\n\n`
542
+ }
543
+
544
+ md += `*Сгенерировано с помощью [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) и @goodandready/dsh-russian-lang*\n`
545
+ return md
546
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-alpha.1+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 5395 keys (1144 core UI keys + 4251 keys across 40+ ecosystem plugins) with zero machine drafts. Features Russian plural forms (one/few/many), automatic typography pass (guillemets, em dashes, non-breaking spaces, optional ё), wrong-layout input hint (Alt+L), browser spellcheck, and runtime snapshot extension without modifying core files.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",