dsh-workspace-api-key 0.2.16 → 0.3.0

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
@@ -5,9 +5,306 @@ window.__ModuleLoader__.load({
5
5
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
6
6
 
7
7
  const React = require('react')
8
- const h = React.createElement
8
+ const rawCreateElement = React.createElement
9
9
  const { useState, useEffect, useCallback, useRef } = React
10
10
 
11
+ // ─────────────────────────── localization ───────────────────────────
12
+ // The web GUI's language is owned by @deepseek-ai/dsh-client-locale. This
13
+ // plugin keeps one English catalog keyed by the Chinese source copy: when
14
+ // the active language is Chinese the catalog is bypassed and every string
15
+ // is used exactly as written, otherwise rendered text is translated.
16
+ // Messages produced by the host half (which has no locale of its own) are
17
+ // covered by EN_HOST and the pattern rules below.
18
+ const EN = {
19
+ "勾选后:该目录下会话的搜索/抓取请求改用聊天用的那把 key(会话单独配置仍然优先)。不勾则用「设置 → 插件 → 网页搜索」里配置的 key。": "When checked: search/fetch calls from sessions in this directory use the chat key (session-level keys still win). Unchecked, they use the key from Settings → Plugins → Web Search.",
20
+ "连接正常": "Connected",
21
+ "key 无效": "Invalid key",
22
+ "余额不足": "Out of balance",
23
+ "被限流": "Rate limited",
24
+ "无法连接": "Unreachable",
25
+ "测试失败": "Test failed",
26
+ "鉴权失败": "Authentication failed",
27
+ "本会话专属": "Session key",
28
+ "工作区专属": "Workspace key",
29
+ "系统默认": "System default",
30
+ "未记录服务商": "Provider not recorded",
31
+ "未知": "Unknown",
32
+ "你打开了「启用新版 Web 搜索 API」,但搜索 key 还没有配置:这个会话里的搜索会直接失败。": "\"New Web Search API\" is enabled, but no search key is configured: searches in this session will fail.",
33
+ "你配置了搜索专用的 API Key,但「启用新版 Web 搜索 API」没有打开,所以那把 key 现在不会被使用。": "A search-only API key is configured, but \"New Web Search API\" is off, so that key is not being used.",
34
+ "(空会话)": "(empty session)",
35
+ "(没有目录)": "(no directory)",
36
+ "模型未知": "Model unknown",
37
+ "搜索默认 key 已清除:搜索改用官方搜索页里配的那把 key。": "Search default key cleared: searches use the key configured on the official search page.",
38
+ "官方「网页搜索」页仍指向原来的 ref,两处暂时不是同一把。": "The official \"Web Search\" page still points at the previous ref, so the two are temporarily different keys.",
39
+ "官方「网页搜索」页的 API Key 输入框也指向这个 ref:以后两边改的是同一把 key。": "The API Key field on the official \"Web Search\" page now points at this ref too: both places edit the same key.",
40
+ "已同步:该目录下会话的搜索请求改用聊天用的那把 key(会话单独配置仍然优先)。": "Synced: searches from sessions in this directory use the chat key (a session-level key still wins).",
41
+ "已取消同步:该目录的搜索请求改用「设置 → 插件 → 网页搜索」里配置的 key。": "Sync off: searches in this directory use the key from Settings → Plugins → Web Search.",
42
+ "旧版(未重启应用)": "older version (app not restarted)",
43
+ "不可用": "Unavailable",
44
+ "(live 兜底)": "(live fallback)",
45
+ "(不指定)": "(not specified)",
46
+ "改用系统默认": "Use system default",
47
+ "为该工作区指定一把专属 key;留空并保存即继续使用系统默认。": "Set a dedicated key for this workspace; leave empty and save to keep using the system default.",
48
+ "sk-…(留空 = 使用上层设置)": "sk-… (empty = use the level above)",
49
+ "这把 key 属于哪个服务商(用于进入会话时判断是否匹配)": "Which provider this key belongs to (used to detect mismatches when entering a session)",
50
+ "保存中…": "Saving…",
51
+ "保存": "Save",
52
+ "用这把 key 测试": "Test this key",
53
+ "取消": "Cancel",
54
+ "凭据引用:保存时由宿主按这个会话/目录的 ID 生成,保存后这里会显示出来": "Credential ref: generated by the host from this session/directory ID on save; it appears here afterwards.",
55
+ "key 已失效": "Key invalid",
56
+ "服务商不匹配": "Provider mismatch",
57
+ " · 未配置 key": " · no key configured",
58
+ "清除标记": "Clear flag",
59
+ "测试中…": "Testing…",
60
+ "测试连接": "Test connection",
61
+ "收起": "Collapse",
62
+ "配置": "Configure",
63
+ "折叠会话": "Collapse sessions",
64
+ "展开会话": "Expand sessions",
65
+ "专属 key": "Dedicated key",
66
+ "未分组目录": "Ungrouped directory",
67
+ "目录已不存在": "Directory no longer exists",
68
+ "勾选后:该目录下会话的搜索/抓取请求改用聊天用的那把 key": "When checked: search/fetch calls from sessions in this directory use the chat key",
69
+ "(会话单独配置仍然优先)。不勾则用「设置 → 插件 → 网页搜索」里配置的 key。": "(session-level keys still win). Unchecked, they use the key from Settings → Plugins → Web Search.",
70
+ "同步搜索 API": "Sync search API",
71
+ "搜索用这里的 key": "Searches use this key",
72
+ "搜索用搜索默认 key": "Searches use the search default key",
73
+ "搜索用搜索插件里的 key": "Searches use the search plugin's key",
74
+ "(搜索插件)": "(search plugin)",
75
+ "使用说明": "Usage guide",
76
+ "推荐用法(两把 key,各管一件事)": "Recommended setup (two keys, one job each)",
77
+ "先到「设置 → 模型」配一把系统默认 key,例如就叫 ": "First, set a system default key in Settings → Models, named e.g. ",
78
+ "。没有单独配置的地方都用它聊天。": ". Everywhere without its own key uses it for chat.",
79
+ "再回到本页的「搜索默认 key」,放一把搜索专用的 key,例如 ": "Then, back on this page, put a search-only key in \"Search default key\", e.g. ",
80
+ "。搜索请求默认用它,和聊天那把分开计费。": ". Searches use it by default, billed separately from the chat key.",
81
+ "保存时插件会把官方「设置 → 插件 → 网页搜索」里的 API Key 指向同一个 ref,": "On save the plugin points the official Settings → Plugins → Web Search API Key at the same ref,",
82
+ "所以那个页面显示、修改的都是这把 key,两边不会各存一份。": "so that page shows and edits this key; there is no second copy.",
83
+ "平时不用再管:聊天按工作区/会话的 key 走,搜索走搜索默认 key,两边账单互不影响。": "Nothing else to do: chat uses workspace/session keys, search uses the search default key, and the bills don't affect each other.",
84
+ "想按工作区核算搜索花费": "Track search spend per workspace",
85
+ "在该工作区行勾上「同步搜索 API」:这个目录下会话的搜索就改用聊天那套层级 key": "Tick \"Sync search API\" on that workspace: searches from its sessions use the chat key cascade",
86
+ "(会话 key → 工作区 key → 系统默认),不再用搜索默认 key。": "(session key → workspace key → system default) instead of the search default key.",
87
+ "不勾的目录继续用搜索默认 key。两种方式可以同时存在,一个工作区一个样。": "Unticked directories keep using the search default key. Both styles can coexist, one per workspace.",
88
+ "搜索 key 的优先级(从高到低)": "Search key precedence (highest first)",
89
+ "会话单独配置的 key —— 该会话的聊天与搜索都用它,永远最优先": "A session's own key — used for both chat and search in that session, always first",
90
+ "工作区勾了「同步搜索 API」—— 该工作区下会话的搜索改用:工作区 key → 系统默认 key": "Workspace with \"Sync search API\" on — its sessions search with: workspace key → system default key",
91
+ "搜索默认 key —— 没勾同步时,搜索用它": "Search default key — used by searches when sync is off",
92
+ "官方「网页搜索」页里配的那把 —— 面板没配搜索默认 key 时的兜底": "The key on the official \"Web Search\" page — fallback when this panel has no search default key",
93
+ "(它默认就是系统默认 key ": "(by default that is the system default key ",
94
+ ",与聊天共用)": ", shared with chat)",
95
+ "官方「设置 → 插件 → 网页搜索」那个 Key 框怎么办": "What about the Key field in Settings → Plugins → Web Search?",
96
+ "官方那个输入框写的永远是「当前 API 段的 ": "That field always writes the ref named by the current API section's ",
97
+ " 指向的 ref」,默认就是聊天那把(": ", which defaults to the chat key (",
98
+ "本插件启动后会把它指到插件自己的搜索 ref(": "On startup this plugin points it at its own search ref (",
99
+ "等于把官方那个 key 设置接管过来:从今往后官方页里填的也是这把搜索 key,": "This takes over the official key setting: from now on that page edits this search key,",
100
+ "不会再把聊天 key 顶掉,也不会出现「两把搜索 key 各管一半」。": "so it can no longer overwrite the chat key or split search between two keys.",
101
+ "所以日常只需要在本页配搜索 key,官方那一页可以不管;本插件只改 ": "So normally you only configure the search key here and can leave the official page alone; the plugin only changes ",
102
+ " 这个指向,永远不动你的 key 内容。": " that pointer and never touches your key values.",
103
+ "会被提醒的几种情况": "Situations that trigger a warning",
104
+ "配了搜索 key,但没打开「启用新版 Web 搜索 API」→ 那把 key 其实不会被用到,面板顶部会提示。": "A search key is set but \"New Web Search API\" is off → that key is unused; the panel shows a notice.",
105
+ "打开了新版搜索 API,但既没有搜索默认 key、官方那把也是空的 → 任何搜索都会失败,面板顶部会提示。": "The new search API is on but neither the search default key nor the official key is set → every search fails; the panel shows a notice.",
106
+ "勾了同步,但该工作区既没有专属 key、系统默认 key 也没配 → 会提示先去配一把。": "Sync is on but the workspace has no key and no system default key is set → a notice asks you to set one first.",
107
+ "搜索默认 key 存好了,但官方页还指着聊天那把 → 会提示点「同步到官方设置」,一键对齐。": "The search default key is saved but the official page still points at the chat key → a notice offers \"Sync to official settings\" to align them.",
108
+ "原则": "Principles",
109
+ "提醒只报告、不替你改 key。插件绝不会自动切换或替换你的 key,避免计费被悄悄改到别的账号上。": "Warnings only report; they never change your key. The plugin never switches or replaces keys automatically, so billing cannot silently move to another account.",
110
+ "知道了": "Got it",
111
+ "尚未配置系统默认 API Key": "No system default API key configured",
112
+ "当前没有任何可用的 DEEPSEEK_API_KEY。可以到「设置 → 模型」里配置系统默认 key,": "There is no usable DEEPSEEK_API_KEY. Set a system default key in Settings → Models,",
113
+ "也可以直接为某个工作区写一把专属 key。": "or write a dedicated key for a workspace below.",
114
+ "可以重新配置这把 key,或让该工作区改用系统默认设置。": "Reconfigure this key, or let the workspace fall back to the system default.",
115
+ "重新配置": "Reconfigure",
116
+ "改用系统设置": "Use system settings",
117
+ "稍后": "Later",
118
+ "返回会话": "Back to conversation",
119
+ "返回": "Back",
120
+ "API Key 分配": "API Keys",
121
+ "按工作区 / 会话隔离计费": "Per-workspace and per-session billing",
122
+ "刷新中…": "Refreshing…",
123
+ "刷新": "Refresh",
124
+ "请确认 DSH Web 服务正在运行、且本插件的宿主半边已加载。": "Make sure the DSH Web service is running and this plugin's host half is loaded.",
125
+ "宿主端插件还是旧版本:所以看不到会话列表,也没有按会话配置。": "The host half is still an older version: no session list and no per-session configuration.",
126
+ "请「完全退出」DSH Desktop 再重新打开(关闭窗口 / 刷新页面都不够:宿主代码只在启动时加载)。": "Fully quit DSH and reopen it (closing the window or refreshing the page is not enough: host code loads at startup).",
127
+ "当前 DSH 版本没有提供会话列表服务,只能按工作区配置 key。": "This DSH version provides no session list service; only workspace keys can be configured.",
128
+ "宿主报告 0 个会话,浏览器端也没有拿到会话列表,所以没有可展开的列表。": "The host reports 0 sessions and the browser has no session list either, so there is nothing to expand.",
129
+ "只有「会话记录里带 cwd」的会话才能挂到工作区/未分组目录下。": "Only sessions whose log records a cwd can be attached to a workspace or ungrouped directory.",
130
+ "拿到会话了,但没有一个能对上工作区/目录(路径不匹配)。": "Sessions were found, but none matches a workspace/directory (path mismatch).",
131
+ "系统默认设置": "System default",
132
+ "未配置": "Not configured",
133
+ "被环境变量遮蔽(只读)": "Shadowed by an environment variable (read-only)",
134
+ "没有单独配置的工作区/会话,一律使用这把 key。": "Workspaces and sessions without their own key use this one.",
135
+ "搜索默认 key": "Search default key",
136
+ "已配置": "Configured",
137
+ "搜索请求默认用它,和聊天那把 key 分开计费。": "Searches use it by default, billed separately from chat.",
138
+ "工作区勾了「同步搜索 API」时改用该工作区的聊天层级;会话单独配置永远最优先。": "With \"Sync search API\" on for a workspace, searches use its chat cascade; a session's own key always wins.",
139
+ "官方「网页搜索」页现在仍指着聊天那把 key(DEEPSEEK_API_KEY)——保存一次搜索 key 后,插件会把它接过来。": "The official \"Web Search\" page still points at the chat key (DEEPSEEK_API_KEY); after you save a search key once, the plugin takes it over.",
140
+ "你之前保存过搜索默认 key,但官方页还指向聊天那把。": "You saved a search default key earlier, but the official page still points at the chat key.",
141
+ "同步到官方设置": "Sync to official settings",
142
+ "sk-…(留空 = 用官方搜索页里配的那把)": "sk-… (empty = use the key from the official search page)",
143
+ "搜索默认": "Search default",
144
+ "清除": "Clear",
145
+ "展开工作区可以看到它下面的会话,并单独给某个会话指定 key(会话 > 工作区 > 系统默认)。": "Expand a workspace to see its sessions and give one its own key (session > workspace > system default).",
146
+ "还没有工作区。先在侧栏「工作区」里新建一个。": "No workspaces yet. Create one in the Workspaces section of the sidebar first.",
147
+ "这些目录里的会话没有挂在工作区下(侧栏的「未分组」),同样可以单独配置。": "Sessions in these directories are not under a workspace (the sidebar's \"Ungrouped\"); they can be configured the same way.",
148
+ "只有侧栏真的会显示的目录才列在这里(刚建好、还没发过消息的空会话不算)。": "Only directories the sidebar actually shows are listed (brand-new empty sessions with no message do not count).",
149
+ "这些目录已经不是注册的工作区了,对应的 key 仍留在凭据库里。可改用系统默认以清理。": "These directories are no longer registered workspaces; their keys remain in the credential store. Switch them to the system default to clean up.",
150
+ "专属 key 存进 DSH 凭据库中的独立引用,因此换 key 不写进会话日志、也不影响历史回放。": "Dedicated keys are stored as separate refs in the DSH credential store, so changing a key never enters session logs or affects replay.",
151
+ "这个会话用的 API Key 已失效": "The API key for this session is invalid",
152
+ "这个会话的 API Key 可能不匹配": "The API key for this session may not match",
153
+ "网页搜索的配置需要确认": "Web search configuration needs attention",
154
+ "会话": "Session",
155
+ "模型": "Model",
156
+ "生效 key": "Effective key",
157
+ "插件不会自动更换 key —— 用哪把 key 由你决定,避免计费被悄悄改到别的账号上。": "The plugin never swaps keys automatically — you decide which key is used, so billing cannot silently move to another account.",
158
+ "去配置": "Configure",
159
+ "继续用当前 key": "Keep current key",
160
+ "清除本会话配置": "Clear this session's setting",
161
+ "API Key 提醒": "API Key notice",
162
+ "面板入口": "Panel entry",
163
+ "左侧栏的「API Key 分配」图标可以像其他面板一样取消固定。取消之后它还在侧栏底部的「更多」菜单里,并不是被卸载。": "The \"API Key Allocation\" icon in the left sidebar can be unpinned like any other panel. Unpinning only moves it into the sidebar's More menu; the panel itself is not removed.",
164
+ "如果找不到它,可以:": "If you cannot find it, you can:",
165
+ "点下面的按钮,直接打开面板;": "press the button below to open the panel directly;",
166
+ "或者点侧栏底部的「更多」菜单,未固定的面板都列在那里;": "or open the More menu at the sidebar's bottom, where unpinned panels are listed;",
167
+ "再或者按 Ctrl+R 刷新页面 —— 面板固定状态只存在内存里,刷新就会恢复默认。": "or press Ctrl+R to reload the page — the pin state lives only in memory, so a reload restores the defaults.",
168
+ "打开 API Key 分配面板": "Open the API Key Allocation panel",
169
+ "已切到该面板:如果它还是没出现,说明面板正在加载,稍等一下。": "Switched to that panel: if it has not appeared yet, it is still loading — give it a moment.",
170
+ "这里同时管着每个工作区 / 会话的 key 与搜索通道;面板右上角的「使用说明」有完整的优先级说明。": "This panel manages the key and search channel of every workspace and session; the \"How to use\" button in its header explains the full precedence order.",
171
+ "搜索与获取": "Search & fetch",
172
+ "新版宿主用「计费方式」管理网页搜索 / 网页获取:key 由官方设置页决定,本面板不再接管,也不再显示旧的搜索 key。": "The newer host manages web search and fetch by billing mode: the key is decided on the official settings page, so this panel no longer takes over search keys or shows the old search key.",
173
+ "实验性「统一搜索 key」已开启:所有会话 / 工作区搜索都使用同一把 key。可在「设置 → API Key 分配」里更换或关闭。": "The experimental unified search key is on: every session and workspace searches with the same key. Change or disable it in Settings → API Key Allocation.",
174
+ "计费方式为「DeepSeek API Key」时,搜索会跟随当前会话 / 工作区的聊天 key;为「已登录的 DeepSeek 账号」时,搜索走账号令牌,插件无法覆盖。": "With billing set to \"DeepSeek API Key\", searches follow the current session/workspace chat key; with \"Signed-in DeepSeek account\", searches use the account token, which this plugin cannot override.",
175
+ "自动": "Automatic",
176
+ "已登录的 DeepSeek 账号": "Signed-in DeepSeek account",
177
+ "打开官方搜索设置": "Open official search settings",
178
+ "检测到的宿主版本:": "Detected host version: ",
179
+ "搜索 API(实验性)": "Search API (experimental)",
180
+ "新版宿主由官方「设置 → 插件 → 网页搜索」统一管理搜索/获取的计费方式。这里只保留一个实验开关:把所有会话的搜索强制指向插件持有的同一把 key,适合“一把搜索 key 走天下”。": "The newer host manages search/fetch billing on the official Settings → Plugins → Web Search page. Only one experimental switch remains here: force every session's searches to use a single key held by this plugin — handy for a single search key everywhere.",
181
+ "当前已启用:所有会话 / 工作区搜索都使用 DEEPSEEK_API_KEY_SEARCH 这一把 key,不再跟随各自的聊天 key。": "Currently on: every session and workspace searches with DEEPSEEK_API_KEY_SEARCH instead of its chat key.",
182
+ "粘贴新的搜索 key(sk-…)": "Paste a new search key (sk-…)",
183
+ "保存并继续使用": "Save and keep using",
184
+ "关闭统一搜索 key": "Disable unified search key",
185
+ "开启后,插件会把官方搜索配置的 key 指向自己持有的 ref,所有会话 / 工作区搜索都用这一把,不再跟随各自的聊天 key。关闭时会把官方配置清回默认计费方式。": "When enabled, the plugin points the official search key at its own ref, so every session and workspace searches with that single key instead of its chat key. Disabling clears the official setting back to the default billing mode.",
186
+ "粘贴搜索 key(sk-…)": "Paste a search key (sk-…)",
187
+ "开启": "Enable",
188
+ "已开启:所有会话/工作区共用这把搜索 key。": "Enabled: every session and workspace now shares this search key.",
189
+ "已关闭统一搜索 key,回到官方计费方式。": "Unified search key disabled; back to the official billing mode.",
190
+ "搜索 / 获取已交给官方设置": "Search & fetch are handled by the official settings",
191
+ "当前宿主的网页搜索与网页获取按「计费方式」选择用哪把 key,本插件不再接管搜索 key,也不显示旧的搜索配置。": "The current host picks the key for web search and fetch by billing mode; this plugin no longer takes over search keys or shows the old search configuration.",
192
+ "计费方式为「DeepSeek API Key」:搜索跟随当前会话 / 工作区的聊天 key,账单一起算。": "Billing \"DeepSeek API Key\": searches follow the current session/workspace chat key and share its bill.",
193
+ "计费方式为「已登录的 DeepSeek 账号」:搜索走账号令牌,插件无法覆盖。": "Billing \"Signed-in DeepSeek account\": searches use the account token, which this plugin cannot override.",
194
+ "「自动」:看当前会话走的是哪个模型路由,规则由宿主决定。": "\"Automatic\": follows the model route of the current session; the host decides the rules.",
195
+ "要改计费方式,点面板里的「打开官方搜索设置」,或手动到「设置 → 插件 → 网页搜索」。": "To change billing, press \"Open official search settings\" in the panel, or go to Settings → Plugins → Web Search manually.",
196
+ "实验性的「统一搜索 key」在「设置 → API Key 分配」页里,开启后所有会话搜索共用插件持有的一把 key。": "The experimental unified search key lives on the Settings → API Key Allocation page; when on, every session searches with one key held by the plugin.",
197
+ "聊天 key 的优先级": "Chat key precedence",
198
+ "会话单独配置的 key": "a key configured for the session",
199
+ "工作区专属 key": "a workspace-specific key",
200
+ "系统默认 key": "the system default key"
201
+ }
202
+
203
+ const EN_HOST = {
204
+ "现在搜索走的是 DeepSeek Messages 通道,用的是聊天那把 key。": "Searches currently use the DeepSeek Messages channel and the chat key.",
205
+ "这些工作区勾了「同步搜索 API」,但既没有工作区专属 key,系统默认 key 也没配置:": "These workspaces have \"Sync search API\" on, but neither a workspace key nor a system default key is configured:",
206
+ "搜索会用空 key 发出去。给这些工作区各配一把,或先配好系统默认 key。": "searches would go out with an empty key. Give each of them a key, or configure the system default first.",
207
+ "当前搜索走旧的 Messages 通道,它本来就用聊天通道的 key(会话 → 工作区 → 系统默认),": "Searches use the legacy Messages channel, which already uses the chat key (session → workspace → system default),",
208
+ "所以这个开关只影响「启用新版 Web 搜索 API」之后的行为。": "so this switch only affects behavior after \"New Web Search API\" is enabled.",
209
+ "要让它生效,请到「设置 → 插件 → 网页搜索」打开那个开关;若暂时不想用新版 API,可以忽略本条。": "To enable it, turn on the switch in Settings → Plugins → Web Search; ignore this notice if you do not want the new API for now.",
210
+ "点面板上的「同步到官方设置」,官方页的 API Key 输入框就会改到同一个 ref,两处以后改的是同一把 key。": "Click \"Sync to official settings\" in the panel and the official API Key field moves to the same ref; both then edit the same key.",
211
+ "在本面板的「搜索默认 key」里保存一把后,插件会自动把官方配置指过去,两边就分开了。": "Save one under \"Search default key\" in this panel and the plugin points the official setting at it, separating the two.",
212
+ "连接正常,key 可用": "Connected; key works",
213
+ "key 无效(401/403)": "Invalid key (401/403)",
214
+ "被限流(429),但 key 本身可能有效": "Rate limited (429), but the key itself may be valid",
215
+ "余额/配额不足(402)": "Balance/quota exhausted (402)",
216
+ "请求参数被拒": "request rejected",
217
+ "credentials 服务不可用": "credentials service unavailable",
218
+ "credentials 服务不可用,无法写入 API key": "credentials service unavailable; cannot store the API key",
219
+ "面板请求超时(8 秒):DSH 的某个服务没有响应": "Panel request timed out (8s): a DSH service did not respond",
220
+ "缺少 sessionId": "missing sessionId",
221
+ "缺少 path": "missing path",
222
+ "无法为该会话生成凭据引用": "cannot derive a credential ref for this session",
223
+ "无法为该路径生成凭据引用": "cannot derive a credential ref for this path",
224
+ "无法规范化该路径": "cannot normalize this path",
225
+ "搜索 key 已配置,但「启用新版 Web 搜索 API」没打开": "Search key is configured, but \"New Web Search API\" is off",
226
+ "搜索默认 key 还没配置:搜索暂时沿用聊天那把 key": "No search default key yet: searches currently share the chat key",
227
+ "新版 Web 搜索 API 已启用,但搜索 key 没有配置": "New Web Search API is enabled, but no search key is configured",
228
+ "搜索默认 key 已配置,但「启用新版 Web 搜索 API」没打开": "Search default key is configured, but \"New Web Search API\" is off",
229
+ "搜索默认 key 已保存,但官方「网页搜索」页仍指向聊天那把": "Search default key is saved, but the official \"Web Search\" page still points at the chat key",
230
+ "「同步搜索 API」现在没有区别": "\"Sync search API\" currently makes no difference"
231
+ }
232
+
233
+ const EN_HOST_RULES = [
234
+ [/^官方页现在绑的是 (.+)(与聊天共用),所以那里看不到 (.+) 这把。$/, "The official page binds $1 (shared with chat), so it cannot show $2."],
235
+ [/^所以面板里的 (.+) 不会被使用。$/, "so $1 in the panel is not used."],
236
+ [/^现在搜索读的是 (.+)(和聊天共用一把),所以搜索与聊天的花费算在一起。$/, "Searches read $1 (shared with chat), so search and chat spending are counted together."],
237
+ [/^鉴权通过(HTTP 400:(.+))$/, "Authenticated (HTTP 400: $1)"],
238
+ [/^请求超时((\d+)ms):(.+)$/, "Request timed out ($1ms): $2"],
239
+ [/^(404:端点 (.+) 不存在,检查 baseURL)$/, "(404: endpoint $1 not found; check baseURL)"],
240
+ [/^(404:端点 (.+) 不存在,检查「Search \/ Fetch API 基础地址」)$/, "(404: endpoint $1 not found; check the Search / Fetch API base URL)"],
241
+ [/^搜索通道正常,返回 (\d+) 条结果((.+))$/, "Search channel OK, $1 result(s) ($2)"],
242
+ [/^搜索通道正常((.+))$/, "Search channel OK ($1)"]
243
+ ]
244
+
245
+ const i18n = { english: false, listeners: new Set() }
246
+
247
+ function refreshI18n(ctx) {
248
+ let english = false
249
+ try {
250
+ const locale = ctx?.get?.('locale')
251
+ const snapshot = locale?.getSnapshot?.() ?? locale?.getLocale?.()
252
+ const active = snapshot?.active
253
+ english = typeof active === 'string' && !active.toLowerCase().startsWith('zh')
254
+ } catch {
255
+ english = false
256
+ }
257
+ i18n.english = english
258
+ }
259
+
260
+ function notifyI18n() {
261
+ for (const listener of [...i18n.listeners]) {
262
+ try { listener() } catch { /* a broken listener must not break the switch */ }
263
+ }
264
+ }
265
+
266
+ function tr(value) {
267
+ if (typeof value !== 'string' || i18n.english !== true) return value
268
+ const hit = EN[value] ?? EN_HOST[value]
269
+ if (hit !== undefined) return hit
270
+ for (const [pattern, replacement] of EN_HOST_RULES) {
271
+ if (pattern.test(value)) return value.replace(pattern, replacement)
272
+ }
273
+ return value
274
+ }
275
+
276
+ function tf(zh, en) {
277
+ return i18n.english === true ? en : zh
278
+ }
279
+
280
+ function trChild(value) {
281
+ if (typeof value === 'string') return tr(value)
282
+ if (Array.isArray(value)) return value.map(trChild)
283
+ return value
284
+ }
285
+
286
+ const TRANSLATED_PROPS = ['title', 'placeholder', 'aria-label', 'alt', 'label']
287
+ function h(type, props, ...children) {
288
+ let next = props
289
+ if (props !== null && typeof props === 'object') {
290
+ next = { ...props }
291
+ for (const key of TRANSLATED_PROPS) {
292
+ if (typeof next[key] === 'string') next[key] = tr(next[key])
293
+ }
294
+ }
295
+ return rawCreateElement(type, next, ...children.map(trChild))
296
+ }
297
+
298
+ function useI18n() {
299
+ const [, setTick] = useState(0)
300
+ useEffect(() => {
301
+ if (host.ctx !== null) refreshI18n(host.ctx)
302
+ const listener = () => setTick((tick) => tick + 1)
303
+ i18n.listeners.add(listener)
304
+ return () => { i18n.listeners.delete(listener) }
305
+ }, [])
306
+ }
307
+
11
308
  const API_BASE = '/wsk-api'
12
309
  const STYLE_ID = 'wsk-styles'
13
310
  const PANEL_ID = 'workspace-api-key'
@@ -144,15 +441,15 @@ window.__ModuleLoader__.load({
144
441
  try {
145
442
  res = await window.fetch(`${API_BASE}${path}`, init)
146
443
  } catch (err) {
147
- throw new Error(`无法访问插件宿主接口(${String(err?.message ?? err)})`)
444
+ throw new Error(tf(`无法访问插件宿主接口(${String(err?.message ?? err)})`, `Could not reach the plugin host API (${String(err?.message ?? err)})`))
148
445
  }
149
446
  let body = null
150
447
  try {
151
448
  body = await res.json()
152
449
  } catch {
153
- throw new Error(`宿主接口返回了非 JSON 响应(HTTP ${res.status})`)
450
+ throw new Error(tf(`宿主接口返回了非 JSON 响应(HTTP ${res.status})`, `The host API returned a non-JSON response (HTTP ${res.status})`))
154
451
  }
155
- if (body === null || body === undefined) throw new Error(`宿主接口返回空响应(HTTP ${res.status})`)
452
+ if (body === null || body === undefined) throw new Error(tf(`宿主接口返回空响应(HTTP ${res.status})`, `The host API returned an empty response (HTTP ${res.status})`))
156
453
  if (res.ok !== true || body.ok === false) throw new Error(body.error ?? `HTTP ${res.status}`)
157
454
  return body
158
455
  }
@@ -164,27 +461,27 @@ window.__ModuleLoader__.load({
164
461
 
165
462
  function verdictText(verdict) {
166
463
  switch (verdict) {
167
- case 'ok': return '连接正常'
168
- case 'invalid': return 'key 无效'
169
- case 'quota': return '余额不足'
170
- case 'rate-limit': return '被限流'
171
- case 'unreachable': return '无法连接'
172
- default: return '测试失败'
464
+ case 'ok': return tr('连接正常')
465
+ case 'invalid': return tr('key 无效')
466
+ case 'quota': return tr('余额不足')
467
+ case 'rate-limit': return tr('被限流')
468
+ case 'unreachable': return tr('无法连接')
469
+ default: return tr('测试失败')
173
470
  }
174
471
  }
175
472
 
176
473
  function invalidReason(flag) {
177
474
  if (flag === null || flag === undefined) return undefined
178
475
  if (flag.status !== 'invalid') return undefined
179
- return flag.message ?? '鉴权失败'
476
+ return tr(flag.message) ?? tr('鉴权失败')
180
477
  }
181
478
 
182
479
  // 生效层级:会话级 > 工作区级 > 系统默认
183
480
  function levelText(level) {
184
481
  switch (level) {
185
- case 'session': return '本会话专属'
186
- case 'workspace': return '工作区专属'
187
- default: return '系统默认'
482
+ case 'session': return tr('本会话专属')
483
+ case 'workspace': return tr('工作区专属')
484
+ default: return tr('系统默认')
188
485
  }
189
486
  }
190
487
 
@@ -194,9 +491,9 @@ window.__ModuleLoader__.load({
194
491
 
195
492
  function mismatchText(mismatch) {
196
493
  if (mismatch === null || mismatch === undefined) return undefined
197
- const key = mismatch.keyProvider ?? '未记录服务商'
198
- const session = mismatch.sessionProvider ?? '未知'
199
- return `这把 key 是为「${key}」准备的,当前会话用的是「${session}」`
494
+ const key = mismatch.keyProvider ?? tr('未记录服务商')
495
+ const session = mismatch.sessionProvider ?? tr('未知')
496
+ return tf(`这把 key 是为「${key}」准备的,当前会话用的是「${session}」`, `This key is for "${key}", but the current session uses "${session}"`)
200
497
  }
201
498
 
202
499
  /**
@@ -207,19 +504,19 @@ window.__ModuleLoader__.load({
207
504
  if (search === null || search === undefined) return undefined
208
505
  if (search.apiChannel === true) {
209
506
  if (search.keyConfigured === false) {
210
- return '你打开了「启用新版 Web 搜索 API」,但搜索 key 还没有配置:这个会话里的搜索会直接失败。'
507
+ return tr('你打开了「启用新版 Web 搜索 API」,但搜索 key 还没有配置:这个会话里的搜索会直接失败。')
211
508
  }
212
509
  return undefined
213
510
  }
214
511
  if (search.leftoverKey === true) {
215
- return '你配置了搜索专用的 API Key,但「启用新版 Web 搜索 API」没有打开,所以那把 key 现在不会被使用。'
512
+ return tr('你配置了搜索专用的 API Key,但「启用新版 Web 搜索 API」没有打开,所以那把 key 现在不会被使用。')
216
513
  }
217
514
  return undefined
218
515
  }
219
516
 
220
517
  function sessionLabel(session) {
221
518
  const title = typeof session.title === 'string' && session.title !== '' ? session.title : undefined
222
- return title ?? (session.blank === true ? '(空会话)' : String(session.sessionId).slice(0, 12))
519
+ return title ?? (session.blank === true ? tr('(空会话)') : String(session.sessionId).slice(0, 12))
223
520
  }
224
521
 
225
522
  // ─────────────────── 会话树:宿主行 + 浏览器端列表 ───────────────────
@@ -431,8 +728,8 @@ window.__ModuleLoader__.load({
431
728
  }
432
729
 
433
730
  function modelText(session) {
434
- if (typeof session.provider !== 'string' || session.provider === '') return '模型未知'
435
- return `${session.provider} / ${session.model ?? '默认模型'}`
731
+ if (typeof session.provider !== 'string' || session.provider === '') return tr('模型未知')
732
+ return tf(`${session.provider} / ${session.model ?? '默认模型'}`, `${session.provider} / ${session.model ?? 'default model'}`)
436
733
  }
437
734
 
438
735
  // ─────────────────────────────── 图标 ───────────────────────────────
@@ -505,7 +802,196 @@ window.__ModuleLoader__.load({
505
802
 
506
803
  // ─────────────────────────────── 主页面 ───────────────────────────────
507
804
 
805
+ function billingText(value) {
806
+ switch (value) {
807
+ case 'account': return tr('已登录的 DeepSeek 账号')
808
+ case 'api-key': return tr('DeepSeek API Key')
809
+ default: return tr('自动')
810
+ }
811
+ }
812
+
813
+ /**
814
+ * 跳到官方「设置 → 插件 → 网页搜索」。
815
+ *
816
+ * 宿主没有给普通插件开放「把设置打开到某个 section」的 API(settings store 的
817
+ * openSection 只发给 settings.launcher),所以这里做尽力而为的点击导航:
818
+ * 打开设置 → 点「插件」→ 点「网页搜索」。任何一步失败,用户至少停在设置里。
819
+ */
820
+ function openOfficialSearchSettings() {
821
+ const findClickable = (root, pattern) => {
822
+ const nodes = root.querySelectorAll('button, a, [role="tab"], [role="button"], [role="menuitem"]')
823
+ for (const node of nodes) {
824
+ const text = (node.textContent ?? '').trim()
825
+ if (text !== '' && pattern.test(text)) {
826
+ node.click()
827
+ return true
828
+ }
829
+ }
830
+ return false
831
+ }
832
+ try {
833
+ if (document.querySelector('[data-shortcut-modal="settings"]') === null) {
834
+ const trigger = document.querySelector('button[aria-haspopup="dialog"]')
835
+ if (trigger !== null) trigger.click()
836
+ else window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', code: 'Comma', ctrlKey: true, bubbles: true }))
837
+ }
838
+ let tries = 0
839
+ const timer = setInterval(() => {
840
+ tries += 1
841
+ const modal = document.querySelector('[data-shortcut-modal="settings"]')
842
+ const text = modal === null ? '' : (modal.textContent ?? '')
843
+ if (modal !== null && /插件|Plugins?/i.test(text)) {
844
+ if (findClickable(modal, /^插件$|^Plugins?$/i)) {
845
+ clearInterval(timer)
846
+ setTimeout(() => {
847
+ const again = document.querySelector('[data-shortcut-modal="settings"]')
848
+ if (again !== null) findClickable(again, /网页搜索|Web search/i)
849
+ }, 150)
850
+ return
851
+ }
852
+ }
853
+ if (tries > 40) clearInterval(timer)
854
+ }, 50)
855
+ } catch {
856
+ /* 尽力而为:至少把设置打开 */
857
+ }
858
+ }
859
+
860
+ /**
861
+ * 「设置 → API Key 分配」这一栏:侧栏图标没了也能从这里回到面板。
862
+ *
863
+ * 侧栏的面板图标是 DSH 的「面板固定」状态(`@deepseek-ai/dsh-client-ui-sidebar`
864
+ * 的 panel-pins)。取消固定只把图标挪进侧栏底部的「更多」菜单,面板本身没被卸载;
865
+ * 那套固定状态只保存在内存里,刷新页面就恢复默认,而且没有对外 API,
866
+ * 插件没法替用户把图标钉回去 —— 所以这里提供一个不会消失的入口。
867
+ */
868
+ function SettingsSection() {
869
+ useI18n()
870
+ const [opened, setOpened] = useState(false)
871
+ const [state, setState] = useState(null)
872
+ const [unifiedKey, setUnifiedKey] = useState('')
873
+ const [unifiedBusy, setUnifiedBusy] = useState(false)
874
+ const [unifiedNotice, setUnifiedNotice] = useState(null)
875
+ useEffect(() => {
876
+ let alive = true
877
+ api('/state').then((next) => { if (alive) setState(next) }).catch(() => {})
878
+ return () => { alive = false }
879
+ }, [])
880
+ const search = state?.search ?? null
881
+ const billing = search?.mode === 'billing'
882
+ const unified = search?.unified === true
883
+ const saveUnified = async (enabled) => {
884
+ setUnifiedBusy(true)
885
+ setUnifiedNotice(null)
886
+ try {
887
+ const next = await api('/search-unified', {
888
+ method: 'POST',
889
+ body: JSON.stringify({ enabled, key: unifiedKey }),
890
+ })
891
+ setState(next)
892
+ setUnifiedKey('')
893
+ setUnifiedNotice({
894
+ kind: 'ok',
895
+ text: enabled
896
+ ? '已开启:所有会话/工作区共用这把搜索 key。'
897
+ : '已关闭统一搜索 key,回到官方计费方式。',
898
+ })
899
+ } catch (err) {
900
+ setUnifiedNotice({ kind: 'err', text: String(err?.message ?? err) })
901
+ } finally {
902
+ setUnifiedBusy(false)
903
+ }
904
+ }
905
+ const openPanel = () => {
906
+ try {
907
+ const layout = host.ctx === null ? undefined : host.ctx.get('layout')
908
+ if (layout === undefined || layout === null) return
909
+ if (typeof layout.selectPanel !== 'function') return
910
+ layout.selectPanel(PANEL_ID)
911
+ setOpened(true)
912
+ } catch {
913
+ /* layout 不可用时保持说明文字,用户还能走「更多」菜单 */
914
+ }
915
+ }
916
+ return h('div', { className: 'wsk-body' },
917
+ h('div', { className: 'wsk-card' },
918
+ h('h4', null, '面板入口'),
919
+ h('div', { className: 'wsk-hint' },
920
+ '左侧栏的「API Key 分配」图标可以像其他面板一样取消固定。取消之后它还在侧栏底部的「更多」菜单里,并不是被卸载。'),
921
+ h('div', { className: 'wsk-hint' }, '如果找不到它,可以:'),
922
+ h('ul', { className: 'wsk-hint', style: { margin: '6px 0 10px 18px' } },
923
+ h('li', null, '点下面的按钮,直接打开面板;'),
924
+ h('li', null, '或者点侧栏底部的「更多」菜单,未固定的面板都列在那里;'),
925
+ h('li', null, '再或者按 Ctrl+R 刷新页面 —— 面板固定状态只存在内存里,刷新就会恢复默认。')),
926
+ h('button', { type: 'button', className: 'wsk-btn primary', onClick: openPanel },
927
+ '打开 API Key 分配面板'),
928
+ opened
929
+ ? h('div', { className: 'wsk-hint', style: { marginTop: 8 } },
930
+ '已切到该面板:如果它还是没出现,说明面板正在加载,稍等一下。')
931
+ : null,
932
+ h('div', { className: 'wsk-hint', style: { marginTop: 10 } },
933
+ '这里同时管着每个工作区 / 会话的 key 与搜索通道;面板右上角的「使用说明」有完整的优先级说明。')),
934
+ billing
935
+ ? h('div', { className: 'wsk-card' },
936
+ h('h4', null, '搜索 API(实验性)'),
937
+ h('div', { className: 'wsk-hint' },
938
+ '新版宿主由官方「设置 → 插件 → 网页搜索」统一管理搜索/获取的计费方式。这里只保留一个实验开关:把所有会话的搜索强制指向插件持有的同一把 key,适合“一把搜索 key 走天下”。'),
939
+ unified
940
+ ? h('div', null,
941
+ h('div', { className: 'wsk-hint' },
942
+ '当前已启用:所有会话 / 工作区搜索都使用 DEEPSEEK_API_KEY_SEARCH 这一把 key,不再跟随各自的聊天 key。'),
943
+ h('div', { className: 'wsk-row', style: { marginTop: 8 } },
944
+ h('input', {
945
+ className: 'wsk-input',
946
+ type: 'password',
947
+ value: unifiedKey,
948
+ placeholder: '粘贴新的搜索 key(sk-…)',
949
+ onChange: (ev) => setUnifiedKey(ev.target.value),
950
+ }),
951
+ h('button', {
952
+ type: 'button', className: 'wsk-btn primary',
953
+ disabled: unifiedBusy || unifiedKey.trim() === '',
954
+ onClick: () => saveUnified(true),
955
+ }, '保存并继续使用'),
956
+ h('button', {
957
+ type: 'button', className: 'wsk-btn',
958
+ disabled: unifiedBusy,
959
+ onClick: () => saveUnified(false),
960
+ }, '关闭统一搜索 key')))
961
+ : h('div', null,
962
+ h('div', { className: 'wsk-hint' },
963
+ '开启后,插件会把官方搜索配置的 key 指向自己持有的 ref,所有会话 / 工作区搜索都用这一把,不再跟随各自的聊天 key。关闭时会把官方配置清回默认计费方式。'),
964
+ h('div', { className: 'wsk-row', style: { marginTop: 8 } },
965
+ h('input', {
966
+ className: 'wsk-input',
967
+ type: 'password',
968
+ value: unifiedKey,
969
+ placeholder: '粘贴搜索 key(sk-…)',
970
+ onChange: (ev) => setUnifiedKey(ev.target.value),
971
+ }),
972
+ h('button', {
973
+ type: 'button', className: 'wsk-btn primary',
974
+ disabled: unifiedBusy || unifiedKey.trim() === '',
975
+ onClick: () => saveUnified(true),
976
+ }, '开启'))),
977
+ unifiedNotice !== null
978
+ ? h('div', {
979
+ className: 'wsk-hint',
980
+ style: { marginTop: 8, color: unifiedNotice.kind === 'ok' ? undefined : '#e5484d' },
981
+ }, unifiedNotice.text)
982
+ : null,
983
+ h('button', {
984
+ type: 'button', className: 'wsk-btn', style: { marginTop: 8 },
985
+ onClick: openOfficialSearchSettings,
986
+ }, '打开官方搜索设置'))
987
+ : null,
988
+ billing && typeof state?.hostVersion === 'string' && state.hostVersion !== ''
989
+ ? h('div', { className: 'wsk-hint' }, tf(`检测到的宿主版本:${state.hostVersion}`, `Detected host version: ${state.hostVersion}`))
990
+ : null)
991
+ }
992
+
508
993
  function WorkspaceKeyPage(props) {
994
+ useI18n()
509
995
  const [state, setState] = useState(null)
510
996
  const [error, setError] = useState(null)
511
997
  const [busy, setBusy] = useState(false)
@@ -646,12 +1132,12 @@ window.__ModuleLoader__.load({
646
1132
  setNotice({
647
1133
  kind: 'ok',
648
1134
  text: trimmed === ''
649
- ? `${target.title}:已改用${scope === 'session' ? '上层' : '系统默认'}设置`
1135
+ ? tf(`${target.title}:已改用${scope === 'session' ? '上层' : '系统默认'}设置`, `${target.title}: switched to the ${scope === 'session' ? 'level above' : 'system default'}`)
650
1136
  : target.kind === 'session'
651
- ? `${target.title}:已保存本会话专属 key`
1137
+ ? tf(`${target.title}:已保存本会话专属 key`, `${target.title}: saved a key for this session`)
652
1138
  : wasCleared
653
- ? `${target.title}:已改用系统默认设置`
654
- : `${target.title}:已保存专属 key`,
1139
+ ? tf(`${target.title}:已改用系统默认设置`, `${target.title}: switched back to the system default`)
1140
+ : tf(`${target.title}:已保存专属 key`, `${target.title}: saved a dedicated key`),
655
1141
  })
656
1142
  }
657
1143
  return true
@@ -682,14 +1168,14 @@ window.__ModuleLoader__.load({
682
1168
  } else if (bound?.ok === false) {
683
1169
  setNotice({
684
1170
  kind: 'warn',
685
- text: `key 已保存在 ${next?.search?.defaultRef ?? '搜索默认 ref'},但没能改官方设置(${bound.reason}):`
686
- + '官方「网页搜索」页仍指向原来的 ref,两处暂时不是同一把。',
1171
+ text: tf(`key 已保存在 ${next?.search?.defaultRef ?? '搜索默认 ref'},但没能改官方设置(${bound.reason}):`, `Key saved to ${next?.search?.defaultRef ?? 'the search default ref'}, but the official setting could not be changed (${bound.reason}):`)
1172
+ + tf('官方「网页搜索」页仍指向原来的 ref,两处暂时不是同一把。', 'The official "Web Search" page still points at the previous ref, so the two are temporarily different keys.'),
687
1173
  })
688
1174
  } else {
689
1175
  setNotice({
690
1176
  kind: 'ok',
691
- text: `搜索默认 key 已保存到 ${next?.search?.defaultRef ?? '搜索默认 ref'},`
692
- + '官方「网页搜索」页的 API Key 输入框也指向这个 ref:以后两边改的是同一把 key。',
1177
+ text: tf(`搜索默认 key 已保存到 ${next?.search?.defaultRef ?? '搜索默认 ref'},`, `Search default key saved to ${next?.search?.defaultRef ?? 'the search default ref'}, `)
1178
+ + tf('官方「网页搜索」页的 API Key 输入框也指向这个 ref:以后两边改的是同一把 key。', 'The API Key field on the official "Web Search" page now points at this ref too: both places edit the same key.'),
693
1179
  })
694
1180
  }
695
1181
  } catch (err) {
@@ -707,8 +1193,8 @@ window.__ModuleLoader__.load({
707
1193
  applyState(next)
708
1194
  const bound = next?.bound
709
1195
  setNotice(bound?.ok === false
710
- ? { kind: 'warn', text: `没能改官方设置:${bound.reason}` }
711
- : { kind: 'ok', text: `已同步:官方「网页搜索」页现在绑定 ${next?.search?.officialRef ?? next?.search?.defaultRef ?? '同一个 ref'},与面板改的是同一把 key。` })
1196
+ ? { kind: 'warn', text: tf(`没能改官方设置:${bound.reason}`, `Could not change the official setting: ${bound.reason}`) }
1197
+ : { kind: 'ok', text: tf(`已同步:官方「网页搜索」页现在绑定 ${next?.search?.officialRef ?? next?.search?.defaultRef ?? '同一个 ref'},与面板改的是同一把 key。`, `Synced: the official "Web Search" page now binds ${next?.search?.officialRef ?? next?.search?.defaultRef ?? 'the same ref'} — the same key this panel edits.`) })
712
1198
  } catch (err) {
713
1199
  if (alive.current) setNotice({ kind: 'bad', text: `${err?.message ?? err}` })
714
1200
  } finally {
@@ -766,7 +1252,7 @@ window.__ModuleLoader__.load({
766
1252
  applyState(await api('/clear-flag', { method: 'POST', body: JSON.stringify(body) }))
767
1253
  if (alive.current) {
768
1254
  setTests((prev) => ({ ...prev, [id]: undefined }))
769
- setNotice({ kind: 'info', text: `${title}:已清除失效标记` })
1255
+ setNotice({ kind: 'info', text: tf(`${title}:已清除失效标记`, `${title}: invalid flag cleared`) })
770
1256
  }
771
1257
  } catch (err) {
772
1258
  if (alive.current) setNotice({ kind: 'bad', text: String(err?.message ?? err) })
@@ -803,31 +1289,31 @@ window.__ModuleLoader__.load({
803
1289
  if (state === null) return undefined
804
1290
  const hostVersion = typeof state.pluginVersion === 'string'
805
1291
  ? state.pluginVersion
806
- : '旧版(未重启应用)'
1292
+ : tr('旧版(未重启应用)')
807
1293
  const sessions = probe === undefined
808
- ? '未知'
1294
+ ? tr('未知')
809
1295
  : probe.available !== true
810
- ? '不可用'
811
- : `${probe.count} 个${probe.shape !== undefined ? `(${probe.shape})` : ''}`
812
- + (probe.source === 'sessions' ? '(live 兜底)' : '')
813
- const listError = probe?.error !== undefined ? ` · list 报错:${probe.error}` : ''
814
- const noCwd = probe?.withoutCwd > 0 ? ` · ${probe.withoutCwd} 个没有 cwd` : ''
815
- const subs = probe?.subagents > 0 ? ` · 已跳过 ${probe.subagents} 个子代理会话` : ''
816
- const blanks = (view?.sessionBlanks ?? 0) > 0 ? ` · 已跳过 ${view.sessionBlanks} 个空会话` : ''
1296
+ ? tr('不可用')
1297
+ : tf(`${probe.count} 个${probe.shape !== undefined ? `(${probe.shape})` : ''}`, `${probe.count} session(s)${probe.shape !== undefined ? ` (${probe.shape})` : ''}`)
1298
+ + (probe.source === 'sessions' ? tf('(live 兜底)', ' (live fallback)') : '')
1299
+ const listError = probe?.error !== undefined ? tf(` · list 报错:${probe.error}`, ` · list() error: ${tr(probe.error)}`) : ''
1300
+ const noCwd = probe?.withoutCwd > 0 ? tf(` · ${probe.withoutCwd} 个没有 cwd`, ` · ${probe.withoutCwd} without cwd`) : ''
1301
+ const subs = probe?.subagents > 0 ? tf(` · 已跳过 ${probe.subagents} 个子代理会话`, ` · skipped ${probe.subagents} subagent session(s)`) : ''
1302
+ const blanks = (view?.sessionBlanks ?? 0) > 0 ? tf(` · 已跳过 ${view.sessionBlanks} 个空会话`, ` · skipped ${view.sessionBlanks} empty session(s)`) : ''
817
1303
  // 宿主只标注「这个目录里只有还没说过话的空会话」,列不列由本文件按侧栏语义决定
818
1304
  // (空会话里只有当前打开的那一个算数)——所以这里说的是「宿主标了几个」。
819
1305
  const blankDirs = Array.isArray(state.hiddenBlankDirs) && state.hiddenBlankDirs.length > 0
820
- ? ` · ${state.hiddenBlankDirs.length} 个目录只有空会话(未分组里不列)`
1306
+ ? tf(` · ${state.hiddenBlankDirs.length} 个目录只有空会话(未分组里不列)`, ` · ${state.hiddenBlankDirs.length} directories contain only empty sessions (not listed under Ungrouped)`)
821
1307
  : ''
822
1308
  const searchDiag = searchInfo === undefined
823
1309
  ? ''
824
- : ` · 搜索通道:${searchInfo.apiChannel === true ? '新版 API' : 'Messages'}`
825
- + `(${searchInfo.keyRef ?? '—'}${searchInfo.keyMasked ? ` · ${searchInfo.keyMasked}` : ' · 未配置'})`
826
- + (searchInfo.syncCount > 0 ? ` · 已同步 ${searchInfo.syncCount} 个目录` : '')
827
- return `插件 v${hostVersion} · 状态存储 v${state.storeVersion ?? '—'}`
828
- + ` · 宿主会话列表:${sessions}${listError}${noCwd}${subs}`
829
- + ` · 浏览器会话列表:${clientCount} 个${blanks}${blankDirs}`
830
- + ` · 工作区 ${grouped.length} + 未分组 ${ungrouped.length} · 挂在目录下的会话 ${sessionTotal}`
1310
+ : tf(` · 搜索通道:${searchInfo.apiChannel === true ? '新版 API' : 'Messages'}`, ` · search channel: ${searchInfo.apiChannel === true ? 'new API' : 'Messages'}`)
1311
+ + tf(`(${searchInfo.keyRef ?? '—'}${searchInfo.keyMasked ? ` · ${searchInfo.keyMasked}` : ' · 未配置'})`, ` (${searchInfo.keyRef ?? '—'}${searchInfo.keyMasked ? ` · ${searchInfo.keyMasked}` : ' · not configured'})`)
1312
+ + (searchInfo.syncCount > 0 ? tf(` · 已同步 ${searchInfo.syncCount} 个目录`, ` · synced ${searchInfo.syncCount} director${searchInfo.syncCount === 1 ? 'y' : 'ies'}`) : '')
1313
+ return tf(`插件 v${hostVersion} · 状态存储 v${state.storeVersion ?? '—'}`, `plugin v${hostVersion} · state store v${state.storeVersion ?? '—'}`)
1314
+ + tf(` · 宿主会话列表:${sessions}${listError}${noCwd}${subs}`, ` · host sessions: ${sessions}${listError}${noCwd}${subs}`)
1315
+ + tf(` · 浏览器会话列表:${clientCount} 个${blanks}${blankDirs}`, ` · browser sessions: ${clientCount}${blanks}${blankDirs}`)
1316
+ + tf(` · 工作区 ${grouped.length} + 未分组 ${ungrouped.length} · 挂在目录下的会话 ${sessionTotal}`, ` · workspaces ${grouped.length} + ungrouped ${ungrouped.length} · sessions attached to directories ${sessionTotal}`)
831
1317
  + searchDiag
832
1318
  }
833
1319
 
@@ -837,8 +1323,8 @@ window.__ModuleLoader__.load({
837
1323
  const paths = Array.isArray(state.workspacePathSample) ? state.workspacePathSample : []
838
1324
  if (cwds.length === 0 && paths.length === 0) return null
839
1325
  return h('div', { className: 'wsk-hint', style: { marginTop: 4 } },
840
- cwds.length > 0 ? h('div', null, `会话 cwd 样本:${cwds.join(' | ')}`) : null,
841
- paths.length > 0 ? h('div', null, `工作区路径样本:${paths.join(' | ')}`) : null)
1326
+ cwds.length > 0 ? h('div', null, tf(`会话 cwd 样本:${cwds.join(' | ')}`, `Session cwd samples: ${cwds.join(' | ')}`)) : null,
1327
+ paths.length > 0 ? h('div', null, tf(`工作区路径样本:${paths.join(' | ')}`, `Workspace path samples: ${paths.join(' | ')}`)) : null)
842
1328
  }
843
1329
 
844
1330
  const providerOptions = (current, fallback) => {
@@ -854,14 +1340,14 @@ window.__ModuleLoader__.load({
854
1340
  const isSession = target.kind === 'session'
855
1341
  const current = isSession ? target.level : target.configured === true ? 'workspace' : 'default'
856
1342
  const fallbackLabel = isSession
857
- ? `清除本会话配置(改回${target.workspaceConfigured === true ? '工作区' : '系统默认'})`
1343
+ ? tf(`清除本会话配置(改回${target.workspaceConfigured === true ? '工作区' : '系统默认'})`, `Clear this session's setting (back to ${target.workspaceConfigured === true ? 'workspace' : 'system default'})`)
858
1344
  : '改用系统默认'
859
1345
  return h('div', { className: 'wsk-editor' },
860
1346
  h('div', { className: 'wsk-hint' },
861
1347
  isSession
862
- ? `当前生效:${levelText(current)}${target.keyMasked ? `(${target.keyMasked})` : ''}。`
1348
+ ? tf(`当前生效:${levelText(current)}${target.keyMasked ? `(${target.keyMasked})` : ''}。`, `Currently effective: ${levelText(current)}${target.keyMasked ? ` (${target.keyMasked})` : ''}.`)
863
1349
  : target.configured === true
864
- ? `该工作区当前使用专属 key${target.keyMasked ? `(${target.keyMasked})` : ''}。留空并保存即恢复使用系统默认。`
1350
+ ? tf(`该工作区当前使用专属 key${target.keyMasked ? `(${target.keyMasked})` : ''}。留空并保存即恢复使用系统默认。`, `This workspace uses a dedicated key${target.keyMasked ? ` (${target.keyMasked})` : ''}. Leave empty and save to fall back to the system default.`)
865
1351
  : '为该工作区指定一把专属 key;留空并保存即继续使用系统默认。'),
866
1352
  h('input', {
867
1353
  className: 'wsk-input',
@@ -901,7 +1387,7 @@ window.__ModuleLoader__.load({
901
1387
  }, '取消')),
902
1388
  h('div', { className: 'wsk-hint' }, target.ref === undefined || target.ref === null
903
1389
  ? '凭据引用:保存时由宿主按这个会话/目录的 ID 生成,保存后这里会显示出来'
904
- : `凭据引用:${target.ref}`))
1390
+ : tf(`凭据引用:${target.ref}`, `Credential ref: ${target.ref}`)))
905
1391
  }
906
1392
 
907
1393
  const renderSession = (ws, session) => {
@@ -932,8 +1418,8 @@ window.__ModuleLoader__.load({
932
1418
  : null),
933
1419
  h('div', { className: 'wsk-path' },
934
1420
  `${modelText(session)}` +
935
- (session.effectiveKeyMasked ? ` · 生效 key ${session.effectiveKeyMasked}` : ' · 未配置 key') +
936
- (hasOwn && session.keyMasked ? ` · 本会话 ${session.keyMasked}` : '')),
1421
+ (session.effectiveKeyMasked ? tf(` · 生效 key ${session.effectiveKeyMasked}`, ` · effective key ${session.effectiveKeyMasked}`) : ' · 未配置 key') +
1422
+ (hasOwn && session.keyMasked ? tf(` · 本会话 ${session.keyMasked}`, ` · this session ${session.keyMasked}`) : '')),
937
1423
  reason !== undefined
938
1424
  ? h('div', { className: 'wsk-hint', style: { color: 'var(--wsk-bad)', marginTop: 3 } }, reason)
939
1425
  : null,
@@ -992,7 +1478,7 @@ window.__ModuleLoader__.load({
992
1478
  key: 'tgl', type: 'button', className: 'wsk-toggle',
993
1479
  title: open ? '折叠会话' : '展开会话',
994
1480
  onClick: () => toggle(ws.path),
995
- }, caretSvg(12, open), h('span', null, `会话 ${sessions.length}`))
1481
+ }, caretSvg(12, open), h('span', null, tf(`会话 ${sessions.length}`, `Sessions ${sessions.length}`)))
996
1482
  : null,
997
1483
  h('span', { key: 'n' }, title),
998
1484
  reason !== undefined
@@ -1011,7 +1497,7 @@ window.__ModuleLoader__.load({
1011
1497
  }, verdictText(lastTest.verdict))
1012
1498
  : null),
1013
1499
  h('div', { className: 'wsk-path' }, prettyPath(ws.path)),
1014
- ws.kind === 'orphan'
1500
+ ws.kind === 'orphan' || searchInfo?.mode === 'billing'
1015
1501
  ? null
1016
1502
  : h('label', {
1017
1503
  key: 'sync',
@@ -1032,11 +1518,11 @@ window.__ModuleLoader__.load({
1032
1518
  : searchInfo?.defaultConfigured === true
1033
1519
  ? '搜索用搜索默认 key'
1034
1520
  : '搜索用搜索插件里的 key')),
1035
- ws.kind !== 'orphan' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
1521
+ ws.kind !== 'orphan' && searchInfo?.mode !== 'billing' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
1036
1522
  ? h('div', { key: 'sk', className: 'wsk-hint', style: { marginTop: 2 } },
1037
- `搜索生效 key:${ws.searchKeyMasked || '未配置'}`,
1523
+ tf(`搜索生效 key:${ws.searchKeyMasked || '未配置'}`, `Effective search key: ${ws.searchKeyMasked || 'not configured'}`),
1038
1524
  ws.searchSync === true
1039
- ? `(${ws.configured === true ? '本目录专属' : '系统默认'})`
1525
+ ? tf(`(${ws.configured === true ? '本目录专属' : '系统默认'})`, ` (${ws.configured === true ? 'this directory' : 'system default'})`)
1040
1526
  : '(搜索插件)')
1041
1527
  : null,
1042
1528
  reason !== undefined
@@ -1075,7 +1561,30 @@ window.__ModuleLoader__.load({
1075
1561
  : null)
1076
1562
  }
1077
1563
 
1078
- const renderHelp = () => h(Dialog, {
1564
+ const renderHelp = () => (searchInfo?.mode === 'billing'
1565
+ ? h(Dialog, {
1566
+ title: '使用说明',
1567
+ onDismiss: () => setHelp(false),
1568
+ body: h('div', { className: 'wsk-help' },
1569
+ h('h4', null, '搜索 / 获取已交给官方设置'),
1570
+ h('div', { className: 'wsk-hint' },
1571
+ '当前宿主的网页搜索与网页获取按「计费方式」选择用哪把 key,本插件不再接管搜索 key,也不显示旧的搜索配置。'),
1572
+ h('ul', null,
1573
+ h('li', null, '计费方式为「DeepSeek API Key」:搜索跟随当前会话 / 工作区的聊天 key,账单一起算。'),
1574
+ h('li', null, '计费方式为「已登录的 DeepSeek 账号」:搜索走账号令牌,插件无法覆盖。'),
1575
+ h('li', null, '「自动」:看当前会话走的是哪个模型路由,规则由宿主决定。')),
1576
+ h('div', { className: 'wsk-hint' },
1577
+ '要改计费方式,点面板里的「打开官方搜索设置」,或手动到「设置 → 插件 → 网页搜索」。'),
1578
+ h('div', { className: 'wsk-hint' },
1579
+ '实验性的「统一搜索 key」在「设置 → API Key 分配」页里,开启后所有会话搜索共用插件持有的一把 key。'),
1580
+ h('h4', null, '聊天 key 的优先级'),
1581
+ h('ol', null,
1582
+ h('li', null, '会话单独配置的 key'),
1583
+ h('li', null, '工作区专属 key'),
1584
+ h('li', null, '系统默认 key'))),
1585
+ actions: [{ label: '知道了', primary: true, onClick: () => setHelp(false) }],
1586
+ })
1587
+ : h(Dialog, {
1079
1588
  title: '使用说明',
1080
1589
  onDismiss: () => setHelp(false),
1081
1590
  body: h('div', { className: 'wsk-help' },
@@ -1120,7 +1629,7 @@ window.__ModuleLoader__.load({
1120
1629
  h('div', { className: 'wsk-hint' },
1121
1630
  '提醒只报告、不替你改 key。插件绝不会自动切换或替换你的 key,避免计费被悄悄改到别的账号上。')),
1122
1631
  actions: [{ label: '知道了', primary: true, onClick: () => setHelp(false) }],
1123
- })
1632
+ }))
1124
1633
 
1125
1634
  const renderPrompt = () => {
1126
1635
  if (prompt === null) return null
@@ -1136,10 +1645,10 @@ window.__ModuleLoader__.load({
1136
1645
  const ws = prompt.ws
1137
1646
  const reason = invalidReason(ws.invalid)
1138
1647
  return h(Dialog, {
1139
- title: `${ws.title} 的 API Key 已失效`,
1648
+ title: tf(`${ws.title} 的 API Key 已失效`, `${ws.title}: API key invalid`),
1140
1649
  body: h('div', null,
1141
1650
  h('div', null, reason),
1142
- h('div', { className: 'wsk-hint', style: { marginTop: 6 } }, `工作区:${prettyPath(ws.path)}`),
1651
+ h('div', { className: 'wsk-hint', style: { marginTop: 6 } }, tf(`工作区:${prettyPath(ws.path)}`, `Workspace: ${prettyPath(ws.path)}`)),
1143
1652
  h('div', { className: 'wsk-hint' }, '可以重新配置这把 key,或让该工作区改用系统默认设置。')),
1144
1653
  onDismiss: () => setPrompt(null),
1145
1654
  actions: [
@@ -1198,7 +1707,7 @@ window.__ModuleLoader__.load({
1198
1707
  : null,
1199
1708
  error !== null
1200
1709
  ? h('div', { className: 'wsk-msg bad', style: { marginBottom: 12 } },
1201
- `读取失败:${error}`,
1710
+ tf(`读取失败:${error}`, `Failed to load: ${tr(error)}`),
1202
1711
  h('div', { className: 'wsk-hint' }, '请确认 DSH Web 服务正在运行、且本插件的宿主半边已加载。'))
1203
1712
  : null,
1204
1713
  hostIsStale
@@ -1247,14 +1756,14 @@ window.__ModuleLoader__.load({
1247
1756
  h('span', { key: 'r' }, defaultRef?.ref ?? DEFAULT_REF),
1248
1757
  defaultRef?.configured === true
1249
1758
  ? h('span', { key: 's', className: 'wsk-badge ok' },
1250
- `已配置${defaultRef.source ? ` · ${defaultRef.source}` : ''}`)
1759
+ tf(`已配置${defaultRef.source ? ` · ${defaultRef.source}` : ''}`, `Configured${defaultRef.source ? ` · ${defaultRef.source}` : ''}`))
1251
1760
  : h('span', { key: 's', className: 'wsk-badge bad' }, '未配置'),
1252
1761
  defaultRef?.writable === false
1253
1762
  ? h('span', { key: 'w', className: 'wsk-badge warn' }, '被环境变量遮蔽(只读)')
1254
1763
  : null),
1255
1764
  h('div', { className: 'wsk-hint' },
1256
1765
  '没有单独配置的工作区/会话,一律使用这把 key。',
1257
- defaultRef?.keyMasked ? ` 当前值:${defaultRef.keyMasked}` : '')),
1766
+ defaultRef?.keyMasked ? tf(` 当前值:${defaultRef.keyMasked}`, ` current value: ${defaultRef.keyMasked}`) : '')),
1258
1767
  h('div', { className: 'wsk-actions' },
1259
1768
  h('button', {
1260
1769
  type: 'button', className: 'wsk-btn',
@@ -1262,7 +1771,28 @@ window.__ModuleLoader__.load({
1262
1771
  onClick: () => runTest(DEFAULT_KEY, '系统默认', { ref: defaultRef?.ref ?? DEFAULT_REF }),
1263
1772
  }, testing === DEFAULT_KEY ? '测试中…' : '测试连接')))),
1264
1773
 
1265
- searchInfo?.apiChannel === true
1774
+ searchInfo?.mode === 'billing'
1775
+ ? h('div', { className: 'wsk-card' },
1776
+ h('h4', null, '搜索与获取'),
1777
+ h('div', { className: 'wsk-hint' },
1778
+ '新版宿主用「计费方式」管理网页搜索 / 网页获取:key 由官方设置页决定,本面板不再接管,也不再显示旧的搜索 key。'),
1779
+ h('div', { className: 'wsk-hint' },
1780
+ tf(
1781
+ `当前计费方式:搜索「${billingText(searchInfo.billing?.search)}」,获取「${billingText(searchInfo.billing?.fetch)}」`,
1782
+ `Current billing: search "${billingText(searchInfo.billing?.search)}", fetch "${billingText(searchInfo.billing?.fetch)}"`)),
1783
+ searchInfo?.unified === true
1784
+ ? h('div', { className: 'wsk-hint' },
1785
+ '实验性「统一搜索 key」已开启:所有会话 / 工作区搜索都使用同一把 key。可在「设置 → API Key 分配」里更换或关闭。')
1786
+ : h('div', { className: 'wsk-hint' },
1787
+ '计费方式为「DeepSeek API Key」时,搜索会跟随当前会话 / 工作区的聊天 key;为「已登录的 DeepSeek 账号」时,搜索走账号令牌,插件无法覆盖。'),
1788
+ h('div', { className: 'wsk-row', style: { marginTop: 8 } },
1789
+ h('button', {
1790
+ type: 'button', className: 'wsk-btn primary',
1791
+ onClick: openOfficialSearchSettings,
1792
+ }, '打开官方搜索设置')))
1793
+ : null,
1794
+
1795
+ searchInfo?.mode !== 'billing' && searchInfo?.apiChannel === true
1266
1796
  ? h('div', { className: 'wsk-card' },
1267
1797
  h('h4', null, '搜索默认 key'),
1268
1798
  h('div', { className: 'wsk-row' },
@@ -1277,11 +1807,11 @@ window.__ModuleLoader__.load({
1277
1807
  + '工作区勾了「同步搜索 API」时改用该工作区的聊天层级;会话单独配置永远最优先。'),
1278
1808
  h('div', { className: 'wsk-hint' },
1279
1809
  searchInfo.defaultConfigured === true && searchInfo.defaultKeyMasked
1280
- ? `当前值:${searchInfo.defaultKeyMasked}。`
1810
+ ? tf(`当前值:${searchInfo.defaultKeyMasked}。`, `Current value: ${searchInfo.defaultKeyMasked}.`)
1281
1811
  : '',
1282
1812
  searchInfo.officialShared === true
1283
1813
  ? '官方「网页搜索」页现在仍指着聊天那把 key(DEEPSEEK_API_KEY)——保存一次搜索 key 后,插件会把它接过来。'
1284
- : `官方「网页搜索」页已由本插件接管:那里填的也是 ${searchInfo.officialRef ?? 'DEEPSEEK_API_KEY_SEARCH'} 这一把,不会再误改聊天 key。`),
1814
+ : tf(`官方「网页搜索」页已由本插件接管:那里填的也是 ${searchInfo.officialRef ?? 'DEEPSEEK_API_KEY_SEARCH'} 这一把,不会再误改聊天 key。`, `The official "Web Search" page is managed by this plugin: it edits ${searchInfo.officialRef ?? 'DEEPSEEK_API_KEY_SEARCH'}, so it can no longer overwrite the chat key.`)),
1285
1815
  searchInfo.officialShared === true && searchInfo.ownedConfigured === true
1286
1816
  ? h('div', { className: 'wsk-hint' },
1287
1817
  '你之前保存过搜索默认 key,但官方页还指向聊天那把。',
@@ -1331,7 +1861,7 @@ window.__ModuleLoader__.load({
1331
1861
  : null,
1332
1862
 
1333
1863
  h('div', { className: 'wsk-card', style: { background: 'transparent', border: 0, padding: 0 } },
1334
- h('h4', { style: { marginBottom: 6 } }, `工作区(${grouped.length})`),
1864
+ h('h4', { style: { marginBottom: 6 } }, tf(`工作区(${grouped.length})`, `Workspaces (${grouped.length})`)),
1335
1865
  h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
1336
1866
  '展开工作区可以看到它下面的会话,并单独给某个会话指定 key(会话 > 工作区 > 系统默认)。'),
1337
1867
  grouped.length === 0
@@ -1340,7 +1870,7 @@ window.__ModuleLoader__.load({
1340
1870
 
1341
1871
  ungrouped.length > 0
1342
1872
  ? h('div', { className: 'wsk-card' },
1343
- h('h4', null, `未分组目录(${ungrouped.length})`),
1873
+ h('h4', null, tf(`未分组目录(${ungrouped.length})`, `Ungrouped directories (${ungrouped.length})`)),
1344
1874
  h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
1345
1875
  '这些目录里的会话没有挂在工作区下(侧栏的「未分组」),同样可以单独配置。'
1346
1876
  + '只有侧栏真的会显示的目录才列在这里(刚建好、还没发过消息的空会话不算)。'),
@@ -1349,7 +1879,7 @@ window.__ModuleLoader__.load({
1349
1879
 
1350
1880
  orphans.length > 0
1351
1881
  ? h('div', { className: 'wsk-card' },
1352
- h('h4', null, `已失效的配置(${orphans.length})`),
1882
+ h('h4', null, tf(`已失效的配置(${orphans.length})`, `Stale settings (${orphans.length})`)),
1353
1883
  h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
1354
1884
  '这些目录已经不是注册的工作区了,对应的 key 仍留在凭据库里。可改用系统默认以清理。'),
1355
1885
  h('div', { className: 'wsk-list' }, orphans.map((ws) => renderWorkspace(ws))))
@@ -1357,7 +1887,7 @@ window.__ModuleLoader__.load({
1357
1887
 
1358
1888
  h('div', { className: 'wsk-hint' },
1359
1889
  '专属 key 存进 DSH 凭据库中的独立引用,因此换 key 不写进会话日志、也不影响历史回放。',
1360
- state?.statePath ? h('div', null, `状态文件:${state.statePath}`) : null,
1890
+ state?.statePath ? h('div', null, tf(`状态文件:${state.statePath}`, `State file: ${state.statePath}`)) : null,
1361
1891
  diagText() !== undefined ? h('div', { className: 'wsk-diag' }, diagText()) : null)),
1362
1892
 
1363
1893
  renderPrompt(),
@@ -1368,6 +1898,7 @@ window.__ModuleLoader__.load({
1368
1898
 
1369
1899
  function createOverlayWatcher(ctx) {
1370
1900
  return function ApiKeyWatch() {
1901
+ useI18n()
1371
1902
  const [prompt, setPrompt] = useState(null)
1372
1903
  const asked = useRef(new Set())
1373
1904
 
@@ -1489,7 +2020,7 @@ window.__ModuleLoader__.load({
1489
2020
  : '网页搜索的配置需要确认',
1490
2021
  body: h('div', null,
1491
2022
  kv('会话', prompt.title ?? String(prompt.sessionId ?? '').slice(0, 12)),
1492
- kv('模型', `${prompt.provider ?? '未知'} / ${prompt.model ?? '默认模型'}`),
2023
+ kv('模型', tf(`${prompt.provider ?? '未知'} / ${prompt.model ?? '默认模型'}`, `${prompt.provider ?? 'Unknown'} / ${prompt.model ?? 'default model'}`)),
1493
2024
  kv('生效 key', `${levelText(prompt.level)}${prompt.keyMasked ? ` · ${prompt.keyMasked}` : ''}`),
1494
2025
  invalid !== undefined
1495
2026
  ? h('div', { className: 'wsk-hint', style: { marginTop: 8, color: 'var(--wsk-bad)' } }, invalid)
@@ -1520,13 +2051,21 @@ window.__ModuleLoader__.load({
1520
2051
  }
1521
2052
 
1522
2053
  function res2Upper(prompt) {
1523
- return prompt.level === 'session' ? '清除本会话配置' : '改用系统默认'
2054
+ return prompt.level === 'session' ? tr('清除本会话配置') : tr('改用系统默认')
1524
2055
  }
1525
2056
 
1526
2057
  // ─────────────────────────────── 插件入口 ───────────────────────────────
1527
2058
 
1528
2059
  function apply(ctx) {
1529
2060
  ensureStyles()
2061
+ refreshI18n(ctx)
2062
+ const locale = ctx.get('locale')
2063
+ if (locale !== undefined && locale !== null && typeof locale.subscribe === 'function') {
2064
+ ctx.effect(() => locale.subscribe(() => {
2065
+ refreshI18n(ctx)
2066
+ notifyI18n()
2067
+ }), 'workspace-api-key: locale subscription')
2068
+ }
1530
2069
  const slots = ctx.get('slots')
1531
2070
  if (slots === undefined || slots === null) return
1532
2071
  host.ctx = ctx
@@ -1549,7 +2088,7 @@ window.__ModuleLoader__.load({
1549
2088
  name: 'sidebar.panellist',
1550
2089
  id: PANEL_ID,
1551
2090
  order: 1,
1552
- label: () => 'API Key 分配',
2091
+ label: () => tr('API Key 分配'),
1553
2092
  }, KeyIcon)), 'workspace-api-key: sidebar row')
1554
2093
 
1555
2094
  ctx.effect(() => slots.inject('main', () =>
@@ -1559,12 +2098,20 @@ window.__ModuleLoader__.load({
1559
2098
  inject: () => ({ back, sessions: ctx.get('sessions') }),
1560
2099
  }, WorkspaceKeyPage)), 'workspace-api-key: main page')
1561
2100
 
2101
+ ctx.effect(() => slots.inject('settings.section', () =>
2102
+ slots.register({
2103
+ name: 'settings.section',
2104
+ id: PANEL_ID,
2105
+ order: 60,
2106
+ label: () => tr('API Key 分配'),
2107
+ }, SettingsSection)), 'workspace-api-key: settings section')
2108
+
1562
2109
  ctx.effect(() => slots.inject('shell.overlay', () =>
1563
2110
  slots.register({
1564
2111
  name: 'shell.overlay',
1565
2112
  id: OVERLAY_ID,
1566
2113
  order: 60,
1567
- label: 'API Key 提醒',
2114
+ label: tr('API Key 提醒'),
1568
2115
  }, createOverlayWatcher(ctx))), 'workspace-api-key: session entry watcher')
1569
2116
  }
1570
2117