From 589e3a4f14890fbaddb2bb2fef877b52b39c0481 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 22:44:31 +0800 Subject: [PATCH] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=20TypeScript?= =?UTF-8?q?=20=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=8F=8A=E4=BE=9D=E8=B5=96=E9=94=81=E5=AE=9A=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 package.json 中 typecheck 脚本,新增对 tsconfig.web.json 的检查 - 更新 pnpm-lock.yaml 文件,增加多个依赖项和绑定包的版本信息及平台支持 - 新增 react、lightningcss 等多种平台及架构的预编译绑定库 - 添加多种类型定义依赖,提升类型覆盖范围 - 升级部分工具包及插件版本,优化构建和开发体验 --- package.json | 2 +- packages/bundle/README.md | 40 +- packages/bundle/cordis.patch.yml | 8 +- packages/tool-bailian-kb/package.json | 38 +- packages/tool-bailian-kb/src/client.ts | 7 +- packages/tool-bailian-kb/src/index.ts | 36 +- packages/tool-bailian-kb/src/tools.ts | 32 +- .../src/web/BailianCard.module.css | 251 ++++++ .../tool-bailian-kb/src/web/BailianCard.tsx | 136 +++ .../src/web/bailian-card-controller.ts | 216 +++++ .../tool-bailian-kb/src/web/css-modules.d.ts | 9 + packages/tool-bailian-kb/src/web/index.ts | 60 ++ packages/tool-bailian-kb/src/web/locales.ts | 71 ++ packages/tool-bailian-kb/tests/client.test.ts | 20 +- packages/tool-bailian-kb/tests/config.test.ts | 8 +- packages/tool-bailian-kb/tests/tools.test.ts | 37 +- packages/tool-bailian-kb/tsconfig.json | 3 +- packages/tool-bailian-kb/tsconfig.web.json | 11 + packages/tool-bailian-kb/tsdown.config.ts | 129 +++ pnpm-lock.yaml | 840 +++++++++++++++++- pnpm-workspace.yaml | 2 + 21 files changed, 1894 insertions(+), 62 deletions(-) create mode 100644 packages/tool-bailian-kb/src/web/BailianCard.module.css create mode 100644 packages/tool-bailian-kb/src/web/BailianCard.tsx create mode 100644 packages/tool-bailian-kb/src/web/bailian-card-controller.ts create mode 100644 packages/tool-bailian-kb/src/web/css-modules.d.ts create mode 100644 packages/tool-bailian-kb/src/web/index.ts create mode 100644 packages/tool-bailian-kb/src/web/locales.ts create mode 100644 packages/tool-bailian-kb/tsconfig.web.json create mode 100644 packages/tool-bailian-kb/tsdown.config.ts diff --git a/package.json b/package.json index 98b59a3..1ecfd7b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "pnpm -r run build", "test": "vitest run", - "typecheck": "tsc -b packages/tool-bailian-kb" + "typecheck": "tsc -b packages/tool-bailian-kb && tsc -p packages/tool-bailian-kb/tsconfig.web.json" }, "devDependencies": { "typescript": "^5.7.2", diff --git a/packages/bundle/README.md b/packages/bundle/README.md index 126b173..c53b02a 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -1,6 +1,6 @@ # bailian-kb-dsh(分发包) -dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row。 +dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row,并随包分发浏览器端配置卡片(`dsh.client` → `lib/client.js`)。 ## Patch row @@ -12,25 +12,53 @@ dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.p workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID ``` -`workspaceId` 默认从环境变量读取(`~/.dsh/.env` 写 `BAILIAN_WORKSPACE_ID=ws-xxx` 即可运行);未设置时插件加载期 fail loud。 +`workspaceId` 只是解析链的一层,不是唯一来源:config 显式值(含此环境变量读取)per-call 优先;未设置时回退到 `BAILIAN_WORKSPACE_ID` credential。同样回退覆盖 `defaultAgentId`(`BAILIAN_DEFAULT_AGENT_ID`)与 API key(`DASHSCOPE_API_KEY`)。 + +## 三个值的解析链 + +| 值 | 1️⃣ config 显式值(本 patch 或用户覆盖) | 2️⃣ credential(UI 卡片 / `~/.dsh/.credentials.yaml`) | 3️⃣ 都缺失时 | +|---|---|---|---| +| `DASHSCOPE_API_KEY` | —(无 config 面) | ✅ | 工具调用报错并引导配置 | +| `BAILIAN_WORKSPACE_ID` | `workspaceId` | ✅ | 工具调用报错并引导配置 | +| `BAILIAN_DEFAULT_AGENT_ID` | `defaultAgentId` | ✅ | `agent_id` 参数变必填(schema 恒 optional,运行时校验) | + +行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)只在 config 层,见 [tool-bailian-kb README](../tool-bailian-kb/README.md)。 + +## Web UI 配置卡片 + +装进 profile 后,Settings → Plugins 出现“百炼知识库”卡片,可配置三个 credential(写 `~/.dsh/.credentials.yaml`): + +- **DashScope API Key** — write-only,`type=password` 遮罩输入草稿 +- **Bailian Workspace ID** — 明文(便于粘贴核对 workspace id) +- **默认服务 ID(agent_id)** — 明文,附独立“清除”按钮(留空保存 = 不写,清除须显式 unset) + +值永不回显:字段始终空白起步,仅显示 configured/来自环境变量 徽标;来自 shell export 或 `~/.dsh/.env` 的值只读(继承环境层),输入框禁用。 ## 用户覆盖 -用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge),必须连 workspaceId 一起重述**: +用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge)**。`workspaceId`/`defaultAgentId` 均为可选,只需重述想显式固定的字段: ```yaml # ~/.dsh/cordis.patch.yml 或 profile 的 cordis.patch.yml - id: tool-bailian-kb config: - workspaceId: ws-xxx - defaultAgentId: aid-customer-service # 场景固定式部署 + defaultAgentId: aid-customer-service # 场景固定式部署;省略 workspaceId 走 credential chatTimeoutMs: 600000 ``` 禁用:`- id: tool-bailian-kb` + `disabled: true`。 +## 安装(本地 checkout 链接) + +bundle 是 `dsh.bundle` 声明层,真正的插件包 `dsh-tool-bailian-kb` 是它的依赖;`link:` 安装不携带传递依赖,**两个包都要 add**(第二个无 bundle 声明,dsh 会以 plain dependency 装入,CLI 的 warning 即预期行为): + +```sh +dsh plugin --profile web add /path/to/bailian-kb-dsh/packages/bundle +dsh plugin --profile web add /path/to/bailian-kb-dsh/packages/tool-bailian-kb +``` + ## 卸载 ```sh -dsh plugin --profile remove bailian-kb-dsh +dsh plugin --profile remove bailian-kb-dsh dsh-tool-bailian-kb ``` diff --git a/packages/bundle/cordis.patch.yml b/packages/bundle/cordis.patch.yml index 3fce72a..3cf4d60 100644 --- a/packages/bundle/cordis.patch.yml +++ b/packages/bundle/cordis.patch.yml @@ -1,7 +1,9 @@ # bailian-kb-dsh: inserts the Bailian knowledge-base consumer over dsh-base. -# workspaceId reads BAILIAN_WORKSPACE_ID from the environment (~/.dsh/.env) so a -# user patch is only needed to pin defaultAgentId or override the host/timeout. -# An id-targeted user patch replaces this whole config: restate workspaceId too. +# workspaceId here is one resolution layer, not the only one: a config value +# (this env read included) wins per call; when it is unset the plugin resolves +# the BAILIAN_WORKSPACE_ID credential instead (web UI card or +# ~/.dsh/.credentials.yaml). The same fallback covers defaultAgentId via +# BAILIAN_DEFAULT_AGENT_ID, and the API key via DASHSCOPE_API_KEY. - insert: - id: tool-bailian-kb diff --git a/packages/tool-bailian-kb/package.json b/packages/tool-bailian-kb/package.json index 853c5d5..32a1e26 100644 --- a/packages/tool-bailian-kb/package.json +++ b/packages/tool-bailian-kb/package.json @@ -7,16 +7,37 @@ "types": "lib/index.d.ts", "exports": { ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" }, + "./client": { "default": "./lib/web/client.js" }, "./package.json": "./package.json" }, + "dsh": { + "client": { + "inject": [ + "@deepseek-ai/dsh-client-connection", + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-api-remotes", + "@deepseek-ai/dsh-client-ui-settings-plugins" + ], + "platform": "web" + } + }, "files": ["lib", "skills"], - "scripts": { "build": "tsc -b" }, + "scripts": { "build": "tsc -b && tsdown" }, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-tools": "*", "@deepseek-ai/dsh-credentials": "*", "@deepseek-ai/dsh-skill": "*", - "@deepseek-ai/schemastery": "^3.18.1" + "@deepseek-ai/schemastery": "^3.18.1", + "@deepseek-ai/dsh-api-remotes": "*", + "@deepseek-ai/dsh-client-connection": "*", + "@deepseek-ai/dsh-client-locale": "*", + "@deepseek-ai/dsh-client-runtime": "*", + "@deepseek-ai/dsh-client-ui-settings-plugins": "*", + "@deepseek-ai/dsh-client-ui-primitives": "*", + "@deepseek-ai/dsh-client-ui-slots": "*", + "react": "^18.2.0" }, "devDependencies": { "@deepseek-ai/cordis": "link:../../../deepseek-harness/vendor/cordis", @@ -24,6 +45,17 @@ "@deepseek-ai/dsh-credentials": "link:../../../deepseek-harness/packages/credentials/credentials", "@deepseek-ai/dsh-skill": "link:../../../deepseek-harness/packages/skill/skill", "@deepseek-ai/schemastery": "link:../../../deepseek-harness/vendor/schemastery", - "@types/node": "^22.0.0" + "@deepseek-ai/dsh-api-remotes": "link:../../../deepseek-harness/packages/api/remotes", + "@deepseek-ai/dsh-client-connection": "link:../../../deepseek-harness/packages/client/connection", + "@deepseek-ai/dsh-client-locale": "link:../../../deepseek-harness/packages/client/locale", + "@deepseek-ai/dsh-client-runtime": "link:../../../deepseek-harness/packages/client/runtime", + "@deepseek-ai/dsh-client-ui-settings-plugins": "link:../../../deepseek-harness/packages/client/ui-settings-plugins", + "@deepseek-ai/dsh-client-ui-primitives": "link:../../../deepseek-harness/packages/client/ui-primitives", + "@deepseek-ai/dsh-client-ui-slots": "link:../../../deepseek-harness/packages/client/ui-slots", + "@types/node": "^22.0.0", + "@types/react": "~18.3.1", + "lightningcss": "^1.32.0", + "react": "^18.2.0", + "tsdown": "^0.22.2" } } diff --git a/packages/tool-bailian-kb/src/client.ts b/packages/tool-bailian-kb/src/client.ts index ee91c6c..ddae77a 100644 --- a/packages/tool-bailian-kb/src/client.ts +++ b/packages/tool-bailian-kb/src/client.ts @@ -14,7 +14,8 @@ export class KbApiError extends Error { } export interface KbClientOptions { - workspaceId: string + /** Resolves the current workspace id per call (patch config or credential); throws with guidance when unconfigured. */ + resolveWorkspaceId: () => Promise endpointHost: string /** Service version forwarded on search/chat when set (deployment debug choice). */ agentVersion?: string @@ -33,9 +34,9 @@ export class KbClient { } private async post(path: string, body: unknown, accept: string, signal?: AbortSignal): Promise { - const apiKey = await this.opts.resolveApiKey() + const [apiKey, workspaceId] = await Promise.all([this.opts.resolveApiKey(), this.opts.resolveWorkspaceId()]) const fetchImpl = this.opts.fetchImpl ?? fetch - const url = kbEndpoint(this.opts.endpointHost, this.opts.workspaceId, path) + const url = kbEndpoint(this.opts.endpointHost, workspaceId, path) const res = await fetchImpl(url, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': accept }, diff --git a/packages/tool-bailian-kb/src/index.ts b/packages/tool-bailian-kb/src/index.ts index b88aae2..4c41d47 100644 --- a/packages/tool-bailian-kb/src/index.ts +++ b/packages/tool-bailian-kb/src/index.ts @@ -16,11 +16,11 @@ export const inject = ['tools', 'credentials'] /** Bailian knowledge-base plugin configuration. */ export interface Config { - /** Bailian workspace id; the API host is the workspace subdomain `https://.`. */ - workspaceId: string + /** Bailian workspace id; the API host is the workspace subdomain `https://.`. Optional here: an unset value falls back per call to the BAILIAN_WORKSPACE_ID credential (Settings → Plugins card or ~/.dsh/.credentials.yaml). */ + workspaceId?: string /** API host suffix; replace for other regions or private deployments. */ endpointHost: string - /** Retrieval-service id pinned by this deployment; when set, the tools' agent_id parameter becomes optional. */ + /** Retrieval-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_AGENT_ID credential. */ defaultAgentId?: string /** Service version to call: `beta` (draft) or a published number; defaults to the latest published version. Never model-visible. */ agentVersion?: string @@ -28,9 +28,9 @@ export interface Config { chatTimeoutMs: number } -/** Schemastery validation for {@link Config}; a missing workspaceId fails at load. */ +/** Schemastery validation for {@link Config}; workspaceId and defaultAgentId are optional — both resolve per call with a credentials fallback. */ export const Config: z = z.object({ - workspaceId: z.string().required(), + workspaceId: z.string(), endpointHost: z.string().default('cn-beijing.maas.aliyuncs.com'), defaultAgentId: z.string(), agentVersion: z.string(), @@ -44,16 +44,29 @@ export const Config: z = z.object({ * @param config - deployment's workspace, host, pinning, and timeout choices. */ export function apply(ctx: Context, config: Config): void { + const pinnedWorkspaceId = config.workspaceId + const pinnedAgentId = config.defaultAgentId const client = new KbClient({ - workspaceId: config.workspaceId, + resolveWorkspaceId: pinnedWorkspaceId === undefined + ? async () => { + const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_WORKSPACE_ID')) + if (!resolved) { + throw new Error( + 'BAILIAN_WORKSPACE_ID is not configured. Set it in the web UI (Settings → Plugins → Bailian knowledge base) ' + + 'or in ~/.dsh/.credentials.yaml; the workspace id appears as the subdomain of your Bailian endpoints.', + ) + } + return resolved.value + } + : async () => pinnedWorkspaceId, endpointHost: config.endpointHost, ...(config.agentVersion ? { agentVersion: config.agentVersion } : {}), resolveApiKey: async () => { const resolved = await ctx.credentials.resolve(credentialRef('DASHSCOPE_API_KEY')) if (!resolved) { throw new Error( - 'DASHSCOPE_API_KEY is not configured. Set it in ~/.dsh/.env or .credentials.yaml ' - + '(create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).', + 'DASHSCOPE_API_KEY is not configured. Set it in the web UI (Settings → Plugins → Bailian knowledge base) ' + + 'or in ~/.dsh/.credentials.yaml (create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).', ) } return resolved.value @@ -61,7 +74,12 @@ export function apply(ctx: Context, config: Config): void { }) for (const tool of createKbTools({ client, - ...(config.defaultAgentId ? { defaultAgentId: config.defaultAgentId } : {}), + resolveDefaultAgentId: pinnedAgentId !== undefined + ? async () => pinnedAgentId + : async () => { + const resolved = await ctx.credentials.resolve(credentialRef('BAILIAN_DEFAULT_AGENT_ID')) + return resolved?.value + }, chatTimeoutMs: config.chatTimeoutMs, })) { ctx.tools.register(tool) diff --git a/packages/tool-bailian-kb/src/tools.ts b/packages/tool-bailian-kb/src/tools.ts index 01f2d64..7a3253c 100644 --- a/packages/tool-bailian-kb/src/tools.ts +++ b/packages/tool-bailian-kb/src/tools.ts @@ -1,6 +1,9 @@ /** - * The three model-facing knowledge tools. Schemas are static per deployment: a configured - * defaultAgentId downgrades agent_id to optional at build time (never a runtime fallback chain). + * The three model-facing knowledge tools. agent_id stays optional in the schema + * regardless of deployment: the default service (patch config or credential) + * can change at runtime through the credentials domain, so the fallback runs + * per call and a missing default surfaces as an executable error instead of a + * load-time schema difference. */ import { defineTool } from '@deepseek-ai/dsh-tools' @@ -15,7 +18,8 @@ const DEFAULT_TOP_K = 5 export interface KbToolDeps { client: KbClient - defaultAgentId?: string + /** Resolves the default agent id per call (patch config or credential); omitted means no default. */ + resolveDefaultAgentId?: () => Promise chatTimeoutMs: number } @@ -49,18 +53,18 @@ async function withServiceHint(client: KbClient, err: unknown): Promise { * @returns definitions ready for `ctx.tools.register()`. */ export function createKbTools(deps: KbToolDeps) { - const { client, defaultAgentId, chatTimeoutMs } = deps + const { client, resolveDefaultAgentId, chatTimeoutMs } = deps const agentIdParam = { type: 'string' as const, - ...(defaultAgentId === undefined ? { required: true as const } : {}), - description: defaultAgentId === undefined - ? 'Retrieval/Q&A service id (find one via kb_service_list).' - : 'Retrieval/Q&A service id; omit to use this deployment\'s default service.', + description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via kb_service_list).', } - const resolveAgentId = (supplied: string | undefined): string => { - const agentId = supplied ?? defaultAgentId - if (agentId === undefined) throw new Error('agent_id is required: discover services with kb_service_list') - return agentId + const resolveAgentId = async (supplied: string | undefined): Promise => { + if (supplied !== undefined) return supplied + const defaultId = resolveDefaultAgentId === undefined ? undefined : await resolveDefaultAgentId() + if (defaultId === undefined) { + throw new Error('agent_id is required: no default service is configured; discover services with kb_service_list') + } + return defaultId } const serviceList = defineTool({ @@ -163,7 +167,7 @@ export function createKbTools(deps: KbToolDeps) { const topK = args.top_k ?? DEFAULT_TOP_K const body: SearchRequest = { query: args.query, - agent_id: resolveAgentId(args.agent_id), + agent_id: await resolveAgentId(args.agent_id), ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), ...(args.images && args.images.length > 0 ? { images: args.images } : {}), } @@ -210,7 +214,7 @@ export function createKbTools(deps: KbToolDeps) { const body = { input: { messages: [{ role: 'user' as const, content: args.message }] }, parameters: { agent_options: { - agent_id: resolveAgentId(args.agent_id), + agent_id: await resolveAgentId(args.agent_id), ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), } }, stream: true as const, diff --git a/packages/tool-bailian-kb/src/web/BailianCard.module.css b/packages/tool-bailian-kb/src/web/BailianCard.module.css new file mode 100644 index 0000000..f9800a8 --- /dev/null +++ b/packages/tool-bailian-kb/src/web/BailianCard.module.css @@ -0,0 +1,251 @@ +/* Bailian card: header, credential fields, clear control, and save footer. + Mirrors the host plugin-card chrome (an out-of-tree bundle cannot value- + import the host card components, only their platform primitives). */ + +.card { + list-style: none; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 12px; + background: var(--dsw-alias-bg-layer-3); + transition: border-color .16s, background .16s; +} + +.card:hover { + border-color: var(--dsw-alias-label-dimmed); +} + +/* An open card reads as the one being worked on, not merely taller. */ +.cardOpen { + background: var(--dsw-alias-bg-layer-2); + border-color: var(--dsw-alias-label-dimmed); +} + +.header { + width: 100%; + appearance: none; + border: 0; + background: none; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: 12px; +} + +.header:focus-visible { + outline: 2px solid var(--dsw-alias-brand-primary); + outline-offset: -2px; +} + +/* Name over description: the description is what tells two plugins apart. */ +.headText { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.name { + font-size: 15px; + font-weight: 600; + line-height: 1.4; + color: var(--dsw-alias-label-primary); +} + +.description { + font-size: 13px; + line-height: 1.5; + color: var(--dsw-alias-label-tertiary); +} + +.chevron { + flex: none; + color: var(--dsw-alias-label-tertiary); + transition: transform .16s; +} + +.chevronOpen { + transform: rotate(180deg); +} + +.body { + border-top: 1px solid var(--dsw-alias-border-l2); + margin: 0 16px; + padding-bottom: 8px; +} + +/* Carried on the header so a collapsed card still says it holds edits. */ +.pending { + flex: none; + border-radius: 999px; + padding: 1px 8px; + font-size: 11px; + line-height: 17px; + font-weight: 500; + white-space: nowrap; + background: var(--dsw-alias-bg-module-platform); + color: var(--dsw-alias-label-secondary); +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 0; +} + +.field + .field { + border-top: 1px solid var(--dsw-alias-border-l2); +} + +.head { + display: flex; + align-items: center; + gap: 8px; +} + +.label { + flex: 1; + min-width: 0; + font-size: 13px; + font-weight: 500; + line-height: 1.5; + color: var(--dsw-alias-label-primary); +} + +.badges { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.badge { + border-radius: 999px; + padding: 1px 8px; + font-size: 11px; + line-height: 17px; + white-space: nowrap; + font-weight: 500; + background: var(--dsw-alias-bg-module-platform); + color: var(--dsw-alias-label-secondary); +} + +.badgeMuted { + border-radius: 999px; + padding: 1px 8px; + font-size: 11px; + line-height: 17px; + white-space: nowrap; + color: var(--dsw-alias-label-tertiary); +} + +.clear { + border: none; + background: none; + padding: 0; + font: inherit; + font-size: 12px; + line-height: 1.5; + color: var(--dsw-alias-label-secondary); + cursor: pointer; +} + +.clear:hover:not(:disabled) { + color: var(--dsw-alias-label-primary); +} + +.clear:disabled { + cursor: default; +} + +.input { + height: 34px; + padding: 0 12px; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 8px; + background: var(--dsw-alias-bg-layer-3); + font: inherit; + font-size: 13px; + line-height: 1.5; + color: var(--dsw-alias-label-primary); +} + +.input:focus-visible { + outline: none; + border-color: var(--dsw-alias-brand-primary); +} + +.input:disabled { + color: var(--dsw-alias-label-tertiary); + cursor: default; +} + +.hint { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--dsw-alias-label-tertiary); +} + +.footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 12px 0 4px; + border-top: 1px solid var(--dsw-alias-border-l2); +} + +.failed { + flex: 1; + min-width: 0; + margin: 0; + font-size: 12px; + line-height: 1.5; + color: var(--dsw-alias-label-error); +} + +.discard, +.save { + appearance: none; + border: 1px solid transparent; + border-radius: 8px; + padding: 5px 14px; + font: inherit; + font-size: 13px; + line-height: 1.5; + cursor: pointer; +} + +.discard { + border-color: var(--dsw-alias-border-l2); + background: none; + color: var(--dsw-alias-label-secondary); +} + +.discard:hover:not(:disabled) { + color: var(--dsw-alias-label-primary); + border-color: var(--dsw-alias-label-dimmed); +} + +.save { + background: var(--dsw-alias-label-primary); + color: var(--dsw-alias-bg-layer-3); +} + +.discard:disabled, +.save:disabled { + opacity: 0.4; + cursor: default; +} + +.discard:focus-visible, +.save:focus-visible { + outline: 2px solid var(--dsw-alias-brand-primary); + outline-offset: 1px; +} diff --git a/packages/tool-bailian-kb/src/web/BailianCard.tsx b/packages/tool-bailian-kb/src/web/BailianCard.tsx new file mode 100644 index 0000000..98dac71 --- /dev/null +++ b/packages/tool-bailian-kb/src/web/BailianCard.tsx @@ -0,0 +1,136 @@ +/** + * The Bailian knowledge-base card: three write-only credential controls plus + * the default-service clear. Values never ride a response, so each control + * starts blank and reports only configured/unconfigured; the API key drafts + * behind a password mask while the workspace and agent ids draft in the clear + * — they are pasted identifiers, not secrets, and a visible draft can be + * proofread. + */ + +import { useState } from 'react' +import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import { BAILIAN_CARD_REFS, type BailianCardFace, type BailianFieldKey } from './bailian-card-controller.ts' +import type { BailianKbLocaleKey } from './locales.ts' +import css from './BailianCard.module.css' + +/** Props the renderer binds for the Bailian card. */ +export type BailianCardProps = + PropsRuntime<'settings.plugin.item'> + & PropsLocale<'tool-bailian-kb'> + & InjectFace + +/** One field's render description. */ +interface FieldView { + key: BailianFieldKey + labelKey: BailianKbLocaleKey + hintKey: BailianKbLocaleKey + setKey: BailianKbLocaleKey + unsetKey: BailianKbLocaleKey + /** Password-masked drafting; only the API key is an actual secret. */ + secret: boolean +} + +/** The three controls, in card order. */ +const FIELDS: readonly FieldView[] = [ + { key: 'DASHSCOPE_API_KEY', labelKey: 'apiKey', hintKey: 'apiKeyHint', setKey: 'apiKeySet', unsetKey: 'apiKeyUnset', secret: true }, + { key: 'BAILIAN_WORKSPACE_ID', labelKey: 'workspaceId', hintKey: 'workspaceIdHint', setKey: 'workspaceIdSet', unsetKey: 'workspaceIdUnset', secret: false }, + { key: 'BAILIAN_DEFAULT_AGENT_ID', labelKey: 'agentId', hintKey: 'agentIdHint', setKey: 'agentIdSet', unsetKey: 'agentIdUnset', secret: false }, +] + +/** + * Render the Bailian card. + * @param props - locale copy, the card snapshot, and its actions. + * @returns the card. + */ +export function BailianCard(props: BailianCardProps) { + const { t } = props + const state = props.useBailianCard(snapshot => snapshot) + const [open, setOpen] = useState(false) + const dirty = BAILIAN_CARD_REFS.some(key => state.drafts[key] !== '') + const busy = state.saving || state.clearing + return ( +
  • + + {open + ? ( +
    + {FIELDS.map(field => { + const credential = state.credentials[field.key] + // The launch environment wins and refuses writes: the badge says + // where the value lives instead of a control that cannot act. + const stateLabel = credential.configured + ? (credential.writable ? t(field.setKey) : t('fromEnv')) + : t(field.unsetKey) + const showClear = field.key === 'BAILIAN_DEFAULT_AGENT_ID' && credential.configured + return ( +
    +
    + + + {showClear + ? ( + + ) + : null} + {stateLabel} + +
    + { props.edit(field.key, event.target.value) }} + /> +

    {t(field.hintKey)}

    +
    + ) + })} +
    + {state.failed ?

    {t('saveFailed')}

    : null} + + +
    +
    + ) + : null} +
  • + ) +} diff --git a/packages/tool-bailian-kb/src/web/bailian-card-controller.ts b/packages/tool-bailian-kb/src/web/bailian-card-controller.ts new file mode 100644 index 0000000..0965acb --- /dev/null +++ b/packages/tool-bailian-kb/src/web/bailian-card-controller.ts @@ -0,0 +1,216 @@ +/** + * The Bailian card's controller: staged drafts over the credentials domain. + * + * All three values ride credential references (no settings namespace is + * involved — an out-of-tree package cannot expose one to the browser), so the + * card never holds a stored literal: it learns only whether each reference is + * configured and writable, stages drafts locally, and one save writes every + * non-blank draft through `credentials.set`. A blank draft writes nothing and + * keeps the stored value. The default-service reference is the one value a + * user can meaningfully remove, so it alone gets a clear action + * (`credentials.unset`), immediate rather than staged. + */ + +import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client' +import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' + +/** The credential references this card stages, keyed by their ref names. */ +export const BAILIAN_CARD_REFS = [ + 'DASHSCOPE_API_KEY', + 'BAILIAN_WORKSPACE_ID', + 'BAILIAN_DEFAULT_AGENT_ID', +] as const + +/** One card field, addressed by its credential reference. */ +export type BailianFieldKey = (typeof BAILIAN_CARD_REFS)[number] + +/** What the credentials domain reports for one reference (never the value). */ +export interface BailianCredentialView { + /** Whether any layer supplies a value for the reference. */ + configured: boolean + /** Whether `credentials.set` can affect it; false disables the control. */ + writable: boolean +} + +/** What the Bailian card renders. */ +export interface BailianCardState { + /** Staged drafts, blank = keep the stored value. */ + drafts: Record + /** Last credentials-domain answer per reference; unknown refs read as writable. */ + credentials: Record + /** Whether a save is in flight. */ + saving: boolean + /** Whether the default-service clear is in flight. */ + clearing: boolean + /** Whether the last save or clear was refused; drafts are kept for correction. */ + failed: boolean +} + +/** The registration-side face the card's slot entry injects. */ +export interface BailianCardFace { + hooks: { + /** Card snapshot bound by the renderer as useBailianCard. */ + bailianCard: SnapshotStore + } + /** Stage one draft. */ + edit: (key: BailianFieldKey, text: string) => void + /** Write every non-blank draft through `credentials.set`, then re-read. */ + save: () => Promise + /** Drop every staged draft. */ + discard: () => void + /** Remove the stored default service (`credentials.unset`), then re-read. */ + clearDefaultAgent: () => Promise +} + +/** Bridge the credentials domain onto the card. */ +export class BailianCardController { + private readonly store: SnapshotStore + + /** + * @param api - wire face used for the three credential references. + */ + constructor(private readonly api: Pick) { + this.store = createSnapshotStore({ + drafts: { + DASHSCOPE_API_KEY: '', + BAILIAN_WORKSPACE_ID: '', + BAILIAN_DEFAULT_AGENT_ID: '', + }, + credentials: { + DASHSCOPE_API_KEY: { configured: false, writable: true }, + BAILIAN_WORKSPACE_ID: { configured: false, writable: true }, + BAILIAN_DEFAULT_AGENT_ID: { configured: false, writable: true }, + }, + saving: false, + clearing: false, + failed: false, + }) + void this.read() + } + + /** Whether any draft is staged. */ + get dirty(): boolean { + return BAILIAN_CARD_REFS.some(key => this.store.getSnapshot().drafts[key] !== '') + } + + /** + * Stage one draft; any edit clears the failure mark so the banner does not + * outlive the correction it asks for. + * @param key - the field's credential reference. + * @param text - the staged text. + */ + edit(key: BailianFieldKey, text: string): void { + this.store.update(draft => { + draft.drafts[key] = text + draft.failed = false + }) + } + + /** + * Write every non-blank draft, then re-read all references. A refused write + * keeps its draft: the copy tells the user the values were left to correct. + */ + async save(): Promise { + const staged = new Map( + BAILIAN_CARD_REFS + .map(key => [key, this.store.getSnapshot().drafts[key]] as const) + .filter(([, text]) => text !== ''), + ) + if (staged.size === 0 || this.store.getSnapshot().saving) return + this.store.update(draft => { draft.saving = true }) + let failed = false + await Promise.all([...staged].map(async ([ref, value]) => { + try { + const response = await this.api.credentials.set({ ref, value }) + if (!response.result.ok) failed = true + } catch (_credentialWriteFailure) { + failed = true + } + })) + this.store.update(draft => { + draft.saving = false + draft.failed = failed + if (!failed) for (const ref of staged.keys()) draft.drafts[ref] = '' + }) + await this.read() + } + + /** Drop every staged draft and the failure mark. */ + discard(): void { + this.store.update(draft => { + for (const ref of BAILIAN_CARD_REFS) draft.drafts[ref] = '' + draft.failed = false + }) + } + + /** Remove the stored default service so every call names one again. */ + async clearDefaultAgent(): Promise { + if (this.store.getSnapshot().clearing) return + this.store.update(draft => { draft.clearing = true }) + let failed = false + try { + const response = await this.api.credentials.unset({ ref: 'BAILIAN_DEFAULT_AGENT_ID' }) + if (!response.result.ok) failed = true + } catch (_credentialWriteFailure) { + failed = true + } + this.store.update(draft => { + draft.clearing = false + draft.failed = failed + }) + await this.read() + } + + /** + * Re-read after the Host reports a change to a reference this card watches. + * + * A value can be written from somewhere else — the Models page addresses + * DASHSCOPE_API_KEY too, and the file store accepts external edits — so + * without this the badges keep reporting a state the Host already replaced. + * @param ref - the reference the Host reports as changed. + */ + refresh(ref: string): void { + if (!(BAILIAN_CARD_REFS as readonly string[]).includes(ref)) return + void this.read() + } + + /** + * Build the face the card's slot registration injects. + * @returns the card's snapshot and its actions. + */ + inject(): BailianCardFace { + return { + hooks: { bailianCard: this.store }, + edit: (key, text) => { this.edit(key, text) }, + save: () => this.save(), + discard: () => { this.discard() }, + clearDefaultAgent: () => this.clearDefaultAgent(), + } + } + + /** + * Ask the credentials domain about all three references and publish the + * answer. A failed read keeps the last known state: the card stays usable + * and a write still reaches the Host. + */ + private async read(): Promise { + let response: Awaited> + try { + response = await this.api.credentials.describe({ refs: [...BAILIAN_CARD_REFS] }) + } catch (_credentialReadFailure) { + return + } + if (!response.result.ok) return + const view = response.result.value.credentials + this.store.update(draft => { + for (const ref of BAILIAN_CARD_REFS) { + // An unknown reference reads as writable: the control stays usable and + // the Host is what refuses, rather than the card guessing a refusal. + draft.credentials[ref] = { + configured: view[ref]?.configured ?? false, + writable: view[ref]?.writable ?? true, + } + } + }) + } +} diff --git a/packages/tool-bailian-kb/src/web/css-modules.d.ts b/packages/tool-bailian-kb/src/web/css-modules.d.ts new file mode 100644 index 0000000..6da4b80 --- /dev/null +++ b/packages/tool-bailian-kb/src/web/css-modules.d.ts @@ -0,0 +1,9 @@ +/** + * CSS Modules for the browser half: the bundler (tsdown client preset) inlines + * `*.module.css` imports as hashed class maps, this declaration gives the + * import its type in the browser-only project. + */ +declare module '*.module.css' { + const classes: Record + export default classes +} diff --git a/packages/tool-bailian-kb/src/web/index.ts b/packages/tool-bailian-kb/src/web/index.ts new file mode 100644 index 0000000..ec752ba --- /dev/null +++ b/packages/tool-bailian-kb/src/web/index.ts @@ -0,0 +1,60 @@ +/** + * Bailian knowledge-base plugin, browser half: one card in the plugin + * configuration section staging the three credential references the Host half + * resolves per call. The card is pure credentials-domain — this package + * exposes no settings namespace (an out-of-tree package cannot get one onto + * the browser settings surface), so nothing here touches a settings scope. + */ + +import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' +// Type-only: the locale plugin's Context merge (ctx.locale). +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +// Type-only: the remote service's Context merge (ctx.remote) and the forwarded +// credential-update events. +import type {} from '@deepseek-ai/dsh-api-remotes/client' +import type {} from '@deepseek-ai/dsh-client-ui-slots' +// Type-only: the 'settings.plugin.item' SlotMap merge, declared by the plugins +// settings section this card registers into. +import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client' +import { BailianCard } from './BailianCard.tsx' +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 card's copy. */ + 'tool-bailian-kb': BailianKbLocaleKey + } +} + +/** Dictionary namespace owned by this plugin. */ +const NS = 'tool-bailian-kb' + +/** Required services (cordis fiber inject). */ +export const inject = ['slots', 'locale', 'connection', 'remote'] + +/** + * Mount the Bailian card into the plugin configuration section. + * @param ctx - the browser plugin context. + */ +export function apply(ctx: ClientContext): void { + const { api } = ctx.get('connection') as ConnectionHandle + ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'tool-bailian-kb: dictionaries') + + 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( + () => ctx.remote.$on('credentials/updated', ref => { card.refresh(ref) }), + 'tool-bailian-kb: credential invalidations', + ) + + ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ + name: 'settings.plugin.item', + id: 'bailian-kb', + order: 30, + locale: NS, + inject: () => card.inject(), + }, BailianCard)) +} diff --git a/packages/tool-bailian-kb/src/web/locales.ts b/packages/tool-bailian-kb/src/web/locales.ts new file mode 100644 index 0000000..f4b7cf7 --- /dev/null +++ b/packages/tool-bailian-kb/src/web/locales.ts @@ -0,0 +1,71 @@ +/** + * Locale bundles for the Bailian knowledge-base plugin card. The card rides + * the credentials domain for all three values, so every copy is written for + * write-only controls: state is reported as configured/unconfigured, and a + * stored value is never echoed back. + */ + +/** Locale keys this card renders. */ +export type BailianKbLocaleKey = + | 'title' | 'description' + | 'apiKey' | 'apiKeyHint' | 'apiKeySet' | 'apiKeyUnset' + | 'workspaceId' | 'workspaceIdHint' | 'workspaceIdSet' | 'workspaceIdUnset' + | 'agentId' | 'agentIdHint' | 'agentIdSet' | 'agentIdUnset' + | 'fromEnv' | 'clear' | 'clearing' | 'expand' | 'collapse' + | 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed' + +/** English copy. */ +export const en: Record = { + title: 'Bailian knowledge base', + description: 'Account for the knowledge tools: API key, workspace, and default service.', + apiKey: 'API key', + apiKeyHint: 'DashScope API key. Stored in the credentials store and never shown again; leave blank to keep the current one.', + apiKeySet: 'A key is configured.', + apiKeyUnset: 'No key is configured; knowledge tools fail until one is.', + workspaceId: 'Workspace id', + workspaceIdHint: 'Bailian workspace id — the subdomain of your endpoints. Leave blank to keep the current one.', + workspaceIdSet: 'A workspace is configured.', + workspaceIdUnset: 'No workspace is configured; knowledge tools fail until one is.', + agentId: 'Default service id', + agentIdHint: 'agent_id of the default retrieval/Q&A service; when unset, every call must name one (kb_service_list discovers ids). Leave blank to keep the current one.', + agentIdSet: 'A default service is configured.', + agentIdUnset: 'No default service; every call must name one.', + fromEnv: 'Set by the environment (read-only here)', + clear: 'Clear default', + clearing: 'Clearing…', + expand: 'Show settings', + collapse: 'Hide settings', + save: 'Save', + saving: 'Saving…', + discard: 'Discard', + unsaved: 'Unsaved', + saveFailed: 'The Host did not accept these values; they were left for you to correct.', +} + +/** Simplified Chinese copy. */ +export const zh: Record = { + title: '百炼知识库', + description: '知识库工具的账号信息:API 密钥、工作空间与默认服务。', + apiKey: 'API 密钥', + apiKeyHint: 'DashScope API key。保存在凭据存储中且不会再次显示;留空表示保持当前值。', + apiKeySet: '已配置密钥。', + apiKeyUnset: '未配置密钥;配置前知识库工具不可用。', + workspaceId: '工作空间 ID', + workspaceIdHint: '百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。', + workspaceIdSet: '已配置工作空间。', + workspaceIdUnset: '未配置工作空间;配置前知识库工具不可用。', + agentId: '默认服务 ID', + agentIdHint: '默认检索/问答服务的 agent_id;未设置时每次调用都需显式指定(可用 kb_service_list 发现 id)。留空表示保持当前值。', + agentIdSet: '已配置默认服务。', + agentIdUnset: '未配置默认服务;每次调用需显式指定。', + fromEnv: '来自环境变量(此处只读)', + clear: '清除默认', + clearing: '清除中…', + expand: '展开设置', + collapse: '收起设置', + save: '保存', + saving: '保存中…', + discard: '放弃', + unsaved: '未保存', + saveFailed: '宿主未接受这些值,已保留供你修改。', +} diff --git a/packages/tool-bailian-kb/tests/client.test.ts b/packages/tool-bailian-kb/tests/client.test.ts index c1aa003..5b48633 100644 --- a/packages/tool-bailian-kb/tests/client.test.ts +++ b/packages/tool-bailian-kb/tests/client.test.ts @@ -3,7 +3,7 @@ import { KbApiError, KbClient } from '../src/client.js' function makeClient(fetchImpl: typeof fetch) { return new KbClient({ - workspaceId: 'ws-1', + resolveWorkspaceId: async () => 'ws-1', endpointHost: 'cn-beijing.maas.aliyuncs.com', resolveApiKey: async () => 'sk-test', fetchImpl, @@ -36,7 +36,7 @@ describe('KbClient.postJson', () => { const resolveApiKey = vi.fn(async () => 'sk-test') const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 })) const client = new KbClient({ - workspaceId: 'ws-1', + resolveWorkspaceId: async () => 'ws-1', endpointHost: 'h', resolveApiKey, fetchImpl: fetchImpl as unknown as typeof fetch, @@ -45,4 +45,20 @@ describe('KbClient.postJson', () => { await client.postJson('/p', {}) expect(resolveApiKey).toHaveBeenCalledTimes(2) }) + + it('re-resolves the workspace id per call (credential hot-swap contract)', async () => { + let workspaceId = 'ws-1' + const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 })) + const client = new KbClient({ + resolveWorkspaceId: async () => workspaceId, + endpointHost: 'h', + resolveApiKey: async () => 'sk-test', + fetchImpl: fetchImpl as unknown as typeof fetch, + }) + await client.postJson('/p', {}) + workspaceId = 'ws-2' + await client.postJson('/p', {}) + const urls = fetchImpl.mock.calls.map(call => (call as unknown as [string])[0]) + expect(urls).toEqual(['https://ws-1.h/p', 'https://ws-2.h/p']) + }) }) diff --git a/packages/tool-bailian-kb/tests/config.test.ts b/packages/tool-bailian-kb/tests/config.test.ts index f43a698..0030c00 100644 --- a/packages/tool-bailian-kb/tests/config.test.ts +++ b/packages/tool-bailian-kb/tests/config.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { Config } from '../src/index.js' describe('Config', () => { - it('applies defaults and keeps required workspaceId', () => { + it('applies defaults and accepts a pinned workspaceId', () => { const resolved = new Config({ workspaceId: 'ws-1' }) expect(resolved.workspaceId).toBe('ws-1') expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com') @@ -10,7 +10,9 @@ describe('Config', () => { expect(resolved.defaultAgentId).toBeUndefined() }) - it('rejects a missing workspaceId (fail loud at load)', () => { - expect(() => new Config({} as never)).toThrow() + it('accepts a missing workspaceId (per-call credentials fallback)', () => { + const resolved = new Config({} as never) + expect(resolved.workspaceId).toBeUndefined() + expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com') }) }) diff --git a/packages/tool-bailian-kb/tests/tools.test.ts b/packages/tool-bailian-kb/tests/tools.test.ts index 1ac7d1f..14f9632 100644 --- a/packages/tool-bailian-kb/tests/tools.test.ts +++ b/packages/tool-bailian-kb/tests/tools.test.ts @@ -4,9 +4,9 @@ import { createKbTools } from '../src/tools.js' const EXEC = {} as never -function toolsWith(postJson: unknown, postSse?: unknown, defaultAgentId?: string) { +function toolsWith(postJson: unknown, postSse?: unknown, resolveDefaultAgentId?: () => Promise) { const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient - const list = createKbTools({ client, ...(defaultAgentId ? { defaultAgentId } : {}), chatTimeoutMs: 1000 }) + const list = createKbTools({ client, ...(resolveDefaultAgentId ? { resolveDefaultAgentId } : {}), chatTimeoutMs: 1000 }) const byName = Object.fromEntries(list.map(t => [t.name, t])) return { byName, list } } @@ -36,23 +36,46 @@ describe('createKbTools', () => { expect(body.agent_id).toBe('aid-1') }) - it('agent_id is required without defaultAgentId and optional with one', () => { + it('agent_id stays optional in the schema regardless of a configured default', () => { const withoutDefault = toolsWith(vi.fn()).byName.kb_search! - const withDefault = toolsWith(vi.fn(), undefined, 'aid-fixed').byName.kb_search! + const withDefault = toolsWith(vi.fn(), undefined, async () => 'aid-fixed').byName.kb_search! // defineTool compiles the spec into JSON Schema: requiredness lives in the top-level `required` array. const requiredList = (tool: { parameters: Record }) => (tool.parameters.required ?? []) as string[] - expect(requiredList(withoutDefault)).toContain('agent_id') + // The default can arrive or leave at runtime via the credentials domain, so + // the schema cannot promise requiredness either way. + expect(requiredList(withoutDefault)).not.toContain('agent_id') expect(requiredList(withDefault)).not.toContain('agent_id') }) - it('kb_search falls back to defaultAgentId as an explicit resolve step', async () => { + it('kb_search falls back to the per-call default resolver as an explicit resolve step', async () => { const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) - const { byName } = toolsWith(postJson, undefined, 'aid-fixed') + const { byName } = toolsWith(postJson, undefined, async () => 'aid-fixed') await byName.kb_search!.execute({ query: 'q' }, EXEC) expect((postJson.mock.calls[0]![1] as Record).agent_id).toBe('aid-fixed') }) + it('a missing agent_id without any default resolves to executable discovery guidance', async () => { + const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) + const { byName } = toolsWith(postJson) + const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toContain('kb_service_list') + }) + + it('kb_search re-resolves the default per call (credential hot-swap contract)', async () => { + const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) + let current: string | undefined + const resolveDefaultAgentId = vi.fn(async () => current) + const { byName } = toolsWith(postJson, undefined, resolveDefaultAgentId) + current = 'aid-one' + await byName.kb_search!.execute({ query: 'q' }, EXEC) + current = undefined + await byName.kb_search!.execute({ query: 'q' }, EXEC).catch(() => {}) + expect(resolveDefaultAgentId).toHaveBeenCalledTimes(2) + expect((postJson.mock.calls[0]![1] as Record).agent_id).toBe('aid-one') + expect(postJson).toHaveBeenCalledTimes(1) + }) + it('a 4xx failure appends the current service list to the error', async () => { const postJson = vi.fn(async (path: string) => { if (path === '/api/v1/indices/knowledge/search') throw new KbApiError('agent not found', 400) diff --git a/packages/tool-bailian-kb/tsconfig.json b/packages/tool-bailian-kb/tsconfig.json index c714815..7dca09b 100644 --- a/packages/tool-bailian-kb/tsconfig.json +++ b/packages/tool-bailian-kb/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "lib" }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/web"] } diff --git a/packages/tool-bailian-kb/tsconfig.web.json b/packages/tool-bailian-kb/tsconfig.web.json new file mode 100644 index 0000000..5c3bedf --- /dev/null +++ b/packages/tool-bailian-kb/tsconfig.web.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowImportingTsExtensions": true, + "types": [] + }, + "include": ["src/web"] +} diff --git a/packages/tool-bailian-kb/tsdown.config.ts b/packages/tool-bailian-kb/tsdown.config.ts new file mode 100644 index 0000000..c86f53d --- /dev/null +++ b/packages/tool-bailian-kb/tsdown.config.ts @@ -0,0 +1,129 @@ +/** + * Browser bundle for the plugin's client half, mirroring the host's tsdown + * client preset (packages/client/tsdown.client.ts — spelled out here because + * an out-of-tree package cannot import it): a closure-factory artifact that + * calls window.__ModuleLoader__.load({id, factory}) and resolves externals + * through the injected require. CSS Modules are compiled by lightningcss + * inside the bundle: importing `x.module.css` yields the hashed class map and + * auto-injects a