dsh-think-translate 1.2.11 → 1.2.12
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.de.md +3 -1
- package/README.es.md +3 -1
- package/README.fr.md +3 -1
- package/README.ja.md +3 -1
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/README.ru.md +3 -1
- package/README.zh-CN.md +3 -1
- package/lib/client.js +82 -73
- package/lib/index.js +6 -9
- package/lib/text.js +77 -0
- package/package.json +2 -1
package/README.de.md
CHANGED
|
@@ -75,6 +75,8 @@ Für DSH **0.2.0-rc.2** verwenden Sie **dsh-think-translate 1.2.5 oder neuer**.
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 Übertragung und Wiederaufnahme:** Normale HTTP/HTTPS-Übersetzungen nutzen die nativen Netzwerkfunktionen von Node statt eines curl-Prozesses pro Textpaket. SOCKS-Proxys und der Ollama-Installer verwenden weiterhin curl. Fehler bleiben 30 Sekunden im Arbeitsspeicher; danach werden Denk- und Antwortübersetzungen automatisch erneut versucht. Die Zeitlimits berücksichtigen das Laden lokaler Modelle und alternative Anbieter.
|
|
77
77
|
|
|
78
|
+
**1.2.12:** Die Anbieterschalter gelten verbindlich; nach einem Host-Fehler fragt der Browser Google nicht mehr direkt ab. Eingezäunter, eingerückter und Inline-Code wird nicht an Übersetzungsdienste gesendet und exakt wiederhergestellt, auch bei unvollständigem Streaming. Alte Übersetzungs-Caches werden entfernt, Einstellungen bleiben erhalten. Nach dem Update DSH Web oder Desktop vollständig neu starten; bloßes Neuladen kann weiterhin alte Client-Ressourcen verwenden.
|
|
79
|
+
|
|
78
80
|
**1.2.11 Bing-Korrekturen:** Der kostenlose Bing-Anbieter bleibt erhalten. Übersetzungsanfragen gehen an den endgültigen Origin der Sitzungsseite, damit die Weiterleitung `www.bing.com` → `cn.bing.com` in Festlandchina den POST-Body nicht verwirft. SOCKS/curl verwendet dieselben Browser-Header wie der native Transport und behebt damit HTTP 401; leere Antworten und Antworten ohne JSON melden klare Fehler.
|
|
79
81
|
|
|
80
82
|
**1.2.10 DSH-Profilkompatibilität:** Die Erkennung folgt dem laufenden Desktop/Web-Profil und liest `profiles/<profile>/cordis.patch.yml` (`id` + `config`), danach die Home-Patches und Start-Overlays. Das ältere `settings.yaml` bleibt unterstützt. Die Standardmodell-Zeile heißt `default-model`; Zugangsdaten bleiben in `.credentials.yaml` im DSH-Home und nur im Arbeitsspeicher. Die Diagnose zeigt die tatsächlich gelesene Datei.
|
|
@@ -100,7 +102,7 @@ Browser → POST /_xlate/translate (gleiche Origin, kein CORS)
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← Reihenfolge per Drag in den Einstellungen
|
|
101
103
|
google / bing / OpenAI-kompatibel / Anthropic
|
|
102
104
|
Fallback-Kette (optional, standardmäßig aus, in der Config aktivierbar)
|
|
103
|
-
→
|
|
105
|
+
→ Originaltext, wenn alle aktivierten Anbieter fehlschlagen
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- Die **Anbieter-Konfiguration** liegt in `config.json` (zur Laufzeit erzeugt, gitignoriert): `chain` (geordnete IDs), `fallback` (enabled + chain, nur Datei), `providers` (je Anbieter `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). Alte `priority`-Konfigurationen werden automatisch migriert; ein Anbieter mit `apiKeyEnv` löst seinen Schlüssel pro Anfrage aus dieser Umgebungsvariable auf (der literale `apiKey` bleibt als Rückfall) und kein aufgelöster Schlüssel wird je in `config.json` zurückgeschrieben; ein `null` in einem Patch löscht das Feld — so leert die Oberfläche eines
|
package/README.es.md
CHANGED
|
@@ -75,6 +75,8 @@ Para DSH **0.2.0-rc.2**, usa **dsh-think-translate 1.2.5 o posterior**. La versi
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 transporte y recuperación:** las traducciones HTTP/HTTPS normales usan la red nativa de Node en lugar de iniciar curl por cada lote de texto. Los proxies SOCKS y el instalador de Ollama conservan la compatibilidad con curl. Los fallos permanecen en memoria durante 30 segundos y después se reintentan automáticamente las traducciones del razonamiento y las respuestas. Los tiempos de espera permiten cargar modelos locales y probar proveedores alternativos.
|
|
77
77
|
|
|
78
|
+
**1.2.12:** se respetan los proveedores habilitados; el navegador ya no consulta Google directamente si falla el Host. El código delimitado, con sangría o en línea se excluye de las solicitudes y se conserva exactamente, incluso durante streaming incompleto. Se elimina la caché de traducciones anterior y se conservan las preferencias. Tras actualizar, reinicia por completo DSH Web o Desktop; recargar la página puede seguir usando recursos antiguos del cliente.
|
|
79
|
+
|
|
78
80
|
**1.2.11 correcciones de Bing:** se mantiene el proveedor gratuito. Las solicitudes de traducción usan el origen final de la página de sesión para evitar que la redirección `www.bing.com` → `cn.bing.com` en China continental elimine el cuerpo POST. SOCKS/curl comparte las cabeceras de navegador del transporte nativo para corregir HTTP 401; las respuestas vacías o no JSON muestran errores claros.
|
|
79
81
|
|
|
80
82
|
**1.2.10 compatibilidad con perfiles DSH:** el descubrimiento sigue el perfil Desktop/Web activo y lee `profiles/<profile>/cordis.patch.yml` (`id` + `config`), después los parches del directorio DSH y las superposiciones de inicio. Se mantiene el formato anterior `settings.yaml`. El modelo predeterminado usa `default-model`; las credenciales siguen en `.credentials.yaml` del directorio DSH y solo en memoria. El diagnóstico muestra el archivo leído.
|
|
@@ -100,7 +102,7 @@ navegador → POST /_xlate/translate (mismo origen, sin CORS)
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← orden arrastrando en Ajustes
|
|
101
103
|
google / bing / compatible con OpenAI / Anthropic
|
|
102
104
|
cadena fallback (opcional, desactivada por defecto, se activa en el config)
|
|
103
|
-
→
|
|
105
|
+
→ texto original si fallan todos los proveedores habilitados
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- La **configuración de proveedores** vive en `config.json` (generado en tiempo de ejecución, ignorado por git): `chain` (ids ordenados), `fallback` (enabled + chain, solo en el archivo), `providers` (por proveedor `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). Las configuraciones antiguas con `priority` se migran solas; un proveedor que declara `apiKeyEnv` resuelve la clave desde esa variable en cada petición (el `apiKey` literal queda como respaldo) y ninguna clave resuelta se escribe de vuelta en `config.json`; un `null` en un parche borra ese campo, que es como la interfaz vacía uno
|
package/README.fr.md
CHANGED
|
@@ -75,6 +75,8 @@ Pour DSH **0.2.0-rc.2**, utilisez **dsh-think-translate 1.2.5 ou une version ult
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 transport et reprise :** les traductions HTTP/HTTPS ordinaires utilisent le réseau natif de Node au lieu de lancer curl pour chaque lot de texte. Les proxys SOCKS et l’installateur Ollama conservent curl. Les échecs restent en mémoire pendant 30 secondes, puis les traductions du raisonnement et des réponses sont réessayées automatiquement. Les délais tiennent compte du chargement des modèles locaux et du recours à d’autres fournisseurs.
|
|
77
77
|
|
|
78
|
+
**1.2.12 :** les choix de fournisseurs sont respectés ; le navigateur ne contacte plus Google directement après un échec du Host. Le code délimité, indenté ou en ligne est exclu des requêtes et conservé exactement, y compris pendant un flux incomplet. Les anciennes traductions en cache sont supprimées, les préférences conservées. Après la mise à jour, redémarrez entièrement DSH Web ou Desktop ; une simple actualisation peut encore référencer les anciennes ressources du client.
|
|
79
|
+
|
|
78
80
|
**1.2.11 corrections de Bing :** le fournisseur gratuit est conservé. Les requêtes de traduction utilisent l’origine finale de la page de session pour éviter que la redirection `www.bing.com` → `cn.bing.com` en Chine continentale supprime le corps POST. SOCKS/curl utilise les mêmes en-têtes de navigateur que le transport natif pour corriger HTTP 401 ; les réponses vides ou non JSON signalent des erreurs explicites.
|
|
79
81
|
|
|
80
82
|
**1.2.10 compatibilité des profils DSH :** la détection suit le profil Desktop/Web actif et lit `profiles/<profile>/cordis.patch.yml` (`id` + `config`), puis applique les correctifs du répertoire DSH et les surcharges de lancement. L’ancien `settings.yaml` reste pris en charge. Le modèle par défaut utilise `default-model` ; les identifiants restent dans `.credentials.yaml` du répertoire DSH et en mémoire uniquement. Le diagnostic indique le fichier lu.
|
|
@@ -100,7 +102,7 @@ navigateur → POST /_xlate/translate (même origine, sans CORS)
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← ordre par glisser dans les Réglages
|
|
101
103
|
google / bing / compatible OpenAI / Anthropic
|
|
102
104
|
chaîne fallback (facultative, désactivée par défaut, activable dans la config)
|
|
103
|
-
→
|
|
105
|
+
→ texte original si tous les fournisseurs activés échouent
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- La **configuration des fournisseurs** vit dans `config.json` (généré à l'exécution, ignoré par git) : `chain` (ids ordonnés), `fallback` (enabled + chain, fichier uniquement), `providers` (par fournisseur `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). Les anciennes configurations `priority` sont migrées automatiquement ; un fournisseur qui déclare `apiKeyEnv` résout sa clé depuis cette variable à chaque requête (le `apiKey` littéral reste en secours) et aucune clé résolue n'est réécrite dans `config.json` ; un `null` dans un patch supprime ce champ, c'est ainsi que l'interface en vide un
|
package/README.ja.md
CHANGED
|
@@ -75,6 +75,8 @@ DSH **0.2.0-rc.2** では **dsh-think-translate 1.2.5 以降**を使用してく
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 通信と復帰:**通常の HTTP/HTTPS 翻訳は、テキストのバッチごとに curl を起動せず Node の標準ネットワーク機能を使います。SOCKS プロキシと Ollama インストーラーは引き続き curl に対応します。失敗結果をメモリ内に30秒間保持し、その後は思考・回答の翻訳を自動再試行します。タイムアウトはローカルモデルのロードと代替プロバイダーへの切り替えを考慮しています。
|
|
77
77
|
|
|
78
|
+
**1.2.12:**提供元の設定に従い、Host の失敗後にブラウザーから Google へ直接送信しません。フェンス付き・インデント付き・インラインコードは翻訳対象から除外し、ストリーミング中の未完了コードも原文のまま保持します。古い訳文キャッシュを削除し、設定は保持します。 更新後は DSH Web または Desktop を完全に再起動してください。ページの更新だけでは古いクライアント資源を参照する場合があります。
|
|
79
|
+
|
|
78
80
|
**1.2.11 Bing 修正:**無料の Bing を引き続き提供します。翻訳リクエストはセッションページの最終オリジンに送信し、中国本土の `www.bing.com` → `cn.bing.com` リダイレクトによる POST 本文の消失を防ぎます。SOCKS/curl はネイティブ通信と同じブラウザーヘッダーを使用して HTTP 401 を修正。空の応答と JSON 以外の応答には明確なエラーを表示します。
|
|
79
81
|
|
|
80
82
|
**1.2.10 DSH プロファイル対応:**実行中の Desktop/Web プロファイルに従い、`profiles/<profile>/cordis.patch.yml`(`id` + `config`)、DSH home のパッチ、起動時のオーバーレイを順に読み込みます。旧形式の `settings.yaml` も対応。既定モデル行は `default-model` に対応し、資格情報は DSH home の `.credentials.yaml` から解決してメモリ内だけに保持します。再スキャン時に実際の設定パスを表示します。
|
|
@@ -100,7 +102,7 @@ DSH **0.2.0-rc.2** では **dsh-think-translate 1.2.5 以降**を使用してく
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← 設定でドラッグして並べ替え
|
|
101
103
|
google / bing / OpenAI 互換 / Anthropic から選択
|
|
102
104
|
fallback チェーン(任意、既定は無効、設定で有効化)
|
|
103
|
-
→
|
|
105
|
+
→ 有効な提供元がすべて失敗した場合は原文を保持
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- **プロバイダー設定** は `config.json`(実行時生成・gitignore 対象)にあります:`chain`(順序付き ID)、`fallback`(enabled と chain、設定ファイル専用)、`providers`(各項目の `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`)。旧 `priority` 形式は自動移行されます。`apiKeyEnv` を宣言した提供元はリクエスト時にその環境変数からキーを解決し(リテラル `apiKey` はフォールバック)、解決したキーを `config.json` に書き戻すことはありません。パッチ内の `null` はそのフィールドの削除を意味し、UI のクリアはこれを使っています
|
package/README.ko.md
CHANGED
|
@@ -75,6 +75,8 @@ DSH **0.2.0-rc.2**에서는 **dsh-think-translate 1.2.5 이상**을 사용하세
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 전송 및 복구:** 일반 HTTP/HTTPS 번역은 텍스트 배치마다 curl을 실행하는 대신 Node 기본 네트워크 기능을 사용합니다. SOCKS 프록시와 Ollama 설치 프로그램은 curl 호환 경로를 유지합니다. 실패 결과를 메모리에 30초간 보관한 뒤 사고·답변 번역을 자동 재시도합니다. 타임아웃은 로컬 모델 로드와 다른 제공자로의 전환 시간을 고려합니다.
|
|
77
77
|
|
|
78
|
+
**1.2.12:** 제공자 설정을 준수하며 Host 실패 후 브라우저에서 Google로 직접 요청하지 않습니다. 코드 블록, 들여쓰기 코드, 인라인 코드는 번역 서비스에 보내지 않고 원문 그대로 복원합니다. 스트리밍 중 닫히지 않은 코드도 보호하며, 이전 번역 캐시만 삭제하고 설정은 유지합니다. 업데이트 후 DSH Web 또는 Desktop을 완전히 다시 시작하세요. 페이지 새로고침만으로는 이전 클라이언트 리소스가 남을 수 있습니다.
|
|
79
|
+
|
|
78
80
|
**1.2.11 Bing 수정:** 무료 Bing 제공자를 유지합니다. 세션 페이지의 최종 출처로 번역 요청을 보내 중국 본토의 `www.bing.com` → `cn.bing.com` 리디렉션에서 POST 본문이 사라지는 문제를 방지합니다. SOCKS/curl도 네이티브 통신과 같은 브라우저 헤더를 사용해 HTTP 401을 수정하며, 빈 응답과 JSON이 아닌 응답에 명확한 오류를 표시합니다.
|
|
79
81
|
|
|
80
82
|
**1.2.10 DSH 프로필 호환:** 실행 중인 Desktop/Web 프로필의 `profiles/<profile>/cordis.patch.yml`(`id` + `config`)을 읽고 DSH 홈 패치와 시작 오버레이를 순서대로 적용합니다. 이전 `settings.yaml`도 지원합니다. 기본 모델 행은 `default-model`을 지원하며 자격 증명은 DSH 홈의 `.credentials.yaml`에서 읽어 메모리에만 유지합니다. 다시 검사하면 실제 설정 경로가 표시됩니다.
|
|
@@ -100,7 +102,7 @@ DSH **0.2.0-rc.2**에서는 **dsh-think-translate 1.2.5 이상**을 사용하세
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← 설정에서 드래그로 정렬
|
|
101
103
|
google / bing / OpenAI 호환 / Anthropic 중 선택
|
|
102
104
|
fallback 체인(선택, 기본 비활성, 설정에서 활성화)
|
|
103
|
-
→
|
|
105
|
+
→ 활성 제공자가 모두 실패하면 원문 유지
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- **제공자 설정**은 `config.json`(런타임 생성, gitignore 대상)에 있습니다: `chain`(순서 있는 id), `fallback`(enabled + chain, 설정 파일 전용), `providers`(각 항목의 `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). 예전 `priority` 형식은 자동 마이그레이션됩니다. `apiKeyEnv`를 선언한 제공자는 요청 시 해당 환경 변수에서 키를 해석하며(리터럴 `apiKey`는 폴백), 해석된 키를 `config.json`에 다시 쓰지 않습니다. 패치의 `null`은 해당 필드 삭제를 뜻하며 UI가 그렇게 비웁니다
|
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ Version **1.2.6** fixes duplicated thinking and answer text in DSH 0.2: the assi
|
|
|
72
72
|
|
|
73
73
|
**1.2.8 transport and recovery:** ordinary HTTP/HTTPS translation requests use native Node networking instead of spawning curl per text batch; SOCKS proxies and the Ollama installer retain curl compatibility. Failures stay in memory for 30 seconds, then reasoning and answer translation resume automatically. Request deadlines allow time for local-model loading and provider fallback.
|
|
74
74
|
|
|
75
|
+
**1.2.12:** provider switches are authoritative: the browser no longer sends direct Google requests after Host failure. Fenced, indented and inline code is excluded from translation payloads and restored exactly, including unfinished streaming spans. Old translation caches are discarded; preferences are retained. After upgrading, fully restart DSH Web or Desktop; a page refresh alone may still reference old client resources.
|
|
76
|
+
|
|
75
77
|
**1.2.11 Bing fixes:** the free Bing provider is retained. Translation requests use the session page's final origin, preventing the mainland China `www.bing.com` → `cn.bing.com` redirect from dropping the POST body. SOCKS/curl now shares the native browser headers to fix HTTP 401; empty and non-JSON responses report clear errors.
|
|
76
78
|
|
|
77
79
|
**1.2.10 DSH profile compatibility:** discovery follows the running Desktop/Web profile and reads `profiles/<profile>/cordis.patch.yml` (`id` + `config`), with home patches and launcher overlays applied afterward. Legacy `settings.yaml` is still supported. Default model rows use `default-model`; credentials remain in the DSH home `.credentials.yaml` and stay in memory. Rescan diagnostics show the file actually read.
|
|
@@ -102,7 +104,7 @@ browser → POST /_xlate/translate (same-origin, no CORS)
|
|
|
102
104
|
openai (OpenAI-compatible /chat/completions — Ollama local or any gateway)
|
|
103
105
|
anthropic(Anthropic Messages API /v1/messages)
|
|
104
106
|
fallback chain (config-only, off by default) tried when the primary chain fails entirely
|
|
105
|
-
→
|
|
107
|
+
→ original text if all enabled providers fail
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
- **Provider config** lives in `config.json` (runtime, gitignored): `chain` (ordered ids), `fallback` (enabled + chain, config-only), `providers` (per-provider `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). Old `priority`-based configs auto-migrate. A provider that declares `apiKeyEnv` resolves its key from that environment variable at request time (the literal `apiKey` stays as the fallback), and no env-resolved key is ever written back to `config.json`. A `null` field in a config patch deletes that field, which is how the UI clears one.
|
package/README.ru.md
CHANGED
|
@@ -75,6 +75,8 @@ New-Item -ItemType Junction -Path "$HOME\.dsh\profiles\node_modules\dsh-think-tr
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 передача запросов и восстановление:** обычные HTTP/HTTPS-запросы перевода используют сетевые средства Node без запуска curl для каждого пакета текста. Для SOCKS-прокси и установщика Ollama сохранён путь через curl. Ошибки хранятся в памяти 30 секунд, после чего перевод рассуждений и ответов автоматически повторяется. Тайм-ауты учитывают загрузку локальной модели и переход к другим провайдерам.
|
|
77
77
|
|
|
78
|
+
**1.2.12:** настройки провайдеров соблюдаются: после ошибки Host браузер больше не обращается к Google напрямую. Блочный код, код с отступом и встроенный код исключаются из запросов и сохраняются точно, включая незавершённые потоковые фрагменты. Старый кеш переводов очищается, настройки сохраняются. После обновления полностью перезапустите DSH Web или Desktop: простое обновление страницы может продолжить использовать старые ресурсы клиента.
|
|
79
|
+
|
|
78
80
|
**1.2.11 исправления Bing:** бесплатный провайдер сохранён. Запросы перевода отправляются на конечный origin страницы сеанса, чтобы перенаправление `www.bing.com` → `cn.bing.com` в материковом Китае не удаляло тело POST. SOCKS/curl использует те же заголовки браузера, что и нативный транспорт, устраняя HTTP 401; пустые ответы и ответы не в формате JSON дают понятные ошибки.
|
|
79
81
|
|
|
80
82
|
**1.2.10 совместимость с профилями DSH:** обнаружение следует активному профилю Desktop/Web и читает `profiles/<profile>/cordis.patch.yml` (`id` + `config`), затем патчи домашнего каталога DSH и параметры запуска. Прежний `settings.yaml` поддерживается. Модель по умолчанию использует `default-model`; ключи читаются из `.credentials.yaml` домашнего каталога DSH и остаются только в памяти. Диагностика показывает прочитанный файл.
|
|
@@ -100,7 +102,7 @@ New-Item -ItemType Junction -Path "$HOME\.dsh\profiles\node_modules\dsh-think-tr
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← порядок перетаскиванием в настройках
|
|
101
103
|
google / bing / OpenAI-совместимый / Anthropic
|
|
102
104
|
fallback-цепочка (необязательно, по умолчанию выключена, включается в конфиге)
|
|
103
|
-
→
|
|
105
|
+
→ исходный текст, если все включённые провайдеры недоступны
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- **Конфигурация провайдеров** живёт в `config.json` (создаётся во время работы, в gitignore): `chain` (упорядоченные id), `fallback` (enabled + chain, только в файле), `providers` (у каждого `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`). Старые конфиги с `priority` мигрируют автоматически; провайдер с `apiKeyEnv` разрешает ключ из этой переменной на каждый запрос (литеральный `apiKey` остаётся запасным), и разрешённый ключ никогда не пишется обратно в `config.json`; `null` в патче удаляет поле — так интерфейс и очищает значения
|
package/README.zh-CN.md
CHANGED
|
@@ -75,6 +75,8 @@ DSH **0.2.0-rc.2** 请使用 **dsh-think-translate 1.2.5 或更新版本**。1.2
|
|
|
75
75
|
|
|
76
76
|
**1.2.8 传输与恢复:**普通 HTTP/HTTPS 翻译请求改用 Node 原生网络,不再为每个文本批次启动 curl;SOCKS 代理和 Ollama 安装器保留 curl 兼容路径。失败结果在内存缓存 30 秒,冷却结束后思考与回答翻译会自动恢复重试。请求超时为本地模型加载和提供方回退留出时间。
|
|
77
77
|
|
|
78
|
+
**1.2.12:**提供方开关统一由 Host 执行,失败后浏览器不再直连 Google。围栏代码、缩进代码和行内代码不发送给翻译服务,按原文精确拼回;流式输出中尚未闭合的代码同样保护。清理旧译文缓存,保留设置。 升级后请完整重启 DSH Web 或 Desktop;仅刷新页面可能继续引用旧客户端资源。
|
|
79
|
+
|
|
78
80
|
**1.2.11 Bing 修复:**保留免费 Bing 通道。翻译请求跟随会话页最终域名,避免国内 `www.bing.com` → `cn.bing.com` 跳转丢失 POST 正文;SOCKS/curl 同步原生浏览器请求头,修复 HTTP 401。空响应和非 JSON 响应给出明确错误。
|
|
79
81
|
|
|
80
82
|
**1.2.10 DSH 配置兼容:**发现提供方时跟随当前 Desktop/Web profile,读取 `profiles/<profile>/cordis.patch.yml`(`id` + `config`),随后应用 home 补丁与启动覆盖;继续支持旧版 `settings.yaml`。默认模型行支持 `default-model`,凭据仍从 DSH home 的 `.credentials.yaml` 解析且只留在内存;重新扫描会显示实际读取的配置路径。
|
|
@@ -100,7 +102,7 @@ DSH **0.2.0-rc.2** 请使用 **dsh-think-translate 1.2.5 或更新版本**。1.2
|
|
|
100
102
|
chain: [provider1, provider2, ...] ← 设置里拖拽排序
|
|
101
103
|
google / bing / openai 兼容 / anthropic 任选
|
|
102
104
|
fallback 链(可选,默认关闭,配置里开启)
|
|
103
|
-
→
|
|
105
|
+
→ 所有已启用提供方失败时保留原文
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
- **提供方配置** 存在 `config.json`(运行期生成,已 gitignore):`chain`(有序 id)、`fallback`(enabled + chain,仅配置文件)、`providers`(每项的 `type`/`enabled`/`baseURL`/`apiKey`/`apiKeyEnv`/`model`)。旧的 `priority` 配置会自动迁移;声明 `apiKeyEnv` 的提供方在请求时从该环境变量解析密钥(字面 `apiKey` 作为兜底),解析出的密钥绝不写回 `config.json`;patch 里显式的 `null` 表示删除该字段,UI 就是用它清空字段的
|
package/lib/client.js
CHANGED
|
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
// there yet (an older process still serving the port), and the contract test
|
|
24
24
|
// pins PLUGIN_VERSION to package.json so the two can never drift.
|
|
25
25
|
var PLUGIN_NAME = "dsh-think-translate";
|
|
26
|
-
var PLUGIN_VERSION = "1.2.
|
|
26
|
+
var PLUGIN_VERSION = "1.2.12";
|
|
27
27
|
var PLUGIN_REPO = "https://github.com/UncleK/dsh-think-translate";
|
|
28
28
|
// The official MarkdownText contract changed in 0.1.2-alpha: it now takes a
|
|
29
29
|
// nested `labels` object and dereferences it without defaults inside its code
|
|
@@ -653,8 +653,7 @@ window.__ModuleLoader__.load({
|
|
|
653
653
|
};
|
|
654
654
|
|
|
655
655
|
// ------------------------------------------------------------------
|
|
656
|
-
// translation pipeline (
|
|
657
|
-
// system proxy automatically; endpoint returns CORS *)
|
|
656
|
+
// translation pipeline (same-origin Host owns provider routing)
|
|
658
657
|
// ------------------------------------------------------------------
|
|
659
658
|
var mem = new Map();
|
|
660
659
|
// Translation cache persisted to localStorage so switching sessions or
|
|
@@ -662,8 +661,8 @@ window.__ModuleLoader__.load({
|
|
|
662
661
|
// historical message (which wedged the UI when the local model was slow,
|
|
663
662
|
// 2026-08-26). In-memory map is the fast path; localStorage is the
|
|
664
663
|
// durable cross-session floor.
|
|
665
|
-
var MEM_KEY = "dsh-xlate-cache:
|
|
666
|
-
// Discard results
|
|
664
|
+
var MEM_KEY = "dsh-xlate-cache:v3:";
|
|
665
|
+
// Discard old results that may contain translated code; retain preferences.
|
|
667
666
|
try {
|
|
668
667
|
var legacyKeys = [];
|
|
669
668
|
for (var cacheIndex = 0; cacheIndex < localStorage.length; cacheIndex++) {
|
|
@@ -755,6 +754,70 @@ window.__ModuleLoader__.load({
|
|
|
755
754
|
return h.toString(36) + "." + s.length.toString(36);
|
|
756
755
|
}
|
|
757
756
|
|
|
757
|
+
// Inlined from text.js for the standalone client bundle; parity is tested.
|
|
758
|
+
function protectedTextParts(text) {
|
|
759
|
+
const parts = []
|
|
760
|
+
let proseStart = 0
|
|
761
|
+
let cursor = 0
|
|
762
|
+
function protect(start, end, complete) {
|
|
763
|
+
if (start > proseStart) parts.push({ text: text.slice(proseStart, start), start: proseStart, end: start, code: false, complete: true })
|
|
764
|
+
parts.push({ text: text.slice(start, end), start, end, code: true, complete })
|
|
765
|
+
proseStart = end
|
|
766
|
+
cursor = end
|
|
767
|
+
}
|
|
768
|
+
function lineEnd(start) {
|
|
769
|
+
const newline = text.indexOf('\n', start)
|
|
770
|
+
return newline < 0 ? text.length : newline + 1
|
|
771
|
+
}
|
|
772
|
+
while (cursor < text.length) {
|
|
773
|
+
if (cursor === 0 || text[cursor - 1] === '\n') {
|
|
774
|
+
const end = lineEnd(cursor)
|
|
775
|
+
const line = text.slice(cursor, end)
|
|
776
|
+
// Treat indented/quoted fences conservatively as code as well.
|
|
777
|
+
const fence = line.match(/^[\t ]*(?:>[\t ]*)*(?:(?:[-+*]|[0-9]+[.)])[\t ]+)?(`{3,}|~{3,})[^\r\n]*(?:\r?\n)?$/)
|
|
778
|
+
if (fence) {
|
|
779
|
+
const close = new RegExp('^[\\t ]*(?:>[\\t ]*)*' + fence[1][0] + '{' + fence[1].length + ',}[\\t ]*(?:\\r?\\n)?$')
|
|
780
|
+
let stop = end
|
|
781
|
+
let complete = false
|
|
782
|
+
while (stop < text.length) {
|
|
783
|
+
const next = lineEnd(stop)
|
|
784
|
+
if (close.test(text.slice(stop, next))) { stop = next; complete = true; break }
|
|
785
|
+
stop = next
|
|
786
|
+
}
|
|
787
|
+
protect(cursor, stop, complete)
|
|
788
|
+
continue
|
|
789
|
+
}
|
|
790
|
+
if (/^(?: {4}|\t)/.test(line)) {
|
|
791
|
+
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
792
|
+
continue
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (text[cursor] === '`') {
|
|
796
|
+
let slashes = 0
|
|
797
|
+
for (let i = cursor - 1; i >= 0 && text[i] === '\\'; i--) slashes++
|
|
798
|
+
if (slashes % 2) { cursor++; continue }
|
|
799
|
+
let after = cursor + 1
|
|
800
|
+
while (text[after] === '`') after++
|
|
801
|
+
const size = after - cursor
|
|
802
|
+
let next = after
|
|
803
|
+
let stop = text.length
|
|
804
|
+
let complete = false
|
|
805
|
+
while ((next = text.indexOf('`', next)) >= 0) {
|
|
806
|
+
let end = next + 1
|
|
807
|
+
while (text[end] === '`') end++
|
|
808
|
+
if (end - next === size) { stop = end; complete = true; break }
|
|
809
|
+
next = end
|
|
810
|
+
}
|
|
811
|
+
// An unfinished streaming code span must not expose its body as prose.
|
|
812
|
+
protect(cursor, stop, complete)
|
|
813
|
+
continue
|
|
814
|
+
}
|
|
815
|
+
cursor++
|
|
816
|
+
}
|
|
817
|
+
if (proseStart < text.length) parts.push({ text: text.slice(proseStart), start: proseStart, end: text.length, code: false, complete: true })
|
|
818
|
+
return parts
|
|
819
|
+
}
|
|
820
|
+
|
|
758
821
|
function isCjk(code) {
|
|
759
822
|
return (code >= 0x3400 && code <= 0x4dbf) || (code >= 0x4e00 && code <= 0x9fff)
|
|
760
823
|
|| (code >= 0xf900 && code <= 0xfaff) || (code >= 0x20000 && code <= 0x2ffff)
|
|
@@ -778,9 +841,7 @@ window.__ModuleLoader__.load({
|
|
|
778
841
|
return /^(?:[$>#]\s*)?(?:(?:npm|pnpm|yarn|bun|npx|node|git|cd|curl|pwsh|reg)\b|(?:const|let|var|import|export|function|class|return|if|for|while|async|await)\b|[\[{(]|['"]?[\w.-]+['"]?\s*(?:[:=]|=>)|[\w$.]+\s*\(|(?:[~./][\w@~./-]*|[\w.-]+\/[\w@~./-]+)(?::\d+)?$)/.test(line);
|
|
779
842
|
}
|
|
780
843
|
function hasProse(text) {
|
|
781
|
-
var outside = text
|
|
782
|
-
.replace(/```[\s\S]*?```/g, " ")
|
|
783
|
-
.replace(/`[^`\n]*`/g, " ")
|
|
844
|
+
var outside = protectedTextParts(text).filter(function (part) { return !part.code; }).map(function (part) { return part.text; }).join(" ")
|
|
784
845
|
.replace(/https?:\/\/\S+/g, " ");
|
|
785
846
|
var lines = outside.split("\n").map(function (l) { return l.trim(); }).filter(Boolean);
|
|
786
847
|
if (lines.length === 0) return false;
|
|
@@ -790,7 +851,7 @@ window.__ModuleLoader__.load({
|
|
|
790
851
|
}
|
|
791
852
|
// Decide from prose, not paths, identifiers, or structured payloads (#10).
|
|
792
853
|
function proseForDetection(text) {
|
|
793
|
-
let prose = text.
|
|
854
|
+
let prose = protectedTextParts(text).filter(function (part) { return !part.code; }).map(function (part) { return part.text; }).join(' ')
|
|
794
855
|
.replace(/https?:\/\/[^\s<>]+/g, ' ')
|
|
795
856
|
.replace(/\b[A-Za-z]:[\\/][^\s"'`,;,。;!?、)}\]]*/g, ' ')
|
|
796
857
|
.replace(/(^|\s)\/(?:[^\s"'`,;,。;!?、)}\]]+)/g, '$1 ')
|
|
@@ -824,54 +885,6 @@ window.__ModuleLoader__.load({
|
|
|
824
885
|
const traditionalTarget = !/(?:^|-)hans(?:-|$)/i.test(tag) && /(?:^|-)(?:hant|tw|hk|mo)(?:-|$)/i.test(tag)
|
|
825
886
|
return (traditionalTarget ? SIMPLIFIED_CUES : TRADITIONAL_CUES).test(prose)
|
|
826
887
|
}
|
|
827
|
-
function chunkText(text) {
|
|
828
|
-
if (text.length <= 1200) return [text];
|
|
829
|
-
var sentences = text.match(/[\s\S]*?[.。!?!?;\n]|[\s\S]+$/g) || [text];
|
|
830
|
-
var chunks = [], cur = "";
|
|
831
|
-
for (var i = 0; i < sentences.length; i++) {
|
|
832
|
-
var sn = sentences[i];
|
|
833
|
-
if (cur && cur.length + sn.length > 1200) { chunks.push(cur); cur = ""; }
|
|
834
|
-
if (sn.length > 1200) {
|
|
835
|
-
for (var k = 0; k < sn.length; k += 1100) chunks.push(sn.slice(k, k + 1100));
|
|
836
|
-
cur = "";
|
|
837
|
-
} else {
|
|
838
|
-
cur += sn;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (cur.trim()) chunks.push(cur);
|
|
842
|
-
return chunks.length ? chunks : [text];
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
async function gtxChunk(text, target) {
|
|
846
|
-
var qs = "translate_a/single?client=dict-chrome-ex&sl=auto&tl="
|
|
847
|
-
+ encodeURIComponent(target) + "&dt=t&q=" + encodeURIComponent(text);
|
|
848
|
-
// translate.googleapis.com is TLS-reset by Clash/GFW in CN networks;
|
|
849
|
-
// translate.google.com serves the same gtx JSON and is reachable there.
|
|
850
|
-
// Try both so CN and the rest of the world both work.
|
|
851
|
-
var hosts = ["https://translate.google.com/", "https://translate.googleapis.com/"];
|
|
852
|
-
var lastErr = null;
|
|
853
|
-
for (var hi = 0; hi < hosts.length; hi++) {
|
|
854
|
-
var url = hosts[hi] + qs;
|
|
855
|
-
// Bounded wait: when direct google is unreachable this must fail fast
|
|
856
|
-
// instead of hanging on the browser's own timeout (2026-08-22: direct
|
|
857
|
-
// gtx blocked in this network stalled every fallback for ~8s+).
|
|
858
|
-
try {
|
|
859
|
-
var res = await fetchWithTimeout(url, null, 6000);
|
|
860
|
-
if (!res.ok) throw new Error("gtx HTTP " + res.status);
|
|
861
|
-
var arr = await res.json();
|
|
862
|
-
var segs = arr && arr[0];
|
|
863
|
-
if (!Array.isArray(segs)) throw new Error("gtx unexpected shape");
|
|
864
|
-
var out = "";
|
|
865
|
-
for (var i = 0; i < segs.length; i++) {
|
|
866
|
-
if (Array.isArray(segs[i]) && typeof segs[i][0] === "string") out += segs[i][0];
|
|
867
|
-
}
|
|
868
|
-
if (!out.trim()) throw new Error("gtx empty translation");
|
|
869
|
-
return out;
|
|
870
|
-
} catch (e) { lastErr = e; }
|
|
871
|
-
}
|
|
872
|
-
throw lastErr || new Error("gtx unavailable");
|
|
873
|
-
}
|
|
874
|
-
|
|
875
888
|
function sleepMs(ms) { return new Promise(function (res) { setTimeout(res, ms); }); }
|
|
876
889
|
|
|
877
890
|
function fetchWithTimeout(url, opts, ms) {
|
|
@@ -929,14 +942,7 @@ window.__ModuleLoader__.load({
|
|
|
929
942
|
try { r = await hostAttempt(text, target); break; }
|
|
930
943
|
catch (e) { r = null; lastErr = e; if (e && e.retryAt > Date.now()) break; }
|
|
931
944
|
}
|
|
932
|
-
|
|
933
|
-
try {
|
|
934
|
-
var chunks = chunkText(text);
|
|
935
|
-
var outs = [];
|
|
936
|
-
for (var k = 0; k < chunks.length; k++) outs.push(await gtxChunk(chunks[k], target));
|
|
937
|
-
r = { ok: true, text: outs.join(""), provider: "google-direct" };
|
|
938
|
-
} catch (e2) { r = null; if (e2 && e2.message) lastErr = e2; }
|
|
939
|
-
}
|
|
945
|
+
// The Host alone owns routing and provider switches.
|
|
940
946
|
if (r && r.ok) {
|
|
941
947
|
if (r.providerSignature) recordCheck(r.provider, true, r.model, r.providerSignature);
|
|
942
948
|
stats.ok++; stats.lastError = null; stats.lastProvider = r.provider; stats.lastModel = r.model || null;
|
|
@@ -1025,12 +1031,15 @@ window.__ModuleLoader__.load({
|
|
|
1025
1031
|
function latestLine(t) { var v = t.trimEnd(); var i = v.lastIndexOf("\n"); return i === -1 ? v : v.slice(i + 1); }
|
|
1026
1032
|
|
|
1027
1033
|
function splitSentences(text) {
|
|
1028
|
-
var
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
+
var out = [];
|
|
1035
|
+
protectedTextParts(text).forEach(function (part) {
|
|
1036
|
+
if (part.code) { out.push(part); return; }
|
|
1037
|
+
var re = /[^。!?!?;\n]*[。!?!?;\n]+|[^。!?!?;\n]+/g, m;
|
|
1038
|
+
while ((m = re.exec(part.text)) !== null) {
|
|
1039
|
+
if (m[0].length === 0) { re.lastIndex++; continue; }
|
|
1040
|
+
out.push({ start: part.start + m.index, end: part.start + m.index + m[0].length, text: m[0], code: false, complete: true });
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1034
1043
|
return out;
|
|
1035
1044
|
}
|
|
1036
1045
|
var XL_STREAM_MARGIN = 120;
|
|
@@ -1086,7 +1095,7 @@ window.__ModuleLoader__.load({
|
|
|
1086
1095
|
// batches (local 7B models degrade on long blobs).
|
|
1087
1096
|
var batch = "", count = 0, nextIdx = cur.segIdx;
|
|
1088
1097
|
for (var i = cur.segIdx; i < segs.length; i++) {
|
|
1089
|
-
if (isRunning && segs[i].end > latest.length - XL_STREAM_MARGIN) break;
|
|
1098
|
+
if (isRunning && (!segs[i].complete || segs[i].end > latest.length - XL_STREAM_MARGIN)) break;
|
|
1090
1099
|
batch += segs[i].text;
|
|
1091
1100
|
nextIdx = i + 1;
|
|
1092
1101
|
count++;
|
|
@@ -1106,7 +1115,7 @@ window.__ModuleLoader__.load({
|
|
|
1106
1115
|
if (r && r.ok) {
|
|
1107
1116
|
next.failed = false;
|
|
1108
1117
|
next.retryAt = 0;
|
|
1109
|
-
|
|
1118
|
+
next.transText = p.transText + r.text;
|
|
1110
1119
|
next.segIdx = Math.max(p.segIdx, nextIdx);
|
|
1111
1120
|
} else {
|
|
1112
1121
|
// A negative-cache hit is a cooldown, not another failed
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createLocalPreparation, ollamaOrigin, localFirst, validOllamaSignature
|
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
14
|
import { requestTextInfo, isLoopback, DEFAULT_HEADERS } from './transport.js'
|
|
15
15
|
import { scanDshProviders, extractDshProviders, extractCredRefs, extractHarnessDefaultRoute } from './discovery.js'
|
|
16
|
+
import { protectedTextParts, translateProtectedText } from './text.js'
|
|
16
17
|
|
|
17
18
|
export const name = 'dsh-think-translate'
|
|
18
19
|
|
|
@@ -970,9 +971,7 @@ function isCodeOnlyLine(line) {
|
|
|
970
971
|
return /^(?:[$>#]\s*)?(?:(?:npm|pnpm|yarn|bun|npx|node|git|cd|curl|pwsh|reg)\b|(?:const|let|var|import|export|function|class|return|if|for|while|async|await)\b|[\[{(]|['"]?[\w.-]+['"]?\s*(?:[:=]|=>)|[\w$.]+\s*\(|(?:[~./][\w@~./-]*|[\w.-]+\/[\w@~./-]+)(?::\d+)?$)/.test(line)
|
|
971
972
|
}
|
|
972
973
|
function hasProse(text) {
|
|
973
|
-
const outside = text
|
|
974
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
975
|
-
.replace(/`[^`\n]*`/g, ' ')
|
|
974
|
+
const outside = protectedTextParts(text).filter(part => !part.code).map(part => part.text).join(' ')
|
|
976
975
|
.replace(/https?:\/\/\S+/g, ' ')
|
|
977
976
|
const lines = outside.split('\n').map(function (l) { return l.trim() }).filter(Boolean)
|
|
978
977
|
if (lines.length === 0) return false
|
|
@@ -982,7 +981,7 @@ function hasProse(text) {
|
|
|
982
981
|
}
|
|
983
982
|
// Decide from prose, not paths, identifiers, or structured payloads (#10).
|
|
984
983
|
function proseForDetection(text) {
|
|
985
|
-
let prose = text.
|
|
984
|
+
let prose = protectedTextParts(text).filter(part => !part.code).map(part => part.text).join(' ')
|
|
986
985
|
.replace(/https?:\/\/[^\s<>]+/g, ' ')
|
|
987
986
|
.replace(/\b[A-Za-z]:[\\/][^\s"'`,;,。;!?、)}\]]*/g, ' ')
|
|
988
987
|
.replace(/(^|\s)\/(?:[^\s"'`,;,。;!?、)}\]]+)/g, '$1 ')
|
|
@@ -1057,15 +1056,13 @@ async function runChainFor(ids, text, target, cfg) {
|
|
|
1057
1056
|
if (!adapter) continue
|
|
1058
1057
|
try {
|
|
1059
1058
|
if (ollamaOrigin(id, p)) await localPreparation.ensure(p)
|
|
1060
|
-
const
|
|
1061
|
-
const outs = []
|
|
1062
|
-
for (const c of chunks) outs.push(await adapter(c, target, p))
|
|
1059
|
+
const translated = await translateProtectedText(text, chunk => adapter(chunk, target, p), chunkText)
|
|
1063
1060
|
state.ok++
|
|
1064
1061
|
state.lastProvider = id
|
|
1065
1062
|
state.lastError = null
|
|
1066
1063
|
// Carry the concrete model name so the UI can show e.g. "openai: qwen2.5:14b".
|
|
1067
1064
|
attempts.push({ provider: id, signature: providerFingerprint(p), ok: true, model: p.model || null })
|
|
1068
|
-
return { ok: true, text:
|
|
1065
|
+
return { ok: true, text: translated, provider: id, providerSignature: providerFingerprint(p), attempts, ...(p.model ? { model: p.model } : {}) }
|
|
1069
1066
|
} catch (e) {
|
|
1070
1067
|
lastErr = e
|
|
1071
1068
|
attempts.push({ provider: id, signature: providerFingerprint(p), ok: false, model: p.model || null })
|
|
@@ -1117,7 +1114,7 @@ async function handleTranslate(body) {
|
|
|
1117
1114
|
if (!adapter) return { ok: false, text, error: 'unsupported type: ' + p.type }
|
|
1118
1115
|
try {
|
|
1119
1116
|
if (ollamaOrigin(forced, p)) await localPreparation.ensure(p)
|
|
1120
|
-
const out = await
|
|
1117
|
+
const out = await translateProtectedText(text, chunk => adapter(chunk, target, p), chunkText)
|
|
1121
1118
|
return { ok: true, text: out, provider: forced, providerSignature: providerFingerprint(p), ...(p.model ? { model: p.model } : {}) }
|
|
1122
1119
|
} catch (e) {
|
|
1123
1120
|
return { ok: false, text, error: e instanceof Error ? e.message : String(e) }
|
package/lib/text.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Code spans keep their exact source offsets so neither translation nor batching
|
|
2
|
+
// needs placeholders that a provider could translate, remove, or reorder.
|
|
3
|
+
export function protectedTextParts(text) {
|
|
4
|
+
const parts = []
|
|
5
|
+
let proseStart = 0
|
|
6
|
+
let cursor = 0
|
|
7
|
+
function protect(start, end, complete) {
|
|
8
|
+
if (start > proseStart) parts.push({ text: text.slice(proseStart, start), start: proseStart, end: start, code: false, complete: true })
|
|
9
|
+
parts.push({ text: text.slice(start, end), start, end, code: true, complete })
|
|
10
|
+
proseStart = end
|
|
11
|
+
cursor = end
|
|
12
|
+
}
|
|
13
|
+
function lineEnd(start) {
|
|
14
|
+
const newline = text.indexOf('\n', start)
|
|
15
|
+
return newline < 0 ? text.length : newline + 1
|
|
16
|
+
}
|
|
17
|
+
while (cursor < text.length) {
|
|
18
|
+
if (cursor === 0 || text[cursor - 1] === '\n') {
|
|
19
|
+
const end = lineEnd(cursor)
|
|
20
|
+
const line = text.slice(cursor, end)
|
|
21
|
+
// Treat indented/quoted fences conservatively as code as well.
|
|
22
|
+
const fence = line.match(/^[\t ]*(?:>[\t ]*)*(?:(?:[-+*]|[0-9]+[.)])[\t ]+)?(`{3,}|~{3,})[^\r\n]*(?:\r?\n)?$/)
|
|
23
|
+
if (fence) {
|
|
24
|
+
const close = new RegExp('^[\\t ]*(?:>[\\t ]*)*' + fence[1][0] + '{' + fence[1].length + ',}[\\t ]*(?:\\r?\\n)?$')
|
|
25
|
+
let stop = end
|
|
26
|
+
let complete = false
|
|
27
|
+
while (stop < text.length) {
|
|
28
|
+
const next = lineEnd(stop)
|
|
29
|
+
if (close.test(text.slice(stop, next))) { stop = next; complete = true; break }
|
|
30
|
+
stop = next
|
|
31
|
+
}
|
|
32
|
+
protect(cursor, stop, complete)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (/^(?: {4}|\t)/.test(line)) {
|
|
36
|
+
protect(cursor, end, end < text.length || line.endsWith('\n'))
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (text[cursor] === '`') {
|
|
41
|
+
let slashes = 0
|
|
42
|
+
for (let i = cursor - 1; i >= 0 && text[i] === '\\'; i--) slashes++
|
|
43
|
+
if (slashes % 2) { cursor++; continue }
|
|
44
|
+
let after = cursor + 1
|
|
45
|
+
while (text[after] === '`') after++
|
|
46
|
+
const size = after - cursor
|
|
47
|
+
let next = after
|
|
48
|
+
let stop = text.length
|
|
49
|
+
let complete = false
|
|
50
|
+
while ((next = text.indexOf('`', next)) >= 0) {
|
|
51
|
+
let end = next + 1
|
|
52
|
+
while (text[end] === '`') end++
|
|
53
|
+
if (end - next === size) { stop = end; complete = true; break }
|
|
54
|
+
next = end
|
|
55
|
+
}
|
|
56
|
+
// An unfinished streaming code span must not expose its body as prose.
|
|
57
|
+
protect(cursor, stop, complete)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
cursor++
|
|
61
|
+
}
|
|
62
|
+
if (proseStart < text.length) parts.push({ text: text.slice(proseStart), start: proseStart, end: text.length, code: false, complete: true })
|
|
63
|
+
return parts
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Only prose reaches a provider; separators adjoining code stay byte-for-byte.
|
|
67
|
+
export async function translateProtectedText(text, translate, split = value => [value]) {
|
|
68
|
+
const output = []
|
|
69
|
+
for (const part of protectedTextParts(text)) {
|
|
70
|
+
const match = !part.code && part.text.match(/^(\s*)([\s\S]*?)(\s*)$/)
|
|
71
|
+
if (!match || !/\p{L}/u.test(match[2])) { output.push(part.text); continue }
|
|
72
|
+
const chunks = []
|
|
73
|
+
for (const chunk of split(match[2])) chunks.push((await translate(chunk)).trim())
|
|
74
|
+
output.push(match[1] + chunks.join('\n\n') + match[3])
|
|
75
|
+
}
|
|
76
|
+
return output.join('')
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-think-translate",
|
|
3
3
|
"description": "Display-layer translation for the DSH web app: thinking chain, task cards, and optional answer text, in 8 target languages (local Ollama / any OpenAI-compatible or Anthropic endpoint / Google / Bing, fail-open)",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.12",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"lib/ollama.js",
|
|
16
16
|
"lib/transport.js",
|
|
17
17
|
"lib/discovery.js",
|
|
18
|
+
"lib/text.js",
|
|
18
19
|
"cordis.patch.yml",
|
|
19
20
|
"demo/demo.gif",
|
|
20
21
|
"demo/demo2.gif"
|