dsh-workspace-api-key 0.2.2 → 0.2.4
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 +3 -3
- package/lib/client.js +107 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Host half (`lib/index.js`):
|
|
|
36
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`;
|
|
37
37
|
- observes failures through the `agent/request-error` waterfall without ever returning `{kind: 'retry'}` — it only records the verdict and calls `next()`.
|
|
38
38
|
|
|
39
|
-
The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (`order: 1`), the `main` slot (key `workspace-api-key`) and the `shell.overlay` slot (the entering-a-session warning), the same slots `dsh-skill-mcp-panel` and `dsh-429-guard` use. It is plain `React.createElement` with no build step and no bundled dependencies. It merges the host rows with the browser-side `sessions` store (`ctx.get('sessions').list.getSnapshot()`, which is what the sidebar itself renders), so session titles come from `displayTitle
|
|
39
|
+
The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (`order: 1`), the `main` slot (key `workspace-api-key`) and the `shell.overlay` slot (the entering-a-session warning), the same slots `dsh-skill-mcp-panel` and `dsh-429-guard` use. It is plain `React.createElement` with no build step and no bundled dependencies. It merges the host rows with the browser-side `sessions` store (`ctx.get('sessions').list.getSnapshot()`, which is what the sidebar itself renders), so session titles come from `displayTitle`, host rows are never dropped when that store only knows a subset, and sessions stay visible even when the host cannot list them. That store is provided by `dsh-api-session-controller` *after* the plugin's `apply()`, so both the panel and the overlay retry for it (every 500 ms, at most 40 times) instead of giving up on the first render. The overlay asks `/wsk-api/check?sessionId=…&path=…` about the session that currently owns the main view, asks at most once per session, and its buttons only ever navigate to the panel — the panel is where a key can actually be changed.
|
|
40
40
|
|
|
41
41
|
### HTTP API (loopback only)
|
|
42
42
|
|
|
@@ -72,7 +72,7 @@ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `p
|
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
node test/host.test.mjs # 125 assertions
|
|
75
|
-
node test/client.test.mjs #
|
|
75
|
+
node test/client.test.mjs # 130 assertions
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
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, and the session probe including the `{items}` envelope and the live-session fallback. 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, and back-navigation when `ctx.get('layout')` only becomes available after the plugin was applied.
|
package/README.zh.md
CHANGED
|
@@ -36,7 +36,7 @@ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek k
|
|
|
36
36
|
- 自己的账本(哪一层对应哪个 ref、失效标记、key 是给哪个服务商记的)落在 `$DSH_HOME/storages/dsh-workspace-api-key.json`;
|
|
37
37
|
- 通过 `agent/request-error` waterfall 观察失败,但**从不**返回 `{kind: 'retry'}`——只记录判定结果并 `next()` 放行。
|
|
38
38
|
|
|
39
|
-
浏览器半边(`lib/client.js`)注册到 `sidebar.panellist` 槽(`order: 1`)、`main` 槽(key 为 `workspace-api-key`)与 `shell.overlay` 槽(点进会话时的提醒),和 `dsh-skill-mcp-panel`、`dsh-429-guard` 用的是同一套槽。全部是朴素的 `React.createElement`,没有构建步骤,也不打包任何依赖。它会把宿主行与浏览器端 `sessions` 存储(`ctx.get('sessions').list.getSnapshot()`,也就是侧栏自己渲染的数据)合并起来,因此标题取自 `displayTitle
|
|
39
|
+
浏览器半边(`lib/client.js`)注册到 `sidebar.panellist` 槽(`order: 1`)、`main` 槽(key 为 `workspace-api-key`)与 `shell.overlay` 槽(点进会话时的提醒),和 `dsh-skill-mcp-panel`、`dsh-429-guard` 用的是同一套槽。全部是朴素的 `React.createElement`,没有构建步骤,也不打包任何依赖。它会把宿主行与浏览器端 `sessions` 存储(`ctx.get('sessions').list.getSnapshot()`,也就是侧栏自己渲染的数据)合并起来,因此标题取自 `displayTitle`,浏览器端只认识一部分会话时也不会把宿主行挤掉,宿主列不出会话时面板也照样有会话可列。这个存储由 `dsh-api-session-controller` 在插件 `apply()` **之后**才提供,所以面板与覆盖层都会为它重试(每 500ms 一次,最多 40 次),而不是首次渲染拿不到就放弃。覆盖层向 `/wsk-api/check?sessionId=…&path=…` 查询当前占据主视图的会话,每个会话最多问一次,它的按钮只负责把你带到面板——改 key 只能由面板里的操作完成。
|
|
40
40
|
|
|
41
41
|
### HTTP 接口(仅限回环)
|
|
42
42
|
|
|
@@ -72,10 +72,10 @@ pnpm add "dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key"
|
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
node test/host.test.mjs # 125 条断言
|
|
75
|
-
node test/client.test.mjs #
|
|
75
|
+
node test/client.test.mjs # 130 条断言
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
两个套件都不需要启动 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
|
|
78
|
+
两个套件都不需要启动 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 之后才可用时返回键仍然有效。
|
|
79
79
|
|
|
80
80
|
## 要求与限制
|
|
81
81
|
|
package/lib/client.js
CHANGED
|
@@ -212,6 +212,11 @@ window.__ModuleLoader__.load({
|
|
|
212
212
|
return parts[parts.length - 1] ?? p
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** 会话按最近使用排序。 */
|
|
216
|
+
function sortByUpdated(rows) {
|
|
217
|
+
return rows.slice().sort((a, b) => (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0))
|
|
218
|
+
}
|
|
219
|
+
|
|
215
220
|
/** 浏览器端 sessions 服务快照 → 顶层会话条目(子代理会话不算)。 */
|
|
216
221
|
function clientSessionEntries(sessions) {
|
|
217
222
|
const snap = sessions?.list?.getSnapshot?.()
|
|
@@ -237,9 +242,10 @@ window.__ModuleLoader__.load({
|
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* /state
|
|
245
|
+
* 用浏览器端的会话列表补全「工作区/目录 → 会话」的树。
|
|
246
|
+
* 宿主行始终保留(不会被浏览器端列表挤掉),浏览器端额外只做两件事:
|
|
247
|
+
* 给同名会话补标题/cwd,以及把宿主漏掉的会话补进来(层级从 /state 的
|
|
248
|
+
* 覆盖记录推出)。这样宿主列表为空时面板照样有会话可列。
|
|
243
249
|
*/
|
|
244
250
|
function mergeSessions(state, sessions) {
|
|
245
251
|
const entries = clientSessionEntries(sessions)
|
|
@@ -261,18 +267,27 @@ window.__ModuleLoader__.load({
|
|
|
261
267
|
if (key !== undefined) workspaceByPath.set(key, ws)
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
const
|
|
270
|
+
// 第一遍:宿主行能补什么就补什么,宿主没有的会话合成一行。
|
|
271
|
+
const enriched = new Map()
|
|
272
|
+
const additions = []
|
|
266
273
|
for (const entry of entries) {
|
|
267
274
|
const host = hostRows.get(entry.sessionId)
|
|
268
|
-
|
|
275
|
+
if (host !== undefined) {
|
|
276
|
+
const patch = {}
|
|
277
|
+
if ((host.title === undefined || host.title === '') && entry.title !== undefined) patch.title = entry.title
|
|
278
|
+
if ((host.cwd === undefined || host.cwd === '') && entry.cwd !== undefined) patch.cwd = entry.cwd
|
|
279
|
+
if (Object.keys(patch).length > 0) enriched.set(entry.sessionId, { ...host, ...patch })
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
const cwd = entry.cwd
|
|
269
283
|
const pathKey = normKey(cwd)
|
|
270
284
|
const ws = pathKey !== undefined ? workspaceByPath.get(pathKey) : undefined
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
const own = overrideBySession.get(entry.sessionId)
|
|
286
|
+
const level = own !== undefined ? 'session' : ws?.configured === true ? 'workspace' : 'default'
|
|
287
|
+
additions.push({
|
|
288
|
+
pathKey,
|
|
289
|
+
workspace: ws,
|
|
290
|
+
row: {
|
|
276
291
|
sessionId: entry.sessionId,
|
|
277
292
|
title: entry.title,
|
|
278
293
|
cwd,
|
|
@@ -297,45 +312,50 @@ window.__ModuleLoader__.load({
|
|
|
297
312
|
effectiveTitle: undefined,
|
|
298
313
|
invalid: (level === 'session' ? own?.invalid : level === 'workspace' ? ws?.invalid : null) ?? null,
|
|
299
314
|
mismatch: null,
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
const patch = {}
|
|
303
|
-
if ((row.title === undefined || row.title === '') && entry.title !== undefined) patch.title = entry.title
|
|
304
|
-
if ((row.cwd === undefined || row.cwd === '') && cwd !== undefined) patch.cwd = cwd
|
|
305
|
-
if (Object.keys(patch).length > 0) row = { ...row, ...patch }
|
|
306
|
-
}
|
|
307
|
-
const bucketKey = pathKey ?? ''
|
|
308
|
-
const target = ws !== undefined ? buckets : extras
|
|
309
|
-
const list = target.get(ws !== undefined ? ws.path : bucketKey) ?? []
|
|
310
|
-
list.push(row)
|
|
311
|
-
target.set(ws !== undefined ? ws.path : bucketKey, list)
|
|
315
|
+
},
|
|
316
|
+
})
|
|
312
317
|
}
|
|
313
318
|
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
const fill = (rows) => (Array.isArray(rows) ? rows : [])
|
|
320
|
+
.map((row) => enriched.get(row?.sessionId) ?? row)
|
|
321
|
+
|
|
322
|
+
// 第二遍:宿主结构照旧,浏览器端多出来的会话按 cwd 归档。
|
|
323
|
+
const workspaces = (state.workspaces ?? []).map((ws) => {
|
|
324
|
+
const rows = fill(ws.sessions)
|
|
325
|
+
for (const item of additions) {
|
|
326
|
+
if (item.workspace?.path === ws.path) rows.push(item.row)
|
|
327
|
+
}
|
|
328
|
+
return { ...ws, sessions: sortByUpdated(rows) }
|
|
329
|
+
})
|
|
330
|
+
|
|
318
331
|
const ungrouped = []
|
|
332
|
+
const extras = new Map()
|
|
333
|
+
for (const item of additions) {
|
|
334
|
+
if (item.workspace !== undefined) continue
|
|
335
|
+
const key = item.pathKey ?? ''
|
|
336
|
+
const list = extras.get(key) ?? []
|
|
337
|
+
list.push(item.row)
|
|
338
|
+
extras.set(key, list)
|
|
339
|
+
}
|
|
340
|
+
for (const group of (state.ungrouped ?? [])) {
|
|
341
|
+
const key = normKey(group?.path) ?? String(group?.path ?? '')
|
|
342
|
+
const rows = fill(group.sessions)
|
|
343
|
+
for (const row of (extras.get(key) ?? [])) rows.push(row)
|
|
344
|
+
extras.delete(key)
|
|
345
|
+
ungrouped.push({ ...group, sessions: sortByUpdated(rows) })
|
|
346
|
+
}
|
|
319
347
|
for (const [key, rows] of extras) {
|
|
320
|
-
const
|
|
321
|
-
const path = known?.path ?? rows[0]?.cwd ?? undefined
|
|
348
|
+
const path = key !== '' ? key : undefined
|
|
322
349
|
ungrouped.push({
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}),
|
|
331
|
-
sessions: sorted(rows),
|
|
350
|
+
id: undefined,
|
|
351
|
+
path: rows[0]?.cwd ?? path,
|
|
352
|
+
title: path !== undefined && path !== '' ? dirName(path) : '(没有目录)',
|
|
353
|
+
kind: 'directory',
|
|
354
|
+
configured: false,
|
|
355
|
+
exists: true,
|
|
356
|
+
sessions: sortByUpdated(rows),
|
|
332
357
|
})
|
|
333
358
|
}
|
|
334
|
-
for (const group of (state.ungrouped ?? [])) {
|
|
335
|
-
if (!ungrouped.some((item) => item.path === group.path)) {
|
|
336
|
-
ungrouped.push({ ...group, sessions: sorted(buckets.get(group.path) ?? []) })
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
359
|
return { ...state, workspaces, ungrouped, sessionSource: 'client' }
|
|
340
360
|
}
|
|
341
361
|
|
|
@@ -485,12 +505,31 @@ window.__ModuleLoader__.load({
|
|
|
485
505
|
// 浏览器端的会话列表(含标题、cwd)会随时变化:变了就重渲染。
|
|
486
506
|
const sessionStore = sessionsService(props?.sessions)
|
|
487
507
|
const [sessionTick, setSessionTick] = useState(0)
|
|
508
|
+
// 这个服务由 dsh-api-session-controller 在 effect 里晚于插件 apply 提供:
|
|
509
|
+
// 首次渲染拿不到就轮询一段时间,否则面板永远等不到会话列表。
|
|
510
|
+
const [storeMissing, setStoreMissing] = useState(false)
|
|
488
511
|
useEffect(() => {
|
|
489
512
|
const store = sessionStore?.list
|
|
490
|
-
if (store === undefined || store === null || typeof store.subscribe !== 'function')
|
|
513
|
+
if (store === undefined || store === null || typeof store.subscribe !== 'function') {
|
|
514
|
+
setStoreMissing(true)
|
|
515
|
+
return undefined
|
|
516
|
+
}
|
|
517
|
+
setStoreMissing(false)
|
|
491
518
|
const off = store.subscribe(() => setSessionTick((tick) => tick + 1))
|
|
492
519
|
return () => { if (typeof off === 'function') off() }
|
|
493
520
|
}, [sessionStore])
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
if (!storeMissing) return undefined
|
|
523
|
+
let tries = 0
|
|
524
|
+
const timer = setInterval(() => {
|
|
525
|
+
tries += 1
|
|
526
|
+
if (sessionsService(props?.sessions) !== undefined || tries >= 40) {
|
|
527
|
+
clearInterval(timer)
|
|
528
|
+
setSessionTick((tick) => tick + 1)
|
|
529
|
+
}
|
|
530
|
+
}, 500)
|
|
531
|
+
return () => clearInterval(timer)
|
|
532
|
+
}, [storeMissing])
|
|
494
533
|
|
|
495
534
|
// 让覆盖层能立刻刷新面板(点「去配置」时面板可能已经挂载着)。
|
|
496
535
|
useEffect(() => {
|
|
@@ -1011,13 +1050,15 @@ window.__ModuleLoader__.load({
|
|
|
1011
1050
|
|
|
1012
1051
|
useEffect(() => {
|
|
1013
1052
|
ensureStyles()
|
|
1014
|
-
const sessions = ctx.get('sessions')
|
|
1015
|
-
if (sessions === undefined || sessions === null) return undefined
|
|
1016
|
-
|
|
1017
1053
|
let disposed = false
|
|
1054
|
+
let timer = null
|
|
1055
|
+
let unsubscribe = null
|
|
1056
|
+
let tries = 0
|
|
1057
|
+
let list = null
|
|
1058
|
+
|
|
1018
1059
|
const currentSession = () => {
|
|
1019
1060
|
try {
|
|
1020
|
-
const snapshot =
|
|
1061
|
+
const snapshot = list?.getSnapshot?.()
|
|
1021
1062
|
const byId = snapshot?.byId ?? {}
|
|
1022
1063
|
for (const [id, item] of Object.entries(byId)) {
|
|
1023
1064
|
if (Number(item?.retainedBy?.mainView) > 0) return { id, cwd: item?.cwd }
|
|
@@ -1047,12 +1088,26 @@ window.__ModuleLoader__.load({
|
|
|
1047
1088
|
}
|
|
1048
1089
|
}
|
|
1049
1090
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1091
|
+
// 会话列表服务由 dsh-api-session-controller 在 effect 里晚于插件 apply 提供
|
|
1092
|
+
// (覆盖层更早挂载),所以这里要有上限地重试,否则永远看不到会话。
|
|
1093
|
+
const attach = () => {
|
|
1094
|
+
if (disposed) return
|
|
1095
|
+
const sessions = ctx.get('sessions')
|
|
1096
|
+
if (sessions === null || sessions === undefined) {
|
|
1097
|
+
tries += 1
|
|
1098
|
+
if (tries < 40) timer = setTimeout(attach, 500)
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
list = sessions.list ?? null
|
|
1102
|
+
evaluate()
|
|
1103
|
+
unsubscribe = typeof sessions.list?.subscribe === 'function'
|
|
1104
|
+
? sessions.list.subscribe(() => { evaluate() })
|
|
1105
|
+
: null
|
|
1106
|
+
}
|
|
1107
|
+
attach()
|
|
1054
1108
|
return () => {
|
|
1055
1109
|
disposed = true
|
|
1110
|
+
if (timer !== null) clearTimeout(timer)
|
|
1056
1111
|
if (typeof unsubscribe === 'function') unsubscribe()
|
|
1057
1112
|
}
|
|
1058
1113
|
}, [])
|
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.4",
|
|
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",
|