dsh-workspace-api-key 0.2.14 → 0.2.15
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 +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +44 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Host half (`lib/index.js`):
|
|
|
36
36
|
- matches that directory against `workspaceRegistry.list()` after normalizing both sides (absolute, trailing separator removed, lower-cased on Windows);
|
|
37
37
|
- 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;
|
|
38
38
|
- 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;
|
|
39
|
-
- tells search credentials apart from chat credentials
|
|
39
|
+
- 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;
|
|
40
40
|
- 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;
|
|
41
41
|
- 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;
|
|
42
42
|
- 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;
|
|
@@ -79,7 +79,7 @@ The command forwards its arguments to pnpm inside the profile directory, so the
|
|
|
79
79
|
pnpm add dsh-workspace-api-key # or: npm install dsh-workspace-api-key
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Pin a version with `dsh-workspace-api-key@0.2.
|
|
82
|
+
Pin a version with `dsh-workspace-api-key@0.2.15`, or install straight from the repository with `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key` (a git-hosted plugin is built from source on install).
|
|
83
83
|
|
|
84
84
|
Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `package.json` and restart DSH. For local development, use a link instead:
|
|
85
85
|
|
package/README.zh.md
CHANGED
|
@@ -36,7 +36,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
36
36
|
- 把该目录与 `workspaceRegistry.list()` 做规范化后比对(绝对化、去掉尾部分隔符、Windows 折大小写);
|
|
37
37
|
- 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
|
|
38
38
|
- 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
|
|
39
|
-
-
|
|
39
|
+
- 把「搜索请求」和「聊天请求」分开——**按 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 请求;
|
|
40
40
|
- 会话列表来自 `sessionController` 服务(`list()`,会拆开它 `{items: […]}` 的信封),控件缺失 / 抛错 / 列表为空时再退到宿主 live 会话服务(`ctx.get('sessions').list()`),所以那种情况下面板仍能列出会话;侧栏藏起来的两种会话这里也藏:子代理会话(`origin: 'subagent'` 或带 `parentSession`)和**空会话**(建了但一句话都没发过,侧栏只显示当前开着的那个)。两种都不过滤的话,一个只有 1 个真实会话的工作区会列出 13 个;
|
|
41
41
|
- 不是注册工作区的目录,只有侧栏真的会显示它才会被列出来。这件事宿主判不了:「空会话里只有当前开着的那个算数」是浏览器端才知道的事(`retainedBy.mainView`),所以宿主把这种目录连同每个会话的 `blank` 标记一起发出去,并用 `hiddenBlankDirs` 点名「里面全是空会话」的目录,由浏览器半边套用与侧栏一致的判据;
|
|
42
42
|
- `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`shape`、`source`、`count`、`withoutCwd`、`subagents`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错、信封形状不认识,还是会话的 `cwd` 与任何工作区路径都对不上;
|
|
@@ -79,7 +79,7 @@ dsh plugin --profile web add dsh-workspace-api-key
|
|
|
79
79
|
pnpm add dsh-workspace-api-key # 或者:npm install dsh-workspace-api-key
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
锁版本用 `dsh-workspace-api-key@0.2.
|
|
82
|
+
锁版本用 `dsh-workspace-api-key@0.2.15`;直接从仓库装则用 `dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key`(git 来源的插件会在安装时现场构建)。
|
|
83
83
|
|
|
84
84
|
然后把 `"dsh-workspace-api-key"` 加进该 profile `package.json` 的 `dsh.profile.bundles`,重启 DSH。本地开发用 link 代替:
|
|
85
85
|
|
package/lib/index.js
CHANGED
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
|
|
33
33
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
34
34
|
import { createHash } from 'node:crypto'
|
|
35
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
36
35
|
import { homedir } from 'node:os'
|
|
37
36
|
import { join, dirname, basename, resolve as pathResolve } from 'node:path'
|
|
38
37
|
|
|
@@ -72,13 +71,44 @@ const SEARCH_FETCH_PROVIDER_ID = 'deepseek-fetch-api'
|
|
|
72
71
|
const SEARCH_DEFAULT_REF = 'DEEPSEEK_API_KEY_SEARCH'
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
74
|
+
* 官方「网页搜索」配置的缓存。
|
|
75
|
+
*
|
|
76
|
+
* 早期版本包过 `ctx.web.search` / `ctx.web.fetch`(AsyncLocalStorage)来分辨
|
|
77
|
+
* 「这次取 key 的是搜索还是聊天」。那样做把插件挂进了 web 工具的调用链里:一旦
|
|
78
|
+
* wrapper 出问题,`web_search` / `web_fetch` 会一直挂到工具的 30/60 秒超时
|
|
79
|
+
* (连 `http://127.0.0.1:1/` 这种必然立刻失败的地址也一样)。0.2.15 起不再包
|
|
80
|
+
* `ctx.web`,改成**按 ref 分辨**:搜索 provider 解析的是搜索自己的 ref
|
|
81
|
+
* (官方页的 `apiKeyEnv`,被本插件接管后就是 `DEEPSEEK_API_KEY_SEARCH`),
|
|
82
|
+
* 聊天通道永远不会解析这个 ref。
|
|
83
|
+
*
|
|
84
|
+
* 配置本身用缓存:`credentials.resolve` 是热路径,不能每次都去 `settings.describe()`。
|
|
85
|
+
*/
|
|
86
|
+
/** 重新读一次官方「网页搜索」配置(同步,无缓存——配置随时可能被官方页改掉)。 */
|
|
87
|
+
function refreshWebCache(ctx) {
|
|
88
|
+
try {
|
|
89
|
+
return readWebSettings(ctx)
|
|
90
|
+
} catch {
|
|
91
|
+
return webDefaults()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 取官方「网页搜索」配置。 */
|
|
96
|
+
function currentWeb(ctx) {
|
|
97
|
+
return refreshWebCache(ctx)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 这次 `credentials.resolve` 是不是搜索/抓取在要 key。
|
|
102
|
+
* 只看 ref:搜索 ref 由官方页的 `apiKeyEnv` 决定,聊天通道不会用到它。
|
|
79
103
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
function isSearchRefFor(ctx, ref) {
|
|
105
|
+
if (typeof ref !== 'string' || ref === '') return false
|
|
106
|
+
const web = currentWeb(ctx)
|
|
107
|
+
if (web.apiChannel !== true) return false
|
|
108
|
+
// 官方页还没被接管时,搜索用的是聊天那把 ref:这时分辨不了,按聊天处理(安全)。
|
|
109
|
+
if (web.searchKeyEnv === DEFAULT_API_KEY_REF) return ref === SEARCH_DEFAULT_REF
|
|
110
|
+
return ref === SEARCH_DEFAULT_REF || ref === web.searchKeyEnv || ref === web.legacyKeyEnv
|
|
111
|
+
}
|
|
82
112
|
|
|
83
113
|
// 幂等标记:同一进程里可能被 apply 多次(热重载),只装一层 wrapper。
|
|
84
114
|
const INSTALL_KEY = Symbol.for('dsh-workspace-api-key/installed')
|
|
@@ -1262,8 +1292,8 @@ async function probeSearch(ctx, state, key, web) {
|
|
|
1262
1292
|
export function apply(ctx) {
|
|
1263
1293
|
const state = loadState()
|
|
1264
1294
|
|
|
1265
|
-
// ── 1. 包装 credentials.resolve
|
|
1266
|
-
|
|
1295
|
+
// ── 1. 包装 credentials.resolve(按 ref 分辨搜索/抓取通道)──────────
|
|
1296
|
+
refreshWebCache(ctx)
|
|
1267
1297
|
installResolver(ctx, state)
|
|
1268
1298
|
// 「直接禁用官方页面里的搜索 key 设置」:把官方 apiKeyEnv 指到插件自有 ref(异步、失败不影响启动)。
|
|
1269
1299
|
void reconcileOfficialSearchRef(ctx)
|
|
@@ -1316,54 +1346,6 @@ export function apply(ctx) {
|
|
|
1316
1346
|
)
|
|
1317
1347
|
}
|
|
1318
1348
|
|
|
1319
|
-
/**
|
|
1320
|
-
* 装上搜索/抓取通道标记:把 `ctx.web.search` / `ctx.web.fetch` 包一层
|
|
1321
|
-
* AsyncLocalStorage,让 resolve wrapper 知道「这次取 key 的是搜索,不是聊天」。
|
|
1322
|
-
* 装不上就静默跳过——不影响任何既有行为(只是无法区分通道)。
|
|
1323
|
-
*/
|
|
1324
|
-
function installWebScope(ctx) {
|
|
1325
|
-
if (globalThis[WEB_SCOPE_MARK] !== undefined) {
|
|
1326
|
-
log('web scope already installed; skipping')
|
|
1327
|
-
return
|
|
1328
|
-
}
|
|
1329
|
-
let web
|
|
1330
|
-
try {
|
|
1331
|
-
web = ctx.get('web')
|
|
1332
|
-
} catch {
|
|
1333
|
-
web = undefined
|
|
1334
|
-
}
|
|
1335
|
-
if (web === undefined) return
|
|
1336
|
-
|
|
1337
|
-
const originals = {}
|
|
1338
|
-
for (const method of ['search', 'fetch']) {
|
|
1339
|
-
if (typeof web[method] !== 'function') continue
|
|
1340
|
-
const original = web[method].bind(web)
|
|
1341
|
-
originals[method] = original
|
|
1342
|
-
try {
|
|
1343
|
-
web[method] = (...args) => searchScope.run(true, () => original(...args))
|
|
1344
|
-
} catch {
|
|
1345
|
-
delete originals[method]
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
if (Object.keys(originals).length === 0) return
|
|
1349
|
-
|
|
1350
|
-
globalThis[WEB_SCOPE_MARK] = { web, originals }
|
|
1351
|
-
ctx.effect(
|
|
1352
|
-
() => () => {
|
|
1353
|
-
for (const [method, original] of Object.entries(originals)) {
|
|
1354
|
-
try {
|
|
1355
|
-
web[method] = original
|
|
1356
|
-
} catch {
|
|
1357
|
-
/* ignore */
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
delete globalThis[WEB_SCOPE_MARK]
|
|
1361
|
-
},
|
|
1362
|
-
'workspace-api-key: restore web.search/fetch',
|
|
1363
|
-
)
|
|
1364
|
-
log(`web scope wrapped: ${Object.keys(originals).join(',')}`)
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
1349
|
/**
|
|
1368
1350
|
* 装上 resolve wrapper。装不上也不能影响宿主:任何异常都吞掉并退回原行为。
|
|
1369
1351
|
*/
|
|
@@ -1393,8 +1375,9 @@ function installResolver(ctx, state) {
|
|
|
1393
1375
|
|
|
1394
1376
|
// 搜索 / 抓取通道:官方「新版 Web 搜索 API」用的是独立的一套 key 配置,
|
|
1395
1377
|
// 语义与聊天不同,因此单独处理(旧 Messages 通道仍走下面的聊天层级)。
|
|
1396
|
-
|
|
1397
|
-
|
|
1378
|
+
// 分辨方式改成「看 ref」:搜索 provider 解析的一定是搜索自己的 ref,聊天不会解析它。
|
|
1379
|
+
if (isSearchRefFor(ctx, ref)) {
|
|
1380
|
+
const web = currentWeb(ctx)
|
|
1398
1381
|
if (web.apiChannel) {
|
|
1399
1382
|
// ① 会话单独设置的 key 永远优先——哪怕官方搜索插件里配了别的 key。
|
|
1400
1383
|
const sessionKey = sessionOverrideRef(state, sessionId)
|
|
@@ -1745,7 +1728,8 @@ async function snapshot(ctx, state) {
|
|
|
1745
1728
|
}
|
|
1746
1729
|
|
|
1747
1730
|
// ── 搜索 / 抓取通道总览 ──────────────────────────────────────────────
|
|
1748
|
-
|
|
1731
|
+
// 每次面板刷新都重读一次:开关改过之后,缓存必须跟着变。
|
|
1732
|
+
const web = refreshWebCache(ctx)
|
|
1749
1733
|
const searchKeyRef = web.searchKeyEnv
|
|
1750
1734
|
const searchKeyInfo = await describeKey(ctx, searchKeyRef)
|
|
1751
1735
|
const searchKeyHit = await resolveKey(ctx, searchKeyRef)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-api-key",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
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",
|