dsh-workspace-api-key 0.2.16 → 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 +21 -7
- package/README.zh.md +21 -7
- package/lib/client.js +628 -81
- package/lib/index.js +226 -23
- package/package.json +2 -2
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 折大小写。 */
|
|
@@ -243,11 +282,11 @@ function workspaceIndex(ctx) {
|
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
/** cwd → 工作区描述;没有匹配工作区时回退成「以该目录为单位」。 */
|
|
246
|
-
function locateWorkspace(ctx, cwd) {
|
|
285
|
+
function locateWorkspace(ctx, cwd, index) {
|
|
247
286
|
if (cwd === undefined) return undefined
|
|
248
287
|
const norm = normPath(cwd)
|
|
249
288
|
if (norm === undefined) return undefined
|
|
250
|
-
const hit = workspaceIndex(ctx).get(norm)
|
|
289
|
+
const hit = (index ?? workspaceIndex(ctx)).get(norm)
|
|
251
290
|
if (hit !== undefined) return { ...hit, kind: 'workspace' }
|
|
252
291
|
return { id: undefined, path: cwd, title: basename(cwd) || cwd, kind: 'directory' }
|
|
253
292
|
}
|
|
@@ -256,7 +295,7 @@ function locateWorkspace(ctx, cwd) {
|
|
|
256
295
|
* 给定会话/目录,算出当前生效的覆盖层级(纯函数,不读凭据):
|
|
257
296
|
* session → workspace → default。
|
|
258
297
|
*/
|
|
259
|
-
function effectiveOverride(ctx, state, { sessionId, cwd }) {
|
|
298
|
+
function effectiveOverride(ctx, state, { sessionId, cwd }, index) {
|
|
260
299
|
if (sessionId !== undefined) {
|
|
261
300
|
const sref = sessionRef(sessionId)
|
|
262
301
|
if (sref !== undefined && state.overrides[sref] !== undefined) {
|
|
@@ -264,7 +303,9 @@ function effectiveOverride(ctx, state, { sessionId, cwd }) {
|
|
|
264
303
|
}
|
|
265
304
|
}
|
|
266
305
|
if (cwd !== undefined) {
|
|
267
|
-
|
|
306
|
+
// index 是可选的「工作区索引」快照:面板一次要算几十个会话,每行都去
|
|
307
|
+
// registry.list() 一遍会让整个事件循环卡住(0.2.16 实测 /state 要 50 秒)。
|
|
308
|
+
const ws = locateWorkspace(ctx, cwd, index)
|
|
268
309
|
const wref = ws !== undefined ? derivedRef(ws.path) : undefined
|
|
269
310
|
if (wref !== undefined && state.overrides[wref] !== undefined) {
|
|
270
311
|
return { level: 'workspace', ref: wref, entry: state.overrides[wref], workspace: ws }
|
|
@@ -352,6 +393,32 @@ async function bindOfficialSearchRef(ctx, ref) {
|
|
|
352
393
|
return { ok: true, changed }
|
|
353
394
|
}
|
|
354
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
|
+
|
|
355
422
|
/**
|
|
356
423
|
* 启动时把官方「网页搜索」页的 key 指向拉回一致。
|
|
357
424
|
*
|
|
@@ -390,7 +457,7 @@ async function reconcileOfficialSearchRef(ctx) {
|
|
|
390
457
|
* 旧 Messages 通道:搜索就在用聊天 key,固定返回搜索 ref。
|
|
391
458
|
*/
|
|
392
459
|
async function searchRefFor(ctx, state, { sessionId, cwd, web, searchDefault }) {
|
|
393
|
-
const w = web ??
|
|
460
|
+
const w = web ?? currentWeb(ctx)
|
|
394
461
|
if (!w.apiChannel) return w.searchKeyEnv
|
|
395
462
|
const own = sessionOverrideRef(state, sessionId)
|
|
396
463
|
if (own !== undefined) return own
|
|
@@ -803,8 +870,10 @@ function interceptableRefs(ctx, state) {
|
|
|
803
870
|
// 搜索/抓取通道的 ref 也要拦:用户可能把「新版 Web 搜索 API」的 key 指到
|
|
804
871
|
// 独立 ref(例如 DEEPSEEK_SEARCH_API_KEY)。不拦的话,勾了「同步搜索 API」
|
|
805
872
|
// 也会静默不生效——面板显示已同步、实际还在用系统默认 key。
|
|
873
|
+
// 注意这里走 currentWeb(缓存)而不是 readWebSettings:本函数在 credentials.resolve
|
|
874
|
+
// 的热路径上被调用,每次都 settings.describe() 会把聊天通道一并拖慢。
|
|
806
875
|
try {
|
|
807
|
-
const web =
|
|
876
|
+
const web = currentWeb(ctx)
|
|
808
877
|
for (const ref of [web.apiKeyEnv, web.legacyKeyEnv]) {
|
|
809
878
|
if (typeof ref === 'string' && ref !== '') refs.add(ref)
|
|
810
879
|
}
|
|
@@ -875,6 +944,14 @@ function webDefaults() {
|
|
|
875
944
|
apiChannel: false,
|
|
876
945
|
searchKeyEnv: DEFAULT_API_KEY_REF,
|
|
877
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,
|
|
878
955
|
}
|
|
879
956
|
}
|
|
880
957
|
|
|
@@ -899,6 +976,27 @@ function webConfigOf(rows) {
|
|
|
899
976
|
: FALLBACK_FETCH_PROVIDER
|
|
900
977
|
const apiLiteralKey = typeof api.apiKey === 'string' && api.apiKey !== '' ? api.apiKey : undefined
|
|
901
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
|
+
|
|
902
1000
|
return {
|
|
903
1001
|
apiKeyEnv,
|
|
904
1002
|
legacyKeyEnv,
|
|
@@ -911,8 +1009,17 @@ function webConfigOf(rows) {
|
|
|
911
1009
|
/** 新版 Search/Fetch API 是否被选中(官方开关)= provider 选择被改成了 API 版。 */
|
|
912
1010
|
apiChannel: searchProvider === SEARCH_PROVIDER_ID || fetchProvider === SEARCH_FETCH_PROVIDER_ID,
|
|
913
1011
|
/** 搜索请求真正会用的那套配置的 key 引用。 */
|
|
914
|
-
searchKeyEnv: searchProvider === SEARCH_PROVIDER_ID
|
|
1012
|
+
searchKeyEnv: searchProvider === SEARCH_PROVIDER_ID
|
|
1013
|
+
? (mode === 'billing' ? ownSearchRef ?? DEFAULT_API_KEY_REF : apiKeyEnv)
|
|
1014
|
+
: legacyKeyEnv,
|
|
915
1015
|
searchLiteralKey: searchProvider === SEARCH_PROVIDER_ID ? apiLiteralKey : legacyLiteralKey,
|
|
1016
|
+
mode,
|
|
1017
|
+
searchBilling,
|
|
1018
|
+
fetchBilling,
|
|
1019
|
+
ownSearchRef,
|
|
1020
|
+
ownFetchRef,
|
|
1021
|
+
searchBaseURL,
|
|
1022
|
+
fetchBaseURL,
|
|
916
1023
|
}
|
|
917
1024
|
}
|
|
918
1025
|
|
|
@@ -923,6 +1030,30 @@ function webConfigOf(rows) {
|
|
|
923
1030
|
*/
|
|
924
1031
|
async function searchWarnings(ctx, state, web) {
|
|
925
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
|
+
|
|
926
1057
|
const apiKeyConfigured = web.apiKeyEnv === DEFAULT_API_KEY_REF
|
|
927
1058
|
? (await describeKey(ctx, DEFAULT_API_KEY_REF)).configured === true
|
|
928
1059
|
: (await describeKey(ctx, web.apiKeyEnv)).configured === true
|
|
@@ -1317,7 +1448,9 @@ export function apply(ctx) {
|
|
|
1317
1448
|
}
|
|
1318
1449
|
installResolver(ctx, state)
|
|
1319
1450
|
// 「直接禁用官方页面里的搜索 key 设置」:把官方 apiKeyEnv 指到插件自有 ref(异步、失败不影响启动)。
|
|
1320
|
-
|
|
1451
|
+
// 0.2.2-rc.2 起官方改用计费方式(searchBilling/fetchBilling),apiKeyEnv 已不再生效:
|
|
1452
|
+
// 这时再写它只会制造「看起来配了、其实没用」的无效配置,所以只在旧模型下接管。
|
|
1453
|
+
if (currentWeb(ctx).mode !== 'billing') void reconcileOfficialSearchRef(ctx)
|
|
1321
1454
|
|
|
1322
1455
|
// ── 2. REST API(loopback 可信)──────────────────────────────────────
|
|
1323
1456
|
let webServer
|
|
@@ -1400,6 +1533,15 @@ function installResolver(ctx, state) {
|
|
|
1400
1533
|
if (isSearchRefFor(ctx, ref)) {
|
|
1401
1534
|
const web = currentWeb(ctx)
|
|
1402
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
|
+
}
|
|
1403
1545
|
// ① 会话单独设置的 key 永远优先——哪怕官方搜索插件里配了别的 key。
|
|
1404
1546
|
const sessionKey = sessionOverrideRef(state, sessionId)
|
|
1405
1547
|
if (sessionKey !== undefined) {
|
|
@@ -1551,7 +1693,7 @@ async function readJson(req) {
|
|
|
1551
1693
|
}
|
|
1552
1694
|
|
|
1553
1695
|
/** 一个会话的展示 + 生效情况(供面板与会话进入提醒使用)。 */
|
|
1554
|
-
async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
1696
|
+
async function sessionRow(ctx, state, row, fallbackCwd, index) {
|
|
1555
1697
|
const sessionId = typeof row?.sessionId === 'string' && row.sessionId !== '' ? row.sessionId : undefined
|
|
1556
1698
|
if (sessionId === undefined) return undefined
|
|
1557
1699
|
const selection = modelSelectionOf(row)
|
|
@@ -1559,7 +1701,7 @@ async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
|
1559
1701
|
const ref = sessionRef(sessionId)
|
|
1560
1702
|
const entry = ref !== undefined ? state.overrides[ref] : undefined
|
|
1561
1703
|
const directHit = entry !== undefined ? await resolveKey(ctx, ref) : undefined
|
|
1562
|
-
const eff = effectiveOverride(ctx, state, { sessionId, cwd })
|
|
1704
|
+
const eff = effectiveOverride(ctx, state, { sessionId, cwd }, index)
|
|
1563
1705
|
const effHit = eff.ref !== undefined ? await resolveKey(ctx, eff.ref) : undefined
|
|
1564
1706
|
return {
|
|
1565
1707
|
sessionId,
|
|
@@ -1591,14 +1733,19 @@ async function sessionRow(ctx, state, row, fallbackCwd) {
|
|
|
1591
1733
|
}
|
|
1592
1734
|
}
|
|
1593
1735
|
|
|
1594
|
-
async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, claimed) {
|
|
1736
|
+
async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, claimed, env) {
|
|
1737
|
+
// 每个工作区行都要的一份上下文(工作区索引、官方搜索配置、搜索默认 key 是否已配)。
|
|
1738
|
+
// 由 snapshot() 统一算一次传进来:panel 打开一次要渲染 20+ 行,每行各算一遍
|
|
1739
|
+
// settings.describe() + registry.list() 会把事件循环卡住(0.2.16 实测 /state ≈ 50 s)。
|
|
1740
|
+
const index = env?.index
|
|
1741
|
+
const web = env?.web ?? currentWeb(ctx)
|
|
1595
1742
|
const ref = derivedRef(dir)
|
|
1596
1743
|
const configured = ref !== undefined && state.overrides[ref] !== undefined
|
|
1597
1744
|
const desc = configured ? await describeKey(ctx, ref) : undefined
|
|
1598
1745
|
const hit = configured ? await resolveKey(ctx, ref) : undefined
|
|
1599
1746
|
const sessions = []
|
|
1600
1747
|
for (const row of rows) {
|
|
1601
|
-
const item = await sessionRow(ctx, state, row, dir)
|
|
1748
|
+
const item = await sessionRow(ctx, state, row, dir, index)
|
|
1602
1749
|
if (item !== undefined) sessions.push(item)
|
|
1603
1750
|
}
|
|
1604
1751
|
sessions.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))
|
|
@@ -1606,9 +1753,9 @@ async function workspaceRow(ctx, state, { id, path: dir, title, kind }, rows, cl
|
|
|
1606
1753
|
// 搜索通道:只有新版 Search/Fetch API 才有「同步 / 不同步」的区别(旧 Messages
|
|
1607
1754
|
// 通道本来就用聊天 key)。这里给出「该工作区的搜索请求实际会用哪把 key」,
|
|
1608
1755
|
// 让用户勾了复选框之后马上能看到效果,而不用等到搜索失败才发现。
|
|
1609
|
-
const web = readWebSettings(ctx)
|
|
1610
1756
|
const synced = state.searchSync?.[normPath(dir)] === true
|
|
1611
|
-
const searchDefaultConfigured =
|
|
1757
|
+
const searchDefaultConfigured = env?.searchDefaultConfigured
|
|
1758
|
+
?? (web.apiChannel && (await describeKey(ctx, searchDefaultRefOf(web))).configured === true)
|
|
1612
1759
|
const searchKeyRef = web.apiChannel
|
|
1613
1760
|
? synced
|
|
1614
1761
|
? configured
|
|
@@ -1655,13 +1802,21 @@ function hasVisibleSession(rows) {
|
|
|
1655
1802
|
}
|
|
1656
1803
|
|
|
1657
1804
|
async function snapshot(ctx, state) {
|
|
1805
|
+
const startedAt = Date.now()
|
|
1658
1806
|
// 官方「网页搜索」配置:每次面板刷新都重读一次(缓存只在请求入口更新,解析路径只读)。
|
|
1659
|
-
|
|
1807
|
+
// 整个 snapshot 只读这一次:下面每行都用 currentWeb(ctx) 拿缓存,不再各自 describe。
|
|
1808
|
+
const web = refreshWebCache(ctx)
|
|
1809
|
+
// 工作区索引也一样:一次 snapshot 只调一次 registry.list(),行渲染复用同一份快照。
|
|
1810
|
+
const index = workspaceIndex(ctx)
|
|
1660
1811
|
const credentials = ctx.get('credentials')
|
|
1661
1812
|
const defaultRefName = 'DEEPSEEK_API_KEY'
|
|
1662
1813
|
const defaultInfo = await describeKey(ctx, defaultRefName)
|
|
1663
1814
|
const defaultHit = await resolveKey(ctx, defaultRefName)
|
|
1664
1815
|
const probe = await sessionProbe(ctx)
|
|
1816
|
+
const probeMs = Date.now() - startedAt
|
|
1817
|
+
const searchDefaultConfigured = web.apiChannel
|
|
1818
|
+
&& (await describeKey(ctx, searchDefaultRefOf(web))).configured === true
|
|
1819
|
+
const rowEnv = { index, web, searchDefaultConfigured }
|
|
1665
1820
|
const rows = probe.rows
|
|
1666
1821
|
const byCwd = new Map()
|
|
1667
1822
|
for (const row of rows) {
|
|
@@ -1685,7 +1840,7 @@ async function snapshot(ctx, state) {
|
|
|
1685
1840
|
}
|
|
1686
1841
|
if (typeof dir !== 'string' || dir === '') continue
|
|
1687
1842
|
const norm = normPath(dir)
|
|
1688
|
-
workspaces.push(await workspaceRow(ctx, state, { id, path: dir, title, kind: 'workspace' }, byCwd.get(norm) ?? [], claimed))
|
|
1843
|
+
workspaces.push(await workspaceRow(ctx, state, { id, path: dir, title, kind: 'workspace' }, byCwd.get(norm) ?? [], claimed, rowEnv))
|
|
1689
1844
|
}
|
|
1690
1845
|
|
|
1691
1846
|
// 有覆盖记录、但对应工作区已不在注册表里的(被删掉的工作区)也列出来,方便清理。
|
|
@@ -1725,7 +1880,7 @@ async function snapshot(ctx, state) {
|
|
|
1725
1880
|
const dir = typeof list[0]?.cwd === 'string' ? list[0].cwd : norm
|
|
1726
1881
|
// 诊断用:这个目录里一个「真实会话」都没有(界面把它藏起来,但用户能看到原因)。
|
|
1727
1882
|
if (!hasVisibleSession(list)) hiddenBlankDirs.push(dir)
|
|
1728
|
-
ungrouped.push(await workspaceRow(ctx, state, { id: undefined, path: dir, title: basename(dir) || dir, kind: 'directory' }, list))
|
|
1883
|
+
ungrouped.push(await workspaceRow(ctx, state, { id: undefined, path: dir, title: basename(dir) || dir, kind: 'directory' }, list, undefined, rowEnv))
|
|
1729
1884
|
}
|
|
1730
1885
|
|
|
1731
1886
|
// 会话级覆盖总览(含已不在列表里的会话,便于清理)。
|
|
@@ -1751,8 +1906,7 @@ async function snapshot(ctx, state) {
|
|
|
1751
1906
|
}
|
|
1752
1907
|
|
|
1753
1908
|
// ── 搜索 / 抓取通道总览 ──────────────────────────────────────────────
|
|
1754
|
-
//
|
|
1755
|
-
const web = refreshWebCache(ctx)
|
|
1909
|
+
// 上面的 refreshWebCache() 已经把官方配置读到 web 里了,这里直接复用。
|
|
1756
1910
|
const searchKeyRef = web.searchKeyEnv
|
|
1757
1911
|
const searchKeyInfo = await describeKey(ctx, searchKeyRef)
|
|
1758
1912
|
const searchKeyHit = await resolveKey(ctx, searchKeyRef)
|
|
@@ -1763,10 +1917,14 @@ async function snapshot(ctx, state) {
|
|
|
1763
1917
|
const syncPaths = Object.keys(state.searchSync ?? {}).filter((key) => state.searchSync[key] === true)
|
|
1764
1918
|
const warnings = await searchWarnings(ctx, state, web)
|
|
1765
1919
|
|
|
1920
|
+
const totalMs = Date.now() - startedAt
|
|
1766
1921
|
return {
|
|
1767
1922
|
ok: true,
|
|
1768
1923
|
pluginVersion: PLUGIN_VERSION,
|
|
1924
|
+
hostVersion: HOST_VERSION,
|
|
1769
1925
|
storeVersion: STATE_VERSION,
|
|
1926
|
+
// 面板卡顿排查用:会话探针(宿主 list 一次要扫所有会话)与整行的耗时。
|
|
1927
|
+
timings: { probeMs, rowsMs: Math.max(0, totalMs - probeMs), totalMs },
|
|
1770
1928
|
sessionProbe: {
|
|
1771
1929
|
available: probe.available,
|
|
1772
1930
|
count: probe.count,
|
|
@@ -1788,7 +1946,7 @@ async function snapshot(ctx, state) {
|
|
|
1788
1946
|
},
|
|
1789
1947
|
providers: providerList(ctx),
|
|
1790
1948
|
interceptableRefs: [...interceptableRefs(ctx, state)],
|
|
1791
|
-
workspaceCount:
|
|
1949
|
+
workspaceCount: index.size,
|
|
1792
1950
|
credentialsAvailable: credentials !== undefined,
|
|
1793
1951
|
sessionListAvailable: probe.available,
|
|
1794
1952
|
sessionControllerAvailable: sessionListAvailable(ctx),
|
|
@@ -1802,6 +1960,13 @@ async function snapshot(ctx, state) {
|
|
|
1802
1960
|
// 搜索 / 抓取通道:官方「设置 → 插件 → 网页搜索」里的配置 + 本插件怎么参与。
|
|
1803
1961
|
// 面板据此显示通道、生效 key 与提醒;不暴露任何明文。
|
|
1804
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,
|
|
1805
1970
|
channel: web.apiChannel ? 'api' : 'messages',
|
|
1806
1971
|
apiChannel: web.apiChannel,
|
|
1807
1972
|
searchProvider: web.searchProvider,
|
|
@@ -1941,7 +2106,8 @@ async function checkSession(ctx, state, params) {
|
|
|
1941
2106
|
|
|
1942
2107
|
/** 进会话提醒用的搜索通道摘要(不含明文 key)。 */
|
|
1943
2108
|
async function searchStateOf(ctx, state, { sessionId, cwd }) {
|
|
1944
|
-
|
|
2109
|
+
// 面板/会话入口不是热路径,但也没必要每次 describe:缓存由请求入口与设置事件维护。
|
|
2110
|
+
const web = currentWeb(ctx)
|
|
1945
2111
|
const sync = workspaceSynced(state, cwd)
|
|
1946
2112
|
if (!web.apiChannel) {
|
|
1947
2113
|
const idle = web.apiLiteralKey !== undefined
|
|
@@ -2029,7 +2195,7 @@ async function handle(ctx, state, req, res) {
|
|
|
2029
2195
|
// 于是面板和官方页改的是同一把 key,不会各存一份。官方还指向聊天那把(DEEPSEEK_API_KEY)时,
|
|
2030
2196
|
// 先把两套搜索插件的 apiKeyEnv 改到插件自有 ref,再写值。
|
|
2031
2197
|
if (body.scope === 'search-default') {
|
|
2032
|
-
const web =
|
|
2198
|
+
const web = refreshWebCache(ctx)
|
|
2033
2199
|
const official = officialSearchRef(web)
|
|
2034
2200
|
const needsBind = official === DEFAULT_API_KEY_REF
|
|
2035
2201
|
const target = needsBind ? SEARCH_DEFAULT_REF : official
|
|
@@ -2089,7 +2255,7 @@ async function handle(ctx, state, req, res) {
|
|
|
2089
2255
|
|
|
2090
2256
|
if (pathname === `${API_BASE}/test` && method === 'POST') {
|
|
2091
2257
|
const body = await readJson(req)
|
|
2092
|
-
const web =
|
|
2258
|
+
const web = refreshWebCache(ctx)
|
|
2093
2259
|
const channel = body.channel === 'search' ? 'search' : 'chat'
|
|
2094
2260
|
let key = typeof body.key === 'string' && body.key.trim() !== '' ? body.key.trim() : undefined
|
|
2095
2261
|
let sourceRef = typeof body.ref === 'string' && body.ref !== '' ? body.ref : undefined
|
|
@@ -2148,6 +2314,43 @@ async function handle(ctx, state, req, res) {
|
|
|
2148
2314
|
return
|
|
2149
2315
|
}
|
|
2150
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
|
+
|
|
2151
2354
|
if (pathname === `${API_BASE}/search-sync` && method === 'POST') {
|
|
2152
2355
|
const body = await readJson(req)
|
|
2153
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"
|
|
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
|
}
|