dsh-think-translate 1.2.10 → 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 CHANGED
@@ -75,6 +75,10 @@ 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
+
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.
81
+
78
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.
79
83
 
80
84
  **1.2.9 Sprachkorrekturen:** Japanische Vokabelhinweise gelten nur für Japanisch; OpenAI/Anthropic erhalten den vollständigen Sprachnamen. Bing nutzt `auto-detect` und `zh-Hans`/`zh-Hant`. Die Erkennung chinesischer Prosa blendet Pfade, Code und JSON aus: vereinfachte Prosa wird übersprungen, häufige traditionelle Schriftzeichen lösen eine Übersetzungsanfrage aus, japanische/koreanische Prosa wird übersetzt. Alte Übersetzungen werden einmalig aus dem Cache entfernt; Einstellungen bleiben erhalten.
@@ -98,7 +102,7 @@ Browser → POST /_xlate/translate (gleiche Origin, kein CORS)
98
102
  chain: [provider1, provider2, ...] ← Reihenfolge per Drag in den Einstellungen
99
103
  google / bing / OpenAI-kompatibel / Anthropic
100
104
  Fallback-Kette (optional, standardmäßig aus, in der Config aktivierbar)
101
- → direkter Browser-Fallback
105
+ → Originaltext, wenn alle aktivierten Anbieter fehlschlagen
102
106
  ```
103
107
 
104
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,10 @@ 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
+
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.
81
+
78
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.
79
83
 
80
84
  **1.2.9 correcciones de idioma:** las pistas de vocabulario japonés solo se aplican al japonés; OpenAI/Anthropic reciben el nombre completo del idioma. Bing usa `auto-detect` y `zh-Hans`/`zh-Hant`. La detección del chino excluye rutas, código y JSON: omite la prosa ya simplificada, solicita conversión al detectar caracteres tradicionales frecuentes y traduce la prosa japonesa/coreana. Se elimina una vez la caché de traducciones anterior, conservando los ajustes.
@@ -98,7 +102,7 @@ navegador → POST /_xlate/translate (mismo origen, sin CORS)
98
102
  chain: [provider1, provider2, ...] ← orden arrastrando en Ajustes
99
103
  google / bing / compatible con OpenAI / Anthropic
100
104
  cadena fallback (opcional, desactivada por defecto, se activa en el config)
101
- → respaldo directo desde el navegador
105
+ → texto original si fallan todos los proveedores habilitados
102
106
  ```
103
107
 
104
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,10 @@ 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
+
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.
81
+
78
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.
79
83
 
80
84
  **1.2.9 corrections linguistiques :** les indications de vocabulaire japonais ne concernent que le japonais ; OpenAI/Anthropic reçoivent le nom complet de la langue. Bing utilise `auto-detect` et `zh-Hans`/`zh-Hant`. La détection du chinois écarte chemins, code et JSON : elle ignore la prose déjà simplifiée, demande une conversion en présence de caractères traditionnels courants et traduit la prose japonaise/coréenne. Les anciennes traductions en cache sont supprimées une fois, sans toucher aux réglages.
@@ -98,7 +102,7 @@ navigateur → POST /_xlate/translate (même origine, sans CORS)
98
102
  chain: [provider1, provider2, ...] ← ordre par glisser dans les Réglages
99
103
  google / bing / compatible OpenAI / Anthropic
100
104
  chaîne fallback (facultative, désactivée par défaut, activable dans la config)
101
- → repli direct depuis le navigateur
105
+ → texte original si tous les fournisseurs activés échouent
102
106
  ```
103
107
 
104
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,10 @@ 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
+
80
+ **1.2.11 Bing 修正:**無料の Bing を引き続き提供します。翻訳リクエストはセッションページの最終オリジンに送信し、中国本土の `www.bing.com` → `cn.bing.com` リダイレクトによる POST 本文の消失を防ぎます。SOCKS/curl はネイティブ通信と同じブラウザーヘッダーを使用して HTTP 401 を修正。空の応答と JSON 以外の応答には明確なエラーを表示します。
81
+
78
82
  **1.2.10 DSH プロファイル対応:**実行中の Desktop/Web プロファイルに従い、`profiles/<profile>/cordis.patch.yml`(`id` + `config`)、DSH home のパッチ、起動時のオーバーレイを順に読み込みます。旧形式の `settings.yaml` も対応。既定モデル行は `default-model` に対応し、資格情報は DSH home の `.credentials.yaml` から解決してメモリ内だけに保持します。再スキャン時に実際の設定パスを表示します。
79
83
 
80
84
  **1.2.9 言語修正:**日本語の語彙ヒントは日本語の翻訳先に限定し、OpenAI/Anthropic に完全な言語名を渡します。Bing は `auto-detect` と `zh-Hans`/`zh-Hant` を使用。中国語の判定ではパス・コード・JSON を除外し、簡体字の本文はスキップ、よく使われる繁体字を検出すると変換を依頼、日本語・韓国語の本文は翻訳します。旧訳文キャッシュを一度削除し、設定は保持します。
@@ -98,7 +102,7 @@ DSH **0.2.0-rc.2** では **dsh-think-translate 1.2.5 以降**を使用してく
98
102
  chain: [provider1, provider2, ...] ← 設定でドラッグして並べ替え
99
103
  google / bing / OpenAI 互換 / Anthropic から選択
100
104
  fallback チェーン(任意、既定は無効、設定で有効化)
101
- → ブラウザ直結のフォールバック
105
+ → 有効な提供元がすべて失敗した場合は原文を保持
102
106
  ```
103
107
 
104
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,10 @@ 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
+
80
+ **1.2.11 Bing 수정:** 무료 Bing 제공자를 유지합니다. 세션 페이지의 최종 출처로 번역 요청을 보내 중국 본토의 `www.bing.com` → `cn.bing.com` 리디렉션에서 POST 본문이 사라지는 문제를 방지합니다. SOCKS/curl도 네이티브 통신과 같은 브라우저 헤더를 사용해 HTTP 401을 수정하며, 빈 응답과 JSON이 아닌 응답에 명확한 오류를 표시합니다.
81
+
78
82
  **1.2.10 DSH 프로필 호환:** 실행 중인 Desktop/Web 프로필의 `profiles/<profile>/cordis.patch.yml`(`id` + `config`)을 읽고 DSH 홈 패치와 시작 오버레이를 순서대로 적용합니다. 이전 `settings.yaml`도 지원합니다. 기본 모델 행은 `default-model`을 지원하며 자격 증명은 DSH 홈의 `.credentials.yaml`에서 읽어 메모리에만 유지합니다. 다시 검사하면 실제 설정 경로가 표시됩니다.
79
83
 
80
84
  **1.2.9 언어 수정:** 일본어 어휘 힌트는 일본어 대상에만 적용하며 OpenAI/Anthropic에 전체 언어 이름을 전달합니다. Bing은 `auto-detect`와 `zh-Hans`/`zh-Hant`를 사용합니다. 중국어 감지 시 경로·코드·JSON을 제외하고, 이미 간체인 본문은 건너뛰며 자주 쓰는 번체 글자를 감지하면 변환을 요청합니다. 일본어·한국어 본문은 번역합니다. 이전 번역 캐시만 한 번 삭제하고 설정은 유지합니다.
@@ -98,7 +102,7 @@ DSH **0.2.0-rc.2**에서는 **dsh-think-translate 1.2.5 이상**을 사용하세
98
102
  chain: [provider1, provider2, ...] ← 설정에서 드래그로 정렬
99
103
  google / bing / OpenAI 호환 / Anthropic 중 선택
100
104
  fallback 체인(선택, 기본 비활성, 설정에서 활성화)
101
- → 브라우저 직접 연결 폴백
105
+ → 활성 제공자가 모두 실패하면 원문 유지
102
106
  ```
103
107
 
104
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,10 @@ 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
+
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.
78
+
75
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.
76
80
 
77
81
  **1.2.9 language fixes:** Japanese vocabulary hints now apply only to Japanese targets; OpenAI/Anthropic requests include a full language name. Bing uses `auto-detect` and maps Chinese targets to `zh-Hans`/`zh-Hant`. Chinese detection examines prose after removing paths, code and JSON: already-Simplified prose is skipped, common Traditional script cues request conversion, and Japanese/Korean prose is translated. Old persisted translation results are cleared once; settings remain intact.
@@ -100,7 +104,7 @@ browser → POST /_xlate/translate (same-origin, no CORS)
100
104
  openai (OpenAI-compatible /chat/completions — Ollama local or any gateway)
101
105
  anthropic(Anthropic Messages API /v1/messages)
102
106
  fallback chain (config-only, off by default) tried when the primary chain fails entirely
103
- → browser-direct fallback
107
+ → original text if all enabled providers fail
104
108
  ```
105
109
 
106
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,10 @@ 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
+
80
+ **1.2.11 исправления Bing:** бесплатный провайдер сохранён. Запросы перевода отправляются на конечный origin страницы сеанса, чтобы перенаправление `www.bing.com` → `cn.bing.com` в материковом Китае не удаляло тело POST. SOCKS/curl использует те же заголовки браузера, что и нативный транспорт, устраняя HTTP 401; пустые ответы и ответы не в формате JSON дают понятные ошибки.
81
+
78
82
  **1.2.10 совместимость с профилями DSH:** обнаружение следует активному профилю Desktop/Web и читает `profiles/<profile>/cordis.patch.yml` (`id` + `config`), затем патчи домашнего каталога DSH и параметры запуска. Прежний `settings.yaml` поддерживается. Модель по умолчанию использует `default-model`; ключи читаются из `.credentials.yaml` домашнего каталога DSH и остаются только в памяти. Диагностика показывает прочитанный файл.
79
83
 
80
84
  **1.2.9 языковые исправления:** подсказки японской лексики применяются только к японскому; OpenAI/Anthropic получают полное название языка. Bing использует `auto-detect` и `zh-Hans`/`zh-Hant`. Определение китайского исключает пути, код и JSON: упрощённая проза пропускается, частые традиционные иероглифы вызывают запрос преобразования, японская/корейская проза переводится. Старый кэш переводов очищается один раз, настройки сохраняются.
@@ -98,7 +102,7 @@ New-Item -ItemType Junction -Path "$HOME\.dsh\profiles\node_modules\dsh-think-tr
98
102
  chain: [provider1, provider2, ...] ← порядок перетаскиванием в настройках
99
103
  google / bing / OpenAI-совместимый / Anthropic
100
104
  fallback-цепочка (необязательно, по умолчанию выключена, включается в конфиге)
101
- → прямой запасной путь из браузера
105
+ → исходный текст, если все включённые провайдеры недоступны
102
106
  ```
103
107
 
104
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,10 @@ 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
+
80
+ **1.2.11 Bing 修复:**保留免费 Bing 通道。翻译请求跟随会话页最终域名,避免国内 `www.bing.com` → `cn.bing.com` 跳转丢失 POST 正文;SOCKS/curl 同步原生浏览器请求头,修复 HTTP 401。空响应和非 JSON 响应给出明确错误。
81
+
78
82
  **1.2.10 DSH 配置兼容:**发现提供方时跟随当前 Desktop/Web profile,读取 `profiles/<profile>/cordis.patch.yml`(`id` + `config`),随后应用 home 补丁与启动覆盖;继续支持旧版 `settings.yaml`。默认模型行支持 `default-model`,凭据仍从 DSH home 的 `.credentials.yaml` 解析且只留在内存;重新扫描会显示实际读取的配置路径。
79
83
 
80
84
  **1.2.9 语言修复:**日语词汇提示仅用于日语目标,OpenAI/Anthropic 请求带上完整语言名称。Bing 使用 `auto-detect`,并将中文目标映射为 `zh-Hans`/`zh-Hant`。中文检测先排除路径、代码和 JSON:已是简体的正文跳过,常用繁体区别字触发转换请求,日语/韩语正文正常翻译。升级时一次性清理旧译文缓存,保留设置。
@@ -98,7 +102,7 @@ DSH **0.2.0-rc.2** 请使用 **dsh-think-translate 1.2.5 或更新版本**。1.2
98
102
  chain: [provider1, provider2, ...] ← 设置里拖拽排序
99
103
  google / bing / openai 兼容 / anthropic 任选
100
104
  fallback 链(可选,默认关闭,配置里开启)
101
- → 浏览器直连兜底
105
+ → 所有已启用提供方失败时保留原文
102
106
  ```
103
107
 
104
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.10";
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 (browser-side Google gtx; browser applies the
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:v2:";
666
- // Discard results produced before the prompt/language fixes, retaining settings.
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.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`\n]*`/g, ' ')
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
- if (!(r && r.ok)) {
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 re = /[^。!?!?;\n]*[。!?!?;\n]+|[^。!?!?;\n]+/g;
1029
- var out = [], m;
1030
- while ((m = re.exec(text)) !== null) {
1031
- if (m[0].length === 0) { re.lastIndex++; continue; }
1032
- out.push({ start: m.index, end: m.index + m[0].length, text: m[0] });
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
- if (!r.skipped) next.transText = p.transText + r.text;
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
@@ -11,8 +11,9 @@ import { tmpdir } from 'node:os'
11
11
  import { randomUUID } from 'node:crypto'
12
12
  import { createLocalPreparation, ollamaOrigin, localFirst, validOllamaSignature } from './ollama.js'
13
13
  import { fileURLToPath } from 'node:url'
14
- import { requestText, isLoopback } from './transport.js'
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
 
@@ -332,24 +333,39 @@ async function curlArgs(extra) {
332
333
  return args.concat(extra)
333
334
  }
334
335
 
335
- async function httpFetch(url, options = {}, timeoutMs = 25000) {
336
+ async function httpFetch(url, options = {}, timeoutMs = 25000, wantInfo = false) {
336
337
  const proxy = isLoopback(url) ? null : await getProxy()
337
338
  if (proxy && /^socks/i.test(proxy)) {
338
339
  const extra = ['--fail', '-L', '--max-time', String(Math.ceil(timeoutMs / 1000)), '-X', options.method || 'GET']
339
- for (const [key, value] of Object.entries(options.headers || {})) extra.push('-H', key + ': ' + value)
340
+ // Bing rejects the default curl user agent with HTTP 401. Keep both
341
+ // transports' session and translation headers consistent.
342
+ for (const [key, value] of Object.entries({ ...DEFAULT_HEADERS, ...options.headers })) extra.push('-H', key + ': ' + value)
340
343
  if (options.body !== undefined) extra.push('--data-binary', options.body)
344
+ const marker = '\n__DSH_RESPONSE_INFO__\n'
345
+ if (wantInfo) extra.push('-w', marker + '%{url_effective}\n%{http_code}')
341
346
  extra.push(url)
342
- return runProgram('curl', await curlArgs(extra), timeoutMs + 5000)
347
+ const out = await runProgram('curl', await curlArgs(extra), timeoutMs + 5000)
348
+ if (!wantInfo) return out
349
+ const at = out.lastIndexOf(marker)
350
+ if (at < 0) throw new Error('curl response metadata missing')
351
+ const [finalUrl, status] = out.slice(at + marker.length).trim().split(/\r?\n/)
352
+ if (!/^https?:\/\//i.test(finalUrl) || !/^\d{3}$/.test(status)) throw new Error('curl response metadata invalid')
353
+ return { text: out.slice(0, at), url: finalUrl, status: Number(status) }
343
354
  }
344
355
  // A failed proxy request is a provider failure. Do not silently bypass the
345
356
  // configured proxy or send a second copy of a potentially billable POST.
346
- return requestText(url, { ...options, proxy, timeoutMs })
357
+ const result = await requestTextInfo(url, { ...options, proxy, timeoutMs })
358
+ return wantInfo ? result : result.text
347
359
  }
348
360
 
349
361
  async function curlGet(url, timeoutMs = 25000) {
350
362
  return httpFetch(url, { method: 'GET' }, timeoutMs)
351
363
  }
352
364
 
365
+ async function curlGetInfo(url, timeoutMs = 25000) {
366
+ return httpFetch(url, { method: 'GET' }, timeoutMs, true)
367
+ }
368
+
353
369
  async function curlPostForm(url, form, timeoutMs = 25000) {
354
370
  return httpFetch(url, {
355
371
  method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form,
@@ -405,13 +421,17 @@ async function viaGoogle(text, to) {
405
421
 
406
422
  async function bingSession() {
407
423
  if (bingCache !== null && Date.now() < bingCache.exp) return bingCache
408
- const html = await curlGet('https://www.bing.com/translator')
424
+ const page = await curlGetInfo('https://www.bing.com/translator')
425
+ const html = page.text
409
426
  const ig = html.match(/IG:"([^"]+)"/)
410
427
  const ab = html.match(/params_AbusePreventionHelper\s*=\s*\[([^\]]*)\]/)
411
428
  if (!ig || !ab) throw new Error('bing page shape changed')
412
429
  const parts = ab[1].split(',').map(function (s) { return s.trim().replace(/^"/, '').replace(/"$/, '') })
413
430
  if (parts.length < 2 || !parts[0] || !parts[1]) throw new Error('bing params empty')
414
- bingCache = { ig: ig[1], key: parts[0], token: parts[1], exp: Date.now() + 50 * 60 * 1000 }
431
+ const translator = html.match(/<[^>]*\bid\s*=\s*["']rich_tta["'][^>]*>/i)
432
+ const iid = translator && translator[0].match(/\bdata-iid\s*=\s*["']([^"']+)["']/i)
433
+ bingCache = { ig: ig[1], key: parts[0], token: parts[1],
434
+ origin: new URL(page.url).origin, iid: iid ? iid[1] : 'SERP', exp: Date.now() + 50 * 60 * 1000 }
415
435
  return bingCache
416
436
  }
417
437
 
@@ -425,12 +445,12 @@ function bingTarget(to) {
425
445
  async function viaBing(text, to) {
426
446
  const s = await bingSession()
427
447
  const form = 'fromLang=auto-detect&text=' + encodeURIComponent(text) + '&to=' + encodeURIComponent(bingTarget(to)) + '&token=' + encodeURIComponent(s.token) + '&key=' + encodeURIComponent(s.key)
428
- // The current translator page builds /ttranslatev3?isVertical=1 (the old
429
- // isIntl=1 param is gone); curlPostForm follows redirects (-L), so CN users
430
- // get auto-redirected www.bing.com -> cn.bing.com while the rest of the
431
- // world stays on www.bing.com.
432
- const raw = await curlPostForm('https://www.bing.com/ttranslatev3?isVertical=1&IG=' + encodeURIComponent(s.ig) + '&IID=SERP', form)
433
- const arr = JSON.parse(raw)
448
+ // CN networks redirect the session page to cn.bing.com. Post back to that
449
+ // origin: following a 302 on the POST itself would discard the form body.
450
+ const raw = await curlPostForm(s.origin + '/ttranslatev3?isVertical=1&IG=' + encodeURIComponent(s.ig) + '&IID=' + encodeURIComponent(s.iid), form)
451
+ if (!raw || !raw.trim()) throw new Error('bing empty response')
452
+ let arr
453
+ try { arr = JSON.parse(raw) } catch { throw new Error('bing non-JSON response') }
434
454
  if (!Array.isArray(arr) && arr && arr.statusCode) throw new Error('bing API ' + arr.statusCode + ': ' + (arr.errorMessage || 'request rejected'))
435
455
  const tr = Array.isArray(arr) && arr[0] && arr[0].translations && arr[0].translations[0]
436
456
  if (!tr || typeof tr.text !== 'string' || !tr.text.trim()) throw new Error('bing empty translation')
@@ -951,9 +971,7 @@ function isCodeOnlyLine(line) {
951
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)
952
972
  }
953
973
  function hasProse(text) {
954
- const outside = text
955
- .replace(/```[\s\S]*?```/g, ' ')
956
- .replace(/`[^`\n]*`/g, ' ')
974
+ const outside = protectedTextParts(text).filter(part => !part.code).map(part => part.text).join(' ')
957
975
  .replace(/https?:\/\/\S+/g, ' ')
958
976
  const lines = outside.split('\n').map(function (l) { return l.trim() }).filter(Boolean)
959
977
  if (lines.length === 0) return false
@@ -963,7 +981,7 @@ function hasProse(text) {
963
981
  }
964
982
  // Decide from prose, not paths, identifiers, or structured payloads (#10).
965
983
  function proseForDetection(text) {
966
- let prose = text.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`\n]*`/g, ' ')
984
+ let prose = protectedTextParts(text).filter(part => !part.code).map(part => part.text).join(' ')
967
985
  .replace(/https?:\/\/[^\s<>]+/g, ' ')
968
986
  .replace(/\b[A-Za-z]:[\\/][^\s"'`,;,。;!?、)}\]]*/g, ' ')
969
987
  .replace(/(^|\s)\/(?:[^\s"'`,;,。;!?、)}\]]+)/g, '$1 ')
@@ -1038,15 +1056,13 @@ async function runChainFor(ids, text, target, cfg) {
1038
1056
  if (!adapter) continue
1039
1057
  try {
1040
1058
  if (ollamaOrigin(id, p)) await localPreparation.ensure(p)
1041
- const chunks = chunkText(text)
1042
- const outs = []
1043
- for (const c of chunks) outs.push(await adapter(c, target, p))
1059
+ const translated = await translateProtectedText(text, chunk => adapter(chunk, target, p), chunkText)
1044
1060
  state.ok++
1045
1061
  state.lastProvider = id
1046
1062
  state.lastError = null
1047
1063
  // Carry the concrete model name so the UI can show e.g. "openai: qwen2.5:14b".
1048
1064
  attempts.push({ provider: id, signature: providerFingerprint(p), ok: true, model: p.model || null })
1049
- return { ok: true, text: outs.join('\n\n'), provider: id, providerSignature: providerFingerprint(p), attempts, ...(p.model ? { model: p.model } : {}) }
1065
+ return { ok: true, text: translated, provider: id, providerSignature: providerFingerprint(p), attempts, ...(p.model ? { model: p.model } : {}) }
1050
1066
  } catch (e) {
1051
1067
  lastErr = e
1052
1068
  attempts.push({ provider: id, signature: providerFingerprint(p), ok: false, model: p.model || null })
@@ -1098,7 +1114,7 @@ async function handleTranslate(body) {
1098
1114
  if (!adapter) return { ok: false, text, error: 'unsupported type: ' + p.type }
1099
1115
  try {
1100
1116
  if (ollamaOrigin(forced, p)) await localPreparation.ensure(p)
1101
- const out = await adapter(text, target, p)
1117
+ const out = await translateProtectedText(text, chunk => adapter(chunk, target, p), chunkText)
1102
1118
  return { ok: true, text: out, provider: forced, providerSignature: providerFingerprint(p), ...(p.model ? { model: p.model } : {}) }
1103
1119
  } catch (e) {
1104
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/lib/transport.js CHANGED
@@ -3,6 +3,11 @@ import https from 'node:https'
3
3
  import tls from 'node:tls'
4
4
  import { isIP } from 'node:net'
5
5
 
6
+ export const DEFAULT_HEADERS = {
7
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
8
+ Accept: 'application/json,text/plain,*/*',
9
+ }
10
+
6
11
  export function isLoopback(url) {
7
12
  const host = new URL(url).hostname.replace(/^\[|\]$/g, '').toLowerCase()
8
13
  return ['localhost', '127.0.0.1', '::1', '::ffff:7f00:1'].includes(host)
@@ -101,11 +106,12 @@ function requestOnce(url, options) {
101
106
  })
102
107
  }
103
108
 
104
- export async function requestText(urlString, options = {}) {
109
+ // Report the final URL so session-based callers can post back to the origin
110
+ // that served their page instead of redirecting a POST and losing its body.
111
+ export async function requestTextInfo(urlString, options = {}) {
105
112
  let url = new URL(urlString)
106
113
  let method = options.method || 'GET', body = options.body
107
- let headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
108
- Accept: 'application/json,text/plain,*/*', ...options.headers }
114
+ let headers = { ...DEFAULT_HEADERS, ...options.headers }
109
115
  const deadline = Date.now() + (options.timeoutMs || 25000)
110
116
  for (let redirects = 0; redirects <= 5; redirects++) {
111
117
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Unsupported request protocol: ' + url.protocol)
@@ -130,6 +136,10 @@ export async function requestText(urlString, options = {}) {
130
136
  continue
131
137
  }
132
138
  if (response.status < 200 || response.status >= 300) throw new Error('HTTP ' + response.status)
133
- return response.text
139
+ return { text: response.text, url: url.href, status: response.status }
134
140
  }
135
141
  }
142
+
143
+ export async function requestText(urlString, options = {}) {
144
+ return (await requestTextInfo(urlString, options)).text
145
+ }
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.10",
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"