Skip to content

Commit 0ef7c82

Browse files
committed
fix(settings): 优化 bailian-kb 设置路由的配置管理逻辑
- 更新 package.json 版本至 0.1.3,完善导出和文件配置 - 引入 SettingsProvider 用于支持路径级 unset 操作 - 将 GET 和 POST 请求共用一个 /bailian-kb/settings 路由,避免重复注册 - POST 接口支持批量更新和删除配置字段,删除操作使用
1 parent 6cedca2 commit 0ef7c82

2 files changed

Lines changed: 55 additions & 27 deletions

File tree

‎packages/tool-bailian-kb/package.json‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
{
22
"name": "@ali/bailian-kb-dsh",
3-
"version": "0.1.1",
3+
"version": "0.1.3",
44
"description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the kscli management skill.",
55
"type": "module",
66
"main": "lib/index.js",
77
"types": "lib/index.d.ts",
88
"exports": {
9-
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
10-
"./client": { "default": "./lib/web/client.js" },
9+
".": {
10+
"types": "./lib/index.d.ts",
11+
"default": "./lib/index.js"
12+
},
13+
"./client": {
14+
"default": "./lib/web/client.js"
15+
},
1116
"./package.json": "./package.json"
1217
},
1318
"dsh": {
14-
"bundle": { "patch": "./cordis.patch.yml" },
19+
"bundle": {
20+
"patch": "./cordis.patch.yml"
21+
},
1522
"client": {
1623
"inject": [
1724
"@deepseek-ai/dsh-client-connection",
@@ -23,8 +30,14 @@
2330
"platform": "web"
2431
}
2532
},
26-
"files": ["lib", "skills", "cordis.patch.yml"],
27-
"scripts": { "build": "tsc -b && tsdown" },
33+
"files": [
34+
"lib",
35+
"skills",
36+
"cordis.patch.yml"
37+
],
38+
"scripts": {
39+
"build": "tsc -b && tsdown"
40+
},
2841
"peerDependencies": {
2942
"@deepseek-ai/cordis": "^4.0.1",
3043
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",

‎packages/tool-bailian-kb/src/index.ts‎

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { Context } from '@deepseek-ai/cordis'
88
import type { IncomingMessage, ServerResponse } from 'node:http'
99
import z from '@deepseek-ai/schemastery'
1010
import { credentialRef } from '@deepseek-ai/dsh-credentials'
11-
import { settingsNamespace, type SettingsRegisterOptions, type SettingsScope } from '@deepseek-ai/dsh-settings'
11+
import { settingsNamespace, SettingsProvider, type SettingsRegisterOptions, type SettingsScope } from '@deepseek-ai/dsh-settings'
1212
import { KbClient } from './client.js'
1313
import { registerSkill } from './skill.js'
1414
import { createKbTools } from './tools.js'
@@ -40,6 +40,16 @@ const CREDENTIAL_SEEDS = [
4040
['defaultChatAgentId', 'BAILIAN_DEFAULT_CHAT_AGENT_ID'],
4141
] as const
4242

43+
/**
44+
* Every {@link Config} field the bridge route accepts. A static allowlist,
45+
* NOT `key in current()`: optional fields with no default and no base
46+
* (the default service ids) vanish from the resolved config once cleared,
47+
* and a membership test against it would silently drop their next write.
48+
*/
49+
const CONFIG_FIELDS = new Set<string>([
50+
'workspaceId', 'endpointHost', 'defaultRetrieveAgentId', 'defaultChatAgentId', 'agentVersion', 'chatTimeoutMs',
51+
])
52+
4353
/**
4454
* One-time migration: before this section existed, the workspace and
4555
* default-service ids lived only as credentials, which the wire never echoes.
@@ -111,7 +121,10 @@ export function apply(ctx: Context, config: Config): void {
111121
// browser) and the scope handle the credential migration writes through.
112122
let current: () => Config = () => config
113123
let scope: SettingsScope<Config> | undefined
124+
/** The settings provider, captured for `mutate` (path-level unset) access. */
125+
let settings: SettingsProvider | undefined
114126
ctx.inject(['settings'], (sctx) => {
127+
settings = sctx.settings
115128
// `expose` is the wire opt-in the harness documents as deferred work; the
116129
// assertion keeps this compiling against pristine upstream types, which do
117130
// not declare it yet. Until upstream lands it the option is ignored and
@@ -174,21 +187,20 @@ export function apply(ctx: Context, config: Config): void {
174187
// Bridge routes let the browser settings page read and write the resolved
175188
// section without riding the settings wire (which requires an apiproxy
176189
// allowlist entry the composition does not grant out-of-tree namespaces).
190+
// GET and POST share one exact-route registration: the webServer map keys
191+
// on (kind, path), so two registrations for the same path throw
192+
// "duplicate route" and the second handler silently replaces the first.
177193
ctx.inject(['webServer'], (wctx) => {
178-
wctx.effect(() => wctx.webServer.register({
179-
kind: 'exact',
180-
path: '/bailian-kb/settings',
181-
handler: (_req: IncomingMessage, res: ServerResponse) => {
182-
sendJson(res, 200, current())
183-
},
184-
}), 'tool-bailian-kb: settings GET route')
185-
186194
wctx.effect(() => wctx.webServer.register({
187195
kind: 'exact',
188196
path: '/bailian-kb/settings',
189197
handler: async (req: IncomingMessage, res: ServerResponse) => {
198+
if (req.method === 'GET' || req.method === 'HEAD') {
199+
sendJson(res, 200, current())
200+
return
201+
}
190202
if (req.method !== 'POST') {
191-
sendJson(res, 405, { error: 'use POST' })
203+
sendJson(res, 405, { error: 'use GET or POST' })
192204
return
193205
}
194206
if (!scope) {
@@ -209,27 +221,30 @@ export function apply(ctx: Context, config: Config): void {
209221
const patch: Record<string, unknown> = {}
210222
const removals = new Set<string>()
211223
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
212-
if (!(key in current())) continue
224+
if (!CONFIG_FIELDS.has(key)) continue
213225
if (value === null) { removals.add(key); continue }
214226
patch[key] = value
215227
}
216228
try {
217-
if (removals.size > 0) {
218-
// Remove fields from the user section by replacing it wholesale
219-
// with the current resolved config minus the removed keys.
220-
// Absent keys re-inherit the entry base and schema defaults.
221-
const next = { ...current(), ...patch }
222-
for (const key of removals) delete (next as Record<string, unknown>)[key]
223-
await scope.replace(next)
224-
} else if (Object.keys(patch).length > 0) {
225-
await scope.update(patch)
229+
// Apply non-removal patches first (scope.update merges into the user
230+
// layer without disturbing other fields).
231+
if (Object.keys(patch).length > 0) await scope.update(patch)
232+
// Remove fields via path-level unset ops: this deletes the key from
233+
// the user layer so it re-inherits the entry base and schema defaults.
234+
// Using scope.replace() with the resolved config would bake defaults
235+
// (endpointHost, chatTimeoutMs) and entry values into the user layer,
236+
// shadowing future entry changes and polluting the stored document.
237+
if (removals.size > 0 && settings) {
238+
for (const key of removals) {
239+
await settings.mutate(SETTINGS_NS, [{ op: 'unset', path: [key] }])
240+
}
226241
}
227242
sendJson(res, 200, scope.get())
228243
} catch (err) {
229244
sendJson(res, 500, { error: err instanceof Error ? err.message : 'settings write failed' })
230245
}
231246
},
232-
}), 'tool-bailian-kb: settings POST route')
247+
}), 'tool-bailian-kb: settings bridge route')
233248
})
234249
}
235250

0 commit comments

Comments
 (0)