chore(deps): 更新依赖版本并切换部分包的发布渠道

- 将部分本地链接依赖改为公开版本号依赖
- 升级 vitest 依赖,增加 @types/debug 的版本信息
- 更新 @deepseek-ai 相关包的版本至0.1.0-rc.6和4.0.1
- 调整依赖树,优化部分包的可选 peerDependencies
- 增加多项 @deepseek-ai 相关包的校验和信息
- 解绑部分包的本地路径依赖,改为版本号引用,提高模块兼容性
This commit is contained in:
zeyu.fz
2026-08-17 16:26:55 +08:00
parent 4b3ea28a2d
commit 328739b91a
4 changed files with 2248 additions and 85 deletions
+104 -2
View File
@@ -5,6 +5,7 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import type { IncomingMessage, ServerResponse } from 'node:http'
import z from '@deepseek-ai/schemastery'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { settingsNamespace, type SettingsRegisterOptions, type SettingsScope } from '@deepseek-ai/dsh-settings'
@@ -12,6 +13,20 @@ import { KbClient } from './client.js'
import { registerSkill } from './skill.js'
import { createKbTools } from './tools.js'
/** Minimal webServer route shape (declared inline to avoid a host-package dependency). */
interface WebRoute {
kind: 'exact' | 'prefix'
path: string
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
}
declare module '@deepseek-ai/cordis' {
interface Context {
webServer: {
register(route: WebRoute): () => void
}
}
}
export const name = 'tool-bailian-kb'
export const inject = ['tools', 'credentials']
@@ -95,14 +110,15 @@ export function apply(ctx: Context, config: Config): void {
// not carry: the `expose` opt-in (this page edits the section from the
// browser) and the scope handle the credential migration writes through.
let current: () => Config = () => config
let scope: SettingsScope<Config> | undefined
ctx.inject(['settings'], (sctx) => {
// `expose` is the wire opt-in the harness documents as deferred work; the
// assertion keeps this compiling against pristine upstream types, which do
// not declare it yet. Until upstream lands it the option is ignored and
// the browser page degrades to its credentials-only fallback.
const options = { base: config, expose: true } as SettingsRegisterOptions<Config>
const scope = sctx.settings.register(SETTINGS_NS, Config, options)
current = () => scope.get()
scope = sctx.settings.register(SETTINGS_NS, Config, options)
current = () => scope!.get()
sctx.effect(() => () => { current = () => config }, 'tool-bailian-kb: settings source fallback')
void seedFromCredentials(ctx, scope)
})
@@ -154,4 +170,90 @@ export function apply(ctx: Context, config: Config): void {
ctx.tools.register(tool)
}
registerSkill(ctx)
// Bridge routes let the browser settings page read and write the resolved
// section without riding the settings wire (which requires an apiproxy
// allowlist entry the composition does not grant out-of-tree namespaces).
ctx.inject(['webServer'], (wctx) => {
wctx.effect(() => wctx.webServer.register({
kind: 'exact',
path: '/bailian-kb/settings',
handler: (_req: IncomingMessage, res: ServerResponse) => {
sendJson(res, 200, current())
},
}), 'tool-bailian-kb: settings GET route')
wctx.effect(() => wctx.webServer.register({
kind: 'exact',
path: '/bailian-kb/settings',
handler: async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== 'POST') {
sendJson(res, 405, { error: 'use POST' })
return
}
if (!scope) {
sendJson(res, 503, { error: 'settings service unavailable' })
return
}
let body: unknown
try { body = await readJsonBody(req) } catch (err) {
sendJson(res, 400, { error: err instanceof Error ? err.message : 'bad request' })
return
}
if (typeof body !== 'object' || body === null) {
sendJson(res, 400, { error: 'expected JSON object' })
return
}
// Build a settings update patch. null-valued keys are removals (the
// field falls back to the entry config and then the credential store).
const patch: Record<string, unknown> = {}
const removals = new Set<string>()
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
if (!(key in current())) continue
if (value === null) { removals.add(key); continue }
patch[key] = value
}
try {
if (removals.size > 0) {
// Remove fields from the user section by replacing it wholesale
// with the current resolved config minus the removed keys.
// Absent keys re-inherit the entry base and schema defaults.
const next = { ...current(), ...patch }
for (const key of removals) delete (next as Record<string, unknown>)[key]
await scope.replace(next)
} else if (Object.keys(patch).length > 0) {
await scope.update(patch)
}
sendJson(res, 200, scope.get())
} catch (err) {
sendJson(res, 500, { error: err instanceof Error ? err.message : 'settings write failed' })
}
},
}), 'tool-bailian-kb: settings POST route')
})
}
/** Write a JSON response. */
function sendJson(res: ServerResponse, status: number, data: unknown): void {
res.statusCode = status
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(JSON.stringify(data))
}
/** Read a UTF-8 JSON body up to a size limit. */
function readJsonBody(req: IncomingMessage, maxBytes = 16384): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
let total = 0
req.on('data', (chunk: Buffer) => {
total += chunk.length
if (total > maxBytes) { req.destroy(); reject(new Error('body too large')); return }
chunks.push(chunk)
})
req.on('end', () => {
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))) }
catch (err) { reject(err) }
})
req.on('error', reject)
})
}
@@ -2,13 +2,10 @@
* The Bailian page's controller: a hybrid form over two domains.
*
* The workspace, default-retrieval-service and default-chat-service ids live
* in the `bailian-kb` settings section the Host half registers, so while the
* settings scope is `ready` they ECHO: the page shows the resolved value and
* stages edits over it (clearing a field unsets the user layer, falling back
* to the entry config and then the credential store). When the scope is
* unavailable — a remote browser (memory mode) or a composition without a
* settings service — both fields degrade to the original write-only credential
* controls.
* in the `bailian-kb` settings section the Host half registers. The Host
* exposes them over a bridge route (`/bailian-kb/settings`) so the page can
* read and write without riding the settings wire (which requires an apiproxy
* allowlist entry the composition does not grant out-of-tree namespaces).
*
* The API key always rides its credential reference (write-only by design:
* the wire is structurally value-free), so that control starts blank and
@@ -16,7 +13,7 @@
*/
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
import { createSnapshotStore, type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
/** The credential references this page addresses, keyed by their ref names. */
export const BAILIAN_CARD_REFS = [
@@ -120,17 +117,15 @@ export function dirtyOf(state: BailianCardState): boolean {
return BAILIAN_CARD_REFS.some(key => staged(state, key))
}
/** Bridge the settings scope and the credentials domain onto the page. */
/** Bridge the settings bridge route and the credentials domain onto the page. */
export class BailianCardController {
private readonly store: SnapshotStore<BailianCardState>
/**
* @param api - wire face used for the three credential references.
* @param scope - the bound `bailian-kb` settings scope (echo transport).
*/
constructor(
private readonly api: Pick<IApiClient, 'credentials'>,
private readonly scope: SettingsScope<BailianKbSection>,
) {
this.store = createSnapshotStore<BailianCardState>({
drafts: {
@@ -150,29 +145,35 @@ export class BailianCardController {
clearing: false,
failed: false,
})
this.syncSettings()
void this.fetchSettings()
void this.read()
}
/**
* Mirror the scope snapshot into the page state. Called at construction and
* from the registration-side subscription (the scope self-refreshes on
* pushed document invalidations and connection resets).
* Fetch the current settings from the Host bridge route. Called at
* construction and after every mutation (save, clear).
*/
syncSettings(): void {
const snapshot = this.scope.getSnapshot()
const value = snapshot.value
this.store.update(draft => {
draft.settings = {
status: snapshot.status,
writable: snapshot.writable,
values: {
...(value?.workspaceId !== undefined ? { workspaceId: value.workspaceId } : {}),
...(value?.defaultRetrieveAgentId !== undefined ? { defaultRetrieveAgentId: value.defaultRetrieveAgentId } : {}),
...(value?.defaultChatAgentId !== undefined ? { defaultChatAgentId: value.defaultChatAgentId } : {}),
},
}
})
async fetchSettings(): Promise<void> {
try {
const resp = await fetch('/bailian-kb/settings')
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const value = await resp.json() as Record<string, unknown>
this.store.update(draft => {
draft.settings = {
status: 'ready',
writable: true,
values: {
...(typeof value.workspaceId === 'string' ? { workspaceId: value.workspaceId } : {}),
...(typeof value.defaultRetrieveAgentId === 'string' ? { defaultRetrieveAgentId: value.defaultRetrieveAgentId } : {}),
...(typeof value.defaultChatAgentId === 'string' ? { defaultChatAgentId: value.defaultChatAgentId } : {}),
},
}
})
} catch (_fetchFailure) {
this.store.update(draft => {
draft.settings = { status: 'unavailable', writable: false, values: {} }
})
}
}
/**
@@ -190,28 +191,31 @@ export class BailianCardController {
/**
* Write every staged draft through its domain: echoing fields go to the
* settings user layer (blank = unset, falling back to entry config and the
* credential store), write-only fields go to `credentials.set`. A refused
* credential write keeps its draft; a refused settings write self-heals by
* the scope's own recovery read (the control snaps back to the Host value).
* settings user layer via the bridge route (blank = removal, falling back
* to entry config and the credential store), write-only fields go to
* `credentials.set`. A refused credential write keeps its draft; a refused
* settings write self-heals by re-fetching the Host value.
*/
async save(): Promise<void> {
const state = this.store.getSnapshot()
if (state.saving || !dirtyOf(state)) return
this.store.update(draft => { draft.saving = true })
let failed = false
const writes: Promise<void>[] = []
const settingsPatch: Record<string, unknown> = {}
let hasSettingsWrite = false
const credentialWrites: Promise<void>[] = []
const settled: BailianFieldKey[] = []
for (const key of BAILIAN_CARD_REFS) {
if (!staged(state, key)) continue
const text = state.drafts[key] as string
const field = SETTINGS_FIELDS[key]
if (field !== undefined && state.settings.status === 'ready') {
writes.push(text === '' ? this.scope.unset(field) : this.scope.set(field, text))
settingsPatch[field] = text === '' ? null : text
hasSettingsWrite = true
settled.push(key)
continue
}
writes.push((async () => {
credentialWrites.push((async () => {
try {
const response = await this.api.credentials.set({ ref: key, value: text })
if (response.result.ok) settled.push(key)
@@ -221,13 +225,20 @@ export class BailianCardController {
}
})())
}
await Promise.all(writes)
if (hasSettingsWrite) {
try {
await this.saveSettings(settingsPatch)
} catch (_settingsWriteFailure) {
failed = true
}
}
await Promise.all(credentialWrites)
this.store.update(draft => {
draft.saving = false
draft.failed = failed
for (const key of settled) draft.drafts[key] = undefined
})
this.syncSettings()
await this.fetchSettings()
await this.read()
}
@@ -252,7 +263,13 @@ export class BailianCardController {
if (state.clearing) return
this.store.update(draft => { draft.clearing = true })
let failed = false
if (state.settings.status === 'ready') await this.scope.unset(settingsField)
if (state.settings.status === 'ready') {
try {
await this.saveSettings({ [settingsField]: null })
} catch (_settingsWriteFailure) {
failed = true
}
}
if (state.credentials[key].configured) {
try {
const response = await this.api.credentials.unset({ ref: key })
@@ -266,7 +283,7 @@ export class BailianCardController {
draft.failed = failed
draft.drafts[key] = undefined
})
this.syncSettings()
await this.fetchSettings()
await this.read()
}
@@ -298,6 +315,24 @@ export class BailianCardController {
}
}
/**
* Send a settings patch to the Host bridge route. `null`-valued keys are
* removals (the field falls back to the entry config and then the
* credential store); other values are merged into the user layer.
* @param patch - the partial settings update.
*/
private async saveSettings(patch: Record<string, unknown>): Promise<void> {
const resp = await fetch('/bailian-kb/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
})
if (!resp.ok) {
const body = await resp.json().catch(() => ({})) as { error?: string }
throw new Error(body.error ?? `HTTP ${resp.status}`)
}
}
/**
* Ask the credentials domain about all three references and publish the
* answer. A failed read keeps the last known state: the page stays usable
+11 -17
View File
@@ -1,11 +1,11 @@
/**
* Bailian knowledge-base plugin, browser half: one section page in the
* Settings left nav. The workspace, default-retrieval-service and
* default-chat-service ids ride the `bailian-kb` settings namespace the
* Host half registers (echoing values through `ctx.settingsScope`,
* degrading to write-only credential controls when the scope is
* unavailable); the API key stays pure credentials-domain and never
* echoes.
* default-chat-service ids ride the Host bridge route
* (`/bailian-kb/settings`) the Host half registers, bypassing the settings
* wire (which requires an apiproxy allowlist entry the composition does not
* grant out-of-tree namespaces); the API key stays pure credentials-domain
* and never echoes.
*/
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
@@ -21,9 +21,8 @@ import type {} from '@deepseek-ai/dsh-client-ui-slots'
// through cordis, never a value import).
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import { BailianCard } from './BailianCard.tsx'
import { BailianCardController, type BailianKbSection } from './bailian-card-controller.ts'
import { BailianCardController } from './bailian-card-controller.ts'
import { en, zh, type BailianKbLocaleKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** The Bailian section page's copy. */
@@ -35,7 +34,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
const NS = 'tool-bailian-kb'
/** Required services (cordis fiber inject). */
export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
export const inject = ['slots', 'locale', 'connection', 'remote']
/**
* Mount the Bailian section page into the Settings left nav.
@@ -49,15 +48,10 @@ export function apply(ctx: ClientContext): void {
// render, so copy freshness rides the locale revision without re-registering.
const t = ctx.locale.bind(NS)
// The echo transport: bound on this fiber, self-refreshing on pushed
// settings-document invalidations and connection resets. A remote browser
// binds in memory mode and the page degrades to write-only controls.
const scope = ctx.settingsScope.bind<BailianKbSection>({ namespace: 'bailian-kb' })
const card = new BailianCardController(api, scope)
ctx.effect(
() => scope.subscribe(() => { card.syncSettings() }),
'tool-bailian-kb: settings echo',
)
// The echo transport: the Host bridge route (`/bailian-kb/settings`) lets
// the page read and write the resolved section without riding the settings
// wire (which requires an apiproxy allowlist entry).
const card = new BailianCardController(api)
// Values can change elsewhere (Models page, external file edits); the badges
// must follow the Host, not the card's last write.
ctx.effect(
+2058 -26
View File
File diff suppressed because it is too large Load Diff