dsh-think-translate 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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.7";
26
+ var PLUGIN_VERSION = "1.2.8";
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
@@ -665,7 +665,14 @@ window.__ModuleLoader__.load({
665
665
  var MEM_KEY = "dsh-xlate-cache:";
666
666
  function cacheMemGet(key) {
667
667
  var v = mem.get(key);
668
- if (v !== undefined) return v;
668
+ if (v !== undefined) {
669
+ if (v.ok === false) {
670
+ if (Date.now() < v.retryAt) return v;
671
+ mem.delete(key);
672
+ return undefined;
673
+ }
674
+ return v;
675
+ }
669
676
  try {
670
677
  var lv = localStorage.getItem(MEM_KEY + key);
671
678
  if (lv !== null) {
@@ -705,6 +712,7 @@ window.__ModuleLoader__.load({
705
712
  function cacheMemSet(key, entry) {
706
713
  mem.set(key, entry);
707
714
  if (mem.size > 800) mem.delete(mem.keys().next().value);
715
+ if (entry && !entry.ok) return;
708
716
  try {
709
717
  var stamped = entry && typeof entry === "object" ? Object.assign({ at: Date.now() }, entry) : entry;
710
718
  localStorage.setItem(MEM_KEY + key, JSON.stringify(stamped));
@@ -713,18 +721,18 @@ window.__ModuleLoader__.load({
713
721
  } catch (e) {}
714
722
  }
715
723
  var pend = new Map();
716
- var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
717
- var providerChecks = {};
718
- var providerTestSequence = {};
719
- var statusListeners = new Set();
720
- function providerFingerprint(p) {
721
- return fnv(JSON.stringify({ type: p.type, baseURL: p.baseURL, model: p.model, apiKeyEnv: p.apiKeyEnv,
722
- apiKey: p.source === "dsh" || p.apiKeyEnv ? undefined : p.apiKey }));
723
- }
724
- function recordCheck(id, ok, model, signature) {
725
- providerChecks[id] = { ok: ok, model: model || null, signature: signature };
726
- statusListeners.forEach(function (notify) { notify(); });
727
- }
724
+ var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
725
+ var providerChecks = {};
726
+ var providerTestSequence = {};
727
+ var statusListeners = new Set();
728
+ function providerFingerprint(p) {
729
+ return fnv(JSON.stringify({ type: p.type, baseURL: p.baseURL, model: p.model, apiKeyEnv: p.apiKeyEnv,
730
+ apiKey: p.source === "dsh" || p.apiKeyEnv ? undefined : p.apiKey }));
731
+ }
732
+ function recordCheck(id, ok, model, signature) {
733
+ providerChecks[id] = { ok: ok, model: model || null, signature: signature };
734
+ statusListeners.forEach(function (notify) { notify(); });
735
+ }
728
736
  // Diagnostic ledger surfaced in the settings panel (2026-08-24): bundle
729
737
  // version, apply execution, per-slot registration outcome, current lang.
730
738
  var diag = { version: "v5-mode", applied: false, lang: "?", slots: {}, lastLangChange: 0 };
@@ -849,15 +857,21 @@ window.__ModuleLoader__.load({
849
857
  }
850
858
 
851
859
  async function hostAttempt(text, target) {
860
+ // The host may first load a local model (up to 120s), then infer or
861
+ // walk its fallback providers. Bound the full route, not each hop.
852
862
  var res = await fetchWithTimeout("/_xlate/translate", {
853
863
  method: "POST",
854
864
  headers: { "Content-Type": "application/json" },
855
865
  body: JSON.stringify({ text: text, target: target })
856
- }, 30000);
866
+ }, 180000);
857
867
  if (!res.ok) throw new Error("route HTTP " + res.status);
858
- var j = await res.json();
859
- if (j && Array.isArray(j.attempts)) j.attempts.forEach(function (attempt) { recordCheck(attempt.provider, attempt.ok, attempt.model, attempt.signature); });
860
- if (!(j && j.ok)) throw new Error("route ok=false");
868
+ var j = await res.json();
869
+ if (j && Array.isArray(j.attempts)) j.attempts.forEach(function (attempt) { recordCheck(attempt.provider, attempt.ok, attempt.model, attempt.signature); });
870
+ if (!(j && j.ok)) {
871
+ var error = new Error(j && j.error || "route ok=false");
872
+ if (j && typeof j.retryAt === "number") error.retryAt = j.retryAt;
873
+ throw error;
874
+ }
861
875
  return j;
862
876
  }
863
877
 
@@ -875,14 +889,13 @@ window.__ModuleLoader__.load({
875
889
  return skipped;
876
890
  }
877
891
  // Host chain with bounded retries: a single transient google/proxy
878
- // blip must not flip the row into its permanent failed state
879
- // (2026-08-22: one blip used to kill translation for the whole block).
892
+ // blip must not flip the row into its permanent failed state.
880
893
  var r = null, lastErr = null;
881
- var backoff = [0, 400, 1500];
894
+ var backoff = [0, 400];
882
895
  for (var i = 0; i < backoff.length; i++) {
883
896
  if (backoff[i] > 0) await sleepMs(backoff[i]);
884
897
  try { r = await hostAttempt(text, target); break; }
885
- catch (e) { r = null; lastErr = e; }
898
+ catch (e) { r = null; lastErr = e; if (e && e.retryAt > Date.now()) break; }
886
899
  }
887
900
  if (!(r && r.ok)) {
888
901
  try {
@@ -892,8 +905,8 @@ window.__ModuleLoader__.load({
892
905
  r = { ok: true, text: outs.join(""), provider: "google-direct" };
893
906
  } catch (e2) { r = null; if (e2 && e2.message) lastErr = e2; }
894
907
  }
895
- if (r && r.ok) {
896
- if (r.providerSignature) recordCheck(r.provider, true, r.model, r.providerSignature);
908
+ if (r && r.ok) {
909
+ if (r.providerSignature) recordCheck(r.provider, true, r.model, r.providerSignature);
897
910
  stats.ok++; stats.lastError = null; stats.lastProvider = r.provider; stats.lastModel = r.model || null;
898
911
  memSet(key, r);
899
912
  return r;
@@ -902,7 +915,9 @@ window.__ModuleLoader__.load({
902
915
  } catch (e) {
903
916
  stats.fail++;
904
917
  stats.lastError = e instanceof Error ? e.message : String(e);
905
- return { ok: false, text: text, skipped: false };
918
+ var failedResult = { ok: false, text: text, skipped: false, retryAt: Math.max(Date.now() + 30000, lastErr && lastErr.retryAt || 0) };
919
+ memSet(key, failedResult);
920
+ return failedResult;
906
921
  }
907
922
  })();
908
923
  pend.set(key, p);
@@ -923,21 +938,18 @@ window.__ModuleLoader__.load({
923
938
  useEffect(function () {
924
939
  if (!active || key === null) { setEntry(undefined); return undefined; }
925
940
  var alive = true;
926
- var hit = cacheMemGet(key);
927
- if (hit !== undefined) { setEntry(hit); return undefined; }
928
- requestTranslate(text, s.lang).then(function (r) {
941
+ var retryTimer = null;
942
+ function attempt() {
929
943
  if (!alive) return;
930
- setEntry(r);
931
- if (!(r && r.ok)) {
932
- // One delayed retry for answer text: failures are not cached, so
933
- // this re-issues a real request after the backoff window.
934
- setTimeout(function () {
935
- if (!alive) return;
936
- requestTranslate(text, s.lang).then(function (r2) { if (alive) setEntry(r2); });
937
- }, 3000);
938
- }
939
- });
940
- return function () { alive = false; };
944
+ retryTimer = null;
945
+ requestTranslate(text, s.lang).then(function (r) {
946
+ if (!alive) return;
947
+ setEntry(r);
948
+ if (!(r && r.ok)) retryTimer = setTimeout(attempt, Math.max(1, (r && r.retryAt || Date.now() + 30000) - Date.now()));
949
+ });
950
+ }
951
+ attempt();
952
+ return function () { alive = false; if (retryTimer !== null) clearTimeout(retryTimer); };
941
953
  }, [key, active]);
942
954
  return entry;
943
955
  }
@@ -1026,7 +1038,7 @@ window.__ModuleLoader__.load({
1026
1038
  (async function () {
1027
1039
  while (alive) {
1028
1040
  var cur = tRef.current;
1029
- if (cur.failed || cur.done) return;
1041
+ if (cur.done) return;
1030
1042
  if (cur.busy) { await sleepMs(150); continue; }
1031
1043
  if (cur.retryAt && Date.now() < cur.retryAt) { await sleepMs(300); continue; }
1032
1044
  var latest = stateRef.current.text;
@@ -1060,19 +1072,15 @@ window.__ModuleLoader__.load({
1060
1072
  mutate(function (p) {
1061
1073
  var next = Object.assign({}, p, { busy: false });
1062
1074
  if (r && r.ok) {
1075
+ next.failed = false;
1076
+ next.retryAt = 0;
1063
1077
  if (!r.skipped) next.transText = p.transText + r.text;
1064
1078
  next.segIdx = Math.max(p.segIdx, nextIdx);
1065
1079
  } else {
1066
- var fc = (p.failCount || 0) + 1;
1067
- if (fc >= 3) {
1068
- next.failed = true;
1069
- } else {
1070
- next.failCount = fc;
1071
- next.retryAt = Date.now() + 2500;
1072
- setTimeout(function () {
1073
- mutate(function (q) { return Object.assign({}, q, { retryAt: 0 }); });
1074
- }, 2600);
1075
- }
1080
+ // A negative-cache hit is a cooldown, not another failed
1081
+ // network attempt. Resume automatically after it expires.
1082
+ next.failed = true;
1083
+ next.retryAt = Math.max(Date.now() + 2500, r && r.retryAt || Date.now() + 30000);
1076
1084
  }
1077
1085
  return next;
1078
1086
  });
@@ -1238,9 +1246,9 @@ window.__ModuleLoader__.load({
1238
1246
  }
1239
1247
 
1240
1248
  function AssistantView(props) {
1241
- var node = props.node;
1242
- var data = node.data;
1243
- var groupPart = props.groupPart;
1249
+ var node = props.node;
1250
+ var data = node.data;
1251
+ var groupPart = props.groupPart;
1244
1252
  var useTurnData = props.useTurnData;
1245
1253
  var tt = UI_TEXT[useStore().lang] || UI_TEXT.en;
1246
1254
  var tail = useTurnData("turn-tail");
@@ -1259,12 +1267,12 @@ window.__ModuleLoader__.load({
1259
1267
  var rendered = [];
1260
1268
  var last = blocks.length - 1;
1261
1269
  for (var i = 0; i < blocks.length; i++) {
1262
- var block = blocks[i];
1263
- if (!block) continue;
1264
- // DSH may mount this step separately for reasoning and response.
1265
- // Keep original block indexes so streaming-tail detection is unchanged.
1266
- if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
1267
- if (groupPart === "response" && block.kind === "reasoning") continue;
1270
+ var block = blocks[i];
1271
+ if (!block) continue;
1272
+ // DSH may mount this step separately for reasoning and response.
1273
+ // Keep original block indexes so streaming-tail detection is unchanged.
1274
+ if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
1275
+ if (groupPart === "response" && block.kind === "reasoning") continue;
1268
1276
  if (block.kind === "text") {
1269
1277
  rendered.push(createElement(TextBlockSafe, { key: "t" + i, text: block.text, streaming: streaming }));
1270
1278
  } else if (block.kind === "reasoning") {
@@ -1280,20 +1288,20 @@ window.__ModuleLoader__.load({
1280
1288
  } else {
1281
1289
  rendered.push(createElement("pre", { key: "u" + i, className: "xl-pre" }, JSON.stringify(block, null, 2)));
1282
1290
  }
1283
- }
1284
- var showStopped = interrupted && (groupPart === undefined || groupPart === "response" ||
1285
- !blocks.some(function (b) { return b && b.kind !== "reasoning" && b.kind !== "tool-call"; }));
1286
- return createElement("div", null, rendered,
1287
- showStopped ? createElement("span", { className: "xl-warn" }, (props.t && props.t("message.stopped")) || tt.stopped) : null);
1291
+ }
1292
+ var showStopped = interrupted && (groupPart === undefined || groupPart === "response" ||
1293
+ !blocks.some(function (b) { return b && b.kind !== "reasoning" && b.kind !== "tool-call"; }));
1294
+ return createElement("div", null, rendered,
1295
+ showStopped ? createElement("span", { className: "xl-warn" }, (props.t && props.t("message.stopped")) || tt.stopped) : null);
1288
1296
  }
1289
1297
 
1290
- function originalBlocksText(blocks, groupPart) {
1298
+ function originalBlocksText(blocks, groupPart) {
1291
1299
  var parts = [];
1292
1300
  for (var i = 0; i < blocks.length; i++) {
1293
- var b = blocks[i];
1294
- if (!b) continue;
1295
- if (groupPart === "reasoning" && b.kind !== "reasoning") continue;
1296
- if (groupPart === "response" && b.kind === "reasoning") continue;
1301
+ var b = blocks[i];
1302
+ if (!b) continue;
1303
+ if (groupPart === "reasoning" && b.kind !== "reasoning") continue;
1304
+ if (groupPart === "response" && b.kind === "reasoning") continue;
1297
1305
  if ((b.kind === "text" || b.kind === "reasoning") && typeof b.text === "string" && b.text.trim()) parts.push(b.text);
1298
1306
  }
1299
1307
  return parts;
@@ -1316,7 +1324,7 @@ window.__ModuleLoader__.load({
1316
1324
  try {
1317
1325
  var node = props && props.node;
1318
1326
  var blocks = node && node.data && Array.isArray(node.data.blocks) ? node.data.blocks : [];
1319
- var parts = originalBlocksText(blocks, props.groupPart);
1327
+ var parts = originalBlocksText(blocks, props.groupPart);
1320
1328
  if (parts.length) fb = createElement("div", null, createElement(Markdownish, { text: parts.join("\n\n") }));
1321
1329
  } catch (e) { fb = null; }
1322
1330
  return createElement(XBoundary, { fallback: fb }, createElement(AssistantView, props));
@@ -1500,17 +1508,17 @@ window.__ModuleLoader__.load({
1500
1508
  // `mem` is a plain Map, so clearing it changes nothing React can see: the
1501
1509
  // 缓存条目 counter kept the old number until some other state moved.
1502
1510
  var tickPair = useState(0);
1503
- var bumpTick = tickPair[1];
1504
- useEffect(function () { var notify = function () { bumpTick(function (n) { return n + 1; }); }; statusListeners.add(notify); return function () { statusListeners.delete(notify); }; }, []);
1505
- // A lower-ranked provider test cannot displace a verified higher-ranked one.
1506
- var currentCfg = draft.cfg || {};
1507
- var ids = (currentCfg.chain || []).concat(currentCfg.fallback && currentCfg.fallback.enabled ? currentCfg.fallback.chain || [] : []);
1508
- var current = null;
1509
- for (var i = 0; i < ids.length; i++) {
1510
- var p = currentCfg.providers && currentCfg.providers[ids[i]], check = providerChecks[ids[i]];
1511
- if (p && (p.enabled || p.source === "dsh") && check && check.ok && check.signature === providerFingerprint(p)) { current = { id: ids[i], model: check.model }; break; }
1512
- }
1513
- var inUse = current ? ((t.labels[current.id] || current.id) + (current.model ? " · " + current.model : "")) : (LOCAL_EMPTY[s.lang] || LOCAL_EMPTY.en);
1511
+ var bumpTick = tickPair[1];
1512
+ useEffect(function () { var notify = function () { bumpTick(function (n) { return n + 1; }); }; statusListeners.add(notify); return function () { statusListeners.delete(notify); }; }, []);
1513
+ // A lower-ranked provider test cannot displace a verified higher-ranked one.
1514
+ var currentCfg = draft.cfg || {};
1515
+ var ids = (currentCfg.chain || []).concat(currentCfg.fallback && currentCfg.fallback.enabled ? currentCfg.fallback.chain || [] : []);
1516
+ var current = null;
1517
+ for (var i = 0; i < ids.length; i++) {
1518
+ var p = currentCfg.providers && currentCfg.providers[ids[i]], check = providerChecks[ids[i]];
1519
+ if (p && (p.enabled || p.source === "dsh") && check && check.ok && check.signature === providerFingerprint(p)) { current = { id: ids[i], model: check.model }; break; }
1520
+ }
1521
+ var inUse = current ? ((t.labels[current.id] || current.id) + (current.model ? " · " + current.model : "")) : (LOCAL_EMPTY[s.lang] || LOCAL_EMPTY.en);
1514
1522
  return createElement("div", null,
1515
1523
  // The clear button rides the "当前使用" row, pushed to that row's end:
1516
1524
  // one row per idea (what is in use, then the counters), no stray rows.
@@ -1617,25 +1625,25 @@ window.__ModuleLoader__.load({
1617
1625
  // lives here instead, outside React. Refs (cfgRef/seqRef) deliberately do NOT:
1618
1626
  // they belong to the instance that owns the in-flight requests.
1619
1627
  // ------------------------------------------------------------------
1620
- var draft = {};
1621
- var LOCAL_EMPTY = { "zh-CN": "尚未验证可用提供方", en: "No provider verified yet", ja: "提供元は未確認です", ko: "검증된 제공자가 없습니다", es: "Sin proveedor verificado", fr: "Aucun fournisseur vérifié", de: "Noch kein Anbieter geprüft", ru: "Провайдер пока не проверен" };
1622
- var LOCAL_PHASES = ["checking", "needs_install", "downloading_ollama", "verifying", "installing", "starting", "needs_model", "downloading_model", "loading_model", "ready", "error", "setup", "retry", "manual"];
1623
- var LOCAL_LABELS = {
1624
- "zh-CN": ["检查已有 Ollama", "未找到 Ollama", "下载 Ollama", "验证安装包", "安装 Ollama", "启动 Ollama", "所选模型尚未下载", "下载模型", "加载模型", "本地模型已就绪", "准备失败", "一键安装并准备(含模型下载)", "重试准备", "打开官方安装指南"],
1625
- en: ["Checking existing Ollama", "Ollama not found", "Downloading Ollama", "Verifying installer", "Installing Ollama", "Starting Ollama", "Selected model is not downloaded", "Downloading model", "Loading model", "Local model is ready", "Preparation failed", "Install and prepare (includes model download)", "Retry preparation", "Official installation guide"],
1626
- ja: ["既存の Ollama を確認", "Ollama が見つかりません", "Ollama をダウンロード", "インストーラーを検証", "Ollama をインストール", "Ollama を起動", "モデルは未ダウンロード", "モデルをダウンロード", "モデルをロード", "ローカルモデル準備完了", "準備に失敗", "インストールして準備(モデルを含む)", "準備を再試行", "公式インストールガイド"],
1627
- ko: ["기존 Ollama 확인", "Ollama를 찾을 수 없음", "Ollama 다운로드", "설치 파일 검증", "Ollama 설치", "Ollama 시작", "모델이 다운로드되지 않음", "모델 다운로드", "모델 로드", "로컬 모델 준비 완료", "준비 실패", "설치 및 준비 (모델 다운로드 포함)", "준비 다시 시도", "공식 설치 안내"],
1628
- es: ["Comprobando Ollama", "Ollama no encontrado", "Descargando Ollama", "Verificando instalador", "Instalando Ollama", "Iniciando Ollama", "Modelo sin descargar", "Descargando modelo", "Cargando modelo", "Modelo local listo", "Preparación fallida", "Instalar y preparar (incluye el modelo)", "Reintentar preparación", "Guía oficial de instalación"],
1629
- fr: ["Vérification de Ollama", "Ollama introuvable", "Téléchargement de Ollama", "Vérification du programme", "Installation de Ollama", "Démarrage de Ollama", "Modèle non téléchargé", "Téléchargement du modèle", "Chargement du modèle", "Modèle local prêt", "Échec de préparation", "Installer et préparer (modèle inclus)", "Réessayer", "Guide officiel"],
1630
- de: ["Ollama prüfen", "Ollama nicht gefunden", "Ollama herunterladen", "Installationsdatei prüfen", "Ollama installieren", "Ollama starten", "Modell nicht heruntergeladen", "Modell herunterladen", "Modell laden", "Lokales Modell bereit", "Vorbereitung fehlgeschlagen", "Installieren und vorbereiten (mit Modell)", "Erneut vorbereiten", "Offizielle Installationsanleitung"],
1631
- ru: ["Проверка Ollama", "Ollama не найден", "Загрузка Ollama", "Проверка установщика", "Установка Ollama", "Запуск Ollama", "Модель не скачана", "Скачивание модели", "Загрузка модели", "Локальная модель готова", "Ошибка подготовки", "Установить и подготовить (с моделью)", "Повторить подготовку", "Официальная инструкция"],
1632
- };
1633
- function localLabel(phase, lang) { var index = LOCAL_PHASES.indexOf(phase); return (LOCAL_LABELS[lang] || LOCAL_LABELS.en)[index < 0 ? 0 : index]; }
1634
- function localOrigin(p) {
1635
- try { var u = new URL(p && p.baseURL); return p.type === "openai" && u.protocol === "http:" &&
1636
- ["localhost", "127.0.0.1", "[::1]"].indexOf(u.hostname) >= 0 && u.port === "11434" && /^\/v1\/?$/.test(u.pathname) && !u.username && !u.password && !u.search && !u.hash ? u.origin : null; }
1637
- catch (e) { return null; }
1638
- }
1628
+ var draft = {};
1629
+ var LOCAL_EMPTY = { "zh-CN": "尚未验证可用提供方", en: "No provider verified yet", ja: "提供元は未確認です", ko: "검증된 제공자가 없습니다", es: "Sin proveedor verificado", fr: "Aucun fournisseur vérifié", de: "Noch kein Anbieter geprüft", ru: "Провайдер пока не проверен" };
1630
+ var LOCAL_PHASES = ["checking", "needs_install", "downloading_ollama", "verifying", "installing", "starting", "needs_model", "downloading_model", "loading_model", "ready", "error", "setup", "retry", "manual"];
1631
+ var LOCAL_LABELS = {
1632
+ "zh-CN": ["检查已有 Ollama", "未找到 Ollama", "下载 Ollama", "验证安装包", "安装 Ollama", "启动 Ollama", "所选模型尚未下载", "下载模型", "加载模型", "本地模型已就绪", "准备失败", "一键安装并准备(含模型下载)", "重试准备", "打开官方安装指南"],
1633
+ en: ["Checking existing Ollama", "Ollama not found", "Downloading Ollama", "Verifying installer", "Installing Ollama", "Starting Ollama", "Selected model is not downloaded", "Downloading model", "Loading model", "Local model is ready", "Preparation failed", "Install and prepare (includes model download)", "Retry preparation", "Official installation guide"],
1634
+ ja: ["既存の Ollama を確認", "Ollama が見つかりません", "Ollama をダウンロード", "インストーラーを検証", "Ollama をインストール", "Ollama を起動", "モデルは未ダウンロード", "モデルをダウンロード", "モデルをロード", "ローカルモデル準備完了", "準備に失敗", "インストールして準備(モデルを含む)", "準備を再試行", "公式インストールガイド"],
1635
+ ko: ["기존 Ollama 확인", "Ollama를 찾을 수 없음", "Ollama 다운로드", "설치 파일 검증", "Ollama 설치", "Ollama 시작", "모델이 다운로드되지 않음", "모델 다운로드", "모델 로드", "로컬 모델 준비 완료", "준비 실패", "설치 및 준비 (모델 다운로드 포함)", "준비 다시 시도", "공식 설치 안내"],
1636
+ es: ["Comprobando Ollama", "Ollama no encontrado", "Descargando Ollama", "Verificando instalador", "Instalando Ollama", "Iniciando Ollama", "Modelo sin descargar", "Descargando modelo", "Cargando modelo", "Modelo local listo", "Preparación fallida", "Instalar y preparar (incluye el modelo)", "Reintentar preparación", "Guía oficial de instalación"],
1637
+ fr: ["Vérification de Ollama", "Ollama introuvable", "Téléchargement de Ollama", "Vérification du programme", "Installation de Ollama", "Démarrage de Ollama", "Modèle non téléchargé", "Téléchargement du modèle", "Chargement du modèle", "Modèle local prêt", "Échec de préparation", "Installer et préparer (modèle inclus)", "Réessayer", "Guide officiel"],
1638
+ de: ["Ollama prüfen", "Ollama nicht gefunden", "Ollama herunterladen", "Installationsdatei prüfen", "Ollama installieren", "Ollama starten", "Modell nicht heruntergeladen", "Modell herunterladen", "Modell laden", "Lokales Modell bereit", "Vorbereitung fehlgeschlagen", "Installieren und vorbereiten (mit Modell)", "Erneut vorbereiten", "Offizielle Installationsanleitung"],
1639
+ ru: ["Проверка Ollama", "Ollama не найден", "Загрузка Ollama", "Проверка установщика", "Установка Ollama", "Запуск Ollama", "Модель не скачана", "Скачивание модели", "Загрузка модели", "Локальная модель готова", "Ошибка подготовки", "Установить и подготовить (с моделью)", "Повторить подготовку", "Официальная инструкция"],
1640
+ };
1641
+ function localLabel(phase, lang) { var index = LOCAL_PHASES.indexOf(phase); return (LOCAL_LABELS[lang] || LOCAL_LABELS.en)[index < 0 ? 0 : index]; }
1642
+ function localOrigin(p) {
1643
+ try { var u = new URL(p && p.baseURL); return p.type === "openai" && u.protocol === "http:" &&
1644
+ ["localhost", "127.0.0.1", "[::1]"].indexOf(u.hostname) >= 0 && u.port === "11434" && /^\/v1\/?$/.test(u.pathname) && !u.username && !u.password && !u.search && !u.hash ? u.origin : null; }
1645
+ catch (e) { return null; }
1646
+ }
1639
1647
  function useDraft(key, initial) {
1640
1648
  var pair = useState(key in draft ? draft[key] : initial);
1641
1649
  if (!(key in draft)) draft[key] = pair[0];
@@ -1658,11 +1666,11 @@ window.__ModuleLoader__.load({
1658
1666
  var pair = useDraft("cfg", null);
1659
1667
  var cfg = pair[0], setCfg = pair[1];
1660
1668
  var msgPair = useDraft("msg", "");
1661
- var msg = msgPair[0], setMsg = msgPair[1];
1662
- var localPair = useDraft("localJob", null);
1663
- var localJob = localPair[0];
1664
- var localJobRef = useState(function () { return { current: localJob && localJob.id }; })[0];
1665
- function setLocalJob(job) { localJobRef.current = job && job.id; localPair[1](job); }
1669
+ var msg = msgPair[0], setMsg = msgPair[1];
1670
+ var localPair = useDraft("localJob", null);
1671
+ var localJob = localPair[0];
1672
+ var localJobRef = useState(function () { return { current: localJob && localJob.id }; })[0];
1673
+ function setLocalJob(job) { localJobRef.current = job && job.id; localPair[1](job); }
1666
1674
  var modelsPair = useDraft("models", null);
1667
1675
  var models = modelsPair[0], setModels = modelsPair[1];
1668
1676
  var modelMenuPair = useDraft("modelMenu", null); var modelMenu = modelMenuPair[0], setModelMenu = modelMenuPair[1];
@@ -1710,53 +1718,53 @@ window.__ModuleLoader__.load({
1710
1718
  // Monotonic save sequence: a stale /_xlate/config POST response must
1711
1719
  // not overwrite a newer optimistic state (Important-3/4).
1712
1720
  var seqRefPair = useState(function () { return { current: 0 }; });
1713
- var seqRef = seqRefPair[0];
1714
- var localSeq = useState(function () { return { current: 0 }; })[0];
1715
- function testProvider(id, sequence) {
1716
- var testSequence = (providerTestSequence[id] || 0) + 1;
1717
- providerTestSequence[id] = testSequence;
1718
- var testedProvider = cfgRef.current.providers[id] || {};
1719
- var testedModel = testedProvider.model;
1720
- var testedSignature = providerFingerprint(testedProvider);
1721
- setMsg(t.testing);
1722
- return fetch("/_xlate/translate", { method: "POST", headers: { "Content-Type": "application/json" },
1723
- body: JSON.stringify({ text: "Hello world, connection test. [" + Date.now().toString(36) + "]", target: s.lang, provider: id }) })
1724
- .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
1725
- .then(function (j) { if (sequence !== undefined && sequence !== localSeq.current) return;
1726
- if (providerTestSequence[id] !== testSequence) return;
1727
- var currentProvider = cfgRef.current.providers[id] || {};
1728
- if (providerFingerprint(currentProvider) !== testedSignature || (j.model && testedModel && j.model !== testedModel)) return;
1729
- recordCheck(id, !!(j && j.ok), j.model || testedModel, j.providerSignature || testedSignature);
1730
- if (j && j.ok) stats.lastError = null;
1731
- setMsg(j && j.ok ? t.connOk + " ✓ (" + providerLabel(id, (cfgRef.current.providers || {})[id] || {}, t) + (j.model ? " · " + j.model : "") + ")" : (t.connFail + ": " + (j.error || "unknown"))); })
1732
- .catch(function (e) { if (sequence !== undefined && sequence !== localSeq.current) return;
1733
- if (providerTestSequence[id] !== testSequence || providerFingerprint(cfgRef.current.providers[id] || {}) !== testedSignature) return;
1734
- recordCheck(id, false, testedModel, testedSignature);
1735
- setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
1736
- }
1737
- function prepareLocal(allowSetup, testAfter, downloadOnly) {
1738
- var sequence = ++localSeq.current;
1739
- return fetch("/_xlate/local/prepare", { method: "POST", headers: { "Content-Type": "application/json" },
1740
- body: JSON.stringify({ allowInstall: allowSetup === true, allowDownload: allowSetup === true || downloadOnly === true }) })
1741
- .then(function (r) { return r.json().then(function (j) { if (!r.ok || !j.ok) throw new Error(j.error || "HTTP " + r.status); return j; }); })
1742
- .then(function (j) { if (sequence !== localSeq.current) return; setLocalJob(j.job); if (testAfter) return testProvider("openai", sequence); })
1743
- .catch(function (e) { if (sequence === localSeq.current) setMsg(localLabel("error", s.lang) + ": " + (e.message || e)); });
1744
- }
1745
- var localProvider = cfg && cfg.providers && cfg.providers.openai;
1746
- var firstEnabled = cfg && (cfg.chain || []).filter(function (id) { var p = cfg.providers[id]; return p && (p.enabled || p.source === "dsh"); })[0];
1747
- var localKey = firstEnabled === "openai" && localOrigin(localProvider) && localProvider.model ? localProvider.baseURL + "|" + localProvider.model : "";
1748
- useEffect(function () { if (localKey) prepareLocal(false, false); }, [localKey]);
1749
- useEffect(function () {
1750
- if (!localJob || !localJob.running) return;
1751
- var alive = true;
1752
- var timer = setInterval(function () {
1753
- fetch("/_xlate/local/status?id=" + encodeURIComponent(localJob.id)).then(function (r) { return r.json(); }).then(function (j) {
1754
- if (alive && j.job && localJobRef.current === localJob.id) setLocalJob(j.job);
1755
- }).catch(function () {});
1756
- }, 750);
1757
- return function () { alive = false; clearInterval(timer); };
1758
- }, [localJob ? localJob.id + "|" + localJob.running : ""]);
1759
- useEffect(function () { return function () { localSeq.current++; }; }, []);
1721
+ var seqRef = seqRefPair[0];
1722
+ var localSeq = useState(function () { return { current: 0 }; })[0];
1723
+ function testProvider(id, sequence) {
1724
+ var testSequence = (providerTestSequence[id] || 0) + 1;
1725
+ providerTestSequence[id] = testSequence;
1726
+ var testedProvider = cfgRef.current.providers[id] || {};
1727
+ var testedModel = testedProvider.model;
1728
+ var testedSignature = providerFingerprint(testedProvider);
1729
+ setMsg(t.testing);
1730
+ return fetch("/_xlate/translate", { method: "POST", headers: { "Content-Type": "application/json" },
1731
+ body: JSON.stringify({ text: "Hello world, connection test. [" + Date.now().toString(36) + "]", target: s.lang, provider: id }) })
1732
+ .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
1733
+ .then(function (j) { if (sequence !== undefined && sequence !== localSeq.current) return;
1734
+ if (providerTestSequence[id] !== testSequence) return;
1735
+ var currentProvider = cfgRef.current.providers[id] || {};
1736
+ if (providerFingerprint(currentProvider) !== testedSignature || (j.model && testedModel && j.model !== testedModel)) return;
1737
+ recordCheck(id, !!(j && j.ok), j.model || testedModel, j.providerSignature || testedSignature);
1738
+ if (j && j.ok) stats.lastError = null;
1739
+ setMsg(j && j.ok ? t.connOk + " ✓ (" + providerLabel(id, (cfgRef.current.providers || {})[id] || {}, t) + (j.model ? " · " + j.model : "") + ")" : (t.connFail + ": " + (j.error || "unknown"))); })
1740
+ .catch(function (e) { if (sequence !== undefined && sequence !== localSeq.current) return;
1741
+ if (providerTestSequence[id] !== testSequence || providerFingerprint(cfgRef.current.providers[id] || {}) !== testedSignature) return;
1742
+ recordCheck(id, false, testedModel, testedSignature);
1743
+ setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
1744
+ }
1745
+ function prepareLocal(allowSetup, testAfter, downloadOnly) {
1746
+ var sequence = ++localSeq.current;
1747
+ return fetch("/_xlate/local/prepare", { method: "POST", headers: { "Content-Type": "application/json" },
1748
+ body: JSON.stringify({ allowInstall: allowSetup === true, allowDownload: allowSetup === true || downloadOnly === true }) })
1749
+ .then(function (r) { return r.json().then(function (j) { if (!r.ok || !j.ok) throw new Error(j.error || "HTTP " + r.status); return j; }); })
1750
+ .then(function (j) { if (sequence !== localSeq.current) return; setLocalJob(j.job); if (testAfter) return testProvider("openai", sequence); })
1751
+ .catch(function (e) { if (sequence === localSeq.current) setMsg(localLabel("error", s.lang) + ": " + (e.message || e)); });
1752
+ }
1753
+ var localProvider = cfg && cfg.providers && cfg.providers.openai;
1754
+ var firstEnabled = cfg && (cfg.chain || []).filter(function (id) { var p = cfg.providers[id]; return p && (p.enabled || p.source === "dsh"); })[0];
1755
+ var localKey = firstEnabled === "openai" && localOrigin(localProvider) && localProvider.model ? localProvider.baseURL + "|" + localProvider.model : "";
1756
+ useEffect(function () { if (localKey) prepareLocal(false, false); }, [localKey]);
1757
+ useEffect(function () {
1758
+ if (!localJob || !localJob.running) return;
1759
+ var alive = true;
1760
+ var timer = setInterval(function () {
1761
+ fetch("/_xlate/local/status?id=" + encodeURIComponent(localJob.id)).then(function (r) { return r.json(); }).then(function (j) {
1762
+ if (alive && j.job && localJobRef.current === localJob.id) setLocalJob(j.job);
1763
+ }).catch(function () {});
1764
+ }, 750);
1765
+ return function () { alive = false; clearInterval(timer); };
1766
+ }, [localJob ? localJob.id + "|" + localJob.running : ""]);
1767
+ useEffect(function () { return function () { localSeq.current++; }; }, []);
1760
1768
  // Add/edit inline form state. Lives here (not in a nested component,
1761
1769
  // and before the cfg guard) so re-renders — including the cfg=null →
1762
1770
  // cfg=value transition — never reset or reorder what the user types;
@@ -1871,12 +1879,12 @@ window.__ModuleLoader__.load({
1871
1879
  .catch(function () {});
1872
1880
  });
1873
1881
  };
1874
- var startPull = function (m) {
1875
- if (!m || !m.trim()) return;
1876
- var name = m.trim();
1877
- // Download intent prepares an existing service first. Software install
1878
- // still needs the separately labelled setup button if Ollama is absent.
1879
- pickModel(name).then(function (saved) { if (saved) prepareLocal(false, false, true); });
1882
+ var startPull = function (m) {
1883
+ if (!m || !m.trim()) return;
1884
+ var name = m.trim();
1885
+ // Download intent prepares an existing service first. Software install
1886
+ // still needs the separately labelled setup button if Ollama is absent.
1887
+ pickModel(name).then(function (saved) { if (saved) prepareLocal(false, false, true); });
1880
1888
  };
1881
1889
  var pickModel = function (m) {
1882
1890
  if (!m) return;
@@ -1884,9 +1892,9 @@ window.__ModuleLoader__.load({
1884
1892
  var openai = (cur.providers && cur.providers.openai) || {};
1885
1893
  applyLocal({ providers: { openai: { model: m } } });
1886
1894
  setModelMenu(null);
1887
- return fetch("/_xlate/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providers: { openai: { model: m } } }) })
1895
+ return fetch("/_xlate/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providers: { openai: { model: m } } }) })
1888
1896
  .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
1889
- .then(function (c) { cfgRef.current = c; setCfg(c); return c; })
1897
+ .then(function (c) { cfgRef.current = c; setCfg(c); return c; })
1890
1898
  .catch(function (e) {
1891
1899
  // Do not leave the chip showing a model the host never stored.
1892
1900
  setMsg((t.providerSaveError || t.connFail) + ": " + (e && e.message ? e.message : e));
@@ -1953,9 +1961,9 @@ window.__ModuleLoader__.load({
1953
1961
  applyLocal({ chain: chain });
1954
1962
  saveCfg({ chain: chain });
1955
1963
  },
1956
- test: function (id) {
1957
- if (id === "openai" && localOrigin(cfgRef.current.providers.openai)) prepareLocal(false, true);
1958
- else testProvider(id);
1964
+ test: function (id) {
1965
+ if (id === "openai" && localOrigin(cfgRef.current.providers.openai)) prepareLocal(false, true);
1966
+ else testProvider(id);
1959
1967
  },
1960
1968
  edit: function (id) {
1961
1969
  var p = cfgRef.current.providers && cfgRef.current.providers[id];
@@ -2308,18 +2316,18 @@ window.__ModuleLoader__.load({
2308
2316
  adding || editingId ? null
2309
2317
  : createElement("button", { className: "xl-btn", type: "button", onClick: rescanDsh, style: { marginTop: "4px", marginLeft: "6px" } },
2310
2318
  "↻ " + t.dshImport),
2311
- unusedArea,
2312
- localJob && localProvider && localJob.model === localProvider.model && localJob.origin === localOrigin(localProvider)
2313
- ? createElement("div", { className: "xl-local-status", role: "status", style: { marginTop: "8px" } },
2314
- createElement("div", { className: "xl-note" }, localLabel(localJob.phase, s.lang) + " · " + localJob.model +
2315
- (localJob.percent !== null && localJob.phase.indexOf("downloading") === 0 ? " · " + localJob.percent + "%" : "") +
2316
- (localJob.completed ? " · " + (localJob.completed / 1048576).toFixed(1) + " MB" + (localJob.total ? " / " + (localJob.total / 1048576).toFixed(1) + " MB" : "") : "")),
2317
- localJob.running ? createElement("div", { className: "xl-progress" }, createElement("div", { className: "xl-progress-bar", style: { width: (localJob.percent === null ? 15 : localJob.percent) + "%" } })) : null,
2318
- localJob.error ? createElement("div", { className: "xl-warn" }, localJob.error) : null,
2319
- !localJob.running && (localJob.phase === "needs_model" || localJob.phase === "error" || (localJob.phase === "needs_install" && localJob.canInstall))
2320
- ? createElement("button", { className: "xl-btn", onClick: function () { prepareLocal(true, true); } }, localLabel(localJob.phase === "error" ? "retry" : "setup", s.lang)) : null,
2321
- localJob.phase === "needs_install" && !localJob.canInstall ? createElement("a", { href: "https://ollama.com/download", target: "_blank", rel: "noreferrer" }, localLabel("manual", s.lang)) : null)
2322
- : null,
2319
+ unusedArea,
2320
+ localJob && localProvider && localJob.model === localProvider.model && localJob.origin === localOrigin(localProvider)
2321
+ ? createElement("div", { className: "xl-local-status", role: "status", style: { marginTop: "8px" } },
2322
+ createElement("div", { className: "xl-note" }, localLabel(localJob.phase, s.lang) + " · " + localJob.model +
2323
+ (localJob.percent !== null && localJob.phase.indexOf("downloading") === 0 ? " · " + localJob.percent + "%" : "") +
2324
+ (localJob.completed ? " · " + (localJob.completed / 1048576).toFixed(1) + " MB" + (localJob.total ? " / " + (localJob.total / 1048576).toFixed(1) + " MB" : "") : "")),
2325
+ localJob.running ? createElement("div", { className: "xl-progress" }, createElement("div", { className: "xl-progress-bar", style: { width: (localJob.percent === null ? 15 : localJob.percent) + "%" } })) : null,
2326
+ localJob.error ? createElement("div", { className: "xl-warn" }, localJob.error) : null,
2327
+ !localJob.running && (localJob.phase === "needs_model" || localJob.phase === "error" || (localJob.phase === "needs_install" && localJob.canInstall))
2328
+ ? createElement("button", { className: "xl-btn", onClick: function () { prepareLocal(true, true); } }, localLabel(localJob.phase === "error" ? "retry" : "setup", s.lang)) : null,
2329
+ localJob.phase === "needs_install" && !localJob.canInstall ? createElement("a", { href: "https://ollama.com/download", target: "_blank", rel: "noreferrer" }, localLabel("manual", s.lang)) : null)
2330
+ : null,
2323
2331
  msg ? createElement("div", { className: "xl-note", style: { marginTop: "6px" } }, msg) : null);
2324
2332
  }
2325
2333