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 +5 -1
- package/README.es.md +5 -1
- package/README.fr.md +5 -1
- package/README.ja.md +5 -1
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/README.ru.md +5 -1
- package/README.zh-CN.md +5 -1
- package/lib/client.js +82 -73
- package/lib/index.js +38 -22
- package/lib/text.js +77 -0
- package/lib/transport.js +14 -4
- package/package.json +2 -1
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
|
-
→
|
|
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
|
-
→
|
|
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
|
-
→
|
|
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
|
-
→
|
|
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.
|
|
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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
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"
|