dsh-workspace-api-key 0.2.17 → 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/README.md +19 -7
- package/README.zh.md +19 -7
- package/lib/client.js +227 -7
- package/lib/index.js +185 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,10 +25,20 @@ session key → workspace key → system default
|
|
|
25
25
|
- **Takes over the official search key field, once there is a key to put there** — on startup, while the new Web Search API is on, the plugin points the official `web-deepseek-api` / `web-search-deepseek` entry at `DEEPSEEK_API_KEY_SEARCH` (only that `apiKeyEnv` pointer, never a key value). The `设置 → 插件 → 网页搜索` field then edits the same search key this panel does, so it can no longer overwrite your chat key, and there is no second place where a search key can hide. Until you save a search key, the official entry keeps pointing at the chat key, so upgrading never breaks `web_search`; the panel says so instead of failing silently.
|
|
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
|
+
- *(The four search features above apply to older hosts only. DSH >= `0.2.2-rc.2` replaced them with a `计费方式` (billing mode) model; the old fields no longer take effect. See "Host compatibility" below for the new behaviour.)*
|
|
28
29
|
- **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
30
|
- **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
31
|
- **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.
|
|
31
32
|
|
|
33
|
+
## Host compatibility (0.3.0+)
|
|
34
|
+
|
|
35
|
+
The plugin switches on **host capability**, not on a parsed version: if the `web-deepseek-api` settings row contains `searchBilling` / `fetchBilling`, the host is the new one (DSH >= `0.2.2-rc.2`); otherwise the old behaviour applies.
|
|
36
|
+
|
|
37
|
+
- **New host**: the main panel keeps a single "搜索与获取" card that explains the official settings page owns the search key, shows the current search/fetch billing mode, and offers an "打开官方搜索设置" button. The old "搜索默认 key" card, the "同步搜索 API" switch, the effective-search-key hint, and all three search warnings are hidden — `apiKeyEnv` no longer takes effect on this host, so showing them would only invite misconfiguration. With "DeepSeek API Key" billing, search resolves `DEEPSEEK_API_KEY` and therefore follows the session → workspace → default cascade automatically; with "已登录的 DeepSeek 账号" billing it uses the account token, which the plugin cannot override, and the card says so.
|
|
38
|
+
- **Old host**: the 0.2.x behaviour is unchanged (search default key, `apiKeyEnv` takeover, per-workspace sync switch, search warnings).
|
|
39
|
+
- **Host version**: `hostVersion` in `/wsk-api/state` is best-effort — the plugin reads the `version` of a resolvable `@deepseek-ai/dsh` package.json; the desktop host is packed inside app.asar, so when it cannot be resolved the Settings card shows an unknown version. Capability detection, not the version string, drives behaviour.
|
|
40
|
+
- **Experimental "unified search key"** (Settings → API Key 分配 only, never in the main panel): when enabled, the plugin points the official `searchApiKeyEnv` / `fetchApiKeyEnv` at `DEEPSEEK_API_KEY_SEARCH` and stores that one key, so every session and workspace uses it for `web_search` / `web_fetch` (ahead of session/workspace keys); disabling clears both fields and returns to the official billing mode. It requires the new host — an old host answers the request with 400.
|
|
41
|
+
|
|
32
42
|
## How it works
|
|
33
43
|
|
|
34
44
|
Host half (`lib/index.js`):
|
|
@@ -38,7 +48,7 @@ Host half (`lib/index.js`):
|
|
|
38
48
|
- matches that directory against `workspaceRegistry.list()` after normalizing both sides (absolute, trailing separator removed, lower-cased on Windows);
|
|
39
49
|
- derives a stable ref per level — `DEEPSEEK_API_KEY_WS_<first 16 hex chars of sha1(normalized path), upper-cased>` for a workspace and `DEEPSEEK_API_KEY_SS_<first 16 hex chars of sha1(session id), upper-cased>` for a session (a session id contains `-` and cannot be a ref name) — writes the key through the credentials service, and serves that ref's value;
|
|
40
50
|
- rewrites only refs a provider actually reads: the default ref, every adapter's configured `apiKeyEnv`, and its own generated refs. Every other ref passes through untouched;
|
|
41
|
-
- tells search credentials apart from chat credentials **by ref, not by wrapping the web seam**. The official new Search/Fetch API resolves its key under `web-deepseek-api.apiKeyEnv`, which this plugin points at its own `DEEPSEEK_API_KEY_SEARCH` at startup and whenever the panel saves a search key, so a search resolve is recognisable from the ref alone: chat never asks for it. Earlier 0.2.x releases wrapped `ctx.web.search` / `ctx.web.fetch` in an `AsyncLocalStorage` scope instead; that patch is gone from 0.2.15 on, because a wrapper around the web seam can leave `web_search` / `web_fetch` waiting until their 30/60 s tool budget expires (it hangs even for addresses that fail instantly, e.g. `http://127.0.0.1:1/`). The rule itself is unchanged: a session's own key first (always), then, if the workspace ticked `同步搜索 API`, the workspace key and then the system default, otherwise the panel's search default ref when it holds one, and only then the ref the search plugin configured. The legacy Messages-based search keeps the plain chat cascade, because that route *is* a messages request;
|
|
51
|
+
- tells search credentials apart from chat credentials **by ref, not by wrapping the web seam**. The official new Search/Fetch API resolves its key under `web-deepseek-api.apiKeyEnv`, which this plugin points at its own `DEEPSEEK_API_KEY_SEARCH` at startup and whenever the panel saves a search key, so a search resolve is recognisable from the ref alone: chat never asks for it. Earlier 0.2.x releases wrapped `ctx.web.search` / `ctx.web.fetch` in an `AsyncLocalStorage` scope instead; that patch is gone from 0.2.15 on, because a wrapper around the web seam can leave `web_search` / `web_fetch` waiting until their 30/60 s tool budget expires (it hangs even for addresses that fail instantly, e.g. `http://127.0.0.1:1/`). The rule itself is unchanged: a session's own key first (always), then, if the workspace ticked `同步搜索 API`, the workspace key and then the system default, otherwise the panel's search default ref when it holds one, and only then the ref the search plugin configured. The legacy Messages-based search keeps the plain chat cascade, because that route *is* a messages request. On the new billing model the plugin no longer writes `apiKeyEnv` (a dead field there): `api-key` billing resolves `DEEPSEEK_API_KEY`, so search follows the chat cascade; `account` billing uses the account token, which the plugin cannot override, so it only warns; the experimental unified key points `searchApiKeyEnv` at the plugin-owned `DEEPSEEK_API_KEY_SEARCH` and, recognised by ref, wins first;
|
|
42
52
|
- builds the session list from the `sessionController` service (`list()`, unwrapping its `{items: […]}` envelope) and falls back to the live `sessions` service (`ctx.get('sessions').list()`), so the panel still has sessions when the controller is missing, throws, or returns an empty list. It hides the two kinds of session the sidebar hides: subagent sessions (`origin: 'subagent'`, or a `parentSession`) and *blank* sessions (created but never prompted — the sidebar shows only the one currently open). Without those two filters one workspace with a single real session listed thirteen;
|
|
43
53
|
- reports its own version and a session probe (`available`, `shape`, `source`, `count`, `withoutCwd`, `subagents`, `samples`, `error`, plus sample workspace paths) in `/wsk-api/state`, so the panel can say *why* no sessions are listed: a stale host half (only a page reload instead of an app restart), a missing `sessionController`, a throwing `list()`, an unexpected envelope shape, or a `cwd` that matches no workspace path;
|
|
44
54
|
- lists a directory that is not a registered workspace only if the sidebar would show it. It cannot decide that itself, because "only the currently open blank session counts" is knowledge the browser side holds (`retainedBy.mainView`), so it sends every such directory plus a `blank` flag per session and a `hiddenBlankDirs` list naming the ones whose sessions are all blank — the browser half applies the same verdict the sidebar does;
|
|
@@ -59,6 +69,7 @@ The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (
|
|
|
59
69
|
| POST | `/wsk-api/test` | `{scope, path \| sessionId \| ref}` (or `{key}`) runs one real request and returns the verdict; `invalid` marks the flag, `ok` clears it |
|
|
60
70
|
| POST | `/wsk-api/clear-flag` | `{ref}` or a target drops the invalid/quota flag |
|
|
61
71
|
| POST | `/wsk-api/search-sync` | `{path, enabled}` turns a workspace's `同步搜索 API` switch on or off |
|
|
72
|
+
| POST | `/wsk-api/search-unified` | `{enabled, key?}` — experimental: pins search and fetch for every session/workspace to one key; enabling writes `searchApiKeyEnv` / `fetchApiKeyEnv`, disabling clears them |
|
|
62
73
|
|
|
63
74
|
Only loopback callers are accepted (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) with a same-origin or absent `Origin`. No response ever contains a plaintext key, only a mask.
|
|
64
75
|
|
|
@@ -81,7 +92,7 @@ The command forwards its arguments to pnpm inside the profile directory, so the
|
|
|
81
92
|
pnpm add dsh-workspace-api-key # or: npm install dsh-workspace-api-key
|
|
82
93
|
```
|
|
83
94
|
|
|
84
|
-
Pin a version with `dsh-workspace-api-key@0.
|
|
95
|
+
Pin a version with `dsh-workspace-api-key@0.3.0`, 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).
|
|
85
96
|
|
|
86
97
|
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:
|
|
87
98
|
|
|
@@ -92,19 +103,20 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
|
|
|
92
103
|
## Tests
|
|
93
104
|
|
|
94
105
|
```sh
|
|
95
|
-
node test/host.test.mjs
|
|
96
|
-
node test/client.test.mjs
|
|
106
|
+
node test/host.test.mjs # 196 assertions
|
|
107
|
+
node test/client.test.mjs # 180 assertions
|
|
108
|
+
node test/client-billing.test.mjs # 8 assertions (new billing model)
|
|
97
109
|
```
|
|
98
110
|
|
|
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.
|
|
111
|
+
Neither main 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. `client-billing.test.mjs` covers the new billing model — no legacy search UI, the official-settings entry, and the unified-key notice — and `client-i18n.test.mjs` checks the English dictionary (7 assertions).
|
|
100
112
|
|
|
101
113
|
## Requirements and limits
|
|
102
114
|
|
|
103
|
-
- Verified against DSH Desktop
|
|
115
|
+
- Verified against DSH Desktop with `@deepseek-ai/dsh*` 0.2.2-rc.2 and `@deepseek-ai/cordis` 4.0.4. It has no runtime dependency and imports no `@deepseek-ai/*` package — only the host services (`credentials`, `workspaceRegistry`, `agents`, `sessionController`, `webServer`, `llm`, `settings`) and Node built-ins.
|
|
104
116
|
- One key per level: a session has a single key whichever provider route reads it, and a key recorded for one provider is reported as a mismatch when the session runs on another. Model changes inside one provider are not treated as a mismatch, because a DeepSeek key is account-wide.
|
|
105
117
|
- Sessions are matched to a workspace by their directory (`cwd`). The host's own `list()` skips stored sessions that never got a `cwd`, and the live-session fallback only knows sessions that are currently loaded, so the panel may show fewer sessions than the sidebar. Sessions visible only in the browser-side store carry no `cwd` of their own beyond the one the sidebar knows.
|
|
106
118
|
- The connection test speaks the Anthropic Messages shape (`POST <baseURL>/messages` with `x-api-key` and `anthropic-version: 2023-06-01`), which is what the DeepSeek route uses. A provider that speaks a different protocol can still have keys bound per level, but the test may report a failure for a key that works.
|
|
107
|
-
- Search support needs a DSH build that ships the official Search/Fetch API (`web-deepseek-api`, the 设置 → 插件 → 网页搜索 page with `启用新版 Web 搜索 API`). On a build without it the plugin simply never sees a search request: the switch stays visible, and the panel reports the legacy Messages channel instead of guessing.
|
|
119
|
+
- Search support needs a DSH build that ships the official Search/Fetch API (`web-deepseek-api`, the 设置 → 插件 → 网页搜索 page with `启用新版 Web 搜索 API`). On a build without it the plugin simply never sees a search request: the switch stays visible, and the panel reports the legacy Messages channel instead of guessing. On the new billing model, `account` billing uses the account token and cannot be overridden by the plugin — it can only be reported; choose "DeepSeek API Key" billing to follow session/workspace keys, or use the experimental unified search key in Settings.
|
|
108
120
|
- Keys are stored through the credentials service in `$DSH_HOME/.credentials.yaml` under `refs:`, in plaintext, exactly like the default key. They are not written into session logs, the plugin's state file, or the plugin directory.
|
|
109
121
|
- Matching is textual after normalization. If a session's `cwd` is a symlink or junction to a registered workspace path, it will not match and the level above is used.
|
|
110
122
|
- Uninstalling the plugin leaves the generated `*_WS_*` / `*_SS_*` refs and `DEEPSEEK_API_KEY_SEARCH` in the credentials file; remove them from the Models page.
|
package/README.zh.md
CHANGED
|
@@ -25,10 +25,20 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
25
25
|
- **接管官方搜索 key 设置(先有 key 才接管)** —— 启动时若已启用新版 Web 搜索 API,插件把官方 `web-deepseek-api` / `web-search-deepseek` 条目的 `apiKeyEnv` 指到 `DEEPSEEK_API_KEY_SEARCH`(只改这一个指向,永远不动 key 内容)。此后「设置 → 插件 → 网页搜索」里填的就是本面板这把搜索 key,既不会再顶掉聊天 key,也不存在第二处藏搜索 key 的地方。还没配搜索 key 时,官方条目继续指着聊天那把,所以升级不会让 `web_search` 直接失效,面板会如实提醒「搜索暂时和聊天共用一把 key」。
|
|
26
26
|
- **每个工作区一个搜索开关** —— 每个工作区行上都有「同步搜索 API」复选框。勾选后,该目录下会话发出的 `web_search` / `web_fetch` 请求改用它们聊天时用的那把 key(会话 → 工作区 → 系统默认);不勾选则继续用官方「设置 → 插件 → 网页搜索」里配置的那把 key。会话自己配了 key 时永远优先,所以「工作区同步、会话单独设置」花的是会话那把 key,而不是搜索默认 key。
|
|
27
27
|
- **在失败之前就提醒的搜索告警** —— 配了搜索 key 但没打开「启用新版 Web 搜索 API」,或者新版 API 打开了但它的 key ref 是空的,面板会显示提醒条,点进会话时的覆盖层也会用一句可直接照做的话说明;当生效的搜索通道不是新版 API 时,覆盖层不会拿搜索问题打扰你。
|
|
28
|
+
- *(上面四条搜索能力只适用于旧版宿主。DSH >= `0.2.2-rc.2` 把搜索/获取改成了「计费方式」,旧字段不再生效;新版宿主上的行为见「宿主版本兼容」。)*
|
|
28
29
|
- **使用说明** —— 面板顶部有「使用说明」,推荐两把 key 分工:聊天默认用 `default`,搜索默认用 `search-default`,两份预算互不干扰;里面还画了层级,并列出了会触发提醒的几种情况。
|
|
29
30
|
- **面板入口也能从设置里找回来** —— 「设置 → API Key 分配」里还有一个「打开 API Key 分配面板」按钮:侧栏图标被取消固定时,点它就能回到这里;那里也写明了「更多」菜单和刷新页面两条找回途径。
|
|
30
31
|
- **界面跟随 DSH 语言** —— 面板跟随 `@deepseek-ai/dsh-client-locale`:Web 界面是英文时显示英文,是中文时保持中文;宿主半边返回的提示(连接测试详情、搜索提醒)也在浏览器半边翻译,界面不会中英混排。
|
|
31
32
|
|
|
33
|
+
## 宿主版本兼容(0.3.0 起)
|
|
34
|
+
|
|
35
|
+
插件按**宿主能力**切换界面,不猜版本号:`web-deepseek-api` 的设置里出现 `searchBilling` / `fetchBilling` 字段,就按新版宿主(DSH >= `0.2.2-rc.2`)处理,否则按旧版处理。
|
|
36
|
+
|
|
37
|
+
- **新版宿主**:主面板只保留一张「搜索与获取」卡片,说明官方设置页已经接管搜索 key,显示当前的搜索/获取计费方式,并提供「打开官方搜索设置」按钮。旧版的「搜索默认 key」卡片、「同步搜索 API」开关、「搜索生效 key」提示和三类搜索告警全部隐藏——`apiKeyEnv` 在新版里已不再生效,继续展示只会让人误配。计费方式为「DeepSeek API Key」时,搜索解析的是 `DEEPSEEK_API_KEY`,因此会自动跟随上面讲的会话 → 工作区 → 系统默认级联;计费方式为「已登录的 DeepSeek 账号」时走账号令牌,插件无法覆盖,卡片会如实提示。
|
|
38
|
+
- **旧版宿主**:0.2.x 的行为原样保留(搜索默认 key、接管 `apiKeyEnv`、工作区同步开关、搜索告警)。
|
|
39
|
+
- **宿主版本**:`/wsk-api/state` 里的 `hostVersion` 是尽力而为读出来的——从能解析到的 `@deepseek-ai/dsh` 的 `package.json` 取 `version`;桌面版宿主打在 asar 里读不到时返回空,设置页显示「未知」。所以兼容性判断始终以设置字段为准,版本号只用于展示。
|
|
40
|
+
- **实验性「统一搜索 key」**(只在「设置 → API Key 分配」,不放进主面板):开启后插件把官方 `searchApiKeyEnv` / `fetchApiKeyEnv` 指向 `DEEPSEEK_API_KEY_SEARCH`,并把这把 key 存进凭据服务,于是所有会话、工作区的 `web_search` / `web_fetch` 都用它(优先于会话/工作区 key);关闭时清空这两个字段,回到官方计费方式。它需要新版宿主,旧版宿主调用会返回 400。
|
|
41
|
+
|
|
32
42
|
## 实现方式
|
|
33
43
|
|
|
34
44
|
宿主半边(`lib/index.js`):
|
|
@@ -38,7 +48,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
38
48
|
- 把该目录与 `workspaceRegistry.list()` 做规范化后比对(绝对化、去掉尾部分隔符、Windows 折大小写);
|
|
39
49
|
- 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
|
|
40
50
|
- 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
|
|
41
|
-
- 把「搜索请求」和「聊天请求」分开——**按 ref 分辨,而不是包住 web 服务**。官方新版 Search/Fetch API 自己解析 key,用的是 `web-deepseek-api.apiKeyEnv` 指向的凭据;本插件在启动时(以及面板每次保存搜索 key 时)把它指到插件自有的 `DEEPSEEK_API_KEY_SEARCH`,于是只看 ref 就能认出「这是搜索在要 key」:聊天永远不会解析这个 ref。0.2.15 起删掉了原先「把 `ctx.web.search` / `ctx.web.fetch` 包进 `AsyncLocalStorage`」的做法——包住 web 服务可能让 `web_search` / `web_fetch` 一直挂到工具 30/60 秒预算耗尽(连 `http://127.0.0.1:1/` 这种必然立刻失败的地址也一样挂)。规则本身没变:先看会话自己的 key(永远最优先),再看该工作区有没有勾「同步搜索 API」,勾了就依次用工作区 key、系统默认 key;没勾时先用插件面板自己持有的搜索默认 ref(若配了),最后才用搜索插件配置的那个 ref。旧版基于 Messages 的搜索仍然走原来的聊天层级,因为那条路线本身就是一次 messages
|
|
51
|
+
- 把「搜索请求」和「聊天请求」分开——**按 ref 分辨,而不是包住 web 服务**。官方新版 Search/Fetch API 自己解析 key,用的是 `web-deepseek-api.apiKeyEnv` 指向的凭据;本插件在启动时(以及面板每次保存搜索 key 时)把它指到插件自有的 `DEEPSEEK_API_KEY_SEARCH`,于是只看 ref 就能认出「这是搜索在要 key」:聊天永远不会解析这个 ref。0.2.15 起删掉了原先「把 `ctx.web.search` / `ctx.web.fetch` 包进 `AsyncLocalStorage`」的做法——包住 web 服务可能让 `web_search` / `web_fetch` 一直挂到工具 30/60 秒预算耗尽(连 `http://127.0.0.1:1/` 这种必然立刻失败的地址也一样挂)。规则本身没变:先看会话自己的 key(永远最优先),再看该工作区有没有勾「同步搜索 API」,勾了就依次用工作区 key、系统默认 key;没勾时先用插件面板自己持有的搜索默认 ref(若配了),最后才用搜索插件配置的那个 ref。旧版基于 Messages 的搜索仍然走原来的聊天层级,因为那条路线本身就是一次 messages 请求。新版计费模型下不再写 `apiKeyEnv`(那是死字段):`api-key` 计费时搜索解析 `DEEPSEEK_API_KEY`,自然沿着聊天级联走;`account` 计费走账号令牌,插件覆盖不了,只做提示;实验性统一 key 则把 `searchApiKeyEnv` 指到插件自有的 `DEEPSEEK_API_KEY_SEARCH`,并按 ref 分辨、优先返回这把 key;
|
|
42
52
|
- 会话列表来自 `sessionController` 服务(`list()`,会拆开它 `{items: […]}` 的信封),控件缺失 / 抛错 / 列表为空时再退到宿主 live 会话服务(`ctx.get('sessions').list()`),所以那种情况下面板仍能列出会话;侧栏藏起来的两种会话这里也藏:子代理会话(`origin: 'subagent'` 或带 `parentSession`)和**空会话**(建了但一句话都没发过,侧栏只显示当前开着的那个)。两种都不过滤的话,一个只有 1 个真实会话的工作区会列出 13 个;
|
|
43
53
|
- 不是注册工作区的目录,只有侧栏真的会显示它才会被列出来。这件事宿主判不了:「空会话里只有当前开着的那个算数」是浏览器端才知道的事(`retainedBy.mainView`),所以宿主把这种目录连同每个会话的 `blank` 标记一起发出去,并用 `hiddenBlankDirs` 点名「里面全是空会话」的目录,由浏览器半边套用与侧栏一致的判据;
|
|
44
54
|
- `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`shape`、`source`、`count`、`withoutCwd`、`subagents`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错、信封形状不认识,还是会话的 `cwd` 与任何工作区路径都对不上;
|
|
@@ -59,6 +69,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
59
69
|
| POST | `/wsk-api/test` | `{scope, path \| sessionId \| ref}`(或 `{key}`)真发一次请求并返回判定;`invalid` 打标记,`ok` 清标记 |
|
|
60
70
|
| POST | `/wsk-api/clear-flag` | `{ref}` 或一个目标,清掉失效/配额标记 |
|
|
61
71
|
| POST | `/wsk-api/search-sync` | `{path, enabled}` —— 打开或关闭某个工作区的「同步搜索 API」 |
|
|
72
|
+
| POST | `/wsk-api/search-unified` | `{enabled, key?}` —— 实验性:把所有会话/工作区的搜索与获取固定到同一把 key;开启时写 `searchApiKeyEnv` / `fetchApiKeyEnv`,关闭时清空 |
|
|
62
73
|
|
|
63
74
|
只接受本机回环调用(`127.0.0.1`、`::1`、`::ffff:127.0.0.1`),且 `Origin` 必须同源或缺失。任何响应都不会包含明文 key,只有掩码。
|
|
64
75
|
|
|
@@ -81,7 +92,7 @@ dsh plugin --profile web add dsh-workspace-api-key
|
|
|
81
92
|
pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api-key
|
|
82
93
|
```
|
|
83
94
|
|
|
84
|
-
锁版本用 `dsh-workspace-api-key@0.
|
|
95
|
+
锁版本用 `dsh-workspace-api-key@0.3.0`;直接从仓库装则用 `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key`(git 来源的插件会在安装时现场构建)。
|
|
85
96
|
|
|
86
97
|
然后把 `"dsh-workspace-api-key"` 加进该 profile `package.json` 的 `dsh.profile.bundles`,重启 DSH。本地开发用 link 代替:
|
|
87
98
|
|
|
@@ -92,16 +103,17 @@ pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api
|
|
|
92
103
|
## 测试
|
|
93
104
|
|
|
94
105
|
```sh
|
|
95
|
-
node test/host.test.mjs
|
|
96
|
-
node test/client.test.mjs
|
|
106
|
+
node test/host.test.mjs # 196 条断言
|
|
107
|
+
node test/client.test.mjs # 180 条断言
|
|
108
|
+
node test/client-billing.test.mjs # 8 条断言(新版计费模型)
|
|
97
109
|
```
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
两个主套件都不需要启动 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`、提醒条与「使用说明」弹窗;`client-billing.test.mjs` 专门验证新版计费模型下面板不再出现旧搜索 UI、只给官方设置入口,以及统一搜索 key 的提示。i18n 套件(`client-i18n.test.mjs`,7 条断言)验证英文字典完整。
|
|
100
112
|
|
|
101
113
|
## 要求与限制
|
|
102
114
|
|
|
103
|
-
- 已在 DSH Desktop
|
|
104
|
-
- 搜索功能依赖自带官方 Search/Fetch API 的 DSH(`web-deepseek-api`,即「设置 → 插件 → 网页搜索」里那个「启用新版 Web 搜索 API」)。没有这个能力的版本上,插件根本收不到搜索请求:开关照旧显示,面板会如实报告当前是旧版 Messages
|
|
115
|
+
- 已在 DSH Desktop(`@deepseek-ai/dsh*` 0.2.2-rc.2、`@deepseek-ai/cordis` 4.0.4)上验证。插件没有运行时依赖,也不 import 任何 `@deepseek-ai/*` 包,只用宿主服务(`credentials`、`workspaceRegistry`、`agents`、`sessionController`、`webServer`、`llm`、`settings`)与 Node 内置模块。
|
|
116
|
+
- 搜索功能依赖自带官方 Search/Fetch API 的 DSH(`web-deepseek-api`,即「设置 → 插件 → 网页搜索」里那个「启用新版 Web 搜索 API」)。没有这个能力的版本上,插件根本收不到搜索请求:开关照旧显示,面板会如实报告当前是旧版 Messages 通道,而不是瞎猜。新版宿主改用「计费方式」后,`account` 计费走账号令牌,插件无法覆盖,只能提示;需要按会话/工作区 key 计费时请把计费方式设为「DeepSeek API Key」,或使用设置页里的实验性「统一搜索 key」。
|
|
105
117
|
- 每一层只有一把 key:一个会话无论被哪个 provider 路由读取,用的都是同一把 key;key 记下的服务商与会话当前服务商不一致时会被报为「不匹配」,但同一服务商内换模型不算不匹配,因为 DeepSeek key 是账号级的。
|
|
106
118
|
- 会话按目录(`cwd`)归入工作区。宿主自己的 `list()` 会跳过从未存过 `cwd` 的历史会话,而 live 兜底只认识当前装载中的会话,所以面板列出的会话可能比侧栏少;只在浏览器端存储里可见的会话,除了侧栏知道的那个 `cwd` 之外没有别的路径信息。
|
|
107
119
|
- 「测试连接」说的是 Anthropic Messages 协议(`POST <baseURL>/messages` 带 `x-api-key` 与 `anthropic-version: 2023-06-01`),也就是 DeepSeek 路由用的协议;换了别的协议的 provider 一样可以按层绑 key,只是测试可能把有效的 key 报成失败。
|
package/lib/client.js
CHANGED
|
@@ -167,7 +167,37 @@ window.__ModuleLoader__.load({
|
|
|
167
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
168
|
"打开 API Key 分配面板": "Open the API Key Allocation panel",
|
|
169
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."
|
|
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"
|
|
171
201
|
}
|
|
172
202
|
|
|
173
203
|
const EN_HOST = {
|
|
@@ -772,6 +802,61 @@ window.__ModuleLoader__.load({
|
|
|
772
802
|
|
|
773
803
|
// ─────────────────────────────── 主页面 ───────────────────────────────
|
|
774
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
|
+
|
|
775
860
|
/**
|
|
776
861
|
* 「设置 → API Key 分配」这一栏:侧栏图标没了也能从这里回到面板。
|
|
777
862
|
*
|
|
@@ -783,6 +868,40 @@ window.__ModuleLoader__.load({
|
|
|
783
868
|
function SettingsSection() {
|
|
784
869
|
useI18n()
|
|
785
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
|
+
}
|
|
786
905
|
const openPanel = () => {
|
|
787
906
|
try {
|
|
788
907
|
const layout = host.ctx === null ? undefined : host.ctx.get('layout')
|
|
@@ -811,7 +930,64 @@ window.__ModuleLoader__.load({
|
|
|
811
930
|
'已切到该面板:如果它还是没出现,说明面板正在加载,稍等一下。')
|
|
812
931
|
: null,
|
|
813
932
|
h('div', { className: 'wsk-hint', style: { marginTop: 10 } },
|
|
814
|
-
'这里同时管着每个工作区 / 会话的 key 与搜索通道;面板右上角的「使用说明」有完整的优先级说明。'))
|
|
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)
|
|
815
991
|
}
|
|
816
992
|
|
|
817
993
|
function WorkspaceKeyPage(props) {
|
|
@@ -1321,7 +1497,7 @@ window.__ModuleLoader__.load({
|
|
|
1321
1497
|
}, verdictText(lastTest.verdict))
|
|
1322
1498
|
: null),
|
|
1323
1499
|
h('div', { className: 'wsk-path' }, prettyPath(ws.path)),
|
|
1324
|
-
ws.kind === 'orphan'
|
|
1500
|
+
ws.kind === 'orphan' || searchInfo?.mode === 'billing'
|
|
1325
1501
|
? null
|
|
1326
1502
|
: h('label', {
|
|
1327
1503
|
key: 'sync',
|
|
@@ -1342,7 +1518,7 @@ window.__ModuleLoader__.load({
|
|
|
1342
1518
|
: searchInfo?.defaultConfigured === true
|
|
1343
1519
|
? '搜索用搜索默认 key'
|
|
1344
1520
|
: '搜索用搜索插件里的 key')),
|
|
1345
|
-
ws.kind !== 'orphan' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
|
|
1521
|
+
ws.kind !== 'orphan' && searchInfo?.mode !== 'billing' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
|
|
1346
1522
|
? h('div', { key: 'sk', className: 'wsk-hint', style: { marginTop: 2 } },
|
|
1347
1523
|
tf(`搜索生效 key:${ws.searchKeyMasked || '未配置'}`, `Effective search key: ${ws.searchKeyMasked || 'not configured'}`),
|
|
1348
1524
|
ws.searchSync === true
|
|
@@ -1385,7 +1561,30 @@ window.__ModuleLoader__.load({
|
|
|
1385
1561
|
: null)
|
|
1386
1562
|
}
|
|
1387
1563
|
|
|
1388
|
-
const renderHelp = () =>
|
|
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, {
|
|
1389
1588
|
title: '使用说明',
|
|
1390
1589
|
onDismiss: () => setHelp(false),
|
|
1391
1590
|
body: h('div', { className: 'wsk-help' },
|
|
@@ -1430,7 +1629,7 @@ window.__ModuleLoader__.load({
|
|
|
1430
1629
|
h('div', { className: 'wsk-hint' },
|
|
1431
1630
|
'提醒只报告、不替你改 key。插件绝不会自动切换或替换你的 key,避免计费被悄悄改到别的账号上。')),
|
|
1432
1631
|
actions: [{ label: '知道了', primary: true, onClick: () => setHelp(false) }],
|
|
1433
|
-
})
|
|
1632
|
+
}))
|
|
1434
1633
|
|
|
1435
1634
|
const renderPrompt = () => {
|
|
1436
1635
|
if (prompt === null) return null
|
|
@@ -1572,7 +1771,28 @@ window.__ModuleLoader__.load({
|
|
|
1572
1771
|
onClick: () => runTest(DEFAULT_KEY, '系统默认', { ref: defaultRef?.ref ?? DEFAULT_REF }),
|
|
1573
1772
|
}, testing === DEFAULT_KEY ? '测试中…' : '测试连接')))),
|
|
1574
1773
|
|
|
1575
|
-
searchInfo?.
|
|
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
|
|
1576
1796
|
? h('div', { className: 'wsk-card' },
|
|
1577
1797
|
h('h4', null, '搜索默认 key'),
|
|
1578
1798
|
h('div', { className: 'wsk-row' },
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
34
34
|
import { createHash } from 'node:crypto'
|
|
35
|
+
import { createRequire } from 'node:module'
|
|
35
36
|
import { homedir } from 'node:os'
|
|
36
37
|
import { join, dirname, basename, resolve as pathResolve } from 'node:path'
|
|
37
38
|
|
|
@@ -116,6 +117,11 @@ function isSearchRefFor(ctx, ref) {
|
|
|
116
117
|
if (typeof ref !== 'string' || ref === '') return false
|
|
117
118
|
const web = currentWeb(ctx)
|
|
118
119
|
if (web.apiChannel !== true) return false
|
|
120
|
+
// 新版计费模型:搜索只认 `searchApiKeyEnv` 指向的自有 ref;聊天 ref(DEEPSEEK_API_KEY)
|
|
121
|
+
// 绝不能被当成搜索处理,否则「统一搜索 key」会把聊天也一起换掉。
|
|
122
|
+
if (web.mode === 'billing') {
|
|
123
|
+
return ref === SEARCH_DEFAULT_REF || (web.ownSearchRef !== undefined && ref === web.ownSearchRef)
|
|
124
|
+
}
|
|
119
125
|
// 官方页还没被接管时,搜索用的是聊天那把 ref:这时分辨不了,按聊天处理(安全)。
|
|
120
126
|
if (web.searchKeyEnv === DEFAULT_API_KEY_REF) return ref === SEARCH_DEFAULT_REF
|
|
121
127
|
return ref === SEARCH_DEFAULT_REF || ref === web.searchKeyEnv || ref === web.legacyKeyEnv
|
|
@@ -142,6 +148,39 @@ const PLUGIN_VERSION = (() => {
|
|
|
142
148
|
}
|
|
143
149
|
})()
|
|
144
150
|
|
|
151
|
+
/**
|
|
152
|
+
* 宿主运行时版本(尽力而为,只用于界面展示与日志)。
|
|
153
|
+
*
|
|
154
|
+
* 行为判断**不**依赖它:桌面端宿主代码打在 app.asar 里,`@deepseek-ai/dsh`
|
|
155
|
+
* 不一定能从插件目录解析到;而新版搜索配置的 schema 特征(searchBilling /
|
|
156
|
+
* fetchBilling)与行为一一对应,所以 gating 用特性检测,版本号只是辅助信息。
|
|
157
|
+
*/
|
|
158
|
+
const HOST_VERSION = (() => {
|
|
159
|
+
try {
|
|
160
|
+
const req = createRequire(import.meta.url)
|
|
161
|
+
let main
|
|
162
|
+
try {
|
|
163
|
+
main = req.resolve('@deepseek-ai/dsh')
|
|
164
|
+
} catch {
|
|
165
|
+
main = undefined
|
|
166
|
+
}
|
|
167
|
+
if (main === undefined) return undefined
|
|
168
|
+
// 从入口文件往上找最近的 package.json。
|
|
169
|
+
let dir = dirname(main)
|
|
170
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
171
|
+
const candidate = join(dir, 'package.json')
|
|
172
|
+
if (existsSync(candidate)) {
|
|
173
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'))
|
|
174
|
+
if (typeof pkg?.version === 'string') return pkg.version
|
|
175
|
+
}
|
|
176
|
+
dir = dirname(dir)
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
return undefined
|
|
182
|
+
})()
|
|
183
|
+
|
|
145
184
|
// ─────────────────────────── 路径 / 工作区键 ───────────────────────────
|
|
146
185
|
|
|
147
186
|
/** 统一的路径比较用规范形:绝对化 + 分隔符统一 + Windows 折大小写。 */
|
|
@@ -354,6 +393,32 @@ async function bindOfficialSearchRef(ctx, ref) {
|
|
|
354
393
|
return { ok: true, changed }
|
|
355
394
|
}
|
|
356
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 写新版搜索 API 的 schema 字段(实验性「统一搜索 key」用)。
|
|
398
|
+
*
|
|
399
|
+
* 只改 `searchApiKeyEnv` / `fetchApiKeyEnv` 这类声明过的字段,不碰任何 key 值;
|
|
400
|
+
* 空字符串表示「清掉自有 ref、回到官方计费方式」(官方 resolveTarget 用 nonEmpty 判断)。
|
|
401
|
+
*/
|
|
402
|
+
async function updateSearchApiSettings(ctx, patch) {
|
|
403
|
+
let settings
|
|
404
|
+
try {
|
|
405
|
+
settings = ctx.get('settings')
|
|
406
|
+
} catch {
|
|
407
|
+
settings = undefined
|
|
408
|
+
}
|
|
409
|
+
if (typeof settings?.update !== 'function' || typeof settings?.describe !== 'function') {
|
|
410
|
+
return { ok: false, reason: 'settings-unavailable' }
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const row = (settings.describe() ?? []).find((item) => item?.ns === SEARCH_API_NS)
|
|
414
|
+
if (row === undefined) return { ok: false, reason: 'search-settings-missing' }
|
|
415
|
+
await settings.update(SEARCH_API_NS, patch, row.revision)
|
|
416
|
+
return { ok: true }
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return { ok: false, reason: error?.message ?? String(error) }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
357
422
|
/**
|
|
358
423
|
* 启动时把官方「网页搜索」页的 key 指向拉回一致。
|
|
359
424
|
*
|
|
@@ -879,6 +944,14 @@ function webDefaults() {
|
|
|
879
944
|
apiChannel: false,
|
|
880
945
|
searchKeyEnv: DEFAULT_API_KEY_REF,
|
|
881
946
|
searchLiteralKey: undefined,
|
|
947
|
+
// 搜索配置模型:'ref' = 旧版(apiKeyEnv 指针);'billing' = 0.2.2-rc.2 起的计费方式模型。
|
|
948
|
+
mode: 'ref',
|
|
949
|
+
searchBilling: undefined,
|
|
950
|
+
fetchBilling: undefined,
|
|
951
|
+
ownSearchRef: undefined,
|
|
952
|
+
ownFetchRef: undefined,
|
|
953
|
+
searchBaseURL: undefined,
|
|
954
|
+
fetchBaseURL: undefined,
|
|
882
955
|
}
|
|
883
956
|
}
|
|
884
957
|
|
|
@@ -903,6 +976,27 @@ function webConfigOf(rows) {
|
|
|
903
976
|
: FALLBACK_FETCH_PROVIDER
|
|
904
977
|
const apiLiteralKey = typeof api.apiKey === 'string' && api.apiKey !== '' ? api.apiKey : undefined
|
|
905
978
|
const legacyLiteralKey = typeof legacy.apiKey === 'string' && legacy.apiKey !== '' ? legacy.apiKey : undefined
|
|
979
|
+
|
|
980
|
+
// 0.2.2-rc.2 起,搜索改由「计费方式」决定凭据:searchBilling / fetchBilling +
|
|
981
|
+
// (可选的)自有端点/ref。旧字段 apiKeyEnv 还能加载,但已经不再生效。
|
|
982
|
+
// 用「行里有没有 *_Billing」做特性检测,而不是解析宿主版本号:版本号在桌面端
|
|
983
|
+
// 拿不到(宿主代码打在 asar 里),而这个 schema 特征与行为是一一对应的。
|
|
984
|
+
const searchBilling = typeof api.searchBilling === 'string' && api.searchBilling !== '' ? api.searchBilling : undefined
|
|
985
|
+
const fetchBilling = typeof api.fetchBilling === 'string' && api.fetchBilling !== '' ? api.fetchBilling : undefined
|
|
986
|
+
const mode = searchBilling !== undefined || fetchBilling !== undefined ? 'billing' : 'ref'
|
|
987
|
+
const ownSearchRef = typeof api.searchApiKeyEnv === 'string' && api.searchApiKeyEnv !== ''
|
|
988
|
+
? api.searchApiKeyEnv
|
|
989
|
+
: undefined
|
|
990
|
+
const ownFetchRef = typeof api.fetchApiKeyEnv === 'string' && api.fetchApiKeyEnv !== ''
|
|
991
|
+
? api.fetchApiKeyEnv
|
|
992
|
+
: undefined
|
|
993
|
+
const searchBaseURL = typeof api.searchBaseURL === 'string' && api.searchBaseURL !== ''
|
|
994
|
+
? api.searchBaseURL
|
|
995
|
+
: FALLBACK_SEARCH_BASE_URL
|
|
996
|
+
const fetchBaseURL = typeof api.fetchBaseURL === 'string' && api.fetchBaseURL !== ''
|
|
997
|
+
? api.fetchBaseURL
|
|
998
|
+
: FALLBACK_SEARCH_BASE_URL
|
|
999
|
+
|
|
906
1000
|
return {
|
|
907
1001
|
apiKeyEnv,
|
|
908
1002
|
legacyKeyEnv,
|
|
@@ -915,8 +1009,17 @@ function webConfigOf(rows) {
|
|
|
915
1009
|
/** 新版 Search/Fetch API 是否被选中(官方开关)= provider 选择被改成了 API 版。 */
|
|
916
1010
|
apiChannel: searchProvider === SEARCH_PROVIDER_ID || fetchProvider === SEARCH_FETCH_PROVIDER_ID,
|
|
917
1011
|
/** 搜索请求真正会用的那套配置的 key 引用。 */
|
|
918
|
-
searchKeyEnv: searchProvider === SEARCH_PROVIDER_ID
|
|
1012
|
+
searchKeyEnv: searchProvider === SEARCH_PROVIDER_ID
|
|
1013
|
+
? (mode === 'billing' ? ownSearchRef ?? DEFAULT_API_KEY_REF : apiKeyEnv)
|
|
1014
|
+
: legacyKeyEnv,
|
|
919
1015
|
searchLiteralKey: searchProvider === SEARCH_PROVIDER_ID ? apiLiteralKey : legacyLiteralKey,
|
|
1016
|
+
mode,
|
|
1017
|
+
searchBilling,
|
|
1018
|
+
fetchBilling,
|
|
1019
|
+
ownSearchRef,
|
|
1020
|
+
ownFetchRef,
|
|
1021
|
+
searchBaseURL,
|
|
1022
|
+
fetchBaseURL,
|
|
920
1023
|
}
|
|
921
1024
|
}
|
|
922
1025
|
|
|
@@ -927,6 +1030,30 @@ function webConfigOf(rows) {
|
|
|
927
1030
|
*/
|
|
928
1031
|
async function searchWarnings(ctx, state, web) {
|
|
929
1032
|
const out = []
|
|
1033
|
+
|
|
1034
|
+
// 新版计费模型:搜索不再读 apiKeyEnv 指针,旧提醒(同步/搜索默认 key)全部失去意义。
|
|
1035
|
+
// 这里只保留两条与新版真正相关的提示,避免面板给出过时或错误的引导。
|
|
1036
|
+
if (web.mode === 'billing') {
|
|
1037
|
+
if (web.ownSearchRef === SEARCH_DEFAULT_REF) {
|
|
1038
|
+
out.push({
|
|
1039
|
+
code: 'search-unified',
|
|
1040
|
+
level: 'info',
|
|
1041
|
+
title: '实验性:所有会话的搜索共用同一把 key',
|
|
1042
|
+
detail: `搜索/获取被指向插件自有的 ${SEARCH_DEFAULT_REF},官方「计费方式」暂时不生效。`
|
|
1043
|
+
+ '要恢复按会话/工作区计费,到「设置 → API Key 分配」里关掉这项实验功能。',
|
|
1044
|
+
})
|
|
1045
|
+
} else if (web.searchBilling === 'account') {
|
|
1046
|
+
out.push({
|
|
1047
|
+
code: 'search-billing-account',
|
|
1048
|
+
level: 'warn',
|
|
1049
|
+
title: '搜索当前按「已登录的 DeepSeek 账号」计费',
|
|
1050
|
+
detail: '计费方式设为 account 时,搜索只使用账号令牌,本插件配置的 API key 不会参与搜索。'
|
|
1051
|
+
+ '要让搜索跟随会话/工作区的 key,请到「设置 → 插件 → 网页搜索」把计费方式改成「DeepSeek API Key」。',
|
|
1052
|
+
})
|
|
1053
|
+
}
|
|
1054
|
+
return out
|
|
1055
|
+
}
|
|
1056
|
+
|
|
930
1057
|
const apiKeyConfigured = web.apiKeyEnv === DEFAULT_API_KEY_REF
|
|
931
1058
|
? (await describeKey(ctx, DEFAULT_API_KEY_REF)).configured === true
|
|
932
1059
|
: (await describeKey(ctx, web.apiKeyEnv)).configured === true
|
|
@@ -1321,7 +1448,9 @@ export function apply(ctx) {
|
|
|
1321
1448
|
}
|
|
1322
1449
|
installResolver(ctx, state)
|
|
1323
1450
|
// 「直接禁用官方页面里的搜索 key 设置」:把官方 apiKeyEnv 指到插件自有 ref(异步、失败不影响启动)。
|
|
1324
|
-
|
|
1451
|
+
// 0.2.2-rc.2 起官方改用计费方式(searchBilling/fetchBilling),apiKeyEnv 已不再生效:
|
|
1452
|
+
// 这时再写它只会制造「看起来配了、其实没用」的无效配置,所以只在旧模型下接管。
|
|
1453
|
+
if (currentWeb(ctx).mode !== 'billing') void reconcileOfficialSearchRef(ctx)
|
|
1325
1454
|
|
|
1326
1455
|
// ── 2. REST API(loopback 可信)──────────────────────────────────────
|
|
1327
1456
|
let webServer
|
|
@@ -1404,6 +1533,15 @@ function installResolver(ctx, state) {
|
|
|
1404
1533
|
if (isSearchRefFor(ctx, ref)) {
|
|
1405
1534
|
const web = currentWeb(ctx)
|
|
1406
1535
|
if (web.apiChannel) {
|
|
1536
|
+
// 实验性「统一搜索 key」:搜索/获取固定用插件自有的那一把,优先于会话/工作区配置。
|
|
1537
|
+
// 只在计费模型下生效:旧模型里 searchApiKeyEnv 就是「搜索默认 key」,语义不同。
|
|
1538
|
+
if (web.mode === 'billing' && web.ownSearchRef === SEARCH_DEFAULT_REF) {
|
|
1539
|
+
const forced = await resolveKey(ctx, SEARCH_DEFAULT_REF)
|
|
1540
|
+
if (forced.value !== undefined && forced.value !== '') {
|
|
1541
|
+
log(`search ref ${ref} → ${SEARCH_DEFAULT_REF} (unified)`)
|
|
1542
|
+
return { value: forced.value, source: forced.source ?? 'plugin' }
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1407
1545
|
// ① 会话单独设置的 key 永远优先——哪怕官方搜索插件里配了别的 key。
|
|
1408
1546
|
const sessionKey = sessionOverrideRef(state, sessionId)
|
|
1409
1547
|
if (sessionKey !== undefined) {
|
|
@@ -1783,6 +1921,7 @@ async function snapshot(ctx, state) {
|
|
|
1783
1921
|
return {
|
|
1784
1922
|
ok: true,
|
|
1785
1923
|
pluginVersion: PLUGIN_VERSION,
|
|
1924
|
+
hostVersion: HOST_VERSION,
|
|
1786
1925
|
storeVersion: STATE_VERSION,
|
|
1787
1926
|
// 面板卡顿排查用:会话探针(宿主 list 一次要扫所有会话)与整行的耗时。
|
|
1788
1927
|
timings: { probeMs, rowsMs: Math.max(0, totalMs - probeMs), totalMs },
|
|
@@ -1821,6 +1960,13 @@ async function snapshot(ctx, state) {
|
|
|
1821
1960
|
// 搜索 / 抓取通道:官方「设置 → 插件 → 网页搜索」里的配置 + 本插件怎么参与。
|
|
1822
1961
|
// 面板据此显示通道、生效 key 与提醒;不暴露任何明文。
|
|
1823
1962
|
search: {
|
|
1963
|
+
// 'billing' = 0.2.2-rc.2 起的计费方式模型;'ref' = 旧版 apiKeyEnv 指针模型。
|
|
1964
|
+
mode: web.mode ?? 'ref',
|
|
1965
|
+
billing: { search: web.searchBilling, fetch: web.fetchBilling },
|
|
1966
|
+
ownSearchRef: web.ownSearchRef,
|
|
1967
|
+
ownFetchRef: web.ownFetchRef,
|
|
1968
|
+
// 实验性「统一搜索 key」:把官方 searchApiKeyEnv 指向插件自有 ref。
|
|
1969
|
+
unified: web.mode === 'billing' && web.searchKeyEnv === SEARCH_DEFAULT_REF,
|
|
1824
1970
|
channel: web.apiChannel ? 'api' : 'messages',
|
|
1825
1971
|
apiChannel: web.apiChannel,
|
|
1826
1972
|
searchProvider: web.searchProvider,
|
|
@@ -2168,6 +2314,43 @@ async function handle(ctx, state, req, res) {
|
|
|
2168
2314
|
return
|
|
2169
2315
|
}
|
|
2170
2316
|
|
|
2317
|
+
if (pathname === `${API_BASE}/search-unified` && method === 'POST') {
|
|
2318
|
+
const body = await readJson(req)
|
|
2319
|
+
const web = refreshWebCache(ctx)
|
|
2320
|
+
if (web.mode !== 'billing') {
|
|
2321
|
+
sendJson(res, 400, { ok: false, error: '当前宿主还是旧版搜索配置,不支持该实验功能' })
|
|
2322
|
+
return
|
|
2323
|
+
}
|
|
2324
|
+
const enabled = body.enabled !== false
|
|
2325
|
+
if (enabled) {
|
|
2326
|
+
const key = typeof body.key === 'string' ? body.key.trim() : ''
|
|
2327
|
+
if (key === '') {
|
|
2328
|
+
sendJson(res, 400, { ok: false, error: '请先填写一把搜索 key' })
|
|
2329
|
+
return
|
|
2330
|
+
}
|
|
2331
|
+
await storeKey(ctx, SEARCH_DEFAULT_REF, key)
|
|
2332
|
+
const updated = await updateSearchApiSettings(ctx, {
|
|
2333
|
+
searchApiKeyEnv: SEARCH_DEFAULT_REF,
|
|
2334
|
+
fetchApiKeyEnv: SEARCH_DEFAULT_REF,
|
|
2335
|
+
})
|
|
2336
|
+
if (updated.ok !== true) {
|
|
2337
|
+
sendJson(res, 500, { ok: false, error: `写入官方搜索配置失败:${updated.reason}` })
|
|
2338
|
+
return
|
|
2339
|
+
}
|
|
2340
|
+
} else {
|
|
2341
|
+
const updated = await updateSearchApiSettings(ctx, { searchApiKeyEnv: '', fetchApiKeyEnv: '' })
|
|
2342
|
+
if (updated.ok !== true) {
|
|
2343
|
+
sendJson(res, 500, { ok: false, error: `写入官方搜索配置失败:${updated.reason}` })
|
|
2344
|
+
return
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
webCache = undefined
|
|
2348
|
+
refreshWebCache(ctx)
|
|
2349
|
+
log(`unified search key ${enabled ? 'on' : 'off'}`)
|
|
2350
|
+
sendJson(res, 200, { ok: true, enabled, ...(await snapshot(ctx, state)) })
|
|
2351
|
+
return
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2171
2354
|
if (pathname === `${API_BASE}/search-sync` && method === 'POST') {
|
|
2172
2355
|
const body = await readJson(req)
|
|
2173
2356
|
const dir = typeof body.path === 'string' ? body.path : undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-api-key",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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 && node test/client-i18n.test.mjs"
|
|
53
|
+
"test": "node test/host.test.mjs && node test/client.test.mjs && node test/client-i18n.test.mjs && node test/client-billing.test.mjs"
|
|
54
54
|
}
|
|
55
55
|
}
|