dsh-workspace-api-key 0.2.15 → 0.2.17
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 +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +403 -76
- package/lib/index.js +82 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ session key → workspace key → system default
|
|
|
26
26
|
- **A search switch per workspace** — every workspace row carries a `同步搜索 API` checkbox. Ticked, the `web_search` / `web_fetch` calls made by sessions in that directory use the key they would use for chat (`session → workspace → system default`); unticked, they keep the key the official 设置 → 插件 → 网页搜索 page manages. A session with its own key always wins, so "sync on, session configured separately" spends the session key, not the search key.
|
|
27
27
|
- **Search warnings that arrive before the failure** — when a search key is configured but `启用新版 Web 搜索 API` is off, or when the new API is on but its key ref is empty, the panel shows a warning banner and the entering-session overlay says it too, in one line you can act on. The overlay stays quiet about search problems when the search provider is not the one in use.
|
|
28
28
|
- **A usage guide** — `使用说明` at the top of the panel recommends two keys: a chat default (`default`) and a search default (`search-default`), so the two budgets stay separate; it also draws the cascade and lists the situations that trigger a warning.
|
|
29
|
+
- **A way back from Settings** — Settings → API Key 分配 keeps an "open the panel" button and explains the overflow-menu and reload routes, so an unpinned sidebar icon is never a dead end.
|
|
30
|
+
- **Language-aware UI** — the panel follows the web GUI language from `@deepseek-ai/dsh-client-locale`: English when the GUI is English, Chinese when it is Chinese. Answers produced by the host half (connection-test details, search warnings) are translated in the browser half, so the panel does not mix languages.
|
|
29
31
|
|
|
30
32
|
## How it works
|
|
31
33
|
|
|
@@ -79,7 +81,7 @@ The command forwards its arguments to pnpm inside the profile directory, so the
|
|
|
79
81
|
pnpm add dsh-workspace-api-key # or: npm install dsh-workspace-api-key
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
Pin a version with `dsh-workspace-api-key@0.2.
|
|
84
|
+
Pin a version with `dsh-workspace-api-key@0.2.17`, or install straight from the repository with `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key` (a git-hosted plugin is built from source on install).
|
|
83
85
|
|
|
84
86
|
Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `package.json` and restart DSH. For local development, use a link instead:
|
|
85
87
|
|
|
@@ -91,7 +93,7 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
|
|
|
91
93
|
|
|
92
94
|
```sh
|
|
93
95
|
node test/host.test.mjs # 177 assertions
|
|
94
|
-
node test/client.test.mjs #
|
|
96
|
+
node test/client.test.mjs # 180 assertions
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
Neither suite needs a running DSH process. The host suite drives `apply()` against a mock context (fake `credentials`, `workspaceRegistry`, `agents`, `sessionController`, `sessions`, `webServer`, stubbed `fetch`) and covers ref derivation, the session → workspace → default cascade, workspace matching, redirecting a second adapter's `apiKeyEnv`, clearing back to the default, 401/402/429/5xx classification, the loopback and origin checks, state persistence, the session probe including the `{items}` envelope, the live-session fallback, and subagent and blank sessions being hidden, and the search scope with a stand-in `web` service and a stand-in settings service: the legacy route keeping the chat cascade, the new provider keeping its own key, the workspace switch taking over, a session key outranking both, the panel's own search default key being used and cleared, the three search warnings, and the search connection test. The client suite loads `lib/client.js` through a minimal `window.__ModuleLoader__` plus a small React runtime, renders the panel and the overlay, and asserts slot registration, the first render, session expand/collapse, per-session saving and testing, sessions recovered from the browser-side store when the host has none (with `displayTitle` winning and subagent sessions hidden), the ungrouped-directory card, the stale and mismatch prompts, the "go configure" hand-off, graceful degradation without a session list, back-navigation when `ctx.get('layout')` only becomes available after the plugin was applied, and the search UI: the switch on every workspace row, the `/wsk-api/search-sync` call it makes, the warning banner, and the usage dialog.
|
package/README.zh.md
CHANGED
|
@@ -26,6 +26,8 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
26
26
|
- **每个工作区一个搜索开关** —— 每个工作区行上都有「同步搜索 API」复选框。勾选后,该目录下会话发出的 `web_search` / `web_fetch` 请求改用它们聊天时用的那把 key(会话 → 工作区 → 系统默认);不勾选则继续用官方「设置 → 插件 → 网页搜索」里配置的那把 key。会话自己配了 key 时永远优先,所以「工作区同步、会话单独设置」花的是会话那把 key,而不是搜索默认 key。
|
|
27
27
|
- **在失败之前就提醒的搜索告警** —— 配了搜索 key 但没打开「启用新版 Web 搜索 API」,或者新版 API 打开了但它的 key ref 是空的,面板会显示提醒条,点进会话时的覆盖层也会用一句可直接照做的话说明;当生效的搜索通道不是新版 API 时,覆盖层不会拿搜索问题打扰你。
|
|
28
28
|
- **使用说明** —— 面板顶部有「使用说明」,推荐两把 key 分工:聊天默认用 `default`,搜索默认用 `search-default`,两份预算互不干扰;里面还画了层级,并列出了会触发提醒的几种情况。
|
|
29
|
+
- **面板入口也能从设置里找回来** —— 「设置 → API Key 分配」里还有一个「打开 API Key 分配面板」按钮:侧栏图标被取消固定时,点它就能回到这里;那里也写明了「更多」菜单和刷新页面两条找回途径。
|
|
30
|
+
- **界面跟随 DSH 语言** —— 面板跟随 `@deepseek-ai/dsh-client-locale`:Web 界面是英文时显示英文,是中文时保持中文;宿主半边返回的提示(连接测试详情、搜索提醒)也在浏览器半边翻译,界面不会中英混排。
|
|
29
31
|
|
|
30
32
|
## 实现方式
|
|
31
33
|
|
|
@@ -79,7 +81,7 @@ dsh plugin --profile web add dsh-workspace-api-key
|
|
|
79
81
|
pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api-key
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
锁版本用 `dsh-workspace-api-key@0.2.
|
|
84
|
+
锁版本用 `dsh-workspace-api-key@0.2.17`;直接从仓库装则用 `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key`(git 来源的插件会在安装时现场构建)。
|
|
83
85
|
|
|
84
86
|
然后把 `"dsh-workspace-api-key"` 加进该 profile `package.json` 的 `dsh.profile.bundles`,重启 DSH。本地开发用 link 代替:
|
|
85
87
|
|
|
@@ -91,7 +93,7 @@ pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api
|
|
|
91
93
|
|
|
92
94
|
```sh
|
|
93
95
|
node test/host.test.mjs # 177 条断言
|
|
94
|
-
node test/client.test.mjs #
|
|
96
|
+
node test/client.test.mjs # 180 条断言
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
两个套件都不需要启动 DSH。宿主套件用一个假 ctx(假的 `credentials`、`workspaceRegistry`、`agents`、`sessionController`、`sessions`、`webServer`,以及打桩的 `fetch`)驱动 `apply()`,覆盖 ref 派生、会话→工作区→默认的级联、工作区匹配、把第二个适配器的 `apiKeyEnv` 一起重定向、清回默认、401/402/429/5xx 的判定、回环与来源校验、状态落盘,以及会话探针(含 `{items}` 信封与 live 兜底)。浏览器套件通过一个极小的 `window.__ModuleLoader__` 与自制 React 运行时加载 `lib/client.js`,渲染出面板与覆盖层,断言槽注册、首屏渲染、会话折叠/展开、会话级保存与测试、宿主列不出会话时从浏览器端存储恢复会话(标题取 `displayTitle`、隐藏子代理会话与空会话、浏览器端只认识一部分会话时不挤掉宿主行)、会话列表服务晚于插件提供时的重试、未分组目录卡片、失效与不匹配提醒、「去配置」的交接、没有会话列表时的降级,以及 `ctx.get('layout')` 在插件 apply 之后才可用时返回键仍然有效。宿主套件还带一个假的 `web` 服务与假的设置服务,验证搜索作用域:旧通道沿用聊天层级、新版通道用它自己的 key、工作区开关接管、会话 key 压过两者、面板自己的搜索默认 key 生效与清除、三类搜索提醒,以及搜索连接测试。浏览器套件覆盖搜索相关界面:每个工作区行的「同步搜索 API」复选框、它发出的 `/wsk-api/search-sync`、提醒条与「使用说明」弹窗。
|
package/lib/client.js
CHANGED
|
@@ -5,9 +5,276 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
6
6
|
|
|
7
7
|
const React = require('react')
|
|
8
|
-
const
|
|
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
|
+
}
|
|
172
|
+
|
|
173
|
+
const EN_HOST = {
|
|
174
|
+
"现在搜索走的是 DeepSeek Messages 通道,用的是聊天那把 key。": "Searches currently use the DeepSeek Messages channel and the chat key.",
|
|
175
|
+
"这些工作区勾了「同步搜索 API」,但既没有工作区专属 key,系统默认 key 也没配置:": "These workspaces have \"Sync search API\" on, but neither a workspace key nor a system default key is configured:",
|
|
176
|
+
"搜索会用空 key 发出去。给这些工作区各配一把,或先配好系统默认 key。": "searches would go out with an empty key. Give each of them a key, or configure the system default first.",
|
|
177
|
+
"当前搜索走旧的 Messages 通道,它本来就用聊天通道的 key(会话 → 工作区 → 系统默认),": "Searches use the legacy Messages channel, which already uses the chat key (session → workspace → system default),",
|
|
178
|
+
"所以这个开关只影响「启用新版 Web 搜索 API」之后的行为。": "so this switch only affects behavior after \"New Web Search API\" is enabled.",
|
|
179
|
+
"要让它生效,请到「设置 → 插件 → 网页搜索」打开那个开关;若暂时不想用新版 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.",
|
|
180
|
+
"点面板上的「同步到官方设置」,官方页的 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.",
|
|
181
|
+
"在本面板的「搜索默认 key」里保存一把后,插件会自动把官方配置指过去,两边就分开了。": "Save one under \"Search default key\" in this panel and the plugin points the official setting at it, separating the two.",
|
|
182
|
+
"连接正常,key 可用": "Connected; key works",
|
|
183
|
+
"key 无效(401/403)": "Invalid key (401/403)",
|
|
184
|
+
"被限流(429),但 key 本身可能有效": "Rate limited (429), but the key itself may be valid",
|
|
185
|
+
"余额/配额不足(402)": "Balance/quota exhausted (402)",
|
|
186
|
+
"请求参数被拒": "request rejected",
|
|
187
|
+
"credentials 服务不可用": "credentials service unavailable",
|
|
188
|
+
"credentials 服务不可用,无法写入 API key": "credentials service unavailable; cannot store the API key",
|
|
189
|
+
"面板请求超时(8 秒):DSH 的某个服务没有响应": "Panel request timed out (8s): a DSH service did not respond",
|
|
190
|
+
"缺少 sessionId": "missing sessionId",
|
|
191
|
+
"缺少 path": "missing path",
|
|
192
|
+
"无法为该会话生成凭据引用": "cannot derive a credential ref for this session",
|
|
193
|
+
"无法为该路径生成凭据引用": "cannot derive a credential ref for this path",
|
|
194
|
+
"无法规范化该路径": "cannot normalize this path",
|
|
195
|
+
"搜索 key 已配置,但「启用新版 Web 搜索 API」没打开": "Search key is configured, but \"New Web Search API\" is off",
|
|
196
|
+
"搜索默认 key 还没配置:搜索暂时沿用聊天那把 key": "No search default key yet: searches currently share the chat key",
|
|
197
|
+
"新版 Web 搜索 API 已启用,但搜索 key 没有配置": "New Web Search API is enabled, but no search key is configured",
|
|
198
|
+
"搜索默认 key 已配置,但「启用新版 Web 搜索 API」没打开": "Search default key is configured, but \"New Web Search API\" is off",
|
|
199
|
+
"搜索默认 key 已保存,但官方「网页搜索」页仍指向聊天那把": "Search default key is saved, but the official \"Web Search\" page still points at the chat key",
|
|
200
|
+
"「同步搜索 API」现在没有区别": "\"Sync search API\" currently makes no difference"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const EN_HOST_RULES = [
|
|
204
|
+
[/^官方页现在绑的是 (.+)(与聊天共用),所以那里看不到 (.+) 这把。$/, "The official page binds $1 (shared with chat), so it cannot show $2."],
|
|
205
|
+
[/^所以面板里的 (.+) 不会被使用。$/, "so $1 in the panel is not used."],
|
|
206
|
+
[/^现在搜索读的是 (.+)(和聊天共用一把),所以搜索与聊天的花费算在一起。$/, "Searches read $1 (shared with chat), so search and chat spending are counted together."],
|
|
207
|
+
[/^鉴权通过(HTTP 400:(.+))$/, "Authenticated (HTTP 400: $1)"],
|
|
208
|
+
[/^请求超时((\d+)ms):(.+)$/, "Request timed out ($1ms): $2"],
|
|
209
|
+
[/^(404:端点 (.+) 不存在,检查 baseURL)$/, "(404: endpoint $1 not found; check baseURL)"],
|
|
210
|
+
[/^(404:端点 (.+) 不存在,检查「Search \/ Fetch API 基础地址」)$/, "(404: endpoint $1 not found; check the Search / Fetch API base URL)"],
|
|
211
|
+
[/^搜索通道正常,返回 (\d+) 条结果((.+))$/, "Search channel OK, $1 result(s) ($2)"],
|
|
212
|
+
[/^搜索通道正常((.+))$/, "Search channel OK ($1)"]
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
const i18n = { english: false, listeners: new Set() }
|
|
216
|
+
|
|
217
|
+
function refreshI18n(ctx) {
|
|
218
|
+
let english = false
|
|
219
|
+
try {
|
|
220
|
+
const locale = ctx?.get?.('locale')
|
|
221
|
+
const snapshot = locale?.getSnapshot?.() ?? locale?.getLocale?.()
|
|
222
|
+
const active = snapshot?.active
|
|
223
|
+
english = typeof active === 'string' && !active.toLowerCase().startsWith('zh')
|
|
224
|
+
} catch {
|
|
225
|
+
english = false
|
|
226
|
+
}
|
|
227
|
+
i18n.english = english
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function notifyI18n() {
|
|
231
|
+
for (const listener of [...i18n.listeners]) {
|
|
232
|
+
try { listener() } catch { /* a broken listener must not break the switch */ }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function tr(value) {
|
|
237
|
+
if (typeof value !== 'string' || i18n.english !== true) return value
|
|
238
|
+
const hit = EN[value] ?? EN_HOST[value]
|
|
239
|
+
if (hit !== undefined) return hit
|
|
240
|
+
for (const [pattern, replacement] of EN_HOST_RULES) {
|
|
241
|
+
if (pattern.test(value)) return value.replace(pattern, replacement)
|
|
242
|
+
}
|
|
243
|
+
return value
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function tf(zh, en) {
|
|
247
|
+
return i18n.english === true ? en : zh
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function trChild(value) {
|
|
251
|
+
if (typeof value === 'string') return tr(value)
|
|
252
|
+
if (Array.isArray(value)) return value.map(trChild)
|
|
253
|
+
return value
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const TRANSLATED_PROPS = ['title', 'placeholder', 'aria-label', 'alt', 'label']
|
|
257
|
+
function h(type, props, ...children) {
|
|
258
|
+
let next = props
|
|
259
|
+
if (props !== null && typeof props === 'object') {
|
|
260
|
+
next = { ...props }
|
|
261
|
+
for (const key of TRANSLATED_PROPS) {
|
|
262
|
+
if (typeof next[key] === 'string') next[key] = tr(next[key])
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return rawCreateElement(type, next, ...children.map(trChild))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function useI18n() {
|
|
269
|
+
const [, setTick] = useState(0)
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (host.ctx !== null) refreshI18n(host.ctx)
|
|
272
|
+
const listener = () => setTick((tick) => tick + 1)
|
|
273
|
+
i18n.listeners.add(listener)
|
|
274
|
+
return () => { i18n.listeners.delete(listener) }
|
|
275
|
+
}, [])
|
|
276
|
+
}
|
|
277
|
+
|
|
11
278
|
const API_BASE = '/wsk-api'
|
|
12
279
|
const STYLE_ID = 'wsk-styles'
|
|
13
280
|
const PANEL_ID = 'workspace-api-key'
|
|
@@ -144,15 +411,15 @@ window.__ModuleLoader__.load({
|
|
|
144
411
|
try {
|
|
145
412
|
res = await window.fetch(`${API_BASE}${path}`, init)
|
|
146
413
|
} catch (err) {
|
|
147
|
-
throw new Error(`无法访问插件宿主接口(${String(err?.message ?? err)}
|
|
414
|
+
throw new Error(tf(`无法访问插件宿主接口(${String(err?.message ?? err)})`, `Could not reach the plugin host API (${String(err?.message ?? err)})`))
|
|
148
415
|
}
|
|
149
416
|
let body = null
|
|
150
417
|
try {
|
|
151
418
|
body = await res.json()
|
|
152
419
|
} catch {
|
|
153
|
-
throw new Error(`宿主接口返回了非 JSON 响应(HTTP ${res.status}
|
|
420
|
+
throw new Error(tf(`宿主接口返回了非 JSON 响应(HTTP ${res.status})`, `The host API returned a non-JSON response (HTTP ${res.status})`))
|
|
154
421
|
}
|
|
155
|
-
if (body === null || body === undefined) throw new Error(`宿主接口返回空响应(HTTP ${res.status}
|
|
422
|
+
if (body === null || body === undefined) throw new Error(tf(`宿主接口返回空响应(HTTP ${res.status})`, `The host API returned an empty response (HTTP ${res.status})`))
|
|
156
423
|
if (res.ok !== true || body.ok === false) throw new Error(body.error ?? `HTTP ${res.status}`)
|
|
157
424
|
return body
|
|
158
425
|
}
|
|
@@ -164,27 +431,27 @@ window.__ModuleLoader__.load({
|
|
|
164
431
|
|
|
165
432
|
function verdictText(verdict) {
|
|
166
433
|
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 '测试失败'
|
|
434
|
+
case 'ok': return tr('连接正常')
|
|
435
|
+
case 'invalid': return tr('key 无效')
|
|
436
|
+
case 'quota': return tr('余额不足')
|
|
437
|
+
case 'rate-limit': return tr('被限流')
|
|
438
|
+
case 'unreachable': return tr('无法连接')
|
|
439
|
+
default: return tr('测试失败')
|
|
173
440
|
}
|
|
174
441
|
}
|
|
175
442
|
|
|
176
443
|
function invalidReason(flag) {
|
|
177
444
|
if (flag === null || flag === undefined) return undefined
|
|
178
445
|
if (flag.status !== 'invalid') return undefined
|
|
179
|
-
return flag.message ?? '鉴权失败'
|
|
446
|
+
return tr(flag.message) ?? tr('鉴权失败')
|
|
180
447
|
}
|
|
181
448
|
|
|
182
449
|
// 生效层级:会话级 > 工作区级 > 系统默认
|
|
183
450
|
function levelText(level) {
|
|
184
451
|
switch (level) {
|
|
185
|
-
case 'session': return '本会话专属'
|
|
186
|
-
case 'workspace': return '工作区专属'
|
|
187
|
-
default: return '系统默认'
|
|
452
|
+
case 'session': return tr('本会话专属')
|
|
453
|
+
case 'workspace': return tr('工作区专属')
|
|
454
|
+
default: return tr('系统默认')
|
|
188
455
|
}
|
|
189
456
|
}
|
|
190
457
|
|
|
@@ -194,9 +461,9 @@ window.__ModuleLoader__.load({
|
|
|
194
461
|
|
|
195
462
|
function mismatchText(mismatch) {
|
|
196
463
|
if (mismatch === null || mismatch === undefined) return undefined
|
|
197
|
-
const key = mismatch.keyProvider ?? '未记录服务商'
|
|
198
|
-
const session = mismatch.sessionProvider ?? '未知'
|
|
199
|
-
return `这把 key 是为「${key}」准备的,当前会话用的是「${session}
|
|
464
|
+
const key = mismatch.keyProvider ?? tr('未记录服务商')
|
|
465
|
+
const session = mismatch.sessionProvider ?? tr('未知')
|
|
466
|
+
return tf(`这把 key 是为「${key}」准备的,当前会话用的是「${session}」`, `This key is for "${key}", but the current session uses "${session}"`)
|
|
200
467
|
}
|
|
201
468
|
|
|
202
469
|
/**
|
|
@@ -207,19 +474,19 @@ window.__ModuleLoader__.load({
|
|
|
207
474
|
if (search === null || search === undefined) return undefined
|
|
208
475
|
if (search.apiChannel === true) {
|
|
209
476
|
if (search.keyConfigured === false) {
|
|
210
|
-
return '你打开了「启用新版 Web 搜索 API」,但搜索 key 还没有配置:这个会话里的搜索会直接失败。'
|
|
477
|
+
return tr('你打开了「启用新版 Web 搜索 API」,但搜索 key 还没有配置:这个会话里的搜索会直接失败。')
|
|
211
478
|
}
|
|
212
479
|
return undefined
|
|
213
480
|
}
|
|
214
481
|
if (search.leftoverKey === true) {
|
|
215
|
-
return '你配置了搜索专用的 API Key,但「启用新版 Web 搜索 API」没有打开,所以那把 key 现在不会被使用。'
|
|
482
|
+
return tr('你配置了搜索专用的 API Key,但「启用新版 Web 搜索 API」没有打开,所以那把 key 现在不会被使用。')
|
|
216
483
|
}
|
|
217
484
|
return undefined
|
|
218
485
|
}
|
|
219
486
|
|
|
220
487
|
function sessionLabel(session) {
|
|
221
488
|
const title = typeof session.title === 'string' && session.title !== '' ? session.title : undefined
|
|
222
|
-
return title ?? (session.blank === true ? '(空会话)' : String(session.sessionId).slice(0, 12))
|
|
489
|
+
return title ?? (session.blank === true ? tr('(空会话)') : String(session.sessionId).slice(0, 12))
|
|
223
490
|
}
|
|
224
491
|
|
|
225
492
|
// ─────────────────── 会话树:宿主行 + 浏览器端列表 ───────────────────
|
|
@@ -431,8 +698,8 @@ window.__ModuleLoader__.load({
|
|
|
431
698
|
}
|
|
432
699
|
|
|
433
700
|
function modelText(session) {
|
|
434
|
-
if (typeof session.provider !== 'string' || session.provider === '') return '模型未知'
|
|
435
|
-
return `${session.provider} / ${session.model ?? '默认模型'}`
|
|
701
|
+
if (typeof session.provider !== 'string' || session.provider === '') return tr('模型未知')
|
|
702
|
+
return tf(`${session.provider} / ${session.model ?? '默认模型'}`, `${session.provider} / ${session.model ?? 'default model'}`)
|
|
436
703
|
}
|
|
437
704
|
|
|
438
705
|
// ─────────────────────────────── 图标 ───────────────────────────────
|
|
@@ -505,7 +772,50 @@ window.__ModuleLoader__.load({
|
|
|
505
772
|
|
|
506
773
|
// ─────────────────────────────── 主页面 ───────────────────────────────
|
|
507
774
|
|
|
775
|
+
/**
|
|
776
|
+
* 「设置 → API Key 分配」这一栏:侧栏图标没了也能从这里回到面板。
|
|
777
|
+
*
|
|
778
|
+
* 侧栏的面板图标是 DSH 的「面板固定」状态(`@deepseek-ai/dsh-client-ui-sidebar`
|
|
779
|
+
* 的 panel-pins)。取消固定只把图标挪进侧栏底部的「更多」菜单,面板本身没被卸载;
|
|
780
|
+
* 那套固定状态只保存在内存里,刷新页面就恢复默认,而且没有对外 API,
|
|
781
|
+
* 插件没法替用户把图标钉回去 —— 所以这里提供一个不会消失的入口。
|
|
782
|
+
*/
|
|
783
|
+
function SettingsSection() {
|
|
784
|
+
useI18n()
|
|
785
|
+
const [opened, setOpened] = useState(false)
|
|
786
|
+
const openPanel = () => {
|
|
787
|
+
try {
|
|
788
|
+
const layout = host.ctx === null ? undefined : host.ctx.get('layout')
|
|
789
|
+
if (layout === undefined || layout === null) return
|
|
790
|
+
if (typeof layout.selectPanel !== 'function') return
|
|
791
|
+
layout.selectPanel(PANEL_ID)
|
|
792
|
+
setOpened(true)
|
|
793
|
+
} catch {
|
|
794
|
+
/* layout 不可用时保持说明文字,用户还能走「更多」菜单 */
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return h('div', { className: 'wsk-body' },
|
|
798
|
+
h('div', { className: 'wsk-card' },
|
|
799
|
+
h('h4', null, '面板入口'),
|
|
800
|
+
h('div', { className: 'wsk-hint' },
|
|
801
|
+
'左侧栏的「API Key 分配」图标可以像其他面板一样取消固定。取消之后它还在侧栏底部的「更多」菜单里,并不是被卸载。'),
|
|
802
|
+
h('div', { className: 'wsk-hint' }, '如果找不到它,可以:'),
|
|
803
|
+
h('ul', { className: 'wsk-hint', style: { margin: '6px 0 10px 18px' } },
|
|
804
|
+
h('li', null, '点下面的按钮,直接打开面板;'),
|
|
805
|
+
h('li', null, '或者点侧栏底部的「更多」菜单,未固定的面板都列在那里;'),
|
|
806
|
+
h('li', null, '再或者按 Ctrl+R 刷新页面 —— 面板固定状态只存在内存里,刷新就会恢复默认。')),
|
|
807
|
+
h('button', { type: 'button', className: 'wsk-btn primary', onClick: openPanel },
|
|
808
|
+
'打开 API Key 分配面板'),
|
|
809
|
+
opened
|
|
810
|
+
? h('div', { className: 'wsk-hint', style: { marginTop: 8 } },
|
|
811
|
+
'已切到该面板:如果它还是没出现,说明面板正在加载,稍等一下。')
|
|
812
|
+
: null,
|
|
813
|
+
h('div', { className: 'wsk-hint', style: { marginTop: 10 } },
|
|
814
|
+
'这里同时管着每个工作区 / 会话的 key 与搜索通道;面板右上角的「使用说明」有完整的优先级说明。')))
|
|
815
|
+
}
|
|
816
|
+
|
|
508
817
|
function WorkspaceKeyPage(props) {
|
|
818
|
+
useI18n()
|
|
509
819
|
const [state, setState] = useState(null)
|
|
510
820
|
const [error, setError] = useState(null)
|
|
511
821
|
const [busy, setBusy] = useState(false)
|
|
@@ -646,12 +956,12 @@ window.__ModuleLoader__.load({
|
|
|
646
956
|
setNotice({
|
|
647
957
|
kind: 'ok',
|
|
648
958
|
text: trimmed === ''
|
|
649
|
-
? `${target.title}:已改用${scope === 'session' ? '上层' : '系统默认'}
|
|
959
|
+
? tf(`${target.title}:已改用${scope === 'session' ? '上层' : '系统默认'}设置`, `${target.title}: switched to the ${scope === 'session' ? 'level above' : 'system default'}`)
|
|
650
960
|
: target.kind === 'session'
|
|
651
|
-
? `${target.title}:已保存本会话专属 key`
|
|
961
|
+
? tf(`${target.title}:已保存本会话专属 key`, `${target.title}: saved a key for this session`)
|
|
652
962
|
: wasCleared
|
|
653
|
-
? `${target.title}
|
|
654
|
-
: `${target.title}:已保存专属 key`,
|
|
963
|
+
? tf(`${target.title}:已改用系统默认设置`, `${target.title}: switched back to the system default`)
|
|
964
|
+
: tf(`${target.title}:已保存专属 key`, `${target.title}: saved a dedicated key`),
|
|
655
965
|
})
|
|
656
966
|
}
|
|
657
967
|
return true
|
|
@@ -682,14 +992,14 @@ window.__ModuleLoader__.load({
|
|
|
682
992
|
} else if (bound?.ok === false) {
|
|
683
993
|
setNotice({
|
|
684
994
|
kind: 'warn',
|
|
685
|
-
text: `key 已保存在 ${next?.search?.defaultRef ?? '搜索默认 ref'},但没能改官方设置(${bound.reason}
|
|
686
|
-
+ '官方「网页搜索」页仍指向原来的 ref,两处暂时不是同一把。',
|
|
995
|
+
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}):`)
|
|
996
|
+
+ tf('官方「网页搜索」页仍指向原来的 ref,两处暂时不是同一把。', 'The official "Web Search" page still points at the previous ref, so the two are temporarily different keys.'),
|
|
687
997
|
})
|
|
688
998
|
} else {
|
|
689
999
|
setNotice({
|
|
690
1000
|
kind: 'ok',
|
|
691
|
-
text: `搜索默认 key 已保存到 ${next?.search?.defaultRef ?? '搜索默认 ref'}
|
|
692
|
-
+ '官方「网页搜索」页的 API Key 输入框也指向这个 ref:以后两边改的是同一把 key。',
|
|
1001
|
+
text: tf(`搜索默认 key 已保存到 ${next?.search?.defaultRef ?? '搜索默认 ref'},`, `Search default key saved to ${next?.search?.defaultRef ?? 'the search default ref'}, `)
|
|
1002
|
+
+ 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
1003
|
})
|
|
694
1004
|
}
|
|
695
1005
|
} catch (err) {
|
|
@@ -707,8 +1017,8 @@ window.__ModuleLoader__.load({
|
|
|
707
1017
|
applyState(next)
|
|
708
1018
|
const bound = next?.bound
|
|
709
1019
|
setNotice(bound?.ok === false
|
|
710
|
-
? { kind: 'warn', text: `没能改官方设置:${bound.reason}` }
|
|
711
|
-
: { kind: 'ok', text: `已同步:官方「网页搜索」页现在绑定 ${next?.search?.officialRef ?? next?.search?.defaultRef ?? '同一个 ref'},与面板改的是同一把 key
|
|
1020
|
+
? { kind: 'warn', text: tf(`没能改官方设置:${bound.reason}`, `Could not change the official setting: ${bound.reason}`) }
|
|
1021
|
+
: { 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
1022
|
} catch (err) {
|
|
713
1023
|
if (alive.current) setNotice({ kind: 'bad', text: `${err?.message ?? err}` })
|
|
714
1024
|
} finally {
|
|
@@ -766,7 +1076,7 @@ window.__ModuleLoader__.load({
|
|
|
766
1076
|
applyState(await api('/clear-flag', { method: 'POST', body: JSON.stringify(body) }))
|
|
767
1077
|
if (alive.current) {
|
|
768
1078
|
setTests((prev) => ({ ...prev, [id]: undefined }))
|
|
769
|
-
setNotice({ kind: 'info', text: `${title}
|
|
1079
|
+
setNotice({ kind: 'info', text: tf(`${title}:已清除失效标记`, `${title}: invalid flag cleared`) })
|
|
770
1080
|
}
|
|
771
1081
|
} catch (err) {
|
|
772
1082
|
if (alive.current) setNotice({ kind: 'bad', text: String(err?.message ?? err) })
|
|
@@ -803,31 +1113,31 @@ window.__ModuleLoader__.load({
|
|
|
803
1113
|
if (state === null) return undefined
|
|
804
1114
|
const hostVersion = typeof state.pluginVersion === 'string'
|
|
805
1115
|
? state.pluginVersion
|
|
806
|
-
: '旧版(未重启应用)'
|
|
1116
|
+
: tr('旧版(未重启应用)')
|
|
807
1117
|
const sessions = probe === undefined
|
|
808
|
-
? '未知'
|
|
1118
|
+
? tr('未知')
|
|
809
1119
|
: 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}
|
|
1120
|
+
? tr('不可用')
|
|
1121
|
+
: tf(`${probe.count} 个${probe.shape !== undefined ? `(${probe.shape})` : ''}`, `${probe.count} session(s)${probe.shape !== undefined ? ` (${probe.shape})` : ''}`)
|
|
1122
|
+
+ (probe.source === 'sessions' ? tf('(live 兜底)', ' (live fallback)') : '')
|
|
1123
|
+
const listError = probe?.error !== undefined ? tf(` · list 报错:${probe.error}`, ` · list() error: ${tr(probe.error)}`) : ''
|
|
1124
|
+
const noCwd = probe?.withoutCwd > 0 ? tf(` · ${probe.withoutCwd} 个没有 cwd`, ` · ${probe.withoutCwd} without cwd`) : ''
|
|
1125
|
+
const subs = probe?.subagents > 0 ? tf(` · 已跳过 ${probe.subagents} 个子代理会话`, ` · skipped ${probe.subagents} subagent session(s)`) : ''
|
|
1126
|
+
const blanks = (view?.sessionBlanks ?? 0) > 0 ? tf(` · 已跳过 ${view.sessionBlanks} 个空会话`, ` · skipped ${view.sessionBlanks} empty session(s)`) : ''
|
|
817
1127
|
// 宿主只标注「这个目录里只有还没说过话的空会话」,列不列由本文件按侧栏语义决定
|
|
818
1128
|
// (空会话里只有当前打开的那一个算数)——所以这里说的是「宿主标了几个」。
|
|
819
1129
|
const blankDirs = Array.isArray(state.hiddenBlankDirs) && state.hiddenBlankDirs.length > 0
|
|
820
|
-
? ` · ${state.hiddenBlankDirs.length}
|
|
1130
|
+
? tf(` · ${state.hiddenBlankDirs.length} 个目录只有空会话(未分组里不列)`, ` · ${state.hiddenBlankDirs.length} directories contain only empty sessions (not listed under Ungrouped)`)
|
|
821
1131
|
: ''
|
|
822
1132
|
const searchDiag = searchInfo === undefined
|
|
823
1133
|
? ''
|
|
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}`
|
|
1134
|
+
: tf(` · 搜索通道:${searchInfo.apiChannel === true ? '新版 API' : 'Messages'}`, ` · search channel: ${searchInfo.apiChannel === true ? 'new API' : 'Messages'}`)
|
|
1135
|
+
+ tf(`(${searchInfo.keyRef ?? '—'}${searchInfo.keyMasked ? ` · ${searchInfo.keyMasked}` : ' · 未配置'})`, ` (${searchInfo.keyRef ?? '—'}${searchInfo.keyMasked ? ` · ${searchInfo.keyMasked}` : ' · not configured'})`)
|
|
1136
|
+
+ (searchInfo.syncCount > 0 ? tf(` · 已同步 ${searchInfo.syncCount} 个目录`, ` · synced ${searchInfo.syncCount} director${searchInfo.syncCount === 1 ? 'y' : 'ies'}`) : '')
|
|
1137
|
+
return tf(`插件 v${hostVersion} · 状态存储 v${state.storeVersion ?? '—'}`, `plugin v${hostVersion} · state store v${state.storeVersion ?? '—'}`)
|
|
1138
|
+
+ tf(` · 宿主会话列表:${sessions}${listError}${noCwd}${subs}`, ` · host sessions: ${sessions}${listError}${noCwd}${subs}`)
|
|
1139
|
+
+ tf(` · 浏览器会话列表:${clientCount} 个${blanks}${blankDirs}`, ` · browser sessions: ${clientCount}${blanks}${blankDirs}`)
|
|
1140
|
+
+ tf(` · 工作区 ${grouped.length} + 未分组 ${ungrouped.length} · 挂在目录下的会话 ${sessionTotal}`, ` · workspaces ${grouped.length} + ungrouped ${ungrouped.length} · sessions attached to directories ${sessionTotal}`)
|
|
831
1141
|
+ searchDiag
|
|
832
1142
|
}
|
|
833
1143
|
|
|
@@ -837,8 +1147,8 @@ window.__ModuleLoader__.load({
|
|
|
837
1147
|
const paths = Array.isArray(state.workspacePathSample) ? state.workspacePathSample : []
|
|
838
1148
|
if (cwds.length === 0 && paths.length === 0) return null
|
|
839
1149
|
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)
|
|
1150
|
+
cwds.length > 0 ? h('div', null, tf(`会话 cwd 样本:${cwds.join(' | ')}`, `Session cwd samples: ${cwds.join(' | ')}`)) : null,
|
|
1151
|
+
paths.length > 0 ? h('div', null, tf(`工作区路径样本:${paths.join(' | ')}`, `Workspace path samples: ${paths.join(' | ')}`)) : null)
|
|
842
1152
|
}
|
|
843
1153
|
|
|
844
1154
|
const providerOptions = (current, fallback) => {
|
|
@@ -854,14 +1164,14 @@ window.__ModuleLoader__.load({
|
|
|
854
1164
|
const isSession = target.kind === 'session'
|
|
855
1165
|
const current = isSession ? target.level : target.configured === true ? 'workspace' : 'default'
|
|
856
1166
|
const fallbackLabel = isSession
|
|
857
|
-
? `清除本会话配置(改回${target.workspaceConfigured === true ? '工作区' : '系统默认'}
|
|
1167
|
+
? tf(`清除本会话配置(改回${target.workspaceConfigured === true ? '工作区' : '系统默认'})`, `Clear this session's setting (back to ${target.workspaceConfigured === true ? 'workspace' : 'system default'})`)
|
|
858
1168
|
: '改用系统默认'
|
|
859
1169
|
return h('div', { className: 'wsk-editor' },
|
|
860
1170
|
h('div', { className: 'wsk-hint' },
|
|
861
1171
|
isSession
|
|
862
|
-
? `当前生效:${levelText(current)}${target.keyMasked ? `(${target.keyMasked})` : ''}
|
|
1172
|
+
? tf(`当前生效:${levelText(current)}${target.keyMasked ? `(${target.keyMasked})` : ''}。`, `Currently effective: ${levelText(current)}${target.keyMasked ? ` (${target.keyMasked})` : ''}.`)
|
|
863
1173
|
: target.configured === true
|
|
864
|
-
? `该工作区当前使用专属 key${target.keyMasked ? `(${target.keyMasked})` : ''}
|
|
1174
|
+
? 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
1175
|
: '为该工作区指定一把专属 key;留空并保存即继续使用系统默认。'),
|
|
866
1176
|
h('input', {
|
|
867
1177
|
className: 'wsk-input',
|
|
@@ -901,7 +1211,7 @@ window.__ModuleLoader__.load({
|
|
|
901
1211
|
}, '取消')),
|
|
902
1212
|
h('div', { className: 'wsk-hint' }, target.ref === undefined || target.ref === null
|
|
903
1213
|
? '凭据引用:保存时由宿主按这个会话/目录的 ID 生成,保存后这里会显示出来'
|
|
904
|
-
: `凭据引用:${target.ref}`))
|
|
1214
|
+
: tf(`凭据引用:${target.ref}`, `Credential ref: ${target.ref}`)))
|
|
905
1215
|
}
|
|
906
1216
|
|
|
907
1217
|
const renderSession = (ws, session) => {
|
|
@@ -932,8 +1242,8 @@ window.__ModuleLoader__.load({
|
|
|
932
1242
|
: null),
|
|
933
1243
|
h('div', { className: 'wsk-path' },
|
|
934
1244
|
`${modelText(session)}` +
|
|
935
|
-
(session.effectiveKeyMasked ? ` · 生效 key ${session.effectiveKeyMasked}` : ' · 未配置 key') +
|
|
936
|
-
(hasOwn && session.keyMasked ? ` · 本会话 ${session.keyMasked}` : '')),
|
|
1245
|
+
(session.effectiveKeyMasked ? tf(` · 生效 key ${session.effectiveKeyMasked}`, ` · effective key ${session.effectiveKeyMasked}`) : ' · 未配置 key') +
|
|
1246
|
+
(hasOwn && session.keyMasked ? tf(` · 本会话 ${session.keyMasked}`, ` · this session ${session.keyMasked}`) : '')),
|
|
937
1247
|
reason !== undefined
|
|
938
1248
|
? h('div', { className: 'wsk-hint', style: { color: 'var(--wsk-bad)', marginTop: 3 } }, reason)
|
|
939
1249
|
: null,
|
|
@@ -992,7 +1302,7 @@ window.__ModuleLoader__.load({
|
|
|
992
1302
|
key: 'tgl', type: 'button', className: 'wsk-toggle',
|
|
993
1303
|
title: open ? '折叠会话' : '展开会话',
|
|
994
1304
|
onClick: () => toggle(ws.path),
|
|
995
|
-
}, caretSvg(12, open), h('span', null, `会话 ${sessions.length}`))
|
|
1305
|
+
}, caretSvg(12, open), h('span', null, tf(`会话 ${sessions.length}`, `Sessions ${sessions.length}`)))
|
|
996
1306
|
: null,
|
|
997
1307
|
h('span', { key: 'n' }, title),
|
|
998
1308
|
reason !== undefined
|
|
@@ -1034,9 +1344,9 @@ window.__ModuleLoader__.load({
|
|
|
1034
1344
|
: '搜索用搜索插件里的 key')),
|
|
1035
1345
|
ws.kind !== 'orphan' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
|
|
1036
1346
|
? h('div', { key: 'sk', className: 'wsk-hint', style: { marginTop: 2 } },
|
|
1037
|
-
`搜索生效 key:${ws.searchKeyMasked || '未配置'}`,
|
|
1347
|
+
tf(`搜索生效 key:${ws.searchKeyMasked || '未配置'}`, `Effective search key: ${ws.searchKeyMasked || 'not configured'}`),
|
|
1038
1348
|
ws.searchSync === true
|
|
1039
|
-
? `(${ws.configured === true ? '本目录专属' : '系统默认'}
|
|
1349
|
+
? tf(`(${ws.configured === true ? '本目录专属' : '系统默认'})`, ` (${ws.configured === true ? 'this directory' : 'system default'})`)
|
|
1040
1350
|
: '(搜索插件)')
|
|
1041
1351
|
: null,
|
|
1042
1352
|
reason !== undefined
|
|
@@ -1136,10 +1446,10 @@ window.__ModuleLoader__.load({
|
|
|
1136
1446
|
const ws = prompt.ws
|
|
1137
1447
|
const reason = invalidReason(ws.invalid)
|
|
1138
1448
|
return h(Dialog, {
|
|
1139
|
-
title: `${ws.title} 的 API Key 已失效`,
|
|
1449
|
+
title: tf(`${ws.title} 的 API Key 已失效`, `${ws.title}: API key invalid`),
|
|
1140
1450
|
body: h('div', null,
|
|
1141
1451
|
h('div', null, reason),
|
|
1142
|
-
h('div', { className: 'wsk-hint', style: { marginTop: 6 } }, `工作区:${prettyPath(ws.path)}`),
|
|
1452
|
+
h('div', { className: 'wsk-hint', style: { marginTop: 6 } }, tf(`工作区:${prettyPath(ws.path)}`, `Workspace: ${prettyPath(ws.path)}`)),
|
|
1143
1453
|
h('div', { className: 'wsk-hint' }, '可以重新配置这把 key,或让该工作区改用系统默认设置。')),
|
|
1144
1454
|
onDismiss: () => setPrompt(null),
|
|
1145
1455
|
actions: [
|
|
@@ -1198,7 +1508,7 @@ window.__ModuleLoader__.load({
|
|
|
1198
1508
|
: null,
|
|
1199
1509
|
error !== null
|
|
1200
1510
|
? h('div', { className: 'wsk-msg bad', style: { marginBottom: 12 } },
|
|
1201
|
-
`读取失败:${error}`,
|
|
1511
|
+
tf(`读取失败:${error}`, `Failed to load: ${tr(error)}`),
|
|
1202
1512
|
h('div', { className: 'wsk-hint' }, '请确认 DSH Web 服务正在运行、且本插件的宿主半边已加载。'))
|
|
1203
1513
|
: null,
|
|
1204
1514
|
hostIsStale
|
|
@@ -1247,14 +1557,14 @@ window.__ModuleLoader__.load({
|
|
|
1247
1557
|
h('span', { key: 'r' }, defaultRef?.ref ?? DEFAULT_REF),
|
|
1248
1558
|
defaultRef?.configured === true
|
|
1249
1559
|
? h('span', { key: 's', className: 'wsk-badge ok' },
|
|
1250
|
-
`已配置${defaultRef.source ? ` · ${defaultRef.source}` : ''}`)
|
|
1560
|
+
tf(`已配置${defaultRef.source ? ` · ${defaultRef.source}` : ''}`, `Configured${defaultRef.source ? ` · ${defaultRef.source}` : ''}`))
|
|
1251
1561
|
: h('span', { key: 's', className: 'wsk-badge bad' }, '未配置'),
|
|
1252
1562
|
defaultRef?.writable === false
|
|
1253
1563
|
? h('span', { key: 'w', className: 'wsk-badge warn' }, '被环境变量遮蔽(只读)')
|
|
1254
1564
|
: null),
|
|
1255
1565
|
h('div', { className: 'wsk-hint' },
|
|
1256
1566
|
'没有单独配置的工作区/会话,一律使用这把 key。',
|
|
1257
|
-
defaultRef?.keyMasked ? ` 当前值:${defaultRef.keyMasked}` : '')),
|
|
1567
|
+
defaultRef?.keyMasked ? tf(` 当前值:${defaultRef.keyMasked}`, ` current value: ${defaultRef.keyMasked}`) : '')),
|
|
1258
1568
|
h('div', { className: 'wsk-actions' },
|
|
1259
1569
|
h('button', {
|
|
1260
1570
|
type: 'button', className: 'wsk-btn',
|
|
@@ -1277,11 +1587,11 @@ window.__ModuleLoader__.load({
|
|
|
1277
1587
|
+ '工作区勾了「同步搜索 API」时改用该工作区的聊天层级;会话单独配置永远最优先。'),
|
|
1278
1588
|
h('div', { className: 'wsk-hint' },
|
|
1279
1589
|
searchInfo.defaultConfigured === true && searchInfo.defaultKeyMasked
|
|
1280
|
-
? `当前值:${searchInfo.defaultKeyMasked}
|
|
1590
|
+
? tf(`当前值:${searchInfo.defaultKeyMasked}。`, `Current value: ${searchInfo.defaultKeyMasked}.`)
|
|
1281
1591
|
: '',
|
|
1282
1592
|
searchInfo.officialShared === true
|
|
1283
1593
|
? '官方「网页搜索」页现在仍指着聊天那把 key(DEEPSEEK_API_KEY)——保存一次搜索 key 后,插件会把它接过来。'
|
|
1284
|
-
: `官方「网页搜索」页已由本插件接管:那里填的也是 ${searchInfo.officialRef ?? 'DEEPSEEK_API_KEY_SEARCH'} 这一把,不会再误改聊天 key
|
|
1594
|
+
: 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
1595
|
searchInfo.officialShared === true && searchInfo.ownedConfigured === true
|
|
1286
1596
|
? h('div', { className: 'wsk-hint' },
|
|
1287
1597
|
'你之前保存过搜索默认 key,但官方页还指向聊天那把。',
|
|
@@ -1331,7 +1641,7 @@ window.__ModuleLoader__.load({
|
|
|
1331
1641
|
: null,
|
|
1332
1642
|
|
|
1333
1643
|
h('div', { className: 'wsk-card', style: { background: 'transparent', border: 0, padding: 0 } },
|
|
1334
|
-
h('h4', { style: { marginBottom: 6 } }, `工作区(${grouped.length}
|
|
1644
|
+
h('h4', { style: { marginBottom: 6 } }, tf(`工作区(${grouped.length})`, `Workspaces (${grouped.length})`)),
|
|
1335
1645
|
h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
|
|
1336
1646
|
'展开工作区可以看到它下面的会话,并单独给某个会话指定 key(会话 > 工作区 > 系统默认)。'),
|
|
1337
1647
|
grouped.length === 0
|
|
@@ -1340,7 +1650,7 @@ window.__ModuleLoader__.load({
|
|
|
1340
1650
|
|
|
1341
1651
|
ungrouped.length > 0
|
|
1342
1652
|
? h('div', { className: 'wsk-card' },
|
|
1343
|
-
h('h4', null, `未分组目录(${ungrouped.length}
|
|
1653
|
+
h('h4', null, tf(`未分组目录(${ungrouped.length})`, `Ungrouped directories (${ungrouped.length})`)),
|
|
1344
1654
|
h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
|
|
1345
1655
|
'这些目录里的会话没有挂在工作区下(侧栏的「未分组」),同样可以单独配置。'
|
|
1346
1656
|
+ '只有侧栏真的会显示的目录才列在这里(刚建好、还没发过消息的空会话不算)。'),
|
|
@@ -1349,7 +1659,7 @@ window.__ModuleLoader__.load({
|
|
|
1349
1659
|
|
|
1350
1660
|
orphans.length > 0
|
|
1351
1661
|
? h('div', { className: 'wsk-card' },
|
|
1352
|
-
h('h4', null, `已失效的配置(${orphans.length}
|
|
1662
|
+
h('h4', null, tf(`已失效的配置(${orphans.length})`, `Stale settings (${orphans.length})`)),
|
|
1353
1663
|
h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
|
|
1354
1664
|
'这些目录已经不是注册的工作区了,对应的 key 仍留在凭据库里。可改用系统默认以清理。'),
|
|
1355
1665
|
h('div', { className: 'wsk-list' }, orphans.map((ws) => renderWorkspace(ws))))
|
|
@@ -1357,7 +1667,7 @@ window.__ModuleLoader__.load({
|
|
|
1357
1667
|
|
|
1358
1668
|
h('div', { className: 'wsk-hint' },
|
|
1359
1669
|
'专属 key 存进 DSH 凭据库中的独立引用,因此换 key 不写进会话日志、也不影响历史回放。',
|
|
1360
|
-
state?.statePath ? h('div', null, `状态文件:${state.statePath}`) : null,
|
|
1670
|
+
state?.statePath ? h('div', null, tf(`状态文件:${state.statePath}`, `State file: ${state.statePath}`)) : null,
|
|
1361
1671
|
diagText() !== undefined ? h('div', { className: 'wsk-diag' }, diagText()) : null)),
|
|
1362
1672
|
|
|
1363
1673
|
renderPrompt(),
|
|
@@ -1368,6 +1678,7 @@ window.__ModuleLoader__.load({
|
|
|
1368
1678
|
|
|
1369
1679
|
function createOverlayWatcher(ctx) {
|
|
1370
1680
|
return function ApiKeyWatch() {
|
|
1681
|
+
useI18n()
|
|
1371
1682
|
const [prompt, setPrompt] = useState(null)
|
|
1372
1683
|
const asked = useRef(new Set())
|
|
1373
1684
|
|
|
@@ -1489,7 +1800,7 @@ window.__ModuleLoader__.load({
|
|
|
1489
1800
|
: '网页搜索的配置需要确认',
|
|
1490
1801
|
body: h('div', null,
|
|
1491
1802
|
kv('会话', prompt.title ?? String(prompt.sessionId ?? '').slice(0, 12)),
|
|
1492
|
-
kv('模型', `${prompt.provider ?? '未知'} / ${prompt.model ?? '默认模型'}`),
|
|
1803
|
+
kv('模型', tf(`${prompt.provider ?? '未知'} / ${prompt.model ?? '默认模型'}`, `${prompt.provider ?? 'Unknown'} / ${prompt.model ?? 'default model'}`)),
|
|
1493
1804
|
kv('生效 key', `${levelText(prompt.level)}${prompt.keyMasked ? ` · ${prompt.keyMasked}` : ''}`),
|
|
1494
1805
|
invalid !== undefined
|
|
1495
1806
|
? h('div', { className: 'wsk-hint', style: { marginTop: 8, color: 'var(--wsk-bad)' } }, invalid)
|
|
@@ -1520,13 +1831,21 @@ window.__ModuleLoader__.load({
|
|
|
1520
1831
|
}
|
|
1521
1832
|
|
|
1522
1833
|
function res2Upper(prompt) {
|
|
1523
|
-
return prompt.level === 'session' ? '清除本会话配置' : '改用系统默认'
|
|
1834
|
+
return prompt.level === 'session' ? tr('清除本会话配置') : tr('改用系统默认')
|
|
1524
1835
|
}
|
|
1525
1836
|
|
|
1526
1837
|
// ─────────────────────────────── 插件入口 ───────────────────────────────
|
|
1527
1838
|
|
|
1528
1839
|
function apply(ctx) {
|
|
1529
1840
|
ensureStyles()
|
|
1841
|
+
refreshI18n(ctx)
|
|
1842
|
+
const locale = ctx.get('locale')
|
|
1843
|
+
if (locale !== undefined && locale !== null && typeof locale.subscribe === 'function') {
|
|
1844
|
+
ctx.effect(() => locale.subscribe(() => {
|
|
1845
|
+
refreshI18n(ctx)
|
|
1846
|
+
notifyI18n()
|
|
1847
|
+
}), 'workspace-api-key: locale subscription')
|
|
1848
|
+
}
|
|
1530
1849
|
const slots = ctx.get('slots')
|
|
1531
1850
|
if (slots === undefined || slots === null) return
|
|
1532
1851
|
host.ctx = ctx
|
|
@@ -1549,7 +1868,7 @@ window.__ModuleLoader__.load({
|
|
|
1549
1868
|
name: 'sidebar.panellist',
|
|
1550
1869
|
id: PANEL_ID,
|
|
1551
1870
|
order: 1,
|
|
1552
|
-
label: () => 'API Key 分配',
|
|
1871
|
+
label: () => tr('API Key 分配'),
|
|
1553
1872
|
}, KeyIcon)), 'workspace-api-key: sidebar row')
|
|
1554
1873
|
|
|
1555
1874
|
ctx.effect(() => slots.inject('main', () =>
|
|
@@ -1559,12 +1878,20 @@ window.__ModuleLoader__.load({
|
|
|
1559
1878
|
inject: () => ({ back, sessions: ctx.get('sessions') }),
|
|
1560
1879
|
}, WorkspaceKeyPage)), 'workspace-api-key: main page')
|
|
1561
1880
|
|
|
1881
|
+
ctx.effect(() => slots.inject('settings.section', () =>
|
|
1882
|
+
slots.register({
|
|
1883
|
+
name: 'settings.section',
|
|
1884
|
+
id: PANEL_ID,
|
|
1885
|
+
order: 60,
|
|
1886
|
+
label: () => tr('API Key 分配'),
|
|
1887
|
+
}, SettingsSection)), 'workspace-api-key: settings section')
|
|
1888
|
+
|
|
1562
1889
|
ctx.effect(() => slots.inject('shell.overlay', () =>
|
|
1563
1890
|
slots.register({
|
|
1564
1891
|
name: 'shell.overlay',
|
|
1565
1892
|
id: OVERLAY_ID,
|
|
1566
1893
|
order: 60,
|
|
1567
|
-
label: 'API Key 提醒',
|
|
1894
|
+
label: tr('API Key 提醒'),
|
|
1568
1895
|
}, createOverlayWatcher(ctx))), 'workspace-api-key: session entry watcher')
|
|
1569
1896
|
}
|
|
1570
1897
|
|
package/lib/index.js
CHANGED
|
@@ -83,18 +83,29 @@ const SEARCH_DEFAULT_REF = 'DEEPSEEK_API_KEY_SEARCH'
|
|
|
83
83
|
*
|
|
84
84
|
* 配置本身用缓存:`credentials.resolve` 是热路径,不能每次都去 `settings.describe()`。
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
let webCache
|
|
87
|
+
|
|
88
|
+
/** 重新读一次官方「网页搜索」配置(只在请求/面板刷新这类「非热路径」里调用)。 */
|
|
87
89
|
function refreshWebCache(ctx) {
|
|
88
90
|
try {
|
|
89
|
-
|
|
91
|
+
webCache = readWebSettings(ctx)
|
|
90
92
|
} catch {
|
|
91
|
-
|
|
93
|
+
webCache = webCache ?? webDefaults()
|
|
92
94
|
}
|
|
95
|
+
return webCache
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* 取官方「网页搜索」配置——**只读缓存,绝不在 resolve 热路径里重新解析**。
|
|
100
|
+
*
|
|
101
|
+
* `settings.describe()` 会 interpolate 各条目的配置,而配置里可能有 `credential-ref`,
|
|
102
|
+
* 那等于从 credentials 服务内部再调回 credentials。0.2.15 就是在 resolve 里调 describe:
|
|
103
|
+
* 搜索 → resolve(搜索 ref) → describe → describe 又去 resolve → 服务自己等自己,
|
|
104
|
+
* promise 永远不 settle。表现就是 `web_search` 卡满 60 秒,面板接口也一起挂住。
|
|
105
|
+
* 缓存由 apply()、每个 HTTP 请求入口、面板刷新时更新。
|
|
106
|
+
*/
|
|
96
107
|
function currentWeb(ctx) {
|
|
97
|
-
return
|
|
108
|
+
return webCache ?? webDefaults()
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
/**
|
|
@@ -232,11 +243,11 @@ function workspaceIndex(ctx) {
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/** cwd → 工作区描述;没有匹配工作区时回退成「以该目录为单位」。 */
|
|
235
|
-
function locateWorkspace(ctx, cwd) {
|
|
246
|
+
function locateWorkspace(ctx, cwd, index) {
|
|
236
247
|
if (cwd === undefined) return undefined
|
|
237
248
|
const norm = normPath(cwd)
|
|
238
249
|
if (norm === undefined) return undefined
|
|
239
|
-
const hit = workspaceIndex(ctx).get(norm)
|
|
250
|
+
const hit = (index ?? workspaceIndex(ctx)).get(norm)
|
|
240
251
|
if (hit !== undefined) return { ...hit, kind: 'workspace' }
|
|
241
252
|
return { id: undefined, path: cwd, title: basename(cwd) || cwd, kind: 'directory' }
|
|
242
253
|
}
|
|
@@ -245,7 +256,7 @@ function locateWorkspace(ctx, cwd) {
|
|
|
245
256
|
* 给定会话/目录,算出当前生效的覆盖层级(纯函数,不读凭据):
|
|
246
257
|
* session → workspace → default。
|
|
247
258
|
*/
|
|
248
|
-
function effectiveOverride(ctx, state, { sessionId, cwd }) {
|
|
259
|
+
function effectiveOverride(ctx, state, { sessionId, cwd }, index) {
|
|
249
260
|
if (sessionId !== undefined) {
|
|
250
261
|
const sref = sessionRef(sessionId)
|
|
251
262
|
if (sref !== undefined && state.overrides[sref] !== undefined) {
|
|
@@ -253,7 +264,9 @@ function effectiveOverride(ctx, state, { sessionId, cwd }) {
|
|
|
253
264
|
}
|
|
254
265
|
}
|
|
255
266
|
if (cwd !== undefined) {
|
|
256
|
-
|
|
267
|
+
// index 是可选的「工作区索引」快照:面板一次要算几十个会话,每行都去
|
|
268
|
+
// registry.list() 一遍会让整个事件循环卡住(0.2.16 实测 /state 要 50 秒)。
|
|
269
|
+
const ws = locateWorkspace(ctx, cwd, index)
|
|
257
270
|
const wref = ws !== undefined ? derivedRef(ws.path) : undefined
|
|
258
271
|
if (wref !== undefined && state.overrides[wref] !== undefined) {
|
|
259
272
|
return { level: 'workspace', ref: wref, entry: state.overrides[wref], workspace: ws }
|
|
@@ -379,7 +392,7 @@ async function reconcileOfficialSearchRef(ctx) {
|
|
|
379
392
|
* 旧 Messages 通道:搜索就在用聊天 key,固定返回搜索 ref。
|
|
380
393
|
*/
|
|
381
394
|
async function searchRefFor(ctx, state, { sessionId, cwd, web, searchDefault }) {
|
|
382
|
-
const w = web ??
|
|
395
|
+
const w = web ?? currentWeb(ctx)
|
|
383
396
|
if (!w.apiChannel) return w.searchKeyEnv
|
|
384
397
|
const own = sessionOverrideRef(state, sessionId)
|
|
385
398
|
if (own !== undefined) return own
|
|
@@ -792,8 +805,10 @@ function interceptableRefs(ctx, state) {
|
|
|
792
805
|
// 搜索/抓取通道的 ref 也要拦:用户可能把「新版 Web 搜索 API」的 key 指到
|
|
793
806
|
// 独立 ref(例如 DEEPSEEK_SEARCH_API_KEY)。不拦的话,勾了「同步搜索 API」
|
|
794
807
|
// 也会静默不生效——面板显示已同步、实际还在用系统默认 key。
|
|
808
|
+
// 注意这里走 currentWeb(缓存)而不是 readWebSettings:本函数在 credentials.resolve
|
|
809
|
+
// 的热路径上被调用,每次都 settings.describe() 会把聊天通道一并拖慢。
|
|
795
810
|
try {
|
|
796
|
-
const web =
|
|
811
|
+
const web = currentWeb(ctx)
|
|
797
812
|
for (const ref of [web.apiKeyEnv, web.legacyKeyEnv]) {
|
|
798
813
|
if (typeof ref === 'string' && ref !== '') refs.add(ref)
|
|
799
814
|
}
|
|
@@ -1294,6 +1309,16 @@ export function apply(ctx) {
|
|
|
1294
1309
|
|
|
1295
1310
|
// ── 1. 包装 credentials.resolve(按 ref 分辨搜索/抓取通道)──────────
|
|
1296
1311
|
refreshWebCache(ctx)
|
|
1312
|
+
// 官方页改了「网页搜索」配置就丢掉缓存:解析热路径只读缓存,不能自己去 describe。
|
|
1313
|
+
try {
|
|
1314
|
+
ctx.effect(() =>
|
|
1315
|
+
ctx.on('settings/document-updated', () => {
|
|
1316
|
+
webCache = undefined
|
|
1317
|
+
}),
|
|
1318
|
+
)
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
log('settings listener unavailable: %s', String(err?.message ?? err))
|
|
1321
|
+
}
|
|
1297
1322
|
installResolver(ctx, state)
|
|
1298
1323
|
// 「直接禁用官方页面里的搜索 key 设置」:把官方 apiKeyEnv 指到插件自有 ref(异步、失败不影响启动)。
|
|
1299
1324
|
void reconcileOfficialSearchRef(ctx)
|
|
@@ -1530,7 +1555,7 @@ async function readJson(req) {
|
|
|
1530
1555
|
}
|
|
1531
1556
|
|
|
1532
1557
|
/** 一个会话的展示 + 生效情况(供面板与会话进入提醒使用)。 */
|
|
1533
|
-
async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
1558
|
+
async function sessionRow(ctx, state, row, fallbackCwd, index) {
|
|
1534
1559
|
const sessionId = typeof row?.sessionId === 'string' && row.sessionId !== '' ? row.sessionId : undefined
|
|
1535
1560
|
if (sessionId === undefined) return undefined
|
|
1536
1561
|
const selection = modelSelectionOf(row)
|
|
@@ -1538,7 +1563,7 @@ async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
|
1538
1563
|
const ref = sessionRef(sessionId)
|
|
1539
1564
|
const entry = ref !== undefined ? state.overrides[ref] : undefined
|
|
1540
1565
|
const directHit = entry !== undefined ? await resolveKey(ctx, ref) : undefined
|
|
1541
|
-
const eff = effectiveOverride(ctx, state, { sessionId, cwd })
|
|
1566
|
+
const eff = effectiveOverride(ctx, state, { sessionId, cwd }, index)
|
|
1542
1567
|
const effHit = eff.ref !== undefined ? await resolveKey(ctx, eff.ref) : undefined
|
|
1543
1568
|
return {
|
|
1544
1569
|
sessionId,
|
|
@@ -1570,14 +1595,19 @@ async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
|
1570
1595
|
}
|
|
1571
1596
|
}
|
|
1572
1597
|
|
|
1573
|
-
async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, claimed) {
|
|
1598
|
+
async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, claimed, env) {
|
|
1599
|
+
// 每个工作区行都要的一份上下文(工作区索引、官方搜索配置、搜索默认 key 是否已配)。
|
|
1600
|
+
// 由 snapshot() 统一算一次传进来:panel 打开一次要渲染 20+ 行,每行各算一遍
|
|
1601
|
+
// settings.describe() + registry.list() 会把事件循环卡住(0.2.16 实测 /state ≈ 50 s)。
|
|
1602
|
+
const index = env?.index
|
|
1603
|
+
const web = env?.web ?? currentWeb(ctx)
|
|
1574
1604
|
const ref = derivedRef(dir)
|
|
1575
1605
|
const configured = ref !== undefined && state.overrides[ref] !== undefined
|
|
1576
1606
|
const desc = configured ? await describeKey(ctx, ref) : undefined
|
|
1577
1607
|
const hit = configured ? await resolveKey(ctx, ref) : undefined
|
|
1578
1608
|
const sessions = []
|
|
1579
1609
|
for (const row of rows) {
|
|
1580
|
-
const item = await sessionRow(ctx, state, row, dir)
|
|
1610
|
+
const item = await sessionRow(ctx, state, row, dir, index)
|
|
1581
1611
|
if (item !== undefined) sessions.push(item)
|
|
1582
1612
|
}
|
|
1583
1613
|
sessions.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))
|
|
@@ -1585,9 +1615,9 @@ async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, cl
|
|
|
1585
1615
|
// 搜索通道:只有新版 Search/Fetch API 才有「同步 / 不同步」的区别(旧 Messages
|
|
1586
1616
|
// 通道本来就用聊天 key)。这里给出「该工作区的搜索请求实际会用哪把 key」,
|
|
1587
1617
|
// 让用户勾了复选框之后马上能看到效果,而不用等到搜索失败才发现。
|
|
1588
|
-
const web = readWebSettings(ctx)
|
|
1589
1618
|
const synced = state.searchSync?.[normPath(dir)] === true
|
|
1590
|
-
const searchDefaultConfigured =
|
|
1619
|
+
const searchDefaultConfigured = env?.searchDefaultConfigured
|
|
1620
|
+
?? (web.apiChannel && (await describeKey(ctx, searchDefaultRefOf(web))).configured === true)
|
|
1591
1621
|
const searchKeyRef = web.apiChannel
|
|
1592
1622
|
? synced
|
|
1593
1623
|
? configured
|
|
@@ -1634,11 +1664,21 @@ function hasVisibleSession(rows) {
|
|
|
1634
1664
|
}
|
|
1635
1665
|
|
|
1636
1666
|
async function snapshot(ctx, state) {
|
|
1667
|
+
const startedAt = Date.now()
|
|
1668
|
+
// 官方「网页搜索」配置:每次面板刷新都重读一次(缓存只在请求入口更新,解析路径只读)。
|
|
1669
|
+
// 整个 snapshot 只读这一次:下面每行都用 currentWeb(ctx) 拿缓存,不再各自 describe。
|
|
1670
|
+
const web = refreshWebCache(ctx)
|
|
1671
|
+
// 工作区索引也一样:一次 snapshot 只调一次 registry.list(),行渲染复用同一份快照。
|
|
1672
|
+
const index = workspaceIndex(ctx)
|
|
1637
1673
|
const credentials = ctx.get('credentials')
|
|
1638
1674
|
const defaultRefName = 'DEEPSEEK_API_KEY'
|
|
1639
1675
|
const defaultInfo = await describeKey(ctx, defaultRefName)
|
|
1640
1676
|
const defaultHit = await resolveKey(ctx, defaultRefName)
|
|
1641
1677
|
const probe = await sessionProbe(ctx)
|
|
1678
|
+
const probeMs = Date.now() - startedAt
|
|
1679
|
+
const searchDefaultConfigured = web.apiChannel
|
|
1680
|
+
&& (await describeKey(ctx, searchDefaultRefOf(web))).configured === true
|
|
1681
|
+
const rowEnv = { index, web, searchDefaultConfigured }
|
|
1642
1682
|
const rows = probe.rows
|
|
1643
1683
|
const byCwd = new Map()
|
|
1644
1684
|
for (const row of rows) {
|
|
@@ -1662,7 +1702,7 @@ async function snapshot(ctx, state) {
|
|
|
1662
1702
|
}
|
|
1663
1703
|
if (typeof dir !== 'string' || dir === '') continue
|
|
1664
1704
|
const norm = normPath(dir)
|
|
1665
|
-
workspaces.push(await workspaceRow(ctx, state, { id, path: dir, title, kind: 'workspace' }, byCwd.get(norm) ?? [], claimed))
|
|
1705
|
+
workspaces.push(await workspaceRow(ctx, state, { id, path: dir, title, kind: 'workspace' }, byCwd.get(norm) ?? [], claimed, rowEnv))
|
|
1666
1706
|
}
|
|
1667
1707
|
|
|
1668
1708
|
// 有覆盖记录、但对应工作区已不在注册表里的(被删掉的工作区)也列出来,方便清理。
|
|
@@ -1702,7 +1742,7 @@ async function snapshot(ctx, state) {
|
|
|
1702
1742
|
const dir = typeof list[0]?.cwd === 'string' ? list[0].cwd : norm
|
|
1703
1743
|
// 诊断用:这个目录里一个「真实会话」都没有(界面把它藏起来,但用户能看到原因)。
|
|
1704
1744
|
if (!hasVisibleSession(list)) hiddenBlankDirs.push(dir)
|
|
1705
|
-
ungrouped.push(await workspaceRow(ctx, state, { id: undefined, path: dir, title: basename(dir) || dir, kind: 'directory' }, list))
|
|
1745
|
+
ungrouped.push(await workspaceRow(ctx, state, { id: undefined, path: dir, title: basename(dir) || dir, kind: 'directory' }, list, undefined, rowEnv))
|
|
1706
1746
|
}
|
|
1707
1747
|
|
|
1708
1748
|
// 会话级覆盖总览(含已不在列表里的会话,便于清理)。
|
|
@@ -1728,8 +1768,7 @@ async function snapshot(ctx, state) {
|
|
|
1728
1768
|
}
|
|
1729
1769
|
|
|
1730
1770
|
// ── 搜索 / 抓取通道总览 ──────────────────────────────────────────────
|
|
1731
|
-
//
|
|
1732
|
-
const web = refreshWebCache(ctx)
|
|
1771
|
+
// 上面的 refreshWebCache() 已经把官方配置读到 web 里了,这里直接复用。
|
|
1733
1772
|
const searchKeyRef = web.searchKeyEnv
|
|
1734
1773
|
const searchKeyInfo = await describeKey(ctx, searchKeyRef)
|
|
1735
1774
|
const searchKeyHit = await resolveKey(ctx, searchKeyRef)
|
|
@@ -1740,10 +1779,13 @@ async function snapshot(ctx, state) {
|
|
|
1740
1779
|
const syncPaths = Object.keys(state.searchSync ?? {}).filter((key) => state.searchSync[key] === true)
|
|
1741
1780
|
const warnings = await searchWarnings(ctx, state, web)
|
|
1742
1781
|
|
|
1782
|
+
const totalMs = Date.now() - startedAt
|
|
1743
1783
|
return {
|
|
1744
1784
|
ok: true,
|
|
1745
1785
|
pluginVersion: PLUGIN_VERSION,
|
|
1746
1786
|
storeVersion: STATE_VERSION,
|
|
1787
|
+
// 面板卡顿排查用:会话探针(宿主 list 一次要扫所有会话)与整行的耗时。
|
|
1788
|
+
timings: { probeMs, rowsMs: Math.max(0, totalMs - probeMs), totalMs },
|
|
1747
1789
|
sessionProbe: {
|
|
1748
1790
|
available: probe.available,
|
|
1749
1791
|
count: probe.count,
|
|
@@ -1765,7 +1807,7 @@ async function snapshot(ctx, state) {
|
|
|
1765
1807
|
},
|
|
1766
1808
|
providers: providerList(ctx),
|
|
1767
1809
|
interceptableRefs: [...interceptableRefs(ctx, state)],
|
|
1768
|
-
workspaceCount:
|
|
1810
|
+
workspaceCount: index.size,
|
|
1769
1811
|
credentialsAvailable: credentials !== undefined,
|
|
1770
1812
|
sessionListAvailable: probe.available,
|
|
1771
1813
|
sessionControllerAvailable: sessionListAvailable(ctx),
|
|
@@ -1918,7 +1960,8 @@ async function checkSession(ctx, state, params) {
|
|
|
1918
1960
|
|
|
1919
1961
|
/** 进会话提醒用的搜索通道摘要(不含明文 key)。 */
|
|
1920
1962
|
async function searchStateOf(ctx, state, { sessionId, cwd }) {
|
|
1921
|
-
|
|
1963
|
+
// 面板/会话入口不是热路径,但也没必要每次 describe:缓存由请求入口与设置事件维护。
|
|
1964
|
+
const web = currentWeb(ctx)
|
|
1922
1965
|
const sync = workspaceSynced(state, cwd)
|
|
1923
1966
|
if (!web.apiChannel) {
|
|
1924
1967
|
const idle = web.apiLiteralKey !== undefined
|
|
@@ -1936,6 +1979,20 @@ async function searchStateOf(ctx, state, { sessionId, cwd }) {
|
|
|
1936
1979
|
}
|
|
1937
1980
|
|
|
1938
1981
|
async function handle(ctx, state, req, res) {
|
|
1982
|
+
// 每个请求进来先刷新一次网页搜索配置:解析热路径只读缓存。
|
|
1983
|
+
refreshWebCache(ctx)
|
|
1984
|
+
// 看门狗:任何一次 await 卡住都不该让面板/测试按钮一直转圈。
|
|
1985
|
+
const watchdog = setTimeout(() => {
|
|
1986
|
+
if (res.writableEnded === true) return
|
|
1987
|
+
try {
|
|
1988
|
+
res.writeHead(504, { 'content-type': 'application/json; charset=utf-8' })
|
|
1989
|
+
res.end(JSON.stringify({ ok: false, error: '面板请求超时(8 秒):DSH 的某个服务没有响应' }))
|
|
1990
|
+
} catch {
|
|
1991
|
+
/* 已经写过了就忽略 */
|
|
1992
|
+
}
|
|
1993
|
+
}, 8000)
|
|
1994
|
+
watchdog.unref?.()
|
|
1995
|
+
if (typeof res.on === 'function') res.on('close', () => clearTimeout(watchdog))
|
|
1939
1996
|
if (!isTrusted(req)) {
|
|
1940
1997
|
res.writeHead(403)
|
|
1941
1998
|
res.end('forbidden')
|
|
@@ -1992,7 +2049,7 @@ async function handle(ctx, state, req, res) {
|
|
|
1992
2049
|
// 于是面板和官方页改的是同一把 key,不会各存一份。官方还指向聊天那把(DEEPSEEK_API_KEY)时,
|
|
1993
2050
|
// 先把两套搜索插件的 apiKeyEnv 改到插件自有 ref,再写值。
|
|
1994
2051
|
if (body.scope === 'search-default') {
|
|
1995
|
-
const web =
|
|
2052
|
+
const web = refreshWebCache(ctx)
|
|
1996
2053
|
const official = officialSearchRef(web)
|
|
1997
2054
|
const needsBind = official === DEFAULT_API_KEY_REF
|
|
1998
2055
|
const target = needsBind ? SEARCH_DEFAULT_REF : official
|
|
@@ -2052,7 +2109,7 @@ async function handle(ctx, state, req, res) {
|
|
|
2052
2109
|
|
|
2053
2110
|
if (pathname === `${API_BASE}/test` && method === 'POST') {
|
|
2054
2111
|
const body = await readJson(req)
|
|
2055
|
-
const web =
|
|
2112
|
+
const web = refreshWebCache(ctx)
|
|
2056
2113
|
const channel = body.channel === 'search' ? 'search' : 'chat'
|
|
2057
2114
|
let key = typeof body.key === 'string' && body.key.trim() !== '' ? body.key.trim() : undefined
|
|
2058
2115
|
let sourceRef = typeof body.ref === 'string' && body.ref !== '' ? body.ref : undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-api-key",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "A DeepSeek Harness plugin that binds a separate DeepSeek API key to each workspace, or to a single session, so the spend of one key stays with the sessions you choose.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"lib/client.js"
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
|
-
"test": "node test/host.test.mjs && node test/client.test.mjs"
|
|
53
|
+
"test": "node test/host.test.mjs && node test/client.test.mjs && node test/client-i18n.test.mjs"
|
|
54
54
|
}
|
|
55
55
|
}
|