dsh-workspace-api-key 0.2.0 → 0.2.1
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 +5 -3
- package/README.zh.md +4 -2
- package/lib/client.js +62 -2
- package/lib/index.js +57 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Host half (`lib/index.js`):
|
|
|
32
32
|
- 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;
|
|
33
33
|
- 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;
|
|
34
34
|
- builds the session list from the `sessionController` service (`list()`), so the panel shows the same titles and model projections the sidebar does, and degrades to per-workspace only when that service is absent;
|
|
35
|
+
- reports its own version and a session probe (`available`, `count`, `withoutCwd`, `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()`, or a `cwd` that matches no workspace path;
|
|
35
36
|
- keeps its own bookkeeping (which level maps to which ref, invalid flags, the provider a key was recorded for) in `$DSH_HOME/storages/dsh-workspace-api-key.json`;
|
|
36
37
|
- observes failures through the `agent/request-error` waterfall without ever returning `{kind: 'retry'}` — it only records the verdict and calls `next()`.
|
|
37
38
|
|
|
@@ -41,7 +42,7 @@ The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (
|
|
|
41
42
|
|
|
42
43
|
| Method | Path | Purpose |
|
|
43
44
|
| --- | --- | --- |
|
|
44
|
-
| GET | `/wsk-api/state` | default ref status, workspaces with their sessions, overrides, invalid flags |
|
|
45
|
+
| GET | `/wsk-api/state` | default ref status, workspaces with their sessions, overrides, invalid flags, plugin version and session probe |
|
|
45
46
|
| GET | `/wsk-api/resolve` | which level and ref the current session resolves to right now |
|
|
46
47
|
| GET | `/wsk-api/check` | `?sessionId=…&path=…` — read-only verdict for one session: effective level, key, stale flag, provider mismatch, `needsAttention` |
|
|
47
48
|
| POST | `/wsk-api/set` | `{scope: 'workspace' \| 'session', path \| sessionId, title, key, provider?}` binds a key; `{useDefault: true}` or an empty key clears it |
|
|
@@ -70,8 +71,8 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
|
|
|
70
71
|
## Tests
|
|
71
72
|
|
|
72
73
|
```sh
|
|
73
|
-
node test/host.test.mjs #
|
|
74
|
-
node test/client.test.mjs #
|
|
74
|
+
node test/host.test.mjs # 111 assertions
|
|
75
|
+
node test/client.test.mjs # 106 assertions
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
Neither suite needs a running DSH process. The host suite drives `apply()` against a mock context (fake `credentials`, `workspaceRegistry`, `agents`, `sessionController`, `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, and state persistence. 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, the ungrouped-directory card, the stale and mismatch prompts, the "go configure" hand-off, graceful degradation without a session list, and back-navigation when `ctx.get('layout')` only becomes available after the plugin was applied.
|
|
@@ -85,6 +86,7 @@ Neither suite needs a running DSH process. The host suite drives `apply()` again
|
|
|
85
86
|
- 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.
|
|
86
87
|
- 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.
|
|
87
88
|
- Uninstalling the plugin leaves the generated `*_WS_*` / `*_SS_*` refs in the credentials file; remove them from the Models page.
|
|
89
|
+
- 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, when none of them match, sample session `cwd` values next to sample workspace paths.
|
|
88
90
|
|
|
89
91
|
## License
|
|
90
92
|
|
package/README.zh.md
CHANGED
|
@@ -32,6 +32,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
32
32
|
- 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
|
|
33
33
|
- 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
|
|
34
34
|
- 会话列表来自 `sessionController` 服务(`list()`),所以面板显示的标题与模型投影和侧栏一致;该服务缺失时自动降级为只能按工作区配置;
|
|
35
|
+
- `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`count`、`withoutCwd`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错,还是会话的 `cwd` 与任何工作区路径都对不上;
|
|
35
36
|
- 自己的账本(哪一层对应哪个 ref、失效标记、key 是给哪个服务商记的)落在 `$DSH_HOME/storages/dsh-workspace-api-key.json`;
|
|
36
37
|
- 通过 `agent/request-error` waterfall 观察失败,但**从不**返回 `{kind: 'retry'}`——只记录判定结果并 `next()` 放行。
|
|
37
38
|
|
|
@@ -70,8 +71,8 @@ pnpm add "dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key"
|
|
|
70
71
|
## 测试
|
|
71
72
|
|
|
72
73
|
```sh
|
|
73
|
-
node test/host.test.mjs #
|
|
74
|
-
node test/client.test.mjs #
|
|
74
|
+
node test/host.test.mjs # 111 条断言
|
|
75
|
+
node test/client.test.mjs # 106 条断言
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
两个套件都不需要启动 DSH。宿主套件用一个假 ctx(假的 `credentials`、`workspaceRegistry`、`agents`、`sessionController`、`webServer`,以及打桩的 `fetch`)驱动 `apply()`,覆盖 ref 派生、会话→工作区→默认的级联、工作区匹配、把第二个适配器的 `apiKeyEnv` 一起重定向、清回默认、401/402/429/5xx 的判定、回环与来源校验、状态落盘。浏览器套件通过一个极小的 `window.__ModuleLoader__` 与自制 React 运行时加载 `lib/client.js`,渲染出面板与覆盖层,断言槽注册、首屏渲染、会话折叠/展开、会话级保存与测试、未分组目录卡片、失效与不匹配提醒、「去配置」的交接、没有会话列表时的降级,以及 `ctx.get('layout')` 在插件 apply 之后才可用时返回键仍然有效。
|
|
@@ -85,6 +86,7 @@ node test/client.test.mjs # 94 条断言
|
|
|
85
86
|
- key 通过凭据服务存放在 `$DSH_HOME/.credentials.yaml` 的 `refs:` 里,明文,和默认 key 完全一样。它不会被写进会话日志、插件状态文件或插件目录。
|
|
86
87
|
- 匹配是规范化之后的文本比较。如果某个会话的 `cwd` 是指向已注册工作区路径的符号链接或 junction,则匹配不上,会走上层层级。
|
|
87
88
|
- 卸载插件不会删除已生成的 `*_WS_*` / `*_SS_*` ref,可在「设置 → 模型」里清掉。
|
|
89
|
+
- 如果面板列出了工作区却没有可展开的会话,请看面板底部的诊断行。出现红色「宿主端插件还是旧版本」横幅,说明 DSH 进程没有重启过:宿主半边只在启动时加载,关窗口和刷新页面都不算——请完全退出 DSH Desktop 再打开。其余情况诊断行会写明宿主看到多少个会话;一个都对不上时,还会并排给出会话 `cwd` 样本与工作区路径样本,便于比对。
|
|
88
90
|
|
|
89
91
|
## 许可
|
|
90
92
|
|
package/lib/client.js
CHANGED
|
@@ -68,6 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
background: transparent; color: inherit; font: inherit; font-size: 12px; }
|
|
69
69
|
.wsk-input:focus { outline: 2px solid color-mix(in srgb, #2563eb 45%, transparent); outline-offset: -1px; }
|
|
70
70
|
.wsk-hint { color: var(--wsk-dim); font-size: 11px; line-height: 1.6; }
|
|
71
|
+
.wsk-diag { color: var(--wsk-dim); font-size: 10px; line-height: 1.6; margin-top: 4px; word-break: break-all; }
|
|
71
72
|
.wsk-msg { padding: 7px 10px; border-radius: 6px; font-size: 12px; line-height: 1.5; }
|
|
72
73
|
.wsk-msg.ok { background: color-mix(in srgb, var(--wsk-ok) 12%, transparent); color: var(--wsk-ok); }
|
|
73
74
|
.wsk-msg.bad { background: color-mix(in srgb, var(--wsk-bad) 12%, transparent); color: var(--wsk-bad); }
|
|
@@ -424,6 +425,42 @@ window.__ModuleLoader__.load({
|
|
|
424
425
|
const defaultRef = state?.defaultRef
|
|
425
426
|
const providers = state?.providers ?? []
|
|
426
427
|
|
|
428
|
+
// ── 诊断 ──────────────────────────────────────────────────────────────
|
|
429
|
+
// 面板里「看不到可展开的会话」有两个完全不同的原因,修法也不同:
|
|
430
|
+
// 1) 宿主半边还是旧版本(只刷新了页面 / 没重启应用)→ 响应里没有 storeVersion;
|
|
431
|
+
// 2) 宿主是新版本,但会话列表服务不可用或返回 0 行 → 看 sessionProbe。
|
|
432
|
+
// 这里把两者都显式画出来,用户截一张图就能定位。
|
|
433
|
+
const probe = state?.sessionProbe
|
|
434
|
+
const hostIsStale = state !== null && state.storeVersion === undefined
|
|
435
|
+
const sessionTotal = [...grouped, ...ungrouped].reduce(
|
|
436
|
+
(sum, ws) => sum + (Array.isArray(ws.sessions) ? ws.sessions.length : 0), 0)
|
|
437
|
+
|
|
438
|
+
const diagText = () => {
|
|
439
|
+
if (state === null) return undefined
|
|
440
|
+
const hostVersion = typeof state.pluginVersion === 'string'
|
|
441
|
+
? state.pluginVersion
|
|
442
|
+
: '旧版(未重启应用)'
|
|
443
|
+
const sessions = probe === undefined
|
|
444
|
+
? '未知'
|
|
445
|
+
: probe.available !== true
|
|
446
|
+
? '不可用'
|
|
447
|
+
: `${probe.count} 个`
|
|
448
|
+
const listError = probe?.error !== undefined ? ` · list 报错:${probe.error}` : ''
|
|
449
|
+
const noCwd = probe?.withoutCwd > 0 ? ` · ${probe.withoutCwd} 个没有 cwd` : ''
|
|
450
|
+
return `插件 v${hostVersion} · 状态存储 v${state.storeVersion ?? '—'} · 会话列表:${sessions}${listError}${noCwd}`
|
|
451
|
+
+ ` · 工作区 ${grouped.length} + 未分组 ${ungrouped.length} · 挂在目录下的会话 ${sessionTotal}`
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const diagSamples = () => {
|
|
455
|
+
if (state === null || probe === undefined) return null
|
|
456
|
+
const cwds = Array.isArray(probe.samples) ? probe.samples : []
|
|
457
|
+
const paths = Array.isArray(state.workspacePathSample) ? state.workspacePathSample : []
|
|
458
|
+
if (cwds.length === 0 && paths.length === 0) return null
|
|
459
|
+
return h('div', { className: 'wsk-hint', style: { marginTop: 4 } },
|
|
460
|
+
cwds.length > 0 ? h('div', null, `会话 cwd 样本:${cwds.join(' | ')}`) : null,
|
|
461
|
+
paths.length > 0 ? h('div', null, `工作区路径样本:${paths.join(' | ')}`) : null)
|
|
462
|
+
}
|
|
463
|
+
|
|
427
464
|
const providerOptions = (current, fallback) => {
|
|
428
465
|
const list = [...providers]
|
|
429
466
|
for (const candidate of [current, fallback]) {
|
|
@@ -704,9 +741,31 @@ window.__ModuleLoader__.load({
|
|
|
704
741
|
`读取失败:${error}`,
|
|
705
742
|
h('div', { className: 'wsk-hint' }, '请确认 DSH Web 服务正在运行、且本插件的宿主半边已加载。'))
|
|
706
743
|
: null,
|
|
744
|
+
hostIsStale
|
|
745
|
+
? h('div', { className: 'wsk-msg bad', style: { marginBottom: 12 } },
|
|
746
|
+
'宿主端插件还是旧版本:所以看不到会话列表,也没有按会话配置。',
|
|
747
|
+
h('div', { className: 'wsk-hint' },
|
|
748
|
+
'请「完全退出」DSH Desktop 再重新打开(关闭窗口 / 刷新页面都不够:宿主代码只在启动时加载)。'),
|
|
749
|
+
h('div', { className: 'wsk-hint' }, diagText()))
|
|
750
|
+
: null,
|
|
707
751
|
state !== null && state.sessionListAvailable === false
|
|
708
752
|
? h('div', { className: 'wsk-msg info', style: { marginBottom: 12 } },
|
|
709
|
-
'当前 DSH 版本没有提供会话列表服务,只能按工作区配置 key。'
|
|
753
|
+
'当前 DSH 版本没有提供会话列表服务,只能按工作区配置 key。',
|
|
754
|
+
h('div', { className: 'wsk-hint' }, diagText()))
|
|
755
|
+
: null,
|
|
756
|
+
!hostIsStale && probe?.available === true && probe.count === 0
|
|
757
|
+
? h('div', { className: 'wsk-msg info', style: { marginBottom: 12 } },
|
|
758
|
+
'宿主报告 0 个会话,所以没有可展开的列表。',
|
|
759
|
+
h('div', { className: 'wsk-hint' },
|
|
760
|
+
'只有「会话记录里带 cwd」的会话才能挂到工作区/未分组目录下。'),
|
|
761
|
+
h('div', { className: 'wsk-hint' }, diagText()),
|
|
762
|
+
diagSamples())
|
|
763
|
+
: null,
|
|
764
|
+
!hostIsStale && (probe?.count ?? 0) > 0 && sessionTotal === 0
|
|
765
|
+
? h('div', { className: 'wsk-msg info', style: { marginBottom: 12 } },
|
|
766
|
+
'拿到会话了,但没有一个能对上工作区/目录(路径不匹配)。',
|
|
767
|
+
h('div', { className: 'wsk-hint' }, diagText()),
|
|
768
|
+
diagSamples())
|
|
710
769
|
: null,
|
|
711
770
|
|
|
712
771
|
h('div', { className: 'wsk-card' },
|
|
@@ -758,7 +817,8 @@ window.__ModuleLoader__.load({
|
|
|
758
817
|
|
|
759
818
|
h('div', { className: 'wsk-hint' },
|
|
760
819
|
'专属 key 存进 DSH 凭据库中的独立引用,因此换 key 不写进会话日志、也不影响历史回放。',
|
|
761
|
-
state?.statePath ? h('div', null, `状态文件:${state.statePath}`) : null
|
|
820
|
+
state?.statePath ? h('div', null, `状态文件:${state.statePath}`) : null,
|
|
821
|
+
diagText() !== undefined ? h('div', { className: 'wsk-diag' }, diagText()) : null)),
|
|
762
822
|
|
|
763
823
|
renderPrompt())
|
|
764
824
|
}
|
package/lib/index.js
CHANGED
|
@@ -57,6 +57,20 @@ function log(...args) {
|
|
|
57
57
|
if (DEBUG) console.log('[workspace-api-key]', ...args)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 运行中的宿主半边版本(读自己的 package.json)。
|
|
62
|
+
* 界面把它显示出来是为了区分「宿主半边是旧版本(例如只刷新了页面而没重启应用)」
|
|
63
|
+
* 和「宿主是新版本、只是拿不到会话列表」——这两种情况的修法完全不同。
|
|
64
|
+
*/
|
|
65
|
+
const PLUGIN_VERSION = (() => {
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
68
|
+
return typeof pkg?.version === 'string' ? pkg.version : undefined
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
})()
|
|
73
|
+
|
|
60
74
|
// ─────────────────────────── 路径 / 工作区键 ───────────────────────────
|
|
61
75
|
|
|
62
76
|
/** 统一的路径比较用规范形:绝对化 + 分隔符统一 + Windows 折大小写。 */
|
|
@@ -200,23 +214,48 @@ function sessionListAvailable(ctx) {
|
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
/**
|
|
204
|
-
|
|
217
|
+
/**
|
|
218
|
+
* 会话摘要 + 诊断信息。界面靠 available / count / error / 样本路径来解释
|
|
219
|
+
* 「为什么面板里看不到会话」:服务没挂载、list() 抛错、行里没有 cwd,还是
|
|
220
|
+
* 会话的 cwd 与工作区路径对不上。
|
|
221
|
+
*/
|
|
222
|
+
async function sessionProbe(ctx) {
|
|
223
|
+
let controller
|
|
205
224
|
try {
|
|
206
225
|
controller = ctx.get('sessionController')
|
|
207
|
-
} catch {
|
|
208
|
-
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return { available: false, rows: [], count: 0, error: String(err?.message ?? err) }
|
|
228
|
+
}
|
|
229
|
+
if (controller === undefined) return { available: false, rows: [], count: 0 }
|
|
230
|
+
if (typeof controller.list !== 'function') {
|
|
231
|
+
return { available: false, rows: [], count: 0, error: 'sessionController.list is not a function' }
|
|
209
232
|
}
|
|
210
|
-
if (controller === undefined || typeof controller.list !== 'function') return []
|
|
211
233
|
try {
|
|
212
234
|
const rows = await controller.list()
|
|
213
|
-
|
|
235
|
+
const list = Array.isArray(rows) ? rows : []
|
|
236
|
+
const samples = []
|
|
237
|
+
let withoutCwd = 0
|
|
238
|
+
for (const row of list) {
|
|
239
|
+
const cwd = typeof row?.cwd === 'string' && row.cwd !== '' ? row.cwd : undefined
|
|
240
|
+
if (cwd === undefined) {
|
|
241
|
+
withoutCwd += 1
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (samples.length < 5 && !samples.includes(cwd)) samples.push(cwd)
|
|
245
|
+
}
|
|
246
|
+
log('sessionController.list ok:', list.length, 'rows,', withoutCwd, 'without cwd')
|
|
247
|
+
return { available: true, rows: list, count: list.length, withoutCwd, samples }
|
|
214
248
|
} catch (err) {
|
|
215
249
|
log('sessionController.list failed:', String(err?.message ?? err))
|
|
216
|
-
return []
|
|
250
|
+
return { available: true, rows: [], count: 0, error: String(err?.message ?? err) }
|
|
217
251
|
}
|
|
218
252
|
}
|
|
219
253
|
|
|
254
|
+
/** 只要会话摘要(含冷会话)。服务缺失 / 出错时为空数组,界面退化成只列工作区。 */
|
|
255
|
+
async function sessionSummaries(ctx) {
|
|
256
|
+
return (await sessionProbe(ctx)).rows
|
|
257
|
+
}
|
|
258
|
+
|
|
220
259
|
/** 从摘要里取「当前会话在用的模型选择」,形状 {provider, model}。 */
|
|
221
260
|
function modelSelectionOf(row) {
|
|
222
261
|
const values = row?.projections?.values
|
|
@@ -895,7 +934,8 @@ async function snapshot(ctx, state) {
|
|
|
895
934
|
const defaultRefName = 'DEEPSEEK_API_KEY'
|
|
896
935
|
const defaultInfo = await describeKey(ctx, defaultRefName)
|
|
897
936
|
const defaultHit = await resolveKey(ctx, defaultRefName)
|
|
898
|
-
const
|
|
937
|
+
const probe = await sessionProbe(ctx)
|
|
938
|
+
const rows = probe.rows
|
|
899
939
|
const byCwd = new Map()
|
|
900
940
|
for (const row of rows) {
|
|
901
941
|
const norm = normPath(row?.cwd)
|
|
@@ -977,7 +1017,16 @@ async function snapshot(ctx, state) {
|
|
|
977
1017
|
|
|
978
1018
|
return {
|
|
979
1019
|
ok: true,
|
|
1020
|
+
pluginVersion: PLUGIN_VERSION,
|
|
980
1021
|
storeVersion: STATE_VERSION,
|
|
1022
|
+
sessionProbe: {
|
|
1023
|
+
available: probe.available,
|
|
1024
|
+
count: probe.count,
|
|
1025
|
+
withoutCwd: probe.withoutCwd ?? 0,
|
|
1026
|
+
error: probe.error,
|
|
1027
|
+
samples: probe.samples ?? [],
|
|
1028
|
+
},
|
|
1029
|
+
workspacePathSample: workspaces.slice(0, 5).map((ws) => ws.path),
|
|
981
1030
|
defaultRef: {
|
|
982
1031
|
ref: defaultRefName,
|
|
983
1032
|
configured: defaultInfo.configured === true,
|
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.1",
|
|
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",
|