@lynn123411/dsh-chat-translate 1.1.0 → 1.2.1

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.md CHANGED
@@ -5,12 +5,13 @@ DeepSeek Harness Web 界面的工具调用与思考摘要智能翻译插件。
5
5
  ## 特性
6
6
 
7
7
  - **工具调用与思考摘要自动翻译**:仅在渲染层将工具调用动作描述(如 `Locate DSH home directory structure`)与思考折叠摘要自动翻译为中文,对话正文永不翻译,保持思考正文原生不干扰。
8
- - **AI 翻译通道(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama 等),Base URL 与模型可在设置面板配置;API Key `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY` 读取,填写后立即生效。
8
+ - **AI 翻译通道(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama 等),Base URL 与模型可在设置面板配置;API Key DSH 凭据服务读写 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,填写后立即生效。
9
9
  - **Bing 免 Key 兜底通道**:内置微软 Bing 网页翻译(免 Key、国内直连),AI 未配置或请求失败时自动兜底;双通道均有独立开关,同时关闭则不翻译。
10
10
  - **非侵入式 DOM 挂载与双语对照**:非侵入式包装保留原始 DOM 节点与 React Fiber 事件系统;点击译文可原地在原文与中文之间切换。
11
11
  - **智能内容脱敏与占位符保护**:翻译前对多行代码块、内联代码、URL 链接、文件路径及 CLI 命令行参数进行占位符脱敏与鲁棒还原,杜绝代码与路径被误翻译。
12
12
  - **当前会话作用域**:仅翻译当前查看的会话,切换会话自动跟随新内容;视口懒加载(150px 缓冲)与文档顺序排队,译文按阅读顺序出现。
13
- - **智能调度与熔断保护**:1–100 动态并发限流队列、AI 30s / Bing 2s 超时、连续失败熔断自愈、在途请求合并去重、7 天 LRU 双级缓存(Client `localStorage` + Host 磁盘持久化)。
13
+ - **智能调度与熔断保护**:1–100 动态并发限流队列、AI 30s / Bing 2s 超时、连续失败熔断自愈、在途请求合并去重、7 天 LRU 磁盘持久化缓存(`~/.dsh/dsh-chat-translate/cache.json`)。
14
+ - **DSH 原生配置接入**:配置走 DSH `settings` 服务(用户层写入 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段)、密钥走 DSH `credentials` 服务、设置面板经 `settingsScope` 与 `credentials` Remote API 读写——无任何自研配置文件。
14
15
  - **设置面板集成**:在「设置 - 聊天翻译」中提供总开关、AI/Bing 通道开关、Base URL 与模型配置、通道测试与并发数调节。
15
16
 
16
17
  ## 安装
@@ -21,7 +22,7 @@ dsh plugin --profile web add @lynn123411/dsh-chat-translate
21
22
 
22
23
  ## 配置
23
24
 
24
- 1. **API Key**:打开「设置 - 聊天翻译」,在 AI 翻译卡片中直接填写 API Key 并保存(写入 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,权限 0600,保存后立即生效;留空保存 = 清除该键)。也可以手动编辑该文件:
25
+ 1. **API Key**:打开「设置 - 聊天翻译」,在 AI 翻译卡片中直接填写 API Key 并保存(经 DSH 凭据服务写入 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`,权限 0600,保存后立即生效;留空保存 = 清除该键)。也可以手动编辑该文件:
25
26
 
26
27
  ```yaml
27
28
  refs:
@@ -30,6 +31,19 @@ dsh plugin --profile web add @lynn123411/dsh-chat-translate
30
31
 
31
32
  > ⚠️ **注意**:DSH 的凭据加载器要求 refs 键值非空。手动编辑时要么填写真实 Key,要么**直接删除该行**——不要写成 `TRANSLATE_API_KEY: ""` 这类空值,否则 DSH 启动会失败。未配置时插件自动由 Bing 通道兜底,不会报错。
32
33
 
33
- 2. **Base URL 与模型**:在「设置 - 聊天翻译」中填写 Base URL(如 `https://api.openai.com/v1` 或 `https://api.deepseek.com/v1`)与模型名(如 `gpt-4o-mini`、`deepseek-chat`),点击「测试 AI 通道」验证。
34
+ 2. **Base URL 与模型等设置**:在「设置 - 聊天翻译」中填写 Base URL(如 `https://api.openai.com/v1` 或 `https://api.deepseek.com/v1`)与模型名(如 `gpt-4o-mini`、`deepseek-chat`),点击「测试 AI 通道」验证。所有设置(开关、并发数、超时等)由 DSH 持久化到 `~/.dsh/settings.yaml` 的 `dsh-chat-translate` 段,可直接编辑该文件(DSH 会热同步):
35
+
36
+ ```yaml
37
+ dsh-chat-translate:
38
+ enabled: true
39
+ concurrency: 3
40
+ aiEnabled: true
41
+ bingEnabled: true
42
+ baseUrl: http://172.24.52.126:8081/v1
43
+ model: hy-mt2-1.8b
44
+ targetLang: zh-Hans
45
+ ```
46
+
47
+ > 从 1.2 起,插件不再读写独立的 `~/.dsh/dsh-chat-translate-config.json`:升级后首次加载会自动把旧文件的值迁移进上面的 settings 段并删除旧文件(若你已在 UI 中改过设置,则以你的设置为准)。
34
48
 
35
49
  3. **通道行为**:AI 开启且已配置 → AI 优先、失败降级 Bing;AI 开启但未配置 + Bing 开启 → Bing 翻译;AI 未配置 + Bing 关闭 → 不翻译;AI 关闭 + Bing 开启 → 直接 Bing;双关 → 不翻译。
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-chat-translate",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "Tool-call & think-summary translation with dual AI (OpenAI-compatible) + Bing channels for the DeepSeek Harness Web UI.",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -174,30 +174,6 @@ async function requestTranslateBatch(texts, options = {}) {
174
174
  cached: false
175
175
  }));
176
176
  }
177
- async function fetchServerConfig() {
178
- try {
179
- const res = await fetch("/api/dsh-chat-translate/config");
180
- if (!res.ok) return null;
181
- const json = await res.json();
182
- return json.config;
183
- } catch {
184
- return null;
185
- }
186
- }
187
- async function updateServerConfig(updates) {
188
- try {
189
- const res = await fetch("/api/dsh-chat-translate/config", {
190
- method: "POST",
191
- headers: { "Content-Type": "application/json" },
192
- body: JSON.stringify(updates)
193
- });
194
- if (!res.ok) return null;
195
- const json = await res.json();
196
- return json.config;
197
- } catch {
198
- return null;
199
- }
200
- }
201
177
  async function testServerChannel(channel) {
202
178
  try {
203
179
  const res = await fetch("/api/dsh-chat-translate/test-channel", {
@@ -210,18 +186,6 @@ async function testServerChannel(channel) {
210
186
  return { ok: false, latencyMs: 0, error: err?.message || String(err) };
211
187
  }
212
188
  }
213
- async function saveCredentials(apiKey) {
214
- try {
215
- const res = await fetch("/api/dsh-chat-translate/credentials", {
216
- method: "POST",
217
- headers: { "Content-Type": "application/json" },
218
- body: JSON.stringify({ apiKey })
219
- });
220
- return await res.json();
221
- } catch (err) {
222
- return { ok: false, error: err?.message || String(err) };
223
- }
224
- }
225
189
 
226
190
  // src/client/translate/mount.ts
227
191
  var CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
@@ -745,13 +709,8 @@ var chatTranslateObserver = new ChatTranslateObserver();
745
709
  var import_react = require("react");
746
710
 
747
711
  // src/client/settings/store.ts
748
- var LS_PREFIX = "dsh-chat-translate:";
749
- var LS_ENABLED = `${LS_PREFIX}enabled`;
750
- var LS_CONCURRENCY = `${LS_PREFIX}concurrency`;
751
- var LS_AI_ENABLED = `${LS_PREFIX}aiEnabled`;
752
- var LS_BING_ENABLED = `${LS_PREFIX}bingEnabled`;
753
- var LS_BASE_URL = `${LS_PREFIX}baseUrl`;
754
- var LS_MODEL = `${LS_PREFIX}model`;
712
+ var SETTINGS_NAMESPACE = "dsh-chat-translate";
713
+ var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
755
714
  var DEFAULT_STATE = {
756
715
  enabled: true,
757
716
  concurrency: 3,
@@ -764,115 +723,28 @@ var DEFAULT_STATE = {
764
723
  var SettingsStore = class {
765
724
  state = { ...DEFAULT_STATE };
766
725
  listeners = /* @__PURE__ */ new Set();
767
- storageListener = null;
768
- pushTimer = null;
769
- pushSeq = 0;
770
- userTouched = false;
771
- constructor() {
772
- this.loadFromLocalStorage();
773
- this.initStorageListener();
774
- this.syncFromServer();
775
- }
776
- loadFromLocalStorage() {
777
- if (typeof localStorage === "undefined") return;
778
- try {
779
- const enabledRaw = localStorage.getItem(LS_ENABLED);
780
- if (enabledRaw !== null) {
781
- this.state.enabled = enabledRaw === "true";
782
- }
783
- const concurrencyRaw = localStorage.getItem(LS_CONCURRENCY);
784
- if (concurrencyRaw !== null) {
785
- const c = parseInt(concurrencyRaw, 10);
786
- if (Number.isFinite(c) && c >= 1 && c <= 100) {
787
- this.state.concurrency = c;
788
- }
789
- }
790
- const aiRaw = localStorage.getItem(LS_AI_ENABLED);
791
- if (aiRaw !== null) {
792
- this.state.aiEnabled = aiRaw === "true";
793
- }
794
- const bingRaw = localStorage.getItem(LS_BING_ENABLED);
795
- if (bingRaw !== null) {
796
- this.state.bingEnabled = bingRaw === "true";
797
- }
798
- const baseUrlRaw = localStorage.getItem(LS_BASE_URL);
799
- if (baseUrlRaw !== null) {
800
- this.state.baseUrl = baseUrlRaw;
801
- }
802
- const modelRaw = localStorage.getItem(LS_MODEL);
803
- if (modelRaw !== null) {
804
- this.state.model = modelRaw;
805
- }
806
- } catch {
807
- }
808
- try {
809
- chatTranslateObserver.setEnabled(this.state.enabled);
810
- } catch {
726
+ scope = null;
727
+ credentials = null;
728
+ unsubscribeScope = null;
729
+ keyConfigured = false;
730
+ writeTimer = null;
731
+ pendingFields = /* @__PURE__ */ new Set();
732
+ /**
733
+ * Bind the DSH services. Called once from the settings UI setup; re-binding
734
+ * (e.g. after a reconnect) detaches the previous subscription first.
735
+ */
736
+ attach(scope, credentials) {
737
+ if (this.unsubscribeScope) {
738
+ this.unsubscribeScope();
739
+ this.unsubscribeScope = null;
811
740
  }
812
- }
813
- initStorageListener() {
814
- if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
815
- this.storageListener = (e) => {
816
- if (!e.key || !e.key.startsWith(LS_PREFIX)) return;
817
- let changed = false;
818
- if (e.key === LS_ENABLED && e.newValue !== null) {
819
- const val = e.newValue === "true";
820
- if (this.state.enabled !== val) {
821
- this.state.enabled = val;
822
- chatTranslateObserver.setEnabled(val);
823
- changed = true;
824
- }
825
- } else if (e.key === LS_CONCURRENCY && e.newValue !== null) {
826
- const c = parseInt(e.newValue, 10);
827
- if (Number.isFinite(c) && c >= 1 && c <= 100 && this.state.concurrency !== c) {
828
- this.state.concurrency = c;
829
- changed = true;
830
- }
831
- } else if (e.key === LS_AI_ENABLED && e.newValue !== null) {
832
- const val = e.newValue === "true";
833
- if (this.state.aiEnabled !== val) {
834
- this.state.aiEnabled = val;
835
- changed = true;
836
- }
837
- } else if (e.key === LS_BING_ENABLED && e.newValue !== null) {
838
- const val = e.newValue === "true";
839
- if (this.state.bingEnabled !== val) {
840
- this.state.bingEnabled = val;
841
- changed = true;
842
- }
843
- } else if (e.key === LS_BASE_URL && e.newValue !== null && this.state.baseUrl !== e.newValue) {
844
- this.state.baseUrl = e.newValue;
845
- changed = true;
846
- } else if (e.key === LS_MODEL && e.newValue !== null && this.state.model !== e.newValue) {
847
- this.state.model = e.newValue;
848
- changed = true;
849
- }
850
- if (changed) {
851
- this.notify();
852
- this.schedulePush();
853
- }
854
- };
855
- window.addEventListener("storage", this.storageListener);
856
- }
857
- async syncFromServer() {
858
- try {
859
- const config = await fetchServerConfig();
860
- if (config) {
861
- const touched = this.userTouched;
862
- this.state = {
863
- enabled: !touched && typeof config.enabled === "boolean" ? config.enabled : this.state.enabled,
864
- concurrency: !touched && typeof config.concurrency === "number" && Number.isFinite(config.concurrency) ? config.concurrency : this.state.concurrency,
865
- aiEnabled: !touched && typeof config.aiEnabled === "boolean" ? config.aiEnabled : this.state.aiEnabled,
866
- bingEnabled: !touched && typeof config.bingEnabled === "boolean" ? config.bingEnabled : this.state.bingEnabled,
867
- baseUrl: !touched && typeof config.baseUrl === "string" ? config.baseUrl : this.state.baseUrl,
868
- model: !touched && typeof config.model === "string" ? config.model : this.state.model,
869
- aiConfigured: typeof config.aiConfigured === "boolean" ? config.aiConfigured : this.state.aiConfigured
870
- };
871
- chatTranslateObserver.setEnabled(this.state.enabled);
872
- this.notify();
873
- }
874
- } catch {
741
+ this.scope = scope;
742
+ this.credentials = credentials;
743
+ if (scope) {
744
+ this.unsubscribeScope = scope.subscribe(() => this.derive());
745
+ this.derive();
875
746
  }
747
+ void this.refreshKeyStatus();
876
748
  }
877
749
  getState() {
878
750
  return { ...this.state };
@@ -883,130 +755,139 @@ var SettingsStore = class {
883
755
  this.listeners.delete(listener);
884
756
  };
885
757
  }
886
- notify() {
887
- this.listeners.forEach((l) => {
758
+ /** Map the resolved namespace value into client state and notify. */
759
+ derive() {
760
+ if (!this.scope) return;
761
+ const snap = this.scope.getSnapshot();
762
+ const value = snap.value;
763
+ if (!value || typeof value !== "object") return;
764
+ const next = { ...this.state };
765
+ if (typeof value.enabled === "boolean") next.enabled = value.enabled;
766
+ const c = value.concurrency;
767
+ if (typeof c === "number" && Number.isFinite(c)) {
768
+ next.concurrency = Math.min(Math.max(Math.round(c), 1), 100);
769
+ }
770
+ if (typeof value.aiEnabled === "boolean") next.aiEnabled = value.aiEnabled;
771
+ if (typeof value.bingEnabled === "boolean") next.bingEnabled = value.bingEnabled;
772
+ if (typeof value.baseUrl === "string") next.baseUrl = value.baseUrl;
773
+ if (typeof value.model === "string") next.model = value.model;
774
+ this.applyState(next);
775
+ }
776
+ applyState(next) {
777
+ next.aiConfigured = Boolean(next.baseUrl && next.model && this.keyConfigured);
778
+ const enabledChanged = next.enabled !== this.state.enabled;
779
+ this.state = next;
780
+ if (enabledChanged) {
888
781
  try {
889
- l();
782
+ chatTranslateObserver.setEnabled(this.state.enabled);
890
783
  } catch {
891
784
  }
892
- });
785
+ }
786
+ this.notify();
893
787
  }
894
- /**
895
- * Debounced server push: fast typing in the baseUrl/model/concurrency inputs
896
- * collapses into a single POST (300ms trailing). A monotonic sequence number
897
- * ensures an out-of-order older response never overwrites newer state.
898
- */
899
- schedulePush() {
900
- if (this.pushTimer !== null || typeof window === "undefined") return;
901
- this.pushTimer = window.setTimeout(() => {
902
- this.pushTimer = null;
903
- this.pushToServer();
904
- }, 300);
788
+ notify() {
789
+ for (const listener of this.listeners) {
790
+ try {
791
+ listener();
792
+ } catch {
793
+ }
794
+ }
905
795
  }
906
- async pushToServer() {
907
- const seq = ++this.pushSeq;
908
- const sent = {
909
- enabled: this.state.enabled,
910
- concurrency: this.state.concurrency,
911
- aiEnabled: this.state.aiEnabled,
912
- bingEnabled: this.state.bingEnabled,
913
- baseUrl: this.state.baseUrl,
914
- model: this.state.model
915
- };
796
+ /** Pull the key's status-only view and re-derive aiConfigured. */
797
+ async refreshKeyStatus() {
798
+ if (!this.credentials) return;
916
799
  try {
917
- const updated = await updateServerConfig(sent);
918
- if (!updated || seq !== this.pushSeq) return;
919
- this.state = {
920
- ...this.state,
921
- enabled: this.state.enabled === sent.enabled && typeof updated.enabled === "boolean" ? updated.enabled : this.state.enabled,
922
- concurrency: this.state.concurrency === sent.concurrency && typeof updated.concurrency === "number" ? updated.concurrency : this.state.concurrency,
923
- aiEnabled: this.state.aiEnabled === sent.aiEnabled && typeof updated.aiEnabled === "boolean" ? updated.aiEnabled : this.state.aiEnabled,
924
- bingEnabled: this.state.bingEnabled === sent.bingEnabled && typeof updated.bingEnabled === "boolean" ? updated.bingEnabled : this.state.bingEnabled,
925
- baseUrl: this.state.baseUrl === sent.baseUrl && typeof updated.baseUrl === "string" ? updated.baseUrl : this.state.baseUrl,
926
- model: this.state.model === sent.model && typeof updated.model === "string" ? updated.model : this.state.model,
927
- aiConfigured: typeof updated.aiConfigured === "boolean" ? updated.aiConfigured : this.state.aiConfigured
928
- };
929
- this.notify();
800
+ const res = await this.credentials.describe([TRANSLATE_API_KEY_REF]);
801
+ if (!res.ok) return;
802
+ const info = res.value[TRANSLATE_API_KEY_REF];
803
+ const configured = Boolean(info?.configured);
804
+ if (configured !== this.keyConfigured) {
805
+ this.keyConfigured = configured;
806
+ this.applyState({ ...this.state });
807
+ }
930
808
  } catch {
931
809
  }
932
810
  }
811
+ /**
812
+ * Optimistically apply locally, then persist each touched field through the
813
+ * settings scope. Writes are trailing-debounced (300ms) so typing in the
814
+ * baseUrl/model inputs collapses into a single queued mutation instead of
815
+ * one write per keystroke — which would otherwise flash stale mirror values
816
+ * back into the inputs between commits. A failed write makes the scope
817
+ * reload its mirror, which re-derives this store from the host document
818
+ * (conflict-safe recovery).
819
+ */
933
820
  async update(partial) {
934
- this.userTouched = true;
935
821
  let sanitizedConcurrency = this.state.concurrency;
936
- if (typeof partial.concurrency === "number") {
937
- if (Number.isFinite(partial.concurrency)) {
938
- sanitizedConcurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), 100);
939
- }
822
+ if (typeof partial.concurrency === "number" && Number.isFinite(partial.concurrency)) {
823
+ sanitizedConcurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), 100);
940
824
  }
941
- this.state = {
825
+ const next = {
942
826
  ...this.state,
943
827
  ...partial,
944
828
  concurrency: sanitizedConcurrency
945
829
  };
946
- if (typeof partial.enabled === "boolean") {
947
- try {
948
- localStorage.setItem(LS_ENABLED, String(partial.enabled));
949
- } catch {
950
- }
951
- chatTranslateObserver.setEnabled(partial.enabled);
952
- }
953
- if (typeof partial.concurrency === "number" && Number.isFinite(partial.concurrency)) {
954
- try {
955
- localStorage.setItem(LS_CONCURRENCY, String(sanitizedConcurrency));
956
- } catch {
957
- }
958
- }
959
- if (typeof partial.aiEnabled === "boolean") {
960
- try {
961
- localStorage.setItem(LS_AI_ENABLED, String(partial.aiEnabled));
962
- } catch {
963
- }
964
- }
965
- if (typeof partial.bingEnabled === "boolean") {
966
- try {
967
- localStorage.setItem(LS_BING_ENABLED, String(partial.bingEnabled));
968
- } catch {
969
- }
970
- }
971
- if (typeof partial.baseUrl === "string") {
972
- try {
973
- localStorage.setItem(LS_BASE_URL, partial.baseUrl);
974
- } catch {
830
+ this.applyState(next);
831
+ if (this.scope) {
832
+ const fields = ["enabled", "concurrency", "aiEnabled", "bingEnabled", "baseUrl", "model"];
833
+ for (const field of fields) {
834
+ if (partial[field] !== void 0) {
835
+ this.pendingFields.add(field);
836
+ }
975
837
  }
838
+ this.scheduleWrite();
976
839
  }
977
- if (typeof partial.model === "string") {
978
- try {
979
- localStorage.setItem(LS_MODEL, partial.model);
980
- } catch {
981
- }
840
+ }
841
+ scheduleWrite() {
842
+ if (this.writeTimer !== null || typeof window === "undefined") return;
843
+ this.writeTimer = window.setTimeout(() => {
844
+ this.writeTimer = null;
845
+ void this.flushWrite();
846
+ }, 300);
847
+ }
848
+ async flushWrite() {
849
+ if (!this.scope) return;
850
+ const fields = [...this.pendingFields];
851
+ this.pendingFields.clear();
852
+ const writes = [];
853
+ for (const field of fields) {
854
+ writes.push(this.scope.set(field, this.state[field]));
982
855
  }
983
- this.notify();
984
- this.schedulePush();
856
+ await Promise.all(writes).catch(() => {
857
+ });
985
858
  }
986
859
  async testChannel(channel) {
987
860
  return testServerChannel(channel);
988
861
  }
989
- /** Persist the API key via the host and refresh the server-derived status. */
862
+ /** Write (or clear) the API key through the credentials Remote API. */
990
863
  async saveApiKey(apiKey) {
991
- const res = await saveCredentials(apiKey);
992
- if (res.ok) {
993
- await this.syncFromServer();
864
+ if (!this.credentials) {
865
+ return { ok: false, error: "\u51ED\u636E\u670D\u52A1\u4E0D\u53EF\u7528\uFF08\u975E\u56DE\u73AF\u9875\u9762\uFF09" };
866
+ }
867
+ try {
868
+ const key = apiKey.trim();
869
+ const res = key ? await this.credentials.set(TRANSLATE_API_KEY_REF, key) : await this.credentials.unset(TRANSLATE_API_KEY_REF);
870
+ if (!res.ok) {
871
+ return { ok: false, error: res.error?.message || "\u4FDD\u5B58\u5931\u8D25" };
872
+ }
873
+ await this.refreshKeyStatus();
874
+ return { ok: true };
875
+ } catch (err) {
876
+ return { ok: false, error: err?.message || String(err) };
994
877
  }
995
- return { ok: Boolean(res.ok), error: res.error };
996
- }
997
- /** Re-pull the server config (e.g. after the API key changed). */
998
- async refreshFromServer() {
999
- await this.syncFromServer();
1000
878
  }
1001
879
  dispose() {
1002
- if (this.pushTimer !== null && typeof window !== "undefined") {
1003
- clearTimeout(this.pushTimer);
1004
- this.pushTimer = null;
880
+ if (this.writeTimer !== null && typeof window !== "undefined") {
881
+ clearTimeout(this.writeTimer);
882
+ this.writeTimer = null;
1005
883
  }
1006
- if (this.storageListener && typeof window !== "undefined") {
1007
- window.removeEventListener("storage", this.storageListener);
1008
- this.storageListener = null;
884
+ this.pendingFields.clear();
885
+ if (this.unsubscribeScope) {
886
+ this.unsubscribeScope();
887
+ this.unsubscribeScope = null;
1009
888
  }
889
+ this.scope = null;
890
+ this.credentials = null;
1010
891
  this.listeners.clear();
1011
892
  }
1012
893
  };
@@ -1535,6 +1416,16 @@ function TidySettingsPanel() {
1535
1416
  }
1536
1417
  function setupSettingsUi(ctx) {
1537
1418
  if (typeof window === "undefined") return;
1419
+ try {
1420
+ const settingsScope = ctx?.settingsScope || (ctx?.get ? ctx.get("settingsScope") : null);
1421
+ const remoteCredentials = ctx?.remote?.credentials || (ctx?.get ? ctx.get("remote.credentials") : null);
1422
+ if (settingsScope && typeof settingsScope.bind === "function") {
1423
+ const scope = settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
1424
+ settingsStore.attach(scope, remoteCredentials ?? null);
1425
+ }
1426
+ } catch (err) {
1427
+ console.warn("[dsh-chat-translate] Failed to bind settings scope:", err);
1428
+ }
1538
1429
  try {
1539
1430
  const slots = ctx?.slots || (ctx?.get ? ctx.get("slots") : null);
1540
1431
  if (!slots || typeof slots.inject !== "function") return;
@@ -1543,7 +1434,8 @@ function setupSettingsUi(ctx) {
1543
1434
  {
1544
1435
  name: "settings.section",
1545
1436
  id: "dsh-chat-translate",
1546
- order: 5,
1437
+ // 约定:自有插件设置项 order 从 110 起步进 10(原生最大 100=桌面设置),保证排在所有原生项之下
1438
+ order: 110,
1547
1439
  label: () => "\u804A\u5929\u7FFB\u8BD1"
1548
1440
  },
1549
1441
  TidySettingsPanel
@@ -1556,7 +1448,7 @@ function setupSettingsUi(ctx) {
1556
1448
 
1557
1449
  // src/client/index.ts
1558
1450
  var name = "dsh-chat-translate";
1559
- var inject = ["slots"];
1451
+ var inject = ["slots", "settingsScope", "remote", "remote.credentials"];
1560
1452
  function apply(ctx) {
1561
1453
  ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: title translate observer");
1562
1454
  ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");