dsh-workspace-api-key 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,7 @@ session key → workspace key → system default
21
21
  - **Mismatch warning** — if the key you bound was recorded for one provider (say `deepseek-official`) and the session now runs on another, entering that session reports it, because that request would be sent with a key the provider does not accept.
22
22
  - **No silent substitution** — neither warning ever changes which key is used. The decision stays with you, which is the whole point: an automatically swapped key would defeat the billing separation.
23
23
  - **One-click revert** — clearing a session entry falls back to the workspace key, clearing a workspace entry falls back to the system default.
24
+ - **A search default key of its own** — the `搜索默认 key` card binds a key to `DEEPSEEK_API_KEY_SEARCH`, so search can be billed apart from chat without editing the official configuration: a session's own key wins, then a synced workspace's chat cascade, then this key, then whatever the official search page stores.
24
25
  - **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.
25
26
  - **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.
26
27
  - **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.
@@ -34,7 +35,7 @@ Host half (`lib/index.js`):
34
35
  - matches that directory against `workspaceRegistry.list()` after normalizing both sides (absolute, trailing separator removed, lower-cased on Windows);
35
36
  - 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;
36
37
  - 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;
37
- - tells search credentials apart from chat credentials. The official new Search/Fetch API (`web.searchProvider === 'deepseek-search-api'`) resolves its key itself, and it defaults to the same `DEEPSEEK_API_KEY` the chat uses, so the ref alone cannot say why a key is being asked for. The host half therefore wraps `ctx.web.search` / `ctx.web.fetch` in an `AsyncLocalStorage` scope and applies the search rule only when `credentials.resolve` runs inside it — a session's own key first (always), then, if that workspace ticked `同步搜索 API`, the workspace key and then the system default, and otherwise the ref the search plugin configured. The legacy Messages-based search keeps the plain chat cascade, because that route *is* a messages request;
38
+ - tells search credentials apart from chat credentials. The official new Search/Fetch API (`web.searchProvider === 'deepseek-search-api'`) resolves its key itself, and it defaults to the same `DEEPSEEK_API_KEY` the chat uses, so the ref alone cannot say why a key is being asked for. The host half therefore wraps `ctx.web.search` / `ctx.web.fetch` in an `AsyncLocalStorage` scope and applies the search rule only when `credentials.resolve` runs inside it — a session's own key first (always), then, if that workspace ticked `同步搜索 API`, the workspace key and then the system default, otherwise its own search default ref when the panel 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;
38
39
  - 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;
39
40
  - 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;
40
41
  - 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;
@@ -51,6 +52,7 @@ The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (
51
52
  | GET | `/wsk-api/resolve` | which level and ref the current session resolves to right now |
52
53
  | GET | `/wsk-api/check` | `?sessionId=…&path=…` — read-only verdict for one session: effective level, key, stale flag, provider mismatch, `needsAttention` |
53
54
  | POST | `/wsk-api/set` | `{scope: 'workspace' \| 'session', path \| sessionId, title, key, provider?}` binds a key; `{useDefault: true}` or an empty key clears it |
55
+ | POST | `/wsk-api/set` (search) | `{scope: 'search-default', key}` binds the panel's search default key; `{useDefault: true}` clears it |
54
56
  | 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 |
55
57
  | POST | `/wsk-api/clear-flag` | `{ref}` or a target drops the invalid/quota flag |
56
58
  | POST | `/wsk-api/search-sync` | `{path, enabled}` turns a workspace's `同步搜索 API` switch on or off |
@@ -76,7 +78,7 @@ The command forwards its arguments to pnpm inside the profile directory, so the
76
78
  pnpm add dsh-workspace-api-key # or: npm install dsh-workspace-api-key
77
79
  ```
78
80
 
79
- Pin a version with `dsh-workspace-api-key@0.2.8`, 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).
81
+ Pin a version with `dsh-workspace-api-key@0.2.10`, 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).
80
82
 
81
83
  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:
82
84
 
@@ -87,11 +89,11 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
87
89
  ## Tests
88
90
 
89
91
  ```sh
90
- node test/host.test.mjs # 157 assertions
91
- node test/client.test.mjs # 162 assertions
92
+ node test/host.test.mjs # 165 assertions
93
+ node test/client.test.mjs # 170 assertions
92
94
  ```
93
95
 
94
- 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 two 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.
96
+ 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.
95
97
 
96
98
  ## Requirements and limits
97
99
 
@@ -102,7 +104,7 @@ Neither suite needs a running DSH process. The host suite drives `apply()` again
102
104
  - 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.
103
105
  - 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.
104
106
  - 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.
105
- - Uninstalling the plugin leaves the generated `*_WS_*` / `*_SS_*` refs in the credentials file; remove them from the Models page.
107
+ - Uninstalling the plugin leaves the generated `*_WS_*` / `*_SS_*` refs and `DEEPSEEK_API_KEY_SEARCH` in the credentials file; remove them from the Models page.
106
108
  - If the panel lists workspaces but no expandable sessions, read the diagnostics line at the bottom of the panel. A red "the host half is still an older version" banner means the DSH process was never restarted: the host half is loaded once at startup, so closing the window or reloading the page is not enough — fully quit DSH Desktop and open it again. Otherwise the diagnostics line shows how many sessions the host sees (and from which source: `sessionController`, its live-session fallback, or the browser store), plus sample session `cwd` values next to sample workspace paths when none of them match.
107
109
 
108
110
  ## License
package/README.zh.md CHANGED
@@ -21,6 +21,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
21
21
  - **不匹配提醒** —— 如果绑 key 时记下的是某个服务商(比如 `deepseek-official`),而该会话现在跑在另一个服务商上,点进这个会话时会提醒你,因为那次请求会拿着对方不认的 key 发出去。
22
22
  - **绝不悄悄换 key** —— 两种提醒都不会改动实际使用的 key,决定权始终在你手上;自动替换会直接破坏「按会话分离计费」这个初衷。
23
23
  - **一键回退** —— 清空会话条目回到工作区 key,清空工作区条目回到系统默认。
24
+ - **搜索默认 key 单独一把** —— 「搜索默认 key」卡片把这把 key 绑到 `DEEPSEEK_API_KEY_SEARCH`,于是搜索可以和聊天分开计费,而且不用去改官方配置:会话自己的 key 最优先,其次勾了同步的工作区走聊天层级,再次是这把搜索默认 key,最后才是官方搜索页里配的那把。
24
25
  - **每个工作区一个搜索开关** —— 每个工作区行上都有「同步搜索 API」复选框。勾选后,该目录下会话发出的 `web_search` / `web_fetch` 请求改用它们聊天时用的那把 key(会话 → 工作区 → 系统默认);不勾选则继续用官方「设置 → 插件 → 网页搜索」里配置的那把 key。会话自己配了 key 时永远优先,所以「工作区同步、会话单独设置」花的是会话那把 key,而不是搜索默认 key。
25
26
  - **在失败之前就提醒的搜索告警** —— 配了搜索 key 但没打开「启用新版 Web 搜索 API」,或者新版 API 打开了但它的 key ref 是空的,面板会显示提醒条,点进会话时的覆盖层也会用一句可直接照做的话说明;当生效的搜索通道不是新版 API 时,覆盖层不会拿搜索问题打扰你。
26
27
  - **使用说明** —— 面板顶部有「使用说明」,推荐两把 key 分工:聊天默认用 `default`,搜索默认用 `search-default`,两份预算互不干扰;里面还画了层级,并列出了会触发提醒的几种情况。
@@ -34,7 +35,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
34
35
  - 把该目录与 `workspaceRegistry.list()` 做规范化后比对(绝对化、去掉尾部分隔符、Windows 折大小写);
35
36
  - 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
36
37
  - 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
37
- - 把「搜索请求」和「聊天请求」分开。官方新版 Search/Fetch API(`web.searchProvider === 'deepseek-search-api'`)自己解析 key,而它默认读的又是聊天用的 `DEEPSEEK_API_KEY`,所以光看 ref 分不出这次要 key 是为了什么。宿主半边因此把 `ctx.web.search` / `ctx.web.fetch` 包进一个 `AsyncLocalStorage` 作用域,只有当 `credentials.resolve` 在这个作用域里被调用时才套用搜索规则:先看会话自己的 key(永远最优先),再看该工作区有没有勾「同步搜索 API」,勾了就依次用工作区 key、系统默认 key,没勾就用搜索插件配置的那个 ref。旧版基于 Messages 的搜索仍然走原来的聊天层级,因为那条路线本身就是一次 messages 请求;
38
+ - 把「搜索请求」和「聊天请求」分开。官方新版 Search/Fetch API(`web.searchProvider === 'deepseek-search-api'`)自己解析 key,而它默认读的又是聊天用的 `DEEPSEEK_API_KEY`,所以光看 ref 分不出这次要 key 是为了什么。宿主半边因此把 `ctx.web.search` / `ctx.web.fetch` 包进一个 `AsyncLocalStorage` 作用域,只有当 `credentials.resolve` 在这个作用域里被调用时才套用搜索规则:先看会话自己的 key(永远最优先),再看该工作区有没有勾「同步搜索 API」,勾了就依次用工作区 key、系统默认 key;没勾时先用插件面板自己持有的搜索默认 ref(若配了),最后才用搜索插件配置的那个 ref。旧版基于 Messages 的搜索仍然走原来的聊天层级,因为那条路线本身就是一次 messages 请求;
38
39
  - 会话列表来自 `sessionController` 服务(`list()`,会拆开它 `{items: […]}` 的信封),控件缺失 / 抛错 / 列表为空时再退到宿主 live 会话服务(`ctx.get('sessions').list()`),所以那种情况下面板仍能列出会话;侧栏藏起来的两种会话这里也藏:子代理会话(`origin: 'subagent'` 或带 `parentSession`)和**空会话**(建了但一句话都没发过,侧栏只显示当前开着的那个)。两种都不过滤的话,一个只有 1 个真实会话的工作区会列出 13 个;
39
40
  - 不是注册工作区的目录,只有侧栏真的会显示它才会被列出来。这件事宿主判不了:「空会话里只有当前开着的那个算数」是浏览器端才知道的事(`retainedBy.mainView`),所以宿主把这种目录连同每个会话的 `blank` 标记一起发出去,并用 `hiddenBlankDirs` 点名「里面全是空会话」的目录,由浏览器半边套用与侧栏一致的判据;
40
41
  - `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`shape`、`source`、`count`、`withoutCwd`、`subagents`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错、信封形状不认识,还是会话的 `cwd` 与任何工作区路径都对不上;
@@ -50,6 +51,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
50
51
  | GET | `/wsk-api/state` | 默认 ref 状态、工作区及其会话、覆盖项、失效标记 |
51
52
  | GET | `/wsk-api/resolve` | 当前会话此刻解析到哪一层、哪个 ref |
52
53
  | GET | `/wsk-api/check` | `?sessionId=…&path=…` 只读判定:生效层级、key、失效标记、服务商不匹配、`needsAttention` |
54
+ | POST | `/wsk-api/set`(搜索) | `{scope: 'search-default', key}` 绑定面板自己的搜索默认 key;`{useDefault: true}` 清除 |
53
55
  | POST | `/wsk-api/set` | `{scope: 'workspace' \| 'session', path \| sessionId, title, key, provider?}` 绑定 key;`{useDefault: true}` 或空 key 为清除 |
54
56
  | POST | `/wsk-api/test` | `{scope, path \| sessionId \| ref}`(或 `{key}`)真发一次请求并返回判定;`invalid` 打标记,`ok` 清标记 |
55
57
  | POST | `/wsk-api/clear-flag` | `{ref}` 或一个目标,清掉失效/配额标记 |
@@ -76,7 +78,7 @@ dsh plugin --profile web add dsh-workspace-api-key
76
78
  pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api-key
77
79
  ```
78
80
 
79
- 锁版本用 `dsh-workspace-api-key@0.2.8`;直接从仓库装则用 `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key`(git 来源的插件会在安装时现场构建)。
81
+ 锁版本用 `dsh-workspace-api-key@0.2.10`;直接从仓库装则用 `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key`(git 来源的插件会在安装时现场构建)。
80
82
 
81
83
  然后把 `"dsh-workspace-api-key"` 加进该 profile `package.json` 的 `dsh.profile.bundles`,重启 DSH。本地开发用 link 代替:
82
84
 
@@ -87,11 +89,11 @@ pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api
87
89
  ## 测试
88
90
 
89
91
  ```sh
90
- node test/host.test.mjs # 157 条断言
91
- node test/client.test.mjs # 162 条断言
92
+ node test/host.test.mjs # 165 条断言
93
+ node test/client.test.mjs # 170 条断言
92
94
  ```
93
95
 
94
- 两个套件都不需要启动 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 压过两者、两类搜索提醒,以及搜索连接测试。浏览器套件覆盖搜索相关界面:每个工作区行的「同步搜索 API」复选框、它发出的 `/wsk-api/search-sync`、提醒条与「使用说明」弹窗。
96
+ 两个套件都不需要启动 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`、提醒条与「使用说明」弹窗。
95
97
 
96
98
  ## 要求与限制
97
99
 
package/lib/client.js CHANGED
@@ -515,6 +515,8 @@ window.__ModuleLoader__.load({
515
515
  const [notice, setNotice] = useState(null)
516
516
  const [prompt, setPrompt] = useState(null)
517
517
  const [help, setHelp] = useState(false)
518
+ const [searchOpen, setSearchOpen] = useState(false)
519
+ const [searchDraft, setSearchDraft] = useState('')
518
520
  const [expanded, setExpanded] = useState(() => new Set())
519
521
 
520
522
  const alive = useRef(true)
@@ -661,6 +663,32 @@ window.__ModuleLoader__.load({
661
663
  }
662
664
  }
663
665
 
666
+ // 搜索默认 key:由插件自己持有一个 ref(DEEPSEEK_API_KEY_SEARCH),
667
+ // 搜索请求在没有会话/工作区层级 key 时优先用它,从而和聊天 key 分开计费。
668
+ const saveSearchDefault = async (key) => {
669
+ const trimmed = typeof key === 'string' ? key.trim() : ''
670
+ setBusy(true)
671
+ try {
672
+ const body = { scope: 'search-default' }
673
+ if (trimmed === '') body.useDefault = true
674
+ else body.key = trimmed
675
+ const next = await api('/set', { method: 'POST', body: JSON.stringify(body) })
676
+ applyState(next)
677
+ setSearchDraft('')
678
+ if (trimmed === '') setSearchOpen(false)
679
+ setNotice({
680
+ kind: 'ok',
681
+ text: trimmed === ''
682
+ ? '搜索默认 key 已清除:搜索改用官方搜索页里配的那把 key。'
683
+ : '搜索默认 key 已保存:搜索请求优先用它(工作区同步与会话单独配置仍然优先)。',
684
+ })
685
+ } catch (err) {
686
+ if (alive.current) setNotice({ kind: 'bad', text: `${err?.message ?? err}` })
687
+ } finally {
688
+ if (alive.current) setBusy(false)
689
+ }
690
+ }
691
+
664
692
  const runTest = async (id, title, body, key) => {
665
693
  setTesting(id)
666
694
  setNotice(null)
@@ -972,7 +1000,11 @@ window.__ModuleLoader__.load({
972
1000
  }),
973
1001
  h('span', null, '同步搜索 API'),
974
1002
  h('span', { className: 'wsk-check-note' },
975
- ws.searchSync === true ? '搜索用这里的 key' : '搜索用搜索插件里的 key')),
1003
+ ws.searchSync === true
1004
+ ? '搜索用这里的 key'
1005
+ : searchInfo?.defaultConfigured === true
1006
+ ? '搜索用搜索默认 key'
1007
+ : '搜索用搜索插件里的 key')),
976
1008
  ws.kind !== 'orphan' && searchInfo?.apiChannel === true && ws.searchKeyMasked !== undefined
977
1009
  ? h('div', { key: 'sk', className: 'wsk-hint', style: { marginTop: 2 } },
978
1010
  `搜索生效 key:${ws.searchKeyMasked || '未配置'}`,
@@ -1023,27 +1055,26 @@ window.__ModuleLoader__.load({
1023
1055
  h('h4', null, '推荐用法(两把 key,各管一件事)'),
1024
1056
  h('ol', null,
1025
1057
  h('li', null, '先到「设置 → 模型」配一把系统默认 key,例如就叫 ', h('code', null, 'default'),
1026
- '。没有单独配置的会话都会用它聊天。'),
1027
- h('li', null, '再到「设置 → 插件 → 网页搜索」打开「启用新版 Web 搜索 API」,',
1028
- '并在那里的 API Key 里配一把搜索专用的 key,例如 ', h('code', null, 'search-default'), '。'),
1029
- h('li', null, '平时不用再管:聊天用工作区/会话的 key,搜索用 ', h('code', null, 'search-default'),
1030
- ',两边计费互不影响。')),
1031
- h('h4', null, '层级(从高到低)'),
1058
+ '。没有单独配置的地方都用它聊天。'),
1059
+ h('li', null, '再回到本页的「搜索默认 key」,放一把搜索专用的 key,例如 ', h('code', null, 'search-default'),
1060
+ '。搜索请求默认用它,和聊天那把分开计费。'),
1061
+ h('li', null, '平时不用再管:聊天按工作区/会话的 key 走,搜索走搜索默认 key,两边账单互不影响。')),
1062
+ h('h4', null, '想按工作区核算搜索花费'),
1032
1063
  h('ul', null,
1033
- h('li', null, '会话单独配置 → 覆盖该会话的一切(聊天与搜索都用它)'),
1034
- h('li', null, '工作区专属 key → 该工作区下所有没单独配置的会话'),
1035
- h('li', null, '系统默认 key → 兜底'),
1036
- h('li', null, '搜索默认 key → 只有在工作区没勾「同步搜索 API」、且该会话没有单独配置时才用')),
1037
- h('h4', null, '「同步搜索 API」是什么'),
1038
- h('ul', null,
1039
- h('li', null, '勾选:该目录下会话的搜索/抓取请求改用上面那套层级 key(和聊天完全一致)。'),
1040
- h('li', null, '不勾:搜索用「搜索插件里配置的 key」(例如 ', h('code', null, 'search-default'), ')。'),
1041
- h('li', null, '会话单独配置的 key 永远优先——勾不勾都一样。'),
1042
- h('li', null, '这个开关只对「新版 Web 搜索 API」有效;旧的 Messages 搜索本来就用聊天 key。')),
1064
+ h('li', null, '在该工作区行勾上「同步搜索 API」:这个目录下会话的搜索就改用聊天那套层级 key',
1065
+ '(会话 key → 工作区 key → 系统默认),不再用搜索默认 key。'),
1066
+ h('li', null, '不勾的目录继续用搜索默认 key。两种方式可以同时存在,一个工作区一个样。')),
1067
+ h('h4', null, '搜索 key 的优先级(从高到低)'),
1068
+ h('ol', null,
1069
+ h('li', null, '会话单独配置的 key —— 该会话的聊天与搜索都用它,永远最优先'),
1070
+ h('li', null, '工作区勾了「同步搜索 API」—— 该工作区下会话的搜索改用:工作区 key → 系统默认 key'),
1071
+ h('li', null, '搜索默认 key —— 没勾同步时,搜索用它'),
1072
+ h('li', null, '官方「网页搜索」页里配的那把 —— 面板没配搜索默认 key 时的兜底',
1073
+ '(它默认就是系统默认 key ', h('code', null, 'DEEPSEEK_API_KEY'), ',与聊天共用)')),
1043
1074
  h('h4', null, '会被提醒的几种情况'),
1044
1075
  h('ul', null,
1045
1076
  h('li', null, '配了搜索 key,但没打开「启用新版 Web 搜索 API」→ 那把 key 其实不会被用到,面板顶部会提示。'),
1046
- h('li', null, '打开了新版搜索 API,但搜索 key 是空的 → 任何搜索都会失败,面板顶部会提示。'),
1077
+ h('li', null, '打开了新版搜索 API,但既没有搜索默认 key、官方那把也是空的 → 任何搜索都会失败,面板顶部会提示。'),
1047
1078
  h('li', null, '勾了同步,但该工作区既没有专属 key、系统默认 key 也没配 → 会提示先去配一把。')),
1048
1079
  h('h4', null, '原则'),
1049
1080
  h('div', { className: 'wsk-hint' },
@@ -1191,6 +1222,62 @@ window.__ModuleLoader__.load({
1191
1222
  onClick: () => runTest(DEFAULT_KEY, '系统默认', { ref: defaultRef?.ref ?? DEFAULT_REF }),
1192
1223
  }, testing === DEFAULT_KEY ? '测试中…' : '测试连接')))),
1193
1224
 
1225
+ searchInfo?.apiChannel === true
1226
+ ? h('div', { className: 'wsk-card' },
1227
+ h('h4', null, '搜索默认 key'),
1228
+ h('div', { className: 'wsk-row' },
1229
+ h('div', { className: 'wsk-row-main' },
1230
+ h('div', { className: 'wsk-name' },
1231
+ h('span', { key: 'r' }, searchInfo.defaultRef ?? 'DEEPSEEK_API_KEY_SEARCH'),
1232
+ searchInfo.defaultConfigured === true
1233
+ ? h('span', { key: 's', className: 'wsk-badge ok' }, '已配置')
1234
+ : h('span', { key: 's', className: 'wsk-badge' }, '未配置')),
1235
+ h('div', { className: 'wsk-hint' },
1236
+ '搜索请求默认用它,和聊天那把 key 分开计费。'
1237
+ + '工作区勾了「同步搜索 API」时改用该工作区的聊天层级;会话单独配置永远最优先。',
1238
+ searchInfo.defaultConfigured === true && searchInfo.defaultKeyMasked
1239
+ ? ` 当前值:${searchInfo.defaultKeyMasked}`
1240
+ : '')),
1241
+ h('div', { className: 'wsk-actions' },
1242
+ h('button', {
1243
+ type: 'button', className: 'wsk-btn', disabled: busy,
1244
+ onClick: () => setSearchOpen((open) => !open),
1245
+ }, searchOpen ? '收起' : '配置'))),
1246
+ searchOpen
1247
+ ? h('div', { className: 'wsk-editor' },
1248
+ h('input', {
1249
+ className: 'wsk-input',
1250
+ type: 'text',
1251
+ autoComplete: 'off',
1252
+ spellCheck: false,
1253
+ placeholder: 'sk-…(留空 = 用官方搜索页里配的那把)',
1254
+ value: searchDraft,
1255
+ onChange: (ev) => setSearchDraft(ev.target.value),
1256
+ onKeyDown: (ev) => { if (ev.key === 'Enter') saveSearchDefault(searchDraft) },
1257
+ }),
1258
+ h('div', { className: 'wsk-dialog-actions' },
1259
+ h('button', {
1260
+ type: 'button', className: 'wsk-btn primary', disabled: busy,
1261
+ onClick: () => saveSearchDefault(searchDraft),
1262
+ }, busy ? '保存中…' : '保存'),
1263
+ h('button', {
1264
+ type: 'button', className: 'wsk-btn',
1265
+ disabled: busy || searchDraft.trim() === '',
1266
+ onClick: () => runTest('__search_default__', '搜索默认', { scope: 'search-default', channel: 'search' }, searchDraft),
1267
+ }, testing === '__search_default__' ? '测试中…' : '用这把 key 测试'),
1268
+ searchInfo.defaultConfigured === true
1269
+ ? h('button', {
1270
+ type: 'button', className: 'wsk-btn', disabled: busy,
1271
+ onClick: () => saveSearchDefault(''),
1272
+ }, '清除')
1273
+ : null,
1274
+ h('button', {
1275
+ type: 'button', className: 'wsk-btn',
1276
+ onClick: () => setSearchOpen(false),
1277
+ }, '收起')))
1278
+ : null)
1279
+ : null,
1280
+
1194
1281
  h('div', { className: 'wsk-card', style: { background: 'transparent', border: 0, padding: 0 } },
1195
1282
  h('h4', { style: { marginBottom: 6 } }, `工作区(${grouped.length})`),
1196
1283
  h('div', { className: 'wsk-hint', style: { marginBottom: 6 } },
package/lib/index.js CHANGED
@@ -63,6 +63,13 @@ const FALLBACK_SEARCH_PROVIDER = 'deepseek-official'
63
63
  const FALLBACK_FETCH_PROVIDER = 'http'
64
64
  const SEARCH_PROVIDER_ID = 'deepseek-search-api'
65
65
  const SEARCH_FETCH_PROVIDER_ID = 'deepseek-fetch-api'
66
+ /**
67
+ * 插件自己持有的「搜索默认 key」。官方搜索页的密钥框写的是它那套 API 的
68
+ * `apiKeyEnv`(默认 `DEEPSEEK_API_KEY`,与聊天共用),所以想让搜索单独计费,
69
+ * 就得另立一把 key。这把 key 由面板管理、存在这个 ref 下,搜索时优先于官方页
70
+ * 配的那把——这样「聊天一把、搜索一把」不用去改官方配置。
71
+ */
72
+ const SEARCH_DEFAULT_REF = 'DEEPSEEK_API_KEY_SEARCH'
66
73
 
67
74
  /**
68
75
  * 搜索通道标记:包住 `ctx.web.search` / `ctx.web.fetch`,在它们的异步调用链里
@@ -245,7 +252,7 @@ function workspaceSynced(state, cwd) {
245
252
  * 新版 API 通道:会话单独 key → 工作区勾了同步 ? (工作区 key / 系统默认): 搜索插件自己的 ref
246
253
  * 旧 Messages 通道:搜索就在用聊天 key,固定返回搜索 ref。
247
254
  */
248
- function searchRefFor(ctx, state, { sessionId, cwd, web }) {
255
+ async function searchRefFor(ctx, state, { sessionId, cwd, web, searchDefault }) {
249
256
  const w = web ?? readWebSettings(ctx)
250
257
  if (!w.apiChannel) return w.searchKeyEnv
251
258
  const own = sessionOverrideRef(state, sessionId)
@@ -254,7 +261,9 @@ function searchRefFor(ctx, state, { sessionId, cwd, web }) {
254
261
  const hit = effectiveOverride(ctx, state, { sessionId, cwd })
255
262
  return hit.ref ?? DEFAULT_API_KEY_REF
256
263
  }
257
- return w.searchKeyEnv
264
+ // 没同步:插件自己的「搜索默认 key」优先,其次才是官方搜索页配的那把。
265
+ const configured = searchDefault ?? (await describeKey(ctx, SEARCH_DEFAULT_REF)).configured === true
266
+ return configured === true ? SEARCH_DEFAULT_REF : w.searchKeyEnv
258
267
  }
259
268
 
260
269
  // ───────────────── 会话列表(宿主 sessionController + live 兜底) ─────────────────
@@ -630,7 +639,7 @@ function maskKey(value) {
630
639
  * 保证显式配置过的 ref 一定被拦。
631
640
  */
632
641
  function interceptableRefs(ctx, state) {
633
- const refs = new Set(['DEEPSEEK_API_KEY'])
642
+ const refs = new Set(['DEEPSEEK_API_KEY', SEARCH_DEFAULT_REF])
634
643
  try {
635
644
  const llm = ctx.get('llm')
636
645
  const adapters = llm?.adapters
@@ -688,13 +697,23 @@ function readWebSettings(ctx) {
688
697
  } catch {
689
698
  settings = undefined
690
699
  }
691
- const read = (ns) => {
692
- try {
693
- const value = settings?.get?.(ns)
694
- return typeof value === 'object' && value !== null ? value : {}
695
- } catch {
696
- return {}
700
+ // settings 服务的真实形状是 describe() 返回「按 profile 条目 id 键控」的行(每行带 ns 与活值),
701
+ // 没有 get(ns):早期版本曾按 get 读取,结果永远拿到 undefined,搜索面板会整个消失。
702
+ let rows = []
703
+ try {
704
+ if (typeof settings?.describe === 'function') {
705
+ rows = settings.describe() ?? []
706
+ } else if (typeof settings?.get === 'function') {
707
+ rows = [SEARCH_API_NS, LEGACY_SEARCH_NS, WEB_NS]
708
+ .map((ns) => ({ ns, value: settings.get(ns) }))
709
+ .filter((row) => row.value !== undefined)
697
710
  }
711
+ } catch {
712
+ rows = []
713
+ }
714
+ const read = (ns) => {
715
+ const value = rows.find((row) => row?.ns === ns)?.value
716
+ return typeof value === 'object' && value !== null ? value : {}
698
717
  }
699
718
  const api = read(SEARCH_API_NS)
700
719
  const legacy = read(LEGACY_SEARCH_NS)
@@ -739,6 +758,7 @@ async function searchWarnings(ctx, state, web) {
739
758
  ? (await describeKey(ctx, DEFAULT_API_KEY_REF)).configured === true
740
759
  : (await describeKey(ctx, web.apiKeyEnv)).configured === true
741
760
  const apiKeyPresent = web.apiLiteralKey !== undefined || (web.apiKeyEnv !== DEFAULT_API_KEY_REF && apiKeyConfigured)
761
+ const searchDefaultConfigured = (await describeKey(ctx, SEARCH_DEFAULT_REF)).configured === true
742
762
 
743
763
  // 1) 配了新版搜索 API 的 key,但官方开关没开 → 那把 key 永远不会被读到。
744
764
  if (!web.apiChannel && apiKeyPresent) {
@@ -753,7 +773,7 @@ async function searchWarnings(ctx, state, web) {
753
773
  }
754
774
 
755
775
  // 2) 开了新版 API,却没有可用的搜索 key → 搜索会直接报缺少凭据。
756
- if (web.apiChannel && web.apiLiteralKey === undefined && !apiKeyConfigured) {
776
+ if (web.apiChannel && web.apiLiteralKey === undefined && !apiKeyConfigured && !searchDefaultConfigured) {
757
777
  out.push({
758
778
  code: 'search-key-missing',
759
779
  level: 'bad',
@@ -763,6 +783,18 @@ async function searchWarnings(ctx, state, web) {
763
783
  })
764
784
  }
765
785
 
786
+ // 2b) 面板里配了「搜索默认 key」,但新版 API 没开 → 这把 key 也不会被读到。
787
+ if (!web.apiChannel && searchDefaultConfigured) {
788
+ out.push({
789
+ code: 'search-default-idle',
790
+ level: 'warn',
791
+ title: '搜索默认 key 已配置,但「启用新版 Web 搜索 API」没打开',
792
+ detail: '现在搜索走的是 DeepSeek Messages 通道,用的是聊天那把 key,'
793
+ + `所以面板里的 ${SEARCH_DEFAULT_REF} 不会被使用。`
794
+ + '要让它生效,请到「设置 → 插件 → 网页搜索」打开那个开关;若暂时不想用新版 API,可以忽略本条。',
795
+ })
796
+ }
797
+
766
798
  // 3) 勾了「同步搜索 API」的工作区里,既没有工作区 key,也没有系统默认 key。
767
799
  const defaultConfigured = (await describeKey(ctx, DEFAULT_API_KEY_REF)).configured === true
768
800
  const syncPaths = Object.keys(state.searchSync ?? {}).filter((key) => state.searchSync[key] === true)
@@ -1225,7 +1257,14 @@ function installResolver(ctx, state) {
1225
1257
  const fallback = await original(DEFAULT_API_KEY_REF)
1226
1258
  if (fallback?.value !== undefined && fallback.value !== '') return fallback
1227
1259
  }
1228
- // ③ 没同步:用官方搜索插件自己配的那把 key(原样解析)。
1260
+ // ③ 没同步:插件面板里配的「搜索默认 key」优先,其次才是官方搜索页
1261
+ // 配的那把(默认与聊天共用 DEEPSEEK_API_KEY,所以更常见的路径是把
1262
+ // 搜索 key 放在面板里,聊天 key 留在系统默认)。
1263
+ const searchDefaultHit = await resolveKey(ctx, SEARCH_DEFAULT_REF)
1264
+ if (searchDefaultHit.value !== undefined && searchDefaultHit.value !== '') {
1265
+ log(`search ref ${ref} → ${SEARCH_DEFAULT_REF} (search default)`)
1266
+ return { value: searchDefaultHit.value, source: searchDefaultHit.source ?? 'plugin' }
1267
+ }
1229
1268
  return await original(ref)
1230
1269
  }
1231
1270
  }
@@ -1404,12 +1443,15 @@ async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, cl
1404
1443
  // 让用户勾了复选框之后马上能看到效果,而不用等到搜索失败才发现。
1405
1444
  const web = readWebSettings(ctx)
1406
1445
  const synced = state.searchSync?.[normPath(dir)] === true
1446
+ const searchDefaultConfigured = web.apiChannel && (await describeKey(ctx, SEARCH_DEFAULT_REF)).configured === true
1407
1447
  const searchKeyRef = web.apiChannel
1408
1448
  ? synced
1409
1449
  ? configured
1410
1450
  ? ref
1411
1451
  : DEFAULT_API_KEY_REF
1412
- : web.searchKeyEnv
1452
+ : searchDefaultConfigured
1453
+ ? SEARCH_DEFAULT_REF
1454
+ : web.searchKeyEnv
1413
1455
  : web.searchKeyEnv
1414
1456
  const searchKeyMasked = web.apiChannel ? maskKey((await resolveKey(ctx, searchKeyRef))?.value) : undefined
1415
1457
  return {
@@ -1546,6 +1588,8 @@ async function snapshot(ctx, state) {
1546
1588
  const searchKeyRef = web.searchKeyEnv
1547
1589
  const searchKeyInfo = await describeKey(ctx, searchKeyRef)
1548
1590
  const searchKeyHit = await resolveKey(ctx, searchKeyRef)
1591
+ const defaultSearchInfo = await describeKey(ctx, SEARCH_DEFAULT_REF)
1592
+ const defaultSearchHit = await resolveKey(ctx, SEARCH_DEFAULT_REF)
1549
1593
  const syncPaths = Object.keys(state.searchSync ?? {}).filter((key) => state.searchSync[key] === true)
1550
1594
  const warnings = await searchWarnings(ctx, state, web)
1551
1595
 
@@ -1600,6 +1644,12 @@ async function snapshot(ctx, state) {
1600
1644
  keySource: searchKeyInfo.source,
1601
1645
  keyWritable: searchKeyInfo.writable !== false,
1602
1646
  literalKey: web.searchLiteralKey !== undefined,
1647
+ // 面板自己持有的「搜索默认 key」(优先于官方页配的那把)。
1648
+ defaultRef: SEARCH_DEFAULT_REF,
1649
+ defaultConfigured: defaultSearchInfo.configured === true,
1650
+ defaultKeyMasked: maskKey(defaultSearchHit?.value),
1651
+ defaultSource: defaultSearchInfo.source,
1652
+ defaultWritable: defaultSearchInfo.writable !== false,
1603
1653
  syncCount: syncPaths.length,
1604
1654
  syncPaths,
1605
1655
  warnings,
@@ -1725,9 +1775,9 @@ async function searchStateOf(ctx, state, { sessionId, cwd }) {
1725
1775
  channel: 'api',
1726
1776
  apiChannel: true,
1727
1777
  sync,
1728
- ref: searchRefFor(ctx, state, { sessionId, cwd, web }),
1778
+ ref: await searchRefFor(ctx, state, { sessionId, cwd, web }),
1729
1779
  sessionOwnKey: sessionOverrideRef(state, sessionId) !== undefined,
1730
- keyConfigured: (await describeKey(ctx, searchRefFor(ctx, state, { sessionId, cwd, web }))).configured === true,
1780
+ keyConfigured: (await describeKey(ctx, await searchRefFor(ctx, state, { sessionId, cwd, web }))).configured === true,
1731
1781
  }
1732
1782
  }
1733
1783
 
@@ -1782,12 +1832,27 @@ async function handle(ctx, state, req, res) {
1782
1832
 
1783
1833
  if (pathname === `${API_BASE}/set` && method === 'POST') {
1784
1834
  const body = await readJson(req)
1835
+ const key = typeof body.key === 'string' ? body.key.trim() : ''
1836
+ // 搜索默认 key:不是「某个目录/会话」的覆盖,由插件自己持有一个 ref。
1837
+ if (body.scope === 'search-default') {
1838
+ if (body.useDefault === true || key === '') {
1839
+ await clearKey(ctx, SEARCH_DEFAULT_REF)
1840
+ delete state.invalid[SEARCH_DEFAULT_REF]
1841
+ saveState(state)
1842
+ sendJson(res, 200, { ok: true, cleared: true, ...(await snapshot(ctx, state)) })
1843
+ return
1844
+ }
1845
+ await storeKey(ctx, SEARCH_DEFAULT_REF, key)
1846
+ delete state.invalid[SEARCH_DEFAULT_REF]
1847
+ saveState(state)
1848
+ sendJson(res, 200, { ok: true, ...(await snapshot(ctx, state)) })
1849
+ return
1850
+ }
1785
1851
  const target = targetOf(body)
1786
1852
  if (target.error !== undefined) {
1787
1853
  sendJson(res, 400, { ok: false, error: target.error })
1788
1854
  return
1789
1855
  }
1790
- const key = typeof body.key === 'string' ? body.key.trim() : ''
1791
1856
  if (body.useDefault === true || key === '') {
1792
1857
  await clearKey(ctx, target.ref)
1793
1858
  delete state.overrides[target.ref]
@@ -1821,15 +1886,17 @@ async function handle(ctx, state, req, res) {
1821
1886
  let sourceRef = typeof body.ref === 'string' && body.ref !== '' ? body.ref : undefined
1822
1887
  if (key === undefined) {
1823
1888
  if (sourceRef === undefined) {
1824
- if (body.scope === 'session') {
1889
+ if (body.scope === 'search-default') {
1890
+ sourceRef = SEARCH_DEFAULT_REF
1891
+ } else if (body.scope === 'session') {
1825
1892
  const sessionId = typeof body.sessionId === 'string' && body.sessionId !== '' ? body.sessionId : undefined
1826
1893
  const cwd = typeof body.path === 'string' && body.path !== '' ? body.path : activeCwd(ctx)
1827
1894
  sourceRef = channel === 'search'
1828
- ? searchRefFor(ctx, state, { sessionId, cwd, web })
1895
+ ? await searchRefFor(ctx, state, { sessionId, cwd, web })
1829
1896
  : (sessionId === undefined ? undefined : sessionRef(sessionId))
1830
1897
  } else if (channel === 'search') {
1831
1898
  const dir = typeof body.path === 'string' && body.path !== '' ? body.path : undefined
1832
- sourceRef = searchRefFor(ctx, state, { cwd: dir, web })
1899
+ sourceRef = await searchRefFor(ctx, state, { cwd: dir, web })
1833
1900
  } else {
1834
1901
  const dir = typeof body.path === 'string' ? body.path : undefined
1835
1902
  const overrideRef = dir !== undefined && dir !== '' ? derivedRef(dir) : undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workspace-api-key",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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",