@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 +18 -4
- package/dsh.plugin.json +1 -1
- package/lib/client.js +139 -247
- package/lib/client.js.map +2 -2
- package/lib/index.js +1001 -301
- package/lib/index.js.map +4 -4
- package/lib/types/client/index.d.ts +10 -1
- package/lib/types/client/settings/store.d.ts +77 -22
- package/lib/types/client/translate/api.d.ts +0 -8
- package/lib/types/index.d.ts +32 -2
- package/lib/types/server/adapters/openai.d.ts +2 -2
- package/lib/types/server/cache.d.ts +2 -0
- package/lib/types/server/config.d.ts +54 -9
- package/lib/types/server/credentials.d.ts +45 -29
- package/lib/types/server/dispatcher.d.ts +2 -2
- package/lib/types/server/router.d.ts +1 -2
- package/lib/types/server/types.d.ts +1 -1
- package/package.json +7 -3
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
|
|
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
|
|
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
|
|
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
|
|
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
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
|
|
749
|
-
var
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
782
|
+
chatTranslateObserver.setEnabled(this.state.enabled);
|
|
890
783
|
} catch {
|
|
891
784
|
}
|
|
892
|
-
}
|
|
785
|
+
}
|
|
786
|
+
this.notify();
|
|
893
787
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
|
918
|
-
if (!
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
825
|
+
const next = {
|
|
942
826
|
...this.state,
|
|
943
827
|
...partial,
|
|
944
828
|
concurrency: sanitizedConcurrency
|
|
945
829
|
};
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
984
|
-
|
|
856
|
+
await Promise.all(writes).catch(() => {
|
|
857
|
+
});
|
|
985
858
|
}
|
|
986
859
|
async testChannel(channel) {
|
|
987
860
|
return testServerChannel(channel);
|
|
988
861
|
}
|
|
989
|
-
/**
|
|
862
|
+
/** Write (or clear) the API key through the credentials Remote API. */
|
|
990
863
|
async saveApiKey(apiKey) {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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.
|
|
1003
|
-
clearTimeout(this.
|
|
1004
|
-
this.
|
|
880
|
+
if (this.writeTimer !== null && typeof window !== "undefined") {
|
|
881
|
+
clearTimeout(this.writeTimer);
|
|
882
|
+
this.writeTimer = null;
|
|
1005
883
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
this.
|
|
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
|
|
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");
|