From 3aedb7b7fb81dfc55e64713adcacfdeab9759cc4 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:25:13 +0800 Subject: [PATCH 01/45] chore: scaffold pnpm workspace --- .gitignore | 3 +++ package.json | 14 ++++++++++++++ pnpm-workspace.yaml | 2 ++ tsconfig.base.json | 12 ++++++++++++ vitest.config.ts | 5 +++++ 5 files changed, 36 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c91d0b1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +lib/ +*.tsbuildinfo diff --git a/package.json b/package.json new file mode 100644 index 00000000..98b59a3d --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "bailian-kb-workspace", + "private": true, + "type": "module", + "scripts": { + "build": "pnpm -r run build", + "test": "vitest run", + "typecheck": "tsc -b packages/tool-bailian-kb" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..924b55f4 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..d819af27 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..5262c5b2 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { include: ['packages/*/tests/**/*.test.ts'] }, +}) From fba98bf65eae3be955136e019f010d17bfbc6654 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:28:48 +0800 Subject: [PATCH 02/45] feat: plugin package skeleton with validated Config --- packages/tool-bailian-kb/package.json | 29 + packages/tool-bailian-kb/src/index.ts | 33 + packages/tool-bailian-kb/tests/config.test.ts | 16 + packages/tool-bailian-kb/tsconfig.json | 5 + pnpm-lock.yaml | 1053 +++++++++++++++++ 5 files changed, 1136 insertions(+) create mode 100644 packages/tool-bailian-kb/package.json create mode 100644 packages/tool-bailian-kb/src/index.ts create mode 100644 packages/tool-bailian-kb/tests/config.test.ts create mode 100644 packages/tool-bailian-kb/tsconfig.json create mode 100644 pnpm-lock.yaml diff --git a/packages/tool-bailian-kb/package.json b/packages/tool-bailian-kb/package.json new file mode 100644 index 00000000..853c5d5f --- /dev/null +++ b/packages/tool-bailian-kb/package.json @@ -0,0 +1,29 @@ +{ + "name": "dsh-tool-bailian-kb", + "version": "0.1.0", + "description": "Bailian knowledge-base tools for DeepSeek Harness: kb_service_list, kb_search, kb_chat over the DashScope RAG API, plus the kscli management skill.", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" }, + "./package.json": "./package.json" + }, + "files": ["lib", "skills"], + "scripts": { "build": "tsc -b" }, + "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" + }, + "devDependencies": { + "@deepseek-ai/cordis": "link:../../../deepseek-harness/vendor/cordis", + "@deepseek-ai/dsh-tools": "link:../../../deepseek-harness/packages/core/tools", + "@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" + } +} diff --git a/packages/tool-bailian-kb/src/index.ts b/packages/tool-bailian-kb/src/index.ts new file mode 100644 index 00000000..e6acfb24 --- /dev/null +++ b/packages/tool-bailian-kb/src/index.ts @@ -0,0 +1,33 @@ +/** + * Bailian knowledge-base consumer plugin: registers kb_service_list, kb_search, + * and kb_chat over the DashScope RAG API, plus the kscli management skill. + * @module dsh-tool-bailian-kb + */ + +import z from '@deepseek-ai/schemastery' + +export const name = 'tool-bailian-kb' +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 + /** 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. */ + defaultAgentId?: string + /** Service version to call: `beta` (draft) or a published number; defaults to the latest published version. Never model-visible. */ + agentVersion?: string + /** kb_chat timeout in milliseconds; the server side is a minutes-scale agentic loop. */ + chatTimeoutMs: number +} + +/** Schemastery validation for {@link Config}; a missing workspaceId fails at load. */ +export const Config: z = z.object({ + workspaceId: z.string().required(), + endpointHost: z.string().default('cn-beijing.maas.aliyuncs.com'), + defaultAgentId: z.string(), + agentVersion: z.string(), + chatTimeoutMs: z.number().default(300_000), +}) diff --git a/packages/tool-bailian-kb/tests/config.test.ts b/packages/tool-bailian-kb/tests/config.test.ts new file mode 100644 index 00000000..f43a6981 --- /dev/null +++ b/packages/tool-bailian-kb/tests/config.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' +import { Config } from '../src/index.js' + +describe('Config', () => { + it('applies defaults and keeps required workspaceId', () => { + const resolved = new Config({ workspaceId: 'ws-1' }) + expect(resolved.workspaceId).toBe('ws-1') + expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com') + expect(resolved.chatTimeoutMs).toBe(300_000) + expect(resolved.defaultAgentId).toBeUndefined() + }) + + it('rejects a missing workspaceId (fail loud at load)', () => { + expect(() => new Config({} as never)).toThrow() + }) +}) diff --git a/packages/tool-bailian-kb/tsconfig.json b/packages/tool-bailian-kb/tsconfig.json new file mode 100644 index 00000000..c7148159 --- /dev/null +++ b/packages/tool-bailian-kb/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "lib" }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..66a74d57 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1053 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.7(@types/node@22.20.1) + + packages/tool-bailian-kb: + devDependencies: + '@deepseek-ai/cordis': + specifier: link:../../../deepseek-harness/vendor/cordis + version: link:../../../deepseek-harness/vendor/cordis + '@deepseek-ai/dsh-credentials': + specifier: link:../../../deepseek-harness/packages/credentials/credentials + version: link:../../../deepseek-harness/packages/credentials/credentials + '@deepseek-ai/dsh-skill': + specifier: link:../../../deepseek-harness/packages/skill/skill + version: link:../../../deepseek-harness/packages/skill/skill + '@deepseek-ai/dsh-tools': + specifier: link:../../../deepseek-harness/packages/core/tools + version: link:../../../deepseek-harness/packages/core/tools + '@deepseek-ai/schemastery': + specifier: link:../../../deepseek-harness/vendor/schemastery + version: link:../../../deepseek-harness/vendor/schemastery + '@types/node': + specifier: ^22.0.0 + version: 22.20.1 + +packages: + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fsevents@2.3.3: + optional: true + + js-tokens@9.0.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite-node@3.2.4(@types/node@22.20.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.20.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.20.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@22.20.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.20.1) + vite-node: 3.2.4(@types/node@22.20.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 From bbe96d35a553e70fd5f5432ce16fa93f76c99554 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:30:06 +0800 Subject: [PATCH 03/45] feat: endpoint builder and API types --- packages/tool-bailian-kb/src/api-types.ts | 53 +++++++++++++++++++ packages/tool-bailian-kb/src/endpoints.ts | 19 +++++++ .../tool-bailian-kb/tests/endpoints.test.ts | 14 +++++ 3 files changed, 86 insertions(+) create mode 100644 packages/tool-bailian-kb/src/api-types.ts create mode 100644 packages/tool-bailian-kb/src/endpoints.ts create mode 100644 packages/tool-bailian-kb/tests/endpoints.test.ts diff --git a/packages/tool-bailian-kb/src/api-types.ts b/packages/tool-bailian-kb/src/api-types.ts new file mode 100644 index 00000000..73534e41 --- /dev/null +++ b/packages/tool-bailian-kb/src/api-types.ts @@ -0,0 +1,53 @@ +/** Request/response fields of the three DashScope knowledge endpoints, mirrored from the verified kscli types. */ + +export interface ServiceListRequest { + agent_scene: 'chat' | 'search' + agent_name?: string + page_number: number + page_size: number +} + +export interface ServiceListRow { + agent_id?: string + agent_name?: string + agent_scene?: string + agent_status?: string + pipeline_list?: { pipeline_id?: string; pipeline_name?: string }[] +} + +export interface ServiceListResponse { + code?: string + message?: string + data?: { total_count?: number; rows?: ServiceListRow[] } +} + +export interface SearchRequest { + query: string + agent_id: string + agent_version?: string + images?: string[] +} + +export interface SearchResponse { + request_id?: string + data?: { + total?: number + nodes?: { score: number; text: string; metadata?: Record }[] + } +} + +export interface ChatRequest { + input: { messages: { role: 'user' | 'assistant'; content: string }[] } + parameters: { agent_options: { agent_id: string; agent_version?: string } } + stream: true +} + +export interface ChatStreamChunk { + output?: { + choices?: { + message?: { content?: string; extra?: { step_change?: string } } + finish_reason?: string + }[] + } + request_id?: string +} diff --git a/packages/tool-bailian-kb/src/endpoints.ts b/packages/tool-bailian-kb/src/endpoints.ts new file mode 100644 index 00000000..3ecce537 --- /dev/null +++ b/packages/tool-bailian-kb/src/endpoints.ts @@ -0,0 +1,19 @@ +/** Protocol path constants and the workspace-subdomain URL builder (external API spec; not configurable). */ + +/** DashScope knowledge API paths, mirrored from the verified kscli endpoint table. */ +export const KB_PATHS = { + serviceList: '/api/v1/indices/rag/app/list', + search: '/api/v1/indices/knowledge/search', + chat: '/api/v2/apps/knowledge/chat', +} as const + +/** + * Build one knowledge API endpoint. + * @param endpointHost - host suffix, e.g. `cn-beijing.maas.aliyuncs.com`. + * @param workspaceId - Bailian workspace id used as the subdomain. + * @param path - one {@link KB_PATHS} value. + * @returns the absolute endpoint URL. + */ +export function kbEndpoint(endpointHost: string, workspaceId: string, path: string): string { + return `https://${workspaceId}.${endpointHost}${path}` +} diff --git a/packages/tool-bailian-kb/tests/endpoints.test.ts b/packages/tool-bailian-kb/tests/endpoints.test.ts new file mode 100644 index 00000000..a64ee987 --- /dev/null +++ b/packages/tool-bailian-kb/tests/endpoints.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { KB_PATHS, kbEndpoint } from '../src/endpoints.js' + +describe('kbEndpoint', () => { + it('builds the workspace-subdomain URL', () => { + expect(kbEndpoint('cn-beijing.maas.aliyuncs.com', 'ws-1', KB_PATHS.search)) + .toBe('https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search') + }) + + it('keeps protocol paths as constants', () => { + expect(KB_PATHS.chat).toBe('/api/v2/apps/knowledge/chat') + expect(KB_PATHS.serviceList).toBe('/api/v1/indices/rag/app/list') + }) +}) From cdc1f10882a619698d378920614f186c3bb3d1d1 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:31:15 +0800 Subject: [PATCH 04/45] feat: KbClient with per-call auth and error translation --- packages/tool-bailian-kb/src/client.ts | 79 +++++++++++++++++++ packages/tool-bailian-kb/tests/client.test.ts | 48 +++++++++++ 2 files changed, 127 insertions(+) create mode 100644 packages/tool-bailian-kb/src/client.ts create mode 100644 packages/tool-bailian-kb/tests/client.test.ts diff --git a/packages/tool-bailian-kb/src/client.ts b/packages/tool-bailian-kb/src/client.ts new file mode 100644 index 00000000..ee91c6cd --- /dev/null +++ b/packages/tool-bailian-kb/src/client.ts @@ -0,0 +1,79 @@ +/** Shared HTTP client for the knowledge endpoints: per-call Bearer auth, JSON/SSE POST, and error translation. */ + +import { kbEndpoint } from './endpoints.js' + +/** Maximum error-body characters kept in a translated message. */ +const ERROR_BODY_LIMIT = 500 + +/** One knowledge API failure: HTTP status plus a bounded server-body summary. */ +export class KbApiError extends Error { + constructor(message: string, readonly status?: number) { + super(message) + this.name = 'KbApiError' + } +} + +export interface KbClientOptions { + workspaceId: string + endpointHost: string + /** Service version forwarded on search/chat when set (deployment debug choice). */ + agentVersion?: string + /** Resolves the current DASHSCOPE_API_KEY per call; throws with guidance when unconfigured. */ + resolveApiKey: () => Promise + /** Test seam; defaults to global fetch. */ + fetchImpl?: typeof fetch +} + +export class KbClient { + constructor(private readonly opts: KbClientOptions) {} + + /** The deployment's configured service version, exposed for request builders. */ + get agentVersion(): string | undefined { + return this.opts.agentVersion + } + + private async post(path: string, body: unknown, accept: string, signal?: AbortSignal): Promise { + const apiKey = await this.opts.resolveApiKey() + const fetchImpl = this.opts.fetchImpl ?? fetch + const url = kbEndpoint(this.opts.endpointHost, this.opts.workspaceId, path) + const res = await fetchImpl(url, { + method: 'POST', + headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': accept }, + body: JSON.stringify(body), + signal, + }) + if (!res.ok) { + const raw = (await res.text().catch(() => '')).slice(0, ERROR_BODY_LIMIT) + let detail = raw + try { + const parsed = JSON.parse(raw) as { message?: string; code?: string } + if (parsed.message) detail = parsed.code ? `${parsed.code}: ${parsed.message}` : parsed.message + } catch { /* non-JSON error body: keep the bounded raw text */ } + throw new KbApiError(`knowledge API ${path} failed (HTTP ${res.status}): ${detail}`, res.status) + } + return res + } + + /** + * POST one JSON request and parse the JSON response. + * @param path - one KB_PATHS value. + * @param body - JSON-serializable request body. + * @param signal - optional abort/timeout signal. + * @returns the parsed response. + */ + async postJson(path: string, body: unknown, signal?: AbortSignal): Promise { + const res = await this.post(path, body, 'application/json', signal) + return await res.json() as T + } + + /** + * POST one JSON request expecting an SSE response stream. + * @param path - one KB_PATHS value. + * @param body - JSON-serializable request body. + * @param signal - abort/timeout signal (kb_chat passes its configured timeout). + * @returns the raw Response whose body is the SSE stream. + */ + async postSse(path: string, body: unknown, signal?: AbortSignal): Promise { + return await this.post(path, body, 'text/event-stream', signal) + } +} diff --git a/packages/tool-bailian-kb/tests/client.test.ts b/packages/tool-bailian-kb/tests/client.test.ts new file mode 100644 index 00000000..c1aa0038 --- /dev/null +++ b/packages/tool-bailian-kb/tests/client.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from 'vitest' +import { KbApiError, KbClient } from '../src/client.js' + +function makeClient(fetchImpl: typeof fetch) { + return new KbClient({ + workspaceId: 'ws-1', + endpointHost: 'cn-beijing.maas.aliyuncs.com', + resolveApiKey: async () => 'sk-test', + fetchImpl, + }) +} + +describe('KbClient.postJson', () => { + it('sends Bearer auth to the workspace endpoint and returns parsed JSON', async () => { + const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: 1 }), { status: 200 })) + const client = makeClient(fetchImpl as unknown as typeof fetch) + const result = await client.postJson<{ ok: number }>('/api/v1/indices/knowledge/search', { query: 'q' }) + expect(result.ok).toBe(1) + const [url, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit] + expect(url).toBe('https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search') + expect((init.headers as Record).Authorization).toBe('Bearer sk-test') + expect(init.method).toBe('POST') + }) + + it('translates a non-2xx into KbApiError with status and a bounded body summary', async () => { + const body = JSON.stringify({ code: 'InvalidParameter', message: 'agent not found' }) + const fetchImpl = vi.fn(async () => new Response(body, { status: 400 })) + const client = makeClient(fetchImpl as unknown as typeof fetch) + const err = await client.postJson('/api/v1/indices/knowledge/search', {}).catch((e: unknown) => e) + expect(err).toBeInstanceOf(KbApiError) + expect((err as KbApiError).status).toBe(400) + expect((err as KbApiError).message).toContain('agent not found') + }) + + it('re-resolves the API key per call (credential hot-swap contract)', async () => { + const resolveApiKey = vi.fn(async () => 'sk-test') + const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 })) + const client = new KbClient({ + workspaceId: 'ws-1', + endpointHost: 'h', + resolveApiKey, + fetchImpl: fetchImpl as unknown as typeof fetch, + }) + await client.postJson('/p', {}) + await client.postJson('/p', {}) + expect(resolveApiKey).toHaveBeenCalledTimes(2) + }) +}) From 95708ccf32be3be7a81997891983c9ae48fd01ba Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:36:43 +0800 Subject: [PATCH 05/45] feat: SSE parser and buffered chat consumption --- packages/tool-bailian-kb/src/chat.ts | 41 +++++++++++++++++ packages/tool-bailian-kb/src/sse.ts | 51 +++++++++++++++++++++ packages/tool-bailian-kb/tests/chat.test.ts | 30 ++++++++++++ packages/tool-bailian-kb/tests/sse.test.ts | 29 ++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 packages/tool-bailian-kb/src/chat.ts create mode 100644 packages/tool-bailian-kb/src/sse.ts create mode 100644 packages/tool-bailian-kb/tests/chat.test.ts create mode 100644 packages/tool-bailian-kb/tests/sse.test.ts diff --git a/packages/tool-bailian-kb/src/chat.ts b/packages/tool-bailian-kb/src/chat.ts new file mode 100644 index 00000000..1b547803 --- /dev/null +++ b/packages/tool-bailian-kb/src/chat.ts @@ -0,0 +1,41 @@ +/** Buffered consumption of the knowledge chat SSE stream: deltas concatenate into one complete answer. */ + +import type { ChatStreamChunk } from './api-types.js' +import { KbApiError } from './client.js' +import { parseSseStream } from './sse.js' + +export interface ChatResult { + answer: string + requestId?: string +} + +/** + * Consume one chat SSE response to completion. + * @param res - the SSE response from KbClient.postSse. + * @returns the concatenated answer and the last seen request id. + */ +export async function consumeChatStream(res: Response): Promise { + if (!res.body) throw new KbApiError('knowledge chat returned no response body') + let answer = '' + let requestId: string | undefined + for await (const event of parseSseStream(res.body)) { + if (event.data === '[DONE]') break + if (event.event === 'error') { + let message = `knowledge chat stream error: ${event.data}` + try { + const err = JSON.parse(event.data) as { code?: string; message?: string } + if (err.message) message = `knowledge chat stream error${err.code ? ` (${err.code})` : ''}: ${err.message}` + } catch { /* non-JSON error payload: keep the raw data in the message */ } + throw new KbApiError(message) + } + let parsed: ChatStreamChunk + try { + parsed = JSON.parse(event.data) as ChatStreamChunk + } catch { continue } // unparseable keep-alive/comment payloads carry no answer content + if (parsed.request_id) requestId = parsed.request_id + for (const choice of parsed.output?.choices ?? []) { + if (choice.message?.content) answer += choice.message.content + } + } + return { answer, requestId } +} diff --git a/packages/tool-bailian-kb/src/sse.ts b/packages/tool-bailian-kb/src/sse.ts new file mode 100644 index 00000000..cc9fb8b4 --- /dev/null +++ b/packages/tool-bailian-kb/src/sse.ts @@ -0,0 +1,51 @@ +/** Minimal SSE parser for the knowledge chat stream: `event:`/`data:` lines, events split on blank lines. */ + +export interface SseEvent { + event?: string + data: string +} + +/** + * Parse one SSE byte stream into events. + * @param body - the response body stream. + * @returns events in stream order; multi-`data:` events join with newlines per the SSE spec. + */ +export async function* parseSseStream(body: ReadableStream): AsyncGenerator { + const decoder = new TextDecoder() + let buffer = '' + let event: string | undefined + let data: string[] = [] + + const flush = (): SseEvent | undefined => { + if (data.length === 0) return undefined + const out = { event, data: data.join('\n') } + event = undefined + data = [] + return out + } + + const reader = body.getReader() + while (true) { + const { done, value } = await reader.read() + buffer += done ? '' : decoder.decode(value, { stream: true }) + let newline: number + while ((newline = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newline).replace(/\r$/, '') + buffer = buffer.slice(newline + 1) + if (line === '') { + const out = flush() + if (out) yield out + } else if (line.startsWith('event:')) { + event = line.slice(6).trim() + } else if (line.startsWith('data:')) { + data.push(line.slice(5).trimStart()) + } + // comment/id/retry lines are irrelevant to this API and are skipped + } + if (done) { + const out = flush() + if (out) yield out + return + } + } +} diff --git a/packages/tool-bailian-kb/tests/chat.test.ts b/packages/tool-bailian-kb/tests/chat.test.ts new file mode 100644 index 00000000..fe00a7df --- /dev/null +++ b/packages/tool-bailian-kb/tests/chat.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { consumeChatStream } from '../src/chat.js' + +function sse(text: string): Response { + return new Response(text, { status: 200 }) +} + +function chunk(content: string, finish = ''): string { + return `data: ${JSON.stringify({ output: { choices: [{ message: { content }, finish_reason: finish }] }, request_id: 'r-1' })}\n\n` +} + +describe('consumeChatStream', () => { + it('concatenates delta content across chunks until [DONE]', async () => { + const res = sse(chunk('Hello') + chunk(' world', 'stop') + 'data: [DONE]\n\n') + const out = await consumeChatStream(res) + expect(out.answer).toBe('Hello world') + expect(out.requestId).toBe('r-1') + }) + + it('ignores step_change progress chunks with empty content', async () => { + const progress = `data: ${JSON.stringify({ output: { choices: [{ message: { content: '', extra: { step_change: 'tool_calling' } }, finish_reason: '' }] } })}\n\n` + const res = sse(progress + chunk('answer', 'stop') + 'data: [DONE]\n\n') + expect((await consumeChatStream(res)).answer).toBe('answer') + }) + + it('throws on an SSE error event with the server message', async () => { + const res = sse('event: error\ndata: {"code":"Throttling","message":"rate limited"}\n\n') + await expect(consumeChatStream(res)).rejects.toThrow(/Throttling.*rate limited/) + }) +}) diff --git a/packages/tool-bailian-kb/tests/sse.test.ts b/packages/tool-bailian-kb/tests/sse.test.ts new file mode 100644 index 00000000..b76af494 --- /dev/null +++ b/packages/tool-bailian-kb/tests/sse.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { parseSseStream } from '../src/sse.js' + +function streamOf(text: string): ReadableStream { + return new Response(text).body as ReadableStream +} + +async function collect(text: string) { + const events: { event?: string; data: string }[] = [] + for await (const e of parseSseStream(streamOf(text))) events.push(e) + return events +} + +describe('parseSseStream', () => { + it('yields data events split on blank lines', async () => { + const events = await collect('data: {"a":1}\n\ndata: [DONE]\n\n') + expect(events).toEqual([{ event: undefined, data: '{"a":1}' }, { event: undefined, data: '[DONE]' }]) + }) + + it('carries the event field and parses CRLF lines', async () => { + const events = await collect('event: error\r\ndata: {"message":"boom"}\r\n\r\n') + expect(events[0]).toEqual({ event: 'error', data: '{"message":"boom"}' }) + }) + + it('flushes a final event not terminated by a blank line', async () => { + const events = await collect('data: tail\n') + expect(events).toEqual([{ event: undefined, data: 'tail' }]) + }) +}) From 5800222ea4b7cdea24e82f50fa332dbd08b6dd07 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:37:34 +0800 Subject: [PATCH 06/45] feat: service discovery with scene merge and internalized pagination --- packages/tool-bailian-kb/src/services.ts | 59 +++++++++++++++++++ .../tool-bailian-kb/tests/services.test.ts | 49 +++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/tool-bailian-kb/src/services.ts create mode 100644 packages/tool-bailian-kb/tests/services.test.ts diff --git a/packages/tool-bailian-kb/src/services.ts b/packages/tool-bailian-kb/src/services.ts new file mode 100644 index 00000000..9d37dd4d --- /dev/null +++ b/packages/tool-bailian-kb/src/services.ts @@ -0,0 +1,59 @@ +/** Retrieval-service discovery: per-scene queries merged into one model-facing list; pagination stays internal. */ + +import type { ServiceListResponse } from './api-types.js' +import type { KbClient } from './client.js' +import { KB_PATHS } from './endpoints.js' + +/** Server page-size maximum; one page per scene covers ordinary workspaces. */ +const MAX_PAGE_SIZE = 100 + +export interface ServiceEntry { + agent_id: string + name: string + scene: string + status: string + knowledge_bases: string[] +} + +export interface ServiceList { + services: ServiceEntry[] + total: number + /** True when some scene reported more rows than one max page returned. */ + truncated: boolean +} + +export interface ListServicesQuery { + scene?: 'chat' | 'search' + nameFilter?: string +} + +/** + * List retrieval/Q&A services. An omitted scene fans out to both scenes and merges. + * @param client - the shared knowledge API client. + * @param query - optional scene and fuzzy name filter. + * @returns merged entries, the server-reported total, and the truncation flag. + */ +export async function listServices(client: KbClient, query: ListServicesQuery): Promise { + const scenes: ('chat' | 'search')[] = query.scene ? [query.scene] : ['chat', 'search'] + const services: ServiceEntry[] = [] + let total = 0 + for (const scene of scenes) { + const res = await client.postJson(KB_PATHS.serviceList, { + agent_scene: scene, + ...(query.nameFilter ? { agent_name: query.nameFilter } : {}), + page_number: 1, + page_size: MAX_PAGE_SIZE, + }) + total += res.data?.total_count ?? 0 + for (const row of res.data?.rows ?? []) { + services.push({ + agent_id: row.agent_id ?? '', + name: row.agent_name ?? '', + scene: row.agent_scene ?? scene, + status: row.agent_status ?? '', + knowledge_bases: (row.pipeline_list ?? []).map(p => p.pipeline_name ?? p.pipeline_id ?? '').filter(Boolean), + }) + } + } + return { services, total, truncated: total > services.length } +} diff --git a/packages/tool-bailian-kb/tests/services.test.ts b/packages/tool-bailian-kb/tests/services.test.ts new file mode 100644 index 00000000..f5853cc9 --- /dev/null +++ b/packages/tool-bailian-kb/tests/services.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest' +import type { ServiceListResponse } from '../src/api-types.js' +import type { KbClient } from '../src/client.js' +import { listServices } from '../src/services.js' + +function fakeClient(byScene: Record) { + const postJson = vi.fn(async (_path: string, body: { agent_scene: string }) => byScene[body.agent_scene]) + return { client: { postJson } as unknown as KbClient, postJson } +} + +const row = (id: string, scene: string) => ({ + agent_id: id, agent_name: `svc-${id}`, agent_scene: scene, agent_status: 'deployed', + pipeline_list: [{ pipeline_id: 'p1', pipeline_name: 'kb-one' }], +}) + +describe('listServices', () => { + it('queries both scenes when scene is omitted and merges rows with scene tags', async () => { + const { client, postJson } = fakeClient({ + chat: { data: { total_count: 1, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 1, rows: [row('b', 'search')] } }, + }) + const out = await listServices(client, {}) + expect(postJson).toHaveBeenCalledTimes(2) + expect(out.services.map(s => [s.agent_id, s.scene])).toEqual([['a', 'chat'], ['b', 'search']]) + expect(out.services[0]!.knowledge_bases).toEqual(['kb-one']) + expect(out.total).toBe(2) + expect(out.truncated).toBe(false) + const body = postJson.mock.calls[0]![1] as unknown as Record + expect(body.page_number).toBe(1) + expect(body.page_size).toBe(100) + }) + + it('queries one scene and forwards the name filter', async () => { + const { client, postJson } = fakeClient({ search: { data: { total_count: 0, rows: [] } } }) + await listServices(client, { scene: 'search', nameFilter: '客服' }) + expect(postJson).toHaveBeenCalledTimes(1) + expect((postJson.mock.calls[0]![1] as unknown as Record).agent_name).toBe('客服') + }) + + it('flags truncation when a scene exceeds one max page', async () => { + const { client } = fakeClient({ + chat: { data: { total_count: 250, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 0, rows: [] } }, + }) + const out = await listServices(client, {}) + expect(out.truncated).toBe(true) + expect(out.total).toBe(250) + }) +}) From f2a37755161d18669bd791877b6765b340a73dae Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:41:46 +0800 Subject: [PATCH 07/45] feat: kb_service_list, kb_search, kb_chat tool factory --- packages/tool-bailian-kb/src/tools.ts | 237 +++++++++++++++++++ packages/tool-bailian-kb/tests/tools.test.ts | 81 +++++++ 2 files changed, 318 insertions(+) create mode 100644 packages/tool-bailian-kb/src/tools.ts create mode 100644 packages/tool-bailian-kb/tests/tools.test.ts diff --git a/packages/tool-bailian-kb/src/tools.ts b/packages/tool-bailian-kb/src/tools.ts new file mode 100644 index 00000000..01f2d642 --- /dev/null +++ b/packages/tool-bailian-kb/src/tools.ts @@ -0,0 +1,237 @@ +/** + * 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). + */ + +import { defineTool } from '@deepseek-ai/dsh-tools' +import type { SearchRequest, SearchResponse } from './api-types.js' +import { KbApiError, type KbClient } from './client.js' +import { consumeChatStream } from './chat.js' +import { KB_PATHS } from './endpoints.js' +import { listServices } from './services.js' + +/** Client-side chunk cap applied when the model omits top_k. */ +const DEFAULT_TOP_K = 5 + +export interface KbToolDeps { + client: KbClient + defaultAgentId?: string + chatTimeoutMs: number +} + +/** Format one service list into the error-hint / result text form. */ +function formatServices(services: { agent_id: string; name: string; scene: string; status: string }[]): string { + return services.map(s => `${s.agent_id} (${s.name}, scene=${s.scene}, ${s.status})`).join('; ') +} + +/** + * Append the current service list to a client error so the model can correct + * an invalid agent_id in one step. Auth failures (401/403) keep their own message. + * @param client - the shared knowledge API client used for best-effort discovery. + * @param err - the failure being enriched; always rethrown. + * @returns never; the original or enriched error is thrown. + */ +async function withServiceHint(client: KbClient, err: unknown): Promise { + if (err instanceof KbApiError && err.status !== undefined && err.status >= 400 && err.status !== 401 && err.status !== 403) { + let hint: string | undefined + try { + const { services } = await listServices(client, {}) + if (services.length > 0) hint = formatServices(services) + } catch { /* discovery is best-effort; the original error already carries the failure */ } + if (hint !== undefined) throw new KbApiError(`${err.message}. Available services: ${hint}`, err.status) + } + throw err +} + +/** + * Build the three tool definitions over one shared client. + * @param deps - client plus the deployment's explicit pinning and timeout choices. + * @returns definitions ready for `ctx.tools.register()`. + */ +export function createKbTools(deps: KbToolDeps) { + const { client, defaultAgentId, 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.', + } + 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 serviceList = defineTool({ + name: 'kb_service_list', + description: + 'List the Bailian knowledge retrieval/Q&A services available in this workspace. ' + + 'Each entry names the service id (agent_id) to pass to kb_search (scene=search) or kb_chat (scene=chat), ' + + 'its bound knowledge bases, and its status (prefer deployed). ' + + 'Omit scene to see both kinds; narrow large workspaces with name_filter.', + parameters: { + scene: { type: 'string', enum: ['chat', 'search'], description: 'Only list services for this scene; omitted lists both.' }, + name_filter: { type: 'string', description: 'Fuzzy match on the service name.' }, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + services: { + type: 'array', + required: true, + items: { + type: 'object', + additionalProperties: false, + properties: { + agent_id: { type: 'string', required: true }, + name: { type: 'string', required: true }, + scene: { type: 'string', required: true }, + status: { type: 'string', required: true }, + knowledge_bases: { type: 'array', required: true, items: { type: 'string' } }, + }, + }, + }, + total: { type: 'integer', required: true }, + truncated: { type: 'boolean', required: true }, + }, + }, + render: (_args, value) => [{ + type: 'text', + text: value.services.length === 0 + ? 'No knowledge services found.' + : `${value.services.length} service(s): ${formatServices(value.services)}` + + (value.truncated ? ` — listed first ${value.services.length} of ${value.total}; narrow with name_filter.` : ''), + }], + }, + async execute(args) { + return await listServices(client, { + ...(args.scene === 'chat' || args.scene === 'search' ? { scene: args.scene } : {}), + ...(args.name_filter ? { nameFilter: args.name_filter } : {}), + }) + }, + presentCall: args => ({ card: 'generic', title: 'List knowledge services', kind: 'other', rawInput: args }), + }) + + const search = defineTool({ + name: 'kb_search', + description: + 'Semantic search over a Bailian knowledge base. Returns raw knowledge chunks with scores and source ' + + 'references for you to verify, cite, or combine with other context. Retrieval scope and strategy ' + + '(multi-KB weighting, routing, reranking) come from the service configuration. ' + + 'top_k caps how many chunks return (client-side cut of the score-ranked results). ' + + 'Use kb_chat instead when the user question can be answered by the knowledge base alone.', + parameters: { + query: { type: 'string', required: true, description: 'Search query text.' }, + agent_id: agentIdParam, + top_k: { type: 'integer', description: `Maximum chunks to return; defaults to ${DEFAULT_TOP_K}.` }, + images: { type: 'array', items: { type: 'string' }, description: 'Image URLs for multimodal retrieval.' }, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + chunks: { + type: 'array', + required: true, + items: { + type: 'object', + additionalProperties: false, + properties: { + text: { type: 'string', required: true }, + score: { type: 'number', required: true }, + doc_name: { type: 'string' }, + doc_id: { type: 'string' }, + title: { type: 'string' }, + }, + }, + }, + total: { type: 'integer', required: true }, + }, + }, + render: (_args, value) => [{ + type: 'text', + text: value.chunks.length === 0 + ? 'No matching knowledge chunks.' + : value.chunks.map((c, i) => `[${i + 1}] (score ${c.score.toFixed(2)}${c.doc_name ? `, ${c.doc_name}` : ''}) ${c.text}`).join('\n'), + }], + }, + async execute(args) { + const topK = args.top_k ?? DEFAULT_TOP_K + const body: SearchRequest = { + query: args.query, + agent_id: resolveAgentId(args.agent_id), + ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), + ...(args.images && args.images.length > 0 ? { images: args.images } : {}), + } + const res = await client.postJson(KB_PATHS.search, body).catch(err => withServiceHint(client, err)) + const nodes = (res.data?.nodes ?? []).slice(0, topK) + return { + chunks: nodes.map(n => ({ + text: n.text, + score: n.score, + ...(typeof n.metadata?.doc_name === 'string' ? { doc_name: n.metadata.doc_name } : {}), + ...(typeof n.metadata?.doc_id === 'string' ? { doc_id: n.metadata.doc_id } : {}), + ...(typeof n.metadata?.title === 'string' ? { title: n.metadata.title } : {}), + })), + total: res.data?.total ?? nodes.length, + } + }, + presentCall: args => ({ card: 'generic', title: 'Search knowledge base', kind: 'search', rawInput: args }), + }) + + const chat = defineTool({ + name: 'kb_chat', + description: + 'Ask the knowledge base directly and get a complete, domain-tuned answer from a specialized RAG pipeline ' + + '(multi-round retrieval + reranking + grounded generation). For knowledge Q&A this typically outperforms ' + + 'searching and synthesizing yourself when the question can be answered by the knowledge base alone; ' + + 'use kb_search instead when you need raw chunks to verify, cite, or combine with other work. ' + + 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes.', + parameters: { + message: { type: 'string', required: true, description: 'The question to ask.' }, + agent_id: agentIdParam, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + answer: { type: 'string', required: true }, + request_id: { type: 'string' }, + }, + }, + render: (_args, value) => [{ type: 'text', text: value.answer.length === 0 ? '(empty answer)' : value.answer }], + }, + async execute(args) { + const body = { + input: { messages: [{ role: 'user' as const, content: args.message }] }, + parameters: { agent_options: { + agent_id: resolveAgentId(args.agent_id), + ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), + } }, + stream: true as const, + } + let res: Response + try { + res = await client.postSse(KB_PATHS.chat, body, AbortSignal.timeout(chatTimeoutMs)) + } catch (err) { + if (err instanceof Error && err.name === 'TimeoutError') { + throw new Error( + `knowledge chat timed out after ${chatTimeoutMs}ms; the pipeline runs a multi-round retrieval loop ` + + 'and long questions can exceed the deployment timeout. Retry, or use kb_search for raw chunks instead.', + ) + } + return await withServiceHint(client, err) + } + const { answer, requestId } = await consumeChatStream(res) + return { answer, ...(requestId ? { request_id: requestId } : {}) } + }, + presentCall: args => ({ card: 'generic', title: 'Ask knowledge base (may take a few minutes)', kind: 'fetch', rawInput: args }), + }) + + return [serviceList, search, chat] +} diff --git a/packages/tool-bailian-kb/tests/tools.test.ts b/packages/tool-bailian-kb/tests/tools.test.ts new file mode 100644 index 00000000..1ac7d1f3 --- /dev/null +++ b/packages/tool-bailian-kb/tests/tools.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest' +import { KbApiError, KbClient } from '../src/client.js' +import { createKbTools } from '../src/tools.js' + +const EXEC = {} as never + +function toolsWith(postJson: unknown, postSse?: unknown, defaultAgentId?: string) { + const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient + const list = createKbTools({ client, ...(defaultAgentId ? { defaultAgentId } : {}), chatTimeoutMs: 1000 }) + const byName = Object.fromEntries(list.map(t => [t.name, t])) + return { byName, list } +} + +const searchResponse = { + request_id: 'r1', + data: { total: 3, nodes: [ + { score: 0.9, text: 'A', metadata: { doc_name: 'd1' } }, + { score: 0.8, text: 'B', metadata: {} }, + { score: 0.7, text: 'C', metadata: {} }, + ] }, +} + +describe('createKbTools', () => { + it('registers exactly kb_service_list, kb_search, kb_chat', () => { + const { list } = toolsWith(vi.fn()) + expect(list.map(t => t.name).sort()).toEqual(['kb_chat', 'kb_search', 'kb_service_list']) + }) + + it('kb_search truncates nodes client-side to top_k and never sends top_k to the server', async () => { + const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) + const { byName } = toolsWith(postJson) + const out = await byName.kb_search!.execute({ query: 'q', agent_id: 'aid-1', top_k: 2 }, EXEC) as { chunks: unknown[] } + expect(out.chunks).toHaveLength(2) + const body = postJson.mock.calls[0]![1] as Record + expect(body).not.toHaveProperty('top_k') + expect(body.agent_id).toBe('aid-1') + }) + + it('agent_id is required without defaultAgentId and optional with one', () => { + const withoutDefault = toolsWith(vi.fn()).byName.kb_search! + const withDefault = toolsWith(vi.fn(), undefined, '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') + expect(requiredList(withDefault)).not.toContain('agent_id') + }) + + it('kb_search falls back to defaultAgentId as an explicit resolve step', async () => { + const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse) + const { byName } = toolsWith(postJson, undefined, '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 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) + return { data: { total_count: 1, rows: [{ agent_id: 'aid-9', agent_name: 'faq', agent_scene: 'search', agent_status: 'deployed' }] } } + }) + const { byName } = toolsWith(postJson) + const err = await byName.kb_search!.execute({ query: 'q', agent_id: 'bad' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toContain('aid-9') + }) + + it('kb_chat buffers the SSE stream into one answer', async () => { + const sse = 'data: {"output":{"choices":[{"message":{"content":"hi"},"finish_reason":"stop"}]},"request_id":"r2"}\n\ndata: [DONE]\n\n' + const postSse = vi.fn(async () => new Response(sse, { status: 200 })) + const { byName } = toolsWith(vi.fn(), postSse) + const out = await byName.kb_chat!.execute({ message: 'q', agent_id: 'aid-1' }, EXEC) as { answer: string } + expect(out.answer).toBe('hi') + }) + + it('kb_chat translates a timeout into retry-or-search guidance', async () => { + const timeout = Object.assign(new Error('operation timed out'), { name: 'TimeoutError' }) + const postSse = vi.fn(async () => { throw timeout }) + const { byName } = toolsWith(vi.fn(), postSse) + const err = await byName.kb_chat!.execute({ message: 'q', agent_id: 'aid-1' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toMatch(/timed out.*kb_search/s) + }) +}) From b4129c6cbd34d423cee69a33223fac4241223a16 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:43:12 +0800 Subject: [PATCH 08/45] feat: plugin apply wiring with credential-backed client and skill registration --- .../skills/bailian-kb-management/SKILL.md | 34 +++++++++++++++++ packages/tool-bailian-kb/src/index.ts | 37 +++++++++++++++++++ packages/tool-bailian-kb/src/skill.ts | 30 +++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md create mode 100644 packages/tool-bailian-kb/src/skill.ts diff --git a/packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md b/packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md new file mode 100644 index 00000000..cd17081c --- /dev/null +++ b/packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md @@ -0,0 +1,34 @@ +--- +name: bailian-kb-management +description: 管理阿里云百炼知识库(建库、上传文档、部署检索服务、Chunk 运维)。当用户要创建/更新/删除知识库、上传或导入文档、部署检索服务、管理数据中心文件时使用 kscli。检索与问答不走本 skill——用原生工具 kb_search / kb_chat。 +--- + +# 百炼知识库管理(kscli) + +检索面与管理面的分工:**查知识用 `kb_search`(取证据)/ `kb_chat`(成品问答)原生工具;本 skill 只覆盖管理长尾**——知识库全生命周期、文档、检索服务、Chunk、数据中心。 + +## 前置检查 + +1. `kscli --version` —— 未安装则运行 `npm install -g knowledge-studio-cli`(需 Node.js ≥ 18.17);安装失败时把错误原样报告给用户,不要静默跳过。 +2. 鉴权:需要 `DASHSCOPE_API_KEY`(环境变量,或 `kscli config set --key api_key --value sk-xxx`)。 +3. workspace 解析优先级:`--workspace-id` 参数 > 环境变量 `BAILIAN_WORKSPACE_ID` > `kscli config set --key workspace_id --value ws-xxx`。 + +## 常用工作流:建库到可检索 + +```bash +kscli kb create --name "my-kb" --embedding-model text-embedding-v3 # 1. 建库 +kscli doc upload --kb-id --file ./docs.pdf # 2. 上传本地文档 +kscli doc status --kb-id --doc-id # 3. 轮询至 COMPLETED +kscli service create ... && kscli service deploy ... # 4. 建/部署检索服务 → 得到 agent_id +``` + +部署完成后用 `kb_service_list` 确认服务可见,再用 `kb_search` 带该 `agent_id` 验证检索。 + +## 命令组速查 + +`kb`(list/info/create/update/delete/stats)· `doc`(list/upload/status/delete/tag/import-oss)· `service`(list/get/create/update/deploy/delete/copy)· `chunk`(add/list/update/delete)· `file` / `collection` / `category`(数据中心)。全部命令支持 `--output json`(结构化输出)、`--dry-run`(预览请求)、`--quiet`。完整手册:https://github.com/modelstudioai/cli/blob/main/docs/knowledge-cli-guide.md + +## 最佳实践 + +- 用户反复使用同一检索服务时,建议其把 agent_id 写入项目指令(如 AGENTS.md)或让 agent 记住,后续 kb_search / kb_chat 直接携带。 +- 服务有 draft/deployed 两种状态:只有 deployed 可被默认版本调用;draft 调试用 `--agent-version beta`。 diff --git a/packages/tool-bailian-kb/src/index.ts b/packages/tool-bailian-kb/src/index.ts index e6acfb24..b88aae2e 100644 --- a/packages/tool-bailian-kb/src/index.ts +++ b/packages/tool-bailian-kb/src/index.ts @@ -4,7 +4,12 @@ * @module dsh-tool-bailian-kb */ +import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' +import { credentialRef } from '@deepseek-ai/dsh-credentials' +import { KbClient } from './client.js' +import { registerSkill } from './skill.js' +import { createKbTools } from './tools.js' export const name = 'tool-bailian-kb' export const inject = ['tools', 'credentials'] @@ -31,3 +36,35 @@ export const Config: z = z.object({ agentVersion: z.string(), chatTimeoutMs: z.number().default(300_000), }) + +/** + * Register the three knowledge tools over one shared client, plus the + * management skill when a skills registry is composed. + * @param ctx - registrant context carrying tools and credentials. + * @param config - deployment's workspace, host, pinning, and timeout choices. + */ +export function apply(ctx: Context, config: Config): void { + const client = new KbClient({ + workspaceId: config.workspaceId, + 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).', + ) + } + return resolved.value + }, + }) + for (const tool of createKbTools({ + client, + ...(config.defaultAgentId ? { defaultAgentId: config.defaultAgentId } : {}), + chatTimeoutMs: config.chatTimeoutMs, + })) { + ctx.tools.register(tool) + } + registerSkill(ctx) +} diff --git a/packages/tool-bailian-kb/src/skill.ts b/packages/tool-bailian-kb/src/skill.ts new file mode 100644 index 00000000..af080a56 --- /dev/null +++ b/packages/tool-bailian-kb/src/skill.ts @@ -0,0 +1,30 @@ +/** Runtime skill registration: the packaged kscli-management SKILL.md joins the catalog when a skills registry is composed. */ + +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +// Type-only: resolves ctx.skills for the optional inject below. +import type {} from '@deepseek-ai/dsh-skill' + +const SKILL_DIR = fileURLToPath(new URL('../skills/bailian-kb-management/', import.meta.url)) + +/** + * Register the management skill when the skills registry is composed; headless + * assemblies without the seam stay unaffected. + * @param ctx - the plugin context. + */ +export function registerSkill(ctx: Context): void { + ctx.inject(['skills'], (skillCtx) => { + const content = readFileSync(join(SKILL_DIR, 'SKILL.md'), 'utf8') + skillCtx.skills.register({ + name: 'bailian-kb-management', + description: + 'Manage Bailian knowledge bases with the kscli CLI: create/update KBs, upload documents, deploy ' + + 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools.', + content, + source: 'bundled', + resourceBase: { kind: 'directory', path: SKILL_DIR }, + }) + }) +} From a6ee2670713bab2a762edd88180bffc06f8fc5b9 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:46:32 +0800 Subject: [PATCH 09/45] feat: installable dsh bundle package --- packages/bundle/cordis.patch.yml | 10 ++++++++++ packages/bundle/package.json | 13 +++++++++++++ pnpm-lock.yaml | 6 ++++++ 3 files changed, 29 insertions(+) create mode 100644 packages/bundle/cordis.patch.yml create mode 100644 packages/bundle/package.json diff --git a/packages/bundle/cordis.patch.yml b/packages/bundle/cordis.patch.yml new file mode 100644 index 00000000..b822b52a --- /dev/null +++ b/packages/bundle/cordis.patch.yml @@ -0,0 +1,10 @@ +# bailian-kb-bundle: 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. + +- insert: + - id: tool-bailian-kb + name: dsh-tool-bailian-kb + config: + workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID diff --git a/packages/bundle/package.json b/packages/bundle/package.json new file mode 100644 index 00000000..71a8bb58 --- /dev/null +++ b/packages/bundle/package.json @@ -0,0 +1,13 @@ +{ + "name": "bailian-kb-bundle", + "version": "0.1.0", + "description": "Installable dsh bundle for Bailian knowledge-base tools: kb_service_list, kb_search, kb_chat plus the kscli management skill.", + "type": "module", + "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, + "exports": { + "./cordis.patch.yml": "./cordis.patch.yml", + "./package.json": "./package.json" + }, + "files": ["cordis.patch.yml"], + "dependencies": { "dsh-tool-bailian-kb": "workspace:^" } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66a74d57..b90759ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,12 @@ importers: specifier: ^3.0.0 version: 3.2.7(@types/node@22.20.1) + packages/bundle: + dependencies: + dsh-tool-bailian-kb: + specifier: workspace:^ + version: link:../tool-bailian-kb + packages/tool-bailian-kb: devDependencies: '@deepseek-ai/cordis': From acccca9e2f3fe89cfce7262b806babcac7a9b9f9 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:47:43 +0800 Subject: [PATCH 10/45] docs: package and repository READMEs --- README.md | 50 ++++++++++++++++++++++++++++++ packages/bundle/README.md | 36 +++++++++++++++++++++ packages/tool-bailian-kb/README.md | 39 +++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 README.md create mode 100644 packages/bundle/README.md create mode 100644 packages/tool-bailian-kb/README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..8aa61bd8 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# bailian-kb-bundle + +阿里云百炼知识库能力的 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件 bundle:三个 API 直连模型工具(`kb_service_list` / `kb_search` / `kb_chat`)+ kscli 管理面 skill。 + +设计文档:deepseek-harness 工作区 `docs/superpowers/specs/2026-08-15-bailian-kb-bundle-design.md`。 + +## 仓库结构 + +| 包 | 职责 | +|---|---| +| [`packages/tool-bailian-kb`](packages/tool-bailian-kb/README.md) | 插件本体:Config、KbClient、三个工具、随包打包的管理 skill | +| [`packages/bundle`](packages/bundle/README.md) | 分发面:`dsh.bundle` 声明 + `cordis.patch.yml` | + +## 安装(dsh 用户) + +```sh +dsh plugin --profile web add bailian-kb-bundle # npm 发布后;本地开发用绝对/相对路径 +``` + +安装后 CLI 自动把 bundle 加入 profile 的层栈,无需手改 YAML。 + +配置写入 `~/.dsh/.env`: + +```sh +BAILIAN_WORKSPACE_ID=ws-xxx # 必填:百炼工作空间 id +DASHSCOPE_API_KEY=sk-xxx # 必填:也可放 ~/.dsh/.credentials.yaml +``` + +验证:`dsh --profile web --dump-config` 应能看到 `tool-bailian-kb` row。缺 `BAILIAN_WORKSPACE_ID` 时加载期直接报错(fail loud),不会静默跳过。 + +卸载:`dsh plugin --profile web remove bailian-kb-bundle`。 + +## 开发 + +依赖 dsh 的运行时包(`@deepseek-ai/dsh-tools` 等)以 peerDependencies 声明、由 dsh 安装闭包在运行时提供;开发期通过 `link:` 指向同级的 `../deepseek-harness` checkout(npm registry 尚未发布完整 dsh 闭包)。 + +```sh +pnpm install +pnpm run test # vitest 单元测试 +pnpm run typecheck +pnpm run build # tsc 产出 lib/ +``` + +本地联调:`dsh plugin --profile dev add <本仓库>/packages/bundle`,patch 文件受 HMR 监听。 + +## Known Limitations + +- **无 keyless snapshot / e2e 基建**:首版以单元测试 + 手动集成验收覆盖;snapshot/e2e 依赖 dsh snapshot harness 对 out-of-tree bundle 的支持情况,v0.2 跟进。 +- **kb_chat 执行期无进展显示**:服务端是分钟级 agentic loop,UI 只有 pending → 完成两态;进展会话事件 + Web 渲染器的设计见 spec 附录 A,等真实使用反馈再排期。 +- **服务清单单 scene 上限 100 条**:超出部分靠 `name_filter` 收窄(结果带 truncated 提示)。 diff --git a/packages/bundle/README.md b/packages/bundle/README.md new file mode 100644 index 00000000..66ffd0ad --- /dev/null +++ b/packages/bundle/README.md @@ -0,0 +1,36 @@ +# bailian-kb-bundle(分发包) + +dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row。 + +## Patch row + +```yaml +- insert: + - id: tool-bailian-kb + name: dsh-tool-bailian-kb + config: + workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID +``` + +`workspaceId` 默认从环境变量读取(`~/.dsh/.env` 写 `BAILIAN_WORKSPACE_ID=ws-xxx` 即可运行);未设置时插件加载期 fail loud。 + +## 用户覆盖 + +用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge),必须连 workspaceId 一起重述**: + +```yaml +# ~/.dsh/cordis.patch.yml 或 profile 的 cordis.patch.yml +- id: tool-bailian-kb + config: + workspaceId: ws-xxx + defaultAgentId: aid-customer-service # 场景固定式部署 + chatTimeoutMs: 600000 +``` + +禁用:`- id: tool-bailian-kb` + `disabled: true`。 + +## 卸载 + +```sh +dsh plugin --profile remove bailian-kb-bundle +``` diff --git a/packages/tool-bailian-kb/README.md b/packages/tool-bailian-kb/README.md new file mode 100644 index 00000000..700ca19c --- /dev/null +++ b/packages/tool-bailian-kb/README.md @@ -0,0 +1,39 @@ +# dsh-tool-bailian-kb + +百炼知识库的 dsh 插件本体:在 `ctx.tools` 注册三个模型工具,并在 skills 服务可用时注册管理面 skill。 + +## Config + +| 字段 | 类型 | 默认 | 语义 | +|---|---|---|---| +| `workspaceId` | string | **必填** | 百炼工作空间 id;API host 为 workspace 子域名 `https://.` | +| `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | host 后缀,其他 region/私有化时替换 | +| `defaultAgentId` | string? | — | 场景固定式部署绑定的检索服务;**配置后 `agent_id` 参数在注册期变为可选**(加载期静态决定 schema,非运行时 fallback) | +| `agentVersion` | string? | — | `beta`(草稿调试)或已发布版本号;不暴露给模型 | +| `chatTimeoutMs` | number | 300000 | kb_chat 超时;服务端是分钟级 agentic loop | + +凭证:`DASHSCOPE_API_KEY` 走 `ctx.credentials` 引用,每次调用重新解析(热更换生效),未配置时报错并附获取指引。 + +## 工具 + +| 工具 | 参数 | 返回 | +|---|---|---| +| `kb_service_list` | `scene?`(chat\|search,省略查双场景合并)、`name_filter?` | 服务清单(agent_id、名称、scene、status、绑定知识库)+ total + truncated;分页内部消化(单 scene 100 条上限) | +| `kb_search` | `query`、`agent_id`(见 defaultAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total | +| `kb_chat` | `message`、`agent_id` | 完整答案(内部消费 SSE 流缓冲返回)+ request_id | + +## 错误语义 + +- 4xx(除 401/403):错误信息**附当前服务清单**,模型可一步纠正无效 `agent_id`; +- 凭证缺失:指向 `~/.dsh/.env` / `.credentials.yaml` 配置方式与控制台取 key 页面; +- chat 超时:说明服务端多轮检索特性,建议重试或改用 `kb_search`; +- 服务端错误体截断至 500 字符进入错误信息(优先 `code: message`)。 + +## 管理面 skill + +`skills/bailian-kb-management/SKILL.md` 随包分发,插件通过 `ctx.inject(['skills'])` 在 skills 服务可用时以 `source: 'bundled'` 运行时注册;无 skills 服务的组合(headless 最小装配)不受影响。内容:kscli 安装/鉴权/workspace 解析、建库→上传→部署工作流、agent_id 固定最佳实践。 + +## Known Limitations + +- kb_chat 执行期无进展显示(缓冲式;进展会话事件设计见仓库根 README 与 spec 附录 A)。 +- `top_k` 是客户端截断:请求体不含该参数,服务端返回条数由检索服务配置决定,截断只影响进入模型上下文的量。 From a9ac7c0bea614bef3a7237af289dfc119665747b Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:52:33 +0800 Subject: [PATCH 11/45] docs: record link-install dev workaround for profile integration --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8aa61bd8..b67f2907 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,14 @@ pnpm run typecheck pnpm run build # tsc 产出 lib/ ``` -本地联调:`dsh plugin --profile dev add <本仓库>/packages/bundle`,patch 文件受 HMR 监听。 +本地联调:`link:` 安装不会把被链接包的依赖装进 profile,需要把 bundle 和插件包**都** add 进去(插件包会报 "declares no dsh.bundle — installed as a plain dependency" 警告,符合预期);npm 正式安装无此问题: + +```sh +dsh plugin --profile dev add <本仓库>/packages/bundle +dsh plugin --profile dev add <本仓库>/packages/tool-bailian-kb # 仅 link 联调需要 +``` + +patch 文件受 HMR 监听。 ## Known Limitations From 188090af39f6510502c11035157c5621747efe47 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 18:57:08 +0800 Subject: [PATCH 12/45] docs: widen error-hint contract note to all non-auth HTTP failures --- packages/tool-bailian-kb/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tool-bailian-kb/README.md b/packages/tool-bailian-kb/README.md index 700ca19c..00caa1e4 100644 --- a/packages/tool-bailian-kb/README.md +++ b/packages/tool-bailian-kb/README.md @@ -24,7 +24,7 @@ ## 错误语义 -- 4xx(除 401/403):错误信息**附当前服务清单**,模型可一步纠正无效 `agent_id`; +- HTTP 错误(除 401/403 鉴权类):错误信息**附当前服务清单**,模型可一步纠正无效 `agent_id`(5xx 也附,但通常代表服务端异常); - 凭证缺失:指向 `~/.dsh/.env` / `.credentials.yaml` 配置方式与控制台取 key 页面; - chat 超时:说明服务端多轮检索特性,建议重试或改用 `kb_search`; - 服务端错误体截断至 500 字符进入错误信息(优先 `code: message`)。 From 73233658bfffaf6e1a206c79fb4a50883e007bd1 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 19:08:35 +0800 Subject: [PATCH 13/45] docs: migrate design spec and implementation plan into this repo --- README.md | 2 +- docs/plans/2026-08-15-bailian-kb-bundle.md | 1517 +++++++++++++++++ .../2026-08-15-bailian-kb-bundle-design.md | 195 +++ 3 files changed, 1713 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-08-15-bailian-kb-bundle.md create mode 100644 docs/specs/2026-08-15-bailian-kb-bundle-design.md diff --git a/README.md b/README.md index b67f2907..65e1afd5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 阿里云百炼知识库能力的 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件 bundle:三个 API 直连模型工具(`kb_service_list` / `kb_search` / `kb_chat`)+ kscli 管理面 skill。 -设计文档:deepseek-harness 工作区 `docs/superpowers/specs/2026-08-15-bailian-kb-bundle-design.md`。 +设计文档:[docs/specs/2026-08-15-bailian-kb-bundle-design.md](docs/specs/2026-08-15-bailian-kb-bundle-design.md) · 实现计划:[docs/plans/2026-08-15-bailian-kb-bundle.md](docs/plans/2026-08-15-bailian-kb-bundle.md) ## 仓库结构 diff --git a/docs/plans/2026-08-15-bailian-kb-bundle.md b/docs/plans/2026-08-15-bailian-kb-bundle.md new file mode 100644 index 00000000..77f45c7b --- /dev/null +++ b/docs/plans/2026-08-15-bailian-kb-bundle.md @@ -0,0 +1,1517 @@ +# bailian-kb-bundle 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `/Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle` 新建独立 pnpm workspace,交付一个可通过 `dsh plugin add` 安装的 dsh bundle:三个 API 直连知识库工具(`kb_service_list` / `kb_search` / `kb_chat`)+ kscli 管理面 skill。 + +**Architecture:** 两包结构——`dsh-tool-bailian-kb`(Cordis 函数插件:Config、KbClient、工具工厂、skill 注册)+ `bailian-kb-bundle`(`dsh.bundle` 分发面:cordis.patch.yml)。工具逻辑与 Cordis 解耦为纯函数/类(endpoints、client、SSE、services、tools 工厂),`apply()` 只做装配,测试不需要 Cordis 容器。 + +**Tech Stack:** TypeScript strict + NodeNext ESM(相对导入用 `.js` 后缀)、`@deepseek-ai/cordis@4.0.1`(peer+dev)、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-skill`(类型)、`@deepseek-ai/schemastery@3.18.1`、vitest、tsc 构建(输出 `lib/`)。 + +**设计依据(spec):** `deepseek-harness/docs/superpowers/specs/2026-08-15-bailian-kb-bundle-design.md` + +--- + +## 已验证的 API 事实(实现的唯一依据,来自 modelstudioai/cli 源码) + +所有 endpoint 的 host 为 **workspace 子域名**:`https://${workspaceId}.${endpointHost}`,默认 `endpointHost = cn-beijing.maas.aliyuncs.com`。鉴权统一 `Authorization: Bearer ${DASHSCOPE_API_KEY}`。 + +**1. 服务列表** `POST /api/v1/indices/rag/app/list` +请求体:`{ agent_scene: 'chat'|'search'(必填), agent_name?: string, page_number: number, page_size: number(1-100) }` +响应:`{ code, message, data: { total_count?: number, rows?: Array<{ agent_id?, agent_name?, agent_scene?, agent_status?, pipeline_list?: Array<{ pipeline_id?, pipeline_name? }> }> } }` + +**2. 检索** `POST /api/v1/indices/knowledge/search` +请求体:`{ query: string, agent_id: string, agent_version?: string, images?: string[] }`(**无 top_k 参数**,条数由服务端配置决定;top_k 为客户端截断) +响应:`{ code, status_code, request_id, data: { total, cost_time, nodes: Array<{ score: number, text: string, metadata: { title?, doc_id?, doc_name?, doc_url?, page_number?, ... } }> } }` + +**3. 问答** `POST /api/v2/apps/knowledge/chat`(**仅 SSE**) +请求体:`{ input: { messages: Array<{ role: 'user'|'assistant', content: string }> }, parameters: { agent_options: { agent_id, agent_version? } }, stream: true }` +SSE:`data: [DONE]` 结束;`event: error` 的 data 为 `{ code?, message? }`;普通 chunk: +`{ output: { choices: Array<{ message: { role, content, extra?: { step_change?, step?, group? } }, finish_reason: string }> }, code, message, request_id }` +`content` 为**增量**(delta),拼接即完整答案;事件序列 `tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end`,`tool_calling→tool_return` 可循环多次(服务端 agentic loop,耗时可达分钟级)。 + +**4. dsh 侧已验证接口**:`ctx.tools.register(defineTool({...}))`(模板:deepseek-harness `packages/todo/tool-todo/src/index.ts`);`ctx.credentials.resolve(credentialRef('DASHSCOPE_API_KEY'))` 返回 `Promise<{ value, source } | undefined>`,**逐次调用不缓存**;`ctx.skills.register({ name, description, whenToUse?, content, source: 'bundled', resourceBase? })` 返回 disposer;bundle patch 语法为顶层 `- insert:` 行数组(模板:`packages/bundle/base/cordis.patch.yml`)。 + +## 文件结构 + +``` +/Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle/ +├── package.json # 根:private,scripts(build/test/typecheck) +├── pnpm-workspace.yaml +├── tsconfig.base.json +├── vitest.config.ts +├── .gitignore +└── packages/ + ├── tool-bailian-kb/ + │ ├── package.json + │ ├── tsconfig.json + │ ├── skills/bailian-kb-management/SKILL.md # kscli 管理面 skill + │ ├── src/ + │ │ ├── index.ts # 插件入口:name/inject/Config/apply(只做装配) + │ │ ├── api-types.ts # 上述 API 请求/响应类型 + │ │ ├── endpoints.ts # 纯函数:URL 拼接 + 协议路径常量 + │ │ ├── client.ts # KbClient:Bearer 鉴权、JSON/SSE 请求、KbApiError + │ │ ├── sse.ts # 最小 SSE 解析器(async generator) + │ │ ├── chat.ts # consumeChatStream:SSE → 完整答案(缓冲) + │ │ ├── services.ts # listServices:scene 合并 + 分页内化 + 截断提示 + │ │ ├── tools.ts # createKbTools:三个工具定义(纯工厂) + │ │ └── skill.ts # registerSkill:ctx.skills 运行时注册 + │ └── tests/ + │ ├── config.test.ts + │ ├── endpoints.test.ts + │ ├── client.test.ts + │ ├── sse.test.ts + │ ├── chat.test.ts + │ ├── services.test.ts + │ └── tools.test.ts + └── bundle/ + ├── package.json # bailian-kb-bundle:dsh.bundle 声明 + └── cordis.patch.yml # insert 插件 row(workspaceId 走 !!js env) +``` + +--- + +### Task 1: 仓库脚手架与依赖可用性验证 + +**Files:** +- Create: `package.json`、`pnpm-workspace.yaml`、`tsconfig.base.json`、`vitest.config.ts`、`.gitignore` + +- [ ] **Step 1: 验证 npm 依赖存在** + +```bash +npm view @deepseek-ai/cordis version && npm view @deepseek-ai/dsh-tools version && npm view @deepseek-ai/schemastery version && npm view @deepseek-ai/dsh-credentials version && npm view @deepseek-ai/dsh-skill version +``` + +Expected: 五行版本号(已确认 cordis 4.0.1、dsh-tools 0.0.1-rc.1、schemastery 3.18.1;后两个若 404,改用 `github:` 依赖或从 dsh 安装闭包解析——此时停下向用户报告,不要静默绕过)。 + +- [ ] **Step 2: 初始化目录与 git** + +```bash +mkdir -p /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle && cd /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle && git init +``` + +- [ ] **Step 3: 写根配置文件** + +`package.json`: + +```json +{ + "name": "bailian-kb-workspace", + "private": true, + "type": "module", + "scripts": { + "build": "pnpm -r run build", + "test": "vitest run", + "typecheck": "tsc -b packages/tool-bailian-kb" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} +``` + +`pnpm-workspace.yaml`: + +```yaml +packages: + - packages/* +``` + +`tsconfig.base.json`: + +```json +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + } +} +``` + +`vitest.config.ts`: + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { include: ['packages/*/tests/**/*.test.ts'] }, +}) +``` + +`.gitignore`: + +``` +node_modules/ +lib/ +*.tsbuildinfo +``` + +- [ ] **Step 4: Commit** + +```bash +git add -A && git commit -m "chore: scaffold pnpm workspace" +``` + +### Task 2: 插件包骨架与 Config schema + +**Files:** +- Create: `packages/tool-bailian-kb/package.json`、`packages/tool-bailian-kb/tsconfig.json`、`packages/tool-bailian-kb/src/index.ts`(先只有 Config) +- Test: `packages/tool-bailian-kb/tests/config.test.ts` + +- [ ] **Step 1: 写包配置** + +`packages/tool-bailian-kb/package.json`: + +```json +{ + "name": "dsh-tool-bailian-kb", + "version": "0.1.0", + "description": "Bailian knowledge-base tools for DeepSeek Harness: kb_service_list, kb_search, kb_chat over the DashScope RAG API, plus the kscli management skill.", + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" }, + "./package.json": "./package.json" + }, + "files": ["lib", "skills"], + "scripts": { "build": "tsc -b" }, + "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1" }, + "dependencies": { + "@deepseek-ai/dsh-tools": "^0.0.1-rc.1", + "@deepseek-ai/dsh-credentials": "*", + "@deepseek-ai/dsh-skill": "*", + "@deepseek-ai/schemastery": "^3.18.1" + }, + "devDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@types/node": "^22.0.0" } +} +``` + +(`*` 处安装后锁到 `pnpm install` 解析出的当前 rc 版本,手动改回具体 `^` 范围。) + +`packages/tool-bailian-kb/tsconfig.json`: + +```json +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "lib" }, + "include": ["src"] +} +``` + +- [ ] **Step 2: 写失败测试** + +`packages/tool-bailian-kb/tests/config.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' +import { Config } from '../src/index.js' + +describe('Config', () => { + it('applies defaults and keeps required workspaceId', () => { + const resolved = new Config({ workspaceId: 'ws-1' }) + expect(resolved.workspaceId).toBe('ws-1') + expect(resolved.endpointHost).toBe('cn-beijing.maas.aliyuncs.com') + expect(resolved.chatTimeoutMs).toBe(300_000) + expect(resolved.defaultAgentId).toBeUndefined() + }) + + it('rejects a missing workspaceId (fail loud at load)', () => { + expect(() => new Config({} as never)).toThrow() + }) +}) +``` + +- [ ] **Step 3: 运行确认失败** + +```bash +cd /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle && pnpm install && pnpm vitest run packages/tool-bailian-kb/tests/config.test.ts +``` + +Expected: FAIL(`../src/index.js` 不存在)。 + +- [ ] **Step 4: 实现 Config** + +`packages/tool-bailian-kb/src/index.ts`: + +```ts +/** + * Bailian knowledge-base consumer plugin: registers kb_service_list, kb_search, + * and kb_chat over the DashScope RAG API, plus the kscli management skill. + * @module dsh-tool-bailian-kb + */ + +import z from '@deepseek-ai/schemastery' + +export const name = 'tool-bailian-kb' +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 + /** 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. */ + defaultAgentId?: string + /** Service version to call: `beta` (draft) or a published number; defaults to the latest published version. Never model-visible. */ + agentVersion?: string + /** kb_chat timeout in milliseconds; the server side is a minutes-scale agentic loop. */ + chatTimeoutMs: number +} + +/** Schemastery validation for {@link Config}; a missing workspaceId fails at load. */ +export const Config: z = z.object({ + workspaceId: z.string().required(), + endpointHost: z.string().default('cn-beijing.maas.aliyuncs.com'), + defaultAgentId: z.string(), + agentVersion: z.string(), + chatTimeoutMs: z.number().default(300_000), +}) +``` + +- [ ] **Step 5: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/config.test.ts +git add -A && git commit -m "feat: plugin package skeleton with validated Config" +``` + +### Task 3: endpoints 纯函数与 API 类型 + +**Files:** +- Create: `packages/tool-bailian-kb/src/endpoints.ts`、`packages/tool-bailian-kb/src/api-types.ts` +- Test: `packages/tool-bailian-kb/tests/endpoints.test.ts` + +- [ ] **Step 1: 写失败测试** + +`packages/tool-bailian-kb/tests/endpoints.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' +import { KB_PATHS, kbEndpoint } from '../src/endpoints.js' + +describe('kbEndpoint', () => { + it('builds the workspace-subdomain URL', () => { + expect(kbEndpoint('cn-beijing.maas.aliyuncs.com', 'ws-1', KB_PATHS.search)) + .toBe('https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search') + }) + + it('keeps protocol paths as constants', () => { + expect(KB_PATHS.chat).toBe('/api/v2/apps/knowledge/chat') + expect(KB_PATHS.serviceList).toBe('/api/v1/indices/rag/app/list') + }) +}) +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/endpoints.test.ts +``` + +Expected: FAIL(模块不存在)。 + +- [ ] **Step 3: 实现** + +`packages/tool-bailian-kb/src/endpoints.ts`: + +```ts +/** Protocol path constants and the workspace-subdomain URL builder (external API spec; not configurable). */ + +/** DashScope knowledge API paths, mirrored from the verified kscli endpoint table. */ +export const KB_PATHS = { + serviceList: '/api/v1/indices/rag/app/list', + search: '/api/v1/indices/knowledge/search', + chat: '/api/v2/apps/knowledge/chat', +} as const + +/** + * Build one knowledge API endpoint. + * @param endpointHost - host suffix, e.g. `cn-beijing.maas.aliyuncs.com`. + * @param workspaceId - Bailian workspace id used as the subdomain. + * @param path - one {@link KB_PATHS} value. + * @returns the absolute endpoint URL. + */ +export function kbEndpoint(endpointHost: string, workspaceId: string, path: string): string { + return `https://${workspaceId}.${endpointHost}${path}` +} +``` + +`packages/tool-bailian-kb/src/api-types.ts`: + +```ts +/** Request/response fields of the three DashScope knowledge endpoints, mirrored from the verified kscli types. */ + +export interface ServiceListRequest { + agent_scene: 'chat' | 'search' + agent_name?: string + page_number: number + page_size: number +} + +export interface ServiceListRow { + agent_id?: string + agent_name?: string + agent_scene?: string + agent_status?: string + pipeline_list?: { pipeline_id?: string; pipeline_name?: string }[] +} + +export interface ServiceListResponse { + code?: string + message?: string + data?: { total_count?: number; rows?: ServiceListRow[] } +} + +export interface SearchRequest { + query: string + agent_id: string + agent_version?: string + images?: string[] +} + +export interface SearchResponse { + request_id?: string + data?: { + total?: number + nodes?: { score: number; text: string; metadata?: Record }[] + } +} + +export interface ChatRequest { + input: { messages: { role: 'user' | 'assistant'; content: string }[] } + parameters: { agent_options: { agent_id: string; agent_version?: string } } + stream: true +} + +export interface ChatStreamChunk { + output?: { + choices?: { + message?: { content?: string; extra?: { step_change?: string } } + finish_reason?: string + }[] + } + request_id?: string +} +``` + +- [ ] **Step 4: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/endpoints.test.ts +git add -A && git commit -m "feat: endpoint builder and API types" +``` + +### Task 4: KbClient(鉴权、JSON/SSE 请求、错误翻译) + +**Files:** +- Create: `packages/tool-bailian-kb/src/client.ts` +- Test: `packages/tool-bailian-kb/tests/client.test.ts` + +- [ ] **Step 1: 写失败测试** + +`packages/tool-bailian-kb/tests/client.test.ts`: + +```ts +import { describe, expect, it, vi } from 'vitest' +import { KbApiError, KbClient } from '../src/client.js' + +function makeClient(fetchImpl: typeof fetch) { + return new KbClient({ + workspaceId: 'ws-1', + endpointHost: 'cn-beijing.maas.aliyuncs.com', + resolveApiKey: async () => 'sk-test', + fetchImpl, + }) +} + +describe('KbClient.postJson', () => { + it('sends Bearer auth to the workspace endpoint and returns parsed JSON', async () => { + const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: 1 }), { status: 200 })) + const client = makeClient(fetchImpl as unknown as typeof fetch) + const result = await client.postJson<{ ok: number }>('/api/v1/indices/knowledge/search', { query: 'q' }) + expect(result.ok).toBe(1) + const [url, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit] + expect(url).toBe('https://ws-1.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search') + expect((init.headers as Record).Authorization).toBe('Bearer sk-test') + expect(init.method).toBe('POST') + }) + + it('translates a non-2xx into KbApiError with status and a bounded body summary', async () => { + const body = JSON.stringify({ code: 'InvalidParameter', message: 'agent not found' }) + const fetchImpl = vi.fn(async () => new Response(body, { status: 400 })) + const client = makeClient(fetchImpl as unknown as typeof fetch) + const err = await client.postJson('/api/v1/indices/knowledge/search', {}).catch((e: unknown) => e) + expect(err).toBeInstanceOf(KbApiError) + expect((err as KbApiError).status).toBe(400) + expect((err as KbApiError).message).toContain('agent not found') + }) + + it('re-resolves the API key per call (credential hot-swap contract)', async () => { + const resolveApiKey = vi.fn(async () => 'sk-test') + const fetchImpl = vi.fn(async () => new Response('{}', { status: 200 })) + const client = new KbClient({ + workspaceId: 'ws-1', + endpointHost: 'h', + resolveApiKey, + fetchImpl: fetchImpl as unknown as typeof fetch, + }) + await client.postJson('/p', {}) + await client.postJson('/p', {}) + expect(resolveApiKey).toHaveBeenCalledTimes(2) + }) +}) +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/client.test.ts +``` + +Expected: FAIL(模块不存在)。 + +- [ ] **Step 3: 实现** + +`packages/tool-bailian-kb/src/client.ts`: + +```ts +/** Shared HTTP client for the knowledge endpoints: per-call Bearer auth, JSON/SSE POST, and error translation. */ + +import { kbEndpoint } from './endpoints.js' + +/** Maximum error-body characters kept in a translated message. */ +const ERROR_BODY_LIMIT = 500 + +/** One knowledge API failure: HTTP status plus a bounded server-body summary. */ +export class KbApiError extends Error { + constructor(message: string, readonly status?: number) { + super(message) + this.name = 'KbApiError' + } +} + +export interface KbClientOptions { + workspaceId: string + endpointHost: string + /** Service version forwarded on search/chat when set (deployment debug choice). */ + agentVersion?: string + /** Resolves the current DASHSCOPE_API_KEY per call; throws with guidance when unconfigured. */ + resolveApiKey: () => Promise + /** Test seam; defaults to global fetch. */ + fetchImpl?: typeof fetch +} + +export class KbClient { + constructor(private readonly opts: KbClientOptions) {} + + /** The deployment's configured service version, exposed for request builders. */ + get agentVersion(): string | undefined { + return this.opts.agentVersion + } + + private async post(path: string, body: unknown, accept: string, signal?: AbortSignal): Promise { + const apiKey = await this.opts.resolveApiKey() + const fetchImpl = this.opts.fetchImpl ?? fetch + const url = kbEndpoint(this.opts.endpointHost, this.opts.workspaceId, path) + const res = await fetchImpl(url, { + method: 'POST', + headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': accept }, + body: JSON.stringify(body), + signal, + }) + if (!res.ok) { + const raw = (await res.text().catch(() => '')).slice(0, ERROR_BODY_LIMIT) + let detail = raw + try { + const parsed = JSON.parse(raw) as { message?: string; code?: string } + if (parsed.message) detail = parsed.code ? `${parsed.code}: ${parsed.message}` : parsed.message + } catch { /* non-JSON error body: keep the bounded raw text */ } + throw new KbApiError(`knowledge API ${path} failed (HTTP ${res.status}): ${detail}`, res.status) + } + return res + } + + /** + * POST one JSON request and parse the JSON response. + * @param path - one KB_PATHS value. + * @param body - JSON-serializable request body. + * @param signal - optional abort/timeout signal. + * @returns the parsed response. + */ + async postJson(path: string, body: unknown, signal?: AbortSignal): Promise { + const res = await this.post(path, body, 'application/json', signal) + return await res.json() as T + } + + /** + * POST one JSON request expecting an SSE response stream. + * @param path - one KB_PATHS value. + * @param body - JSON-serializable request body. + * @param signal - abort/timeout signal (kb_chat passes its configured timeout). + * @returns the raw Response whose body is the SSE stream. + */ + async postSse(path: string, body: unknown, signal?: AbortSignal): Promise { + return await this.post(path, body, 'text/event-stream', signal) + } +} +``` + +- [ ] **Step 4: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/client.test.ts +git add -A && git commit -m "feat: KbClient with per-call auth and error translation" +``` + +### Task 5: SSE 解析与 chat 缓冲消费 + +**Files:** +- Create: `packages/tool-bailian-kb/src/sse.ts`、`packages/tool-bailian-kb/src/chat.ts` +- Test: `packages/tool-bailian-kb/tests/sse.test.ts`、`packages/tool-bailian-kb/tests/chat.test.ts` + +- [ ] **Step 1: 写失败测试(SSE 解析器)** + +`packages/tool-bailian-kb/tests/sse.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' +import { parseSseStream } from '../src/sse.js' + +function streamOf(text: string): ReadableStream { + return new Response(text).body as ReadableStream +} + +async function collect(text: string) { + const events: { event?: string; data: string }[] = [] + for await (const e of parseSseStream(streamOf(text))) events.push(e) + return events +} + +describe('parseSseStream', () => { + it('yields data events split on blank lines', async () => { + const events = await collect('data: {"a":1}\n\ndata: [DONE]\n\n') + expect(events).toEqual([{ event: undefined, data: '{"a":1}' }, { event: undefined, data: '[DONE]' }]) + }) + + it('carries the event field and survives chunk boundaries inside a line', async () => { + const events = await collect('event: error\ndata: {"message":"boom"}\n\n') + expect(events[0]).toEqual({ event: 'error', data: '{"message":"boom"}' }) + }) +}) +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/sse.test.ts +``` + +Expected: FAIL(模块不存在)。 + +- [ ] **Step 3: 实现 SSE 解析器** + +`packages/tool-bailian-kb/src/sse.ts`: + +```ts +/** Minimal SSE parser for the knowledge chat stream: `event:`/`data:` lines, events split on blank lines. */ + +export interface SseEvent { + event?: string + data: string +} + +/** + * Parse one SSE byte stream into events. + * @param body - the response body stream. + * @returns events in stream order; multi-`data:` events join with newlines per the SSE spec. + */ +export async function* parseSseStream(body: ReadableStream): AsyncGenerator { + const decoder = new TextDecoder() + let buffer = '' + let event: string | undefined + let data: string[] = [] + + const flush = (): SseEvent | undefined => { + if (data.length === 0) return undefined + const out = { event, data: data.join('\n') } + event = undefined + data = [] + return out + } + + const reader = body.getReader() + while (true) { + const { done, value } = await reader.read() + buffer += done ? '' : decoder.decode(value, { stream: true }) + let newline: number + while ((newline = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newline).replace(/\r$/, '') + buffer = buffer.slice(newline + 1) + if (line === '') { + const out = flush() + if (out) yield out + } else if (line.startsWith('event:')) { + event = line.slice(6).trim() + } else if (line.startsWith('data:')) { + data.push(line.slice(5).trimStart()) + } + // comment/id/retry lines are irrelevant to this API and are skipped + } + if (done) { + const out = flush() + if (out) yield out + return + } + } +} +``` + +- [ ] **Step 4: SSE 测试通过后,写失败测试(chat 缓冲)** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/sse.test.ts # Expected: PASS +``` + +`packages/tool-bailian-kb/tests/chat.test.ts`: + +```ts +import { describe, expect, it } from 'vitest' +import { consumeChatStream } from '../src/chat.js' + +function sse(text: string): Response { + return new Response(text, { status: 200 }) +} + +function chunk(content: string, finish = ''): string { + return `data: ${JSON.stringify({ output: { choices: [{ message: { content }, finish_reason: finish }] }, request_id: 'r-1' })}\n\n` +} + +describe('consumeChatStream', () => { + it('concatenates delta content across chunks until [DONE]', async () => { + const res = sse(chunk('Hello') + chunk(' world', 'stop') + 'data: [DONE]\n\n') + const out = await consumeChatStream(res) + expect(out.answer).toBe('Hello world') + expect(out.requestId).toBe('r-1') + }) + + it('ignores step_change progress chunks with empty content', async () => { + const progress = `data: ${JSON.stringify({ output: { choices: [{ message: { content: '', extra: { step_change: 'tool_calling' } }, finish_reason: '' }] } })}\n\n` + const res = sse(progress + chunk('answer', 'stop') + 'data: [DONE]\n\n') + expect((await consumeChatStream(res)).answer).toBe('answer') + }) + + it('throws on an SSE error event with the server message', async () => { + const res = sse('event: error\ndata: {"code":"Throttling","message":"rate limited"}\n\n') + await expect(consumeChatStream(res)).rejects.toThrow(/Throttling.*rate limited/) + }) +}) +``` + +- [ ] **Step 5: 运行确认失败,然后实现** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/chat.test.ts # Expected: FAIL +``` + +`packages/tool-bailian-kb/src/chat.ts`: + +```ts +/** Buffered consumption of the knowledge chat SSE stream: deltas concatenate into one complete answer. */ + +import type { ChatStreamChunk } from './api-types.js' +import { KbApiError } from './client.js' +import { parseSseStream } from './sse.js' + +export interface ChatResult { + answer: string + requestId?: string +} + +/** + * Consume one chat SSE response to completion. + * @param res - the SSE response from KbClient.postSse. + * @returns the concatenated answer and the last seen request id. + */ +export async function consumeChatStream(res: Response): Promise { + if (!res.body) throw new KbApiError('knowledge chat returned no response body') + let answer = '' + let requestId: string | undefined + for await (const event of parseSseStream(res.body)) { + if (event.data === '[DONE]') break + if (event.event === 'error') { + let message = `knowledge chat stream error: ${event.data}` + try { + const err = JSON.parse(event.data) as { code?: string; message?: string } + if (err.message) message = `knowledge chat stream error${err.code ? ` (${err.code})` : ''}: ${err.message}` + } catch { /* non-JSON error payload: keep the raw data in the message */ } + throw new KbApiError(message) + } + let parsed: ChatStreamChunk + try { + parsed = JSON.parse(event.data) as ChatStreamChunk + } catch { continue } // unparseable keep-alive/comment payloads carry no answer content + if (parsed.request_id) requestId = parsed.request_id + for (const choice of parsed.output?.choices ?? []) { + if (choice.message?.content) answer += choice.message.content + } + } + return { answer, requestId } +} +``` + +- [ ] **Step 6: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/sse.test.ts packages/tool-bailian-kb/tests/chat.test.ts +git add -A && git commit -m "feat: SSE parser and buffered chat consumption" +``` + +### Task 6: listServices(scene 合并、分页内化、截断提示) + +**Files:** +- Create: `packages/tool-bailian-kb/src/services.ts` +- Test: `packages/tool-bailian-kb/tests/services.test.ts` + +- [ ] **Step 1: 写失败测试** + +`packages/tool-bailian-kb/tests/services.test.ts`: + +```ts +import { describe, expect, it, vi } from 'vitest' +import type { ServiceListResponse } from '../src/api-types.js' +import type { KbClient } from '../src/client.js' +import { listServices } from '../src/services.js' + +function fakeClient(byScene: Record) { + const postJson = vi.fn(async (_path: string, body: { agent_scene: string }) => byScene[body.agent_scene]) + return { client: { postJson } as unknown as KbClient, postJson } +} + +const row = (id: string, scene: string) => ({ + agent_id: id, agent_name: `svc-${id}`, agent_scene: scene, agent_status: 'deployed', + pipeline_list: [{ pipeline_id: 'p1', pipeline_name: 'kb-one' }], +}) + +describe('listServices', () => { + it('queries both scenes when scene is omitted and merges rows with scene tags', async () => { + const { client, postJson } = fakeClient({ + chat: { data: { total_count: 1, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 1, rows: [row('b', 'search')] } }, + }) + const out = await listServices(client, {}) + expect(postJson).toHaveBeenCalledTimes(2) + expect(out.services.map(s => [s.agent_id, s.scene])).toEqual([['a', 'chat'], ['b', 'search']]) + expect(out.total).toBe(2) + expect(out.truncated).toBe(false) + const body = postJson.mock.calls[0]![1] as Record + expect(body.page_number).toBe(1) + expect(body.page_size).toBe(100) + }) + + it('queries one scene and forwards the name filter', async () => { + const { client, postJson } = fakeClient({ search: { data: { total_count: 0, rows: [] } } }) + await listServices(client, { scene: 'search', nameFilter: '客服' }) + expect(postJson).toHaveBeenCalledTimes(1) + expect((postJson.mock.calls[0]![1] as Record).agent_name).toBe('客服') + }) + + it('flags truncation when a scene exceeds one max page', async () => { + const { client } = fakeClient({ + chat: { data: { total_count: 250, rows: [row('a', 'chat')] } }, + search: { data: { total_count: 0, rows: [] } }, + }) + const out = await listServices(client, {}) + expect(out.truncated).toBe(true) + expect(out.total).toBe(250) + }) +}) +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/services.test.ts +``` + +Expected: FAIL(模块不存在)。 + +- [ ] **Step 3: 实现** + +`packages/tool-bailian-kb/src/services.ts`: + +```ts +/** Retrieval-service discovery: per-scene queries merged into one model-facing list; pagination stays internal. */ + +import type { ServiceListResponse } from './api-types.js' +import type { KbClient } from './client.js' +import { KB_PATHS } from './endpoints.js' + +/** Server page-size maximum; one page per scene covers ordinary workspaces. */ +const MAX_PAGE_SIZE = 100 + +export interface ServiceEntry { + agent_id: string + name: string + scene: string + status: string + knowledge_bases: string[] +} + +export interface ServiceList { + services: ServiceEntry[] + total: number + /** True when some scene reported more rows than one max page returned. */ + truncated: boolean +} + +export interface ListServicesQuery { + scene?: 'chat' | 'search' + nameFilter?: string +} + +/** + * List retrieval/Q&A services. An omitted scene fans out to both scenes and merges. + * @param client - the shared knowledge API client. + * @param query - optional scene and fuzzy name filter. + * @returns merged entries, the server-reported total, and the truncation flag. + */ +export async function listServices(client: KbClient, query: ListServicesQuery): Promise { + const scenes: ('chat' | 'search')[] = query.scene ? [query.scene] : ['chat', 'search'] + const services: ServiceEntry[] = [] + let total = 0 + for (const scene of scenes) { + const res = await client.postJson(KB_PATHS.serviceList, { + agent_scene: scene, + ...(query.nameFilter ? { agent_name: query.nameFilter } : {}), + page_number: 1, + page_size: MAX_PAGE_SIZE, + }) + total += res.data?.total_count ?? 0 + for (const row of res.data?.rows ?? []) { + services.push({ + agent_id: row.agent_id ?? '', + name: row.agent_name ?? '', + scene: row.agent_scene ?? scene, + status: row.agent_status ?? '', + knowledge_bases: (row.pipeline_list ?? []).map(p => p.pipeline_name ?? p.pipeline_id ?? '').filter(Boolean), + }) + } + } + return { services, total, truncated: total > services.length } +} +``` + +- [ ] **Step 4: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/services.test.ts +git add -A && git commit -m "feat: service discovery with scene merge and internalized pagination" +``` + +### Task 7: createKbTools 工具工厂(三工具 + defaultAgentId 静态 schema + 错误附清单) + +**Files:** +- Create: `packages/tool-bailian-kb/src/tools.ts` +- Test: `packages/tool-bailian-kb/tests/tools.test.ts` + +- [ ] **Step 1: 写失败测试** + +`packages/tool-bailian-kb/tests/tools.test.ts`: + +```ts +import { describe, expect, it, vi } from 'vitest' +import { KbApiError, KbClient } from '../src/client.js' +import { createKbTools } from '../src/tools.js' + +const EXEC = {} as never + +function toolsWith(postJson: unknown, postSse?: unknown, defaultAgentId?: string) { + const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient + const list = createKbTools({ client, defaultAgentId, chatTimeoutMs: 1000 }) + const byName = Object.fromEntries(list.map(t => [t.name, t])) + return { byName, list } +} + +const searchResponse = { + request_id: 'r1', + data: { total: 3, nodes: [ + { score: 0.9, text: 'A', metadata: { doc_name: 'd1' } }, + { score: 0.8, text: 'B', metadata: {} }, + { score: 0.7, text: 'C', metadata: {} }, + ] }, +} + +describe('createKbTools', () => { + it('registers exactly kb_service_list, kb_search, kb_chat', () => { + const { list } = toolsWith(vi.fn()) + expect(list.map(t => t.name).sort()).toEqual(['kb_chat', 'kb_search', 'kb_service_list']) + }) + + it('kb_search truncates nodes client-side to top_k (default 5 documented, explicit here)', async () => { + const postJson = vi.fn(async () => searchResponse) + const { byName } = toolsWith(postJson) + const out = await byName.kb_search!.execute({ query: 'q', agent_id: 'aid-1', top_k: 2 }, EXEC) as { chunks: unknown[] } + expect(out.chunks).toHaveLength(2) + const body = postJson.mock.calls[0]![1] as Record + expect(body).not.toHaveProperty('top_k') // the server API has no such parameter + }) + + it('agent_id is required without defaultAgentId and optional with one', () => { + const withoutDefault = toolsWith(vi.fn()).byName.kb_search! + const withDefault = toolsWith(vi.fn(), undefined, 'aid-fixed').byName.kb_search! + expect((withoutDefault.parameters as Record).agent_id!.required).toBe(true) + expect((withDefault.parameters as Record).agent_id!.required).toBeUndefined() + }) + + it('kb_search falls back to defaultAgentId as an explicit resolve step', async () => { + const postJson = vi.fn(async () => searchResponse) + const { byName } = toolsWith(postJson, undefined, '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 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) + return { data: { total_count: 1, rows: [{ agent_id: 'aid-9', agent_name: 'faq', agent_scene: 'search', agent_status: 'deployed' }] } } + }) + const { byName } = toolsWith(postJson) + const err = await byName.kb_search!.execute({ query: 'q', agent_id: 'bad' }, EXEC).catch((e: unknown) => e) + expect((err as Error).message).toContain('aid-9') + }) + + it('kb_chat buffers the SSE stream into one answer', async () => { + const sse = 'data: {"output":{"choices":[{"message":{"content":"hi"},"finish_reason":"stop"}]},"request_id":"r2"}\n\ndata: [DONE]\n\n' + const postSse = vi.fn(async () => new Response(sse, { status: 200 })) + const { byName } = toolsWith(vi.fn(), postSse) + const out = await byName.kb_chat!.execute({ message: 'q', agent_id: 'aid-1' }, EXEC) as { answer: string } + expect(out.answer).toBe('hi') + }) +}) +``` + +- [ ] **Step 2: 运行确认失败** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/tools.test.ts +``` + +Expected: FAIL(模块不存在)。 + +- [ ] **Step 3: 实现** + +`packages/tool-bailian-kb/src/tools.ts`: + +```ts +/** 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). */ + +import { defineTool } from '@deepseek-ai/dsh-tools' +import type { SearchRequest, SearchResponse } from './api-types.js' +import type { KbClient } from './client.js' +import { KbApiError } from './client.js' +import { consumeChatStream } from './chat.js' +import { KB_PATHS } from './endpoints.js' +import { listServices } from './services.js' + +/** Client-side chunk cap applied when the model omits top_k. */ +const DEFAULT_TOP_K = 5 + +export interface KbToolDeps { + client: KbClient + defaultAgentId?: string + chatTimeoutMs: number +} + +/** Format one service list into the error-hint / result text form. */ +function formatServices(services: { agent_id: string; name: string; scene: string; status: string }[]): string { + return services.map(s => `${s.agent_id} (${s.name}, scene=${s.scene}, ${s.status})`).join('; ') +} + +/** + * Append the current service list to a client error so the model can correct + * an invalid agent_id in one step. Auth failures (401/403) keep their own message. + */ +async function withServiceHint(client: KbClient, err: unknown): Promise { + if (err instanceof KbApiError && err.status !== undefined && err.status >= 400 && err.status !== 401 && err.status !== 403) { + try { + const { services } = await listServices(client, {}) + if (services.length > 0) { + throw new KbApiError(`${err.message}. Available services: ${formatServices(services)}`, err.status) + } + } catch (hintErr) { + if (hintErr instanceof KbApiError && hintErr.message.includes('Available services')) throw hintErr + // discovery is best-effort; fall through to the original error + } + } + throw err +} + +/** + * Build the three tool definitions over one shared client. + * @param deps - client plus the deployment's explicit pinning and timeout choices. + * @returns definitions ready for `ctx.tools.register()`. + */ +export function createKbTools(deps: KbToolDeps) { + const { client, defaultAgentId, 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.`, + } + 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 serviceList = defineTool({ + name: 'kb_service_list', + description: + 'List the Bailian knowledge retrieval/Q&A services available in this workspace. ' + + 'Each entry names the service id (agent_id) to pass to kb_search (scene=search) or kb_chat (scene=chat), ' + + 'its bound knowledge bases, and its status (prefer deployed). ' + + 'Omit scene to see both kinds; narrow large workspaces with name_filter.', + parameters: { + scene: { type: 'string', enum: ['chat', 'search'], description: 'Only list services for this scene; omitted lists both.' }, + name_filter: { type: 'string', description: 'Fuzzy match on the service name.' }, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + services: { + type: 'array', required: true, + items: { + type: 'object', additionalProperties: false, + properties: { + agent_id: { type: 'string', required: true }, + name: { type: 'string', required: true }, + scene: { type: 'string', required: true }, + status: { type: 'string', required: true }, + knowledge_bases: { type: 'array', required: true, items: { type: 'string' } }, + }, + }, + }, + total: { type: 'integer', required: true }, + truncated: { type: 'boolean', required: true }, + }, + }, + render: (_args, value) => [{ + type: 'text', + text: value.services.length === 0 + ? 'No knowledge services found.' + : `${value.services.length} service(s): ${formatServices(value.services)}` + + (value.truncated ? ` — listed first ${value.services.length} of ${value.total}; narrow with name_filter.` : ''), + }], + }, + async execute(args) { + return await listServices(client, { + ...(args.scene === 'chat' || args.scene === 'search' ? { scene: args.scene } : {}), + ...(args.name_filter ? { nameFilter: args.name_filter } : {}), + }) + }, + presentCall: args => ({ card: 'generic', title: 'List knowledge services', kind: 'other', rawInput: args }), + }) + + const search = defineTool({ + name: 'kb_search', + description: + 'Semantic search over a Bailian knowledge base. Returns raw knowledge chunks with scores and source ' + + 'references for you to verify, cite, or combine with other context. Retrieval scope and strategy ' + + '(multi-KB weighting, routing, reranking) come from the service configuration. ' + + 'top_k caps how many chunks return (client-side cut of the score-ranked results). ' + + 'Use kb_chat instead when the user question can be answered by the knowledge base alone.', + parameters: { + query: { type: 'string', required: true, description: 'Search query text.' }, + agent_id: agentIdParam, + top_k: { type: 'integer', description: `Maximum chunks to return; defaults to ${DEFAULT_TOP_K}.` }, + images: { type: 'array', items: { type: 'string' }, description: 'Image URLs for multimodal retrieval.' }, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + chunks: { + type: 'array', required: true, + items: { + type: 'object', additionalProperties: false, + properties: { + text: { type: 'string', required: true }, + score: { type: 'number', required: true }, + doc_name: { type: 'string' }, + doc_id: { type: 'string' }, + title: { type: 'string' }, + }, + }, + }, + total: { type: 'integer', required: true }, + }, + }, + render: (_args, value) => [{ + type: 'text', + text: value.chunks.length === 0 + ? 'No matching knowledge chunks.' + : value.chunks.map((c, i) => `[${i + 1}] (score ${c.score.toFixed(2)}${c.doc_name ? `, ${c.doc_name}` : ''}) ${c.text}`).join('\n'), + }], + }, + async execute(args) { + const topK = args.top_k ?? DEFAULT_TOP_K + const body: SearchRequest = { + query: args.query, + agent_id: resolveAgentId(args.agent_id), + ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), + ...(args.images && args.images.length > 0 ? { images: args.images } : {}), + } + const res = await client.postJson(KB_PATHS.search, body).catch(err => withServiceHint(client, err)) + const nodes = (res.data?.nodes ?? []).slice(0, topK) + return { + chunks: nodes.map(n => ({ + text: n.text, + score: n.score, + ...(typeof n.metadata?.doc_name === 'string' ? { doc_name: n.metadata.doc_name } : {}), + ...(typeof n.metadata?.doc_id === 'string' ? { doc_id: n.metadata.doc_id } : {}), + ...(typeof n.metadata?.title === 'string' ? { title: n.metadata.title } : {}), + })), + total: res.data?.total ?? nodes.length, + } + }, + presentCall: args => ({ card: 'generic', title: 'Search knowledge base', kind: 'read', rawInput: args }), + }) + + const chat = defineTool({ + name: 'kb_chat', + description: + 'Ask the knowledge base directly and get a complete, domain-tuned answer from a specialized RAG pipeline ' + + '(multi-round retrieval + reranking + grounded generation). For knowledge Q&A this typically outperforms ' + + 'searching and synthesizing yourself when the question can be answered by the knowledge base alone; ' + + 'use kb_search instead when you need raw chunks to verify, cite, or combine with other work. ' + + 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes.', + parameters: { + message: { type: 'string', required: true, description: 'The question to ask.' }, + agent_id: agentIdParam, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + answer: { type: 'string', required: true }, + request_id: { type: 'string' }, + }, + }, + render: (_args, value) => [{ type: 'text', text: value.answer.length === 0 ? '(empty answer)' : value.answer }], + }, + async execute(args) { + const body = { + input: { messages: [{ role: 'user' as const, content: args.message }] }, + parameters: { agent_options: { + agent_id: resolveAgentId(args.agent_id), + ...(client.agentVersion ? { agent_version: client.agentVersion } : {}), + } }, + stream: true as const, + } + let res: Response + try { + res = await client.postSse(KB_PATHS.chat, body, AbortSignal.timeout(chatTimeoutMs)) + } catch (err) { + if (err instanceof Error && err.name === 'TimeoutError') { + throw new Error( + `knowledge chat timed out after ${chatTimeoutMs}ms; the pipeline runs a multi-round retrieval loop ` + + 'and long questions can exceed the deployment timeout. Retry, or use kb_search for raw chunks instead.', + ) + } + return await withServiceHint(client, err) + } + const { answer, requestId } = await consumeChatStream(res) + return { answer, ...(requestId ? { request_id: requestId } : {}) } + }, + presentCall: args => ({ card: 'generic', title: 'Ask knowledge base (may take a few minutes)', kind: 'read', rawInput: args }), + }) + + return [serviceList, search, chat] +} +``` + +注意:`defineTool` 的参数 schema 结构以 `@deepseek-ai/dsh-tools` 实际类型为准(模板 `tool-todo`);若 `enum`/`items` 字段名有出入,按其 `ParameterSchemaSpec` 类型修正,测试断言随之调整——**不得**为绕过类型错误引入 `any`。 + +- [ ] **Step 4: 运行确认通过 & Commit** + +```bash +pnpm vitest run packages/tool-bailian-kb/tests/tools.test.ts +git add -A && git commit -m "feat: kb_service_list, kb_search, kb_chat tool factory" +``` + +### Task 8: SKILL.md、skill 注册与插件入口装配 + +**Files:** +- Create: `packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md`、`packages/tool-bailian-kb/src/skill.ts` +- Modify: `packages/tool-bailian-kb/src/index.ts`(追加 apply) + +- [ ] **Step 1: 写 SKILL.md** + +`packages/tool-bailian-kb/skills/bailian-kb-management/SKILL.md`: + +````markdown +--- +name: bailian-kb-management +description: 管理阿里云百炼知识库(建库、上传文档、部署检索服务、Chunk 运维)。当用户要创建/更新/删除知识库、上传或导入文档、部署检索服务、管理数据中心文件时使用 kscli。检索与问答不走本 skill——用原生工具 kb_search / kb_chat。 +--- + +# 百炼知识库管理(kscli) + +检索面与管理面的分工:**查知识用 `kb_search`(取证据)/ `kb_chat`(成品问答)原生工具;本 skill 只覆盖管理长尾**——知识库全生命周期、文档、检索服务、Chunk、数据中心。 + +## 前置检查 + +1. `kscli --version` —— 未安装则运行 `npm install -g knowledge-studio-cli`(需 Node.js ≥ 18.17);安装失败时把错误原样报告给用户,不要静默跳过。 +2. 鉴权:需要 `DASHSCOPE_API_KEY`(环境变量,或 `kscli config set --key api_key --value sk-xxx`)。 +3. workspace 解析优先级:`--workspace-id` 参数 > 环境变量 `BAILIAN_WORKSPACE_ID` > `kscli config set --key workspace_id --value ws-xxx`。 + +## 常用工作流:建库到可检索 + +```bash +kscli kb create --name "my-kb" --embedding-model text-embedding-v3 # 1. 建库 +kscli doc upload --kb-id --file ./docs.pdf # 2. 上传本地文档 +kscli doc status --kb-id --doc-id # 3. 轮询至 COMPLETED +kscli service create ... && kscli service deploy ... # 4. 建/部署检索服务 → 得到 agent_id +``` + +部署完成后用 `kb_service_list` 确认服务可见,再用 `kb_search --agent-id` 验证检索。 + +## 命令组速查 + +`kb`(list/info/create/update/delete/stats)· `doc`(list/upload/status/delete/tag/import-oss)· `service`(list/get/create/update/deploy/delete/copy)· `chunk`(add/list/update/delete)· `file` / `collection` / `category`(数据中心)。全部命令支持 `--output json`(结构化输出)、`--dry-run`(预览请求)、`--quiet`。完整手册:https://github.com/modelstudioai/cli/blob/main/docs/knowledge-cli-guide.md + +## 最佳实践 + +- 用户反复使用同一检索服务时,建议其把 agent_id 写入项目指令(如 AGENTS.md)或让 agent 记住,后续 kb_search / kb_chat 直接携带。 +- 服务有 draft/deployed 两种状态:只有 deployed 可被默认版本调用;draft 调试用 `--agent-version beta`。 +```` + +- [ ] **Step 2: 实现 skill 注册** + +`packages/tool-bailian-kb/src/skill.ts`: + +```ts +/** Runtime skill registration: the packaged kscli-management SKILL.md joins the catalog when a skills registry is composed. */ + +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +// Type-only: resolves ctx.skills for the optional inject below. +import type {} from '@deepseek-ai/dsh-skill' + +const SKILL_DIR = fileURLToPath(new URL('../skills/bailian-kb-management/', import.meta.url)) + +/** + * Register the management skill when the skills registry is composed; headless + * assemblies without the seam stay unaffected. + * @param ctx - the plugin context. + */ +export function registerSkill(ctx: Context): void { + ctx.inject(['skills'], (skillCtx) => { + const content = readFileSync(`${SKILL_DIR}SKILL.md`, 'utf8') + skillCtx.skills.register({ + name: 'bailian-kb-management', + description: + 'Manage Bailian knowledge bases with the kscli CLI: create/update KBs, upload documents, deploy ' + + 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools.', + content, + source: 'bundled', + resourceBase: { kind: 'directory', path: SKILL_DIR }, + }) + }) +} +``` + +(`lib/skill.js` 相对 `../skills/` 解析到包根 `skills/`,与 `files` 白名单一致。frontmatter 保留在 content 内无害;若 dsh 渲染出现重复描述,实现时剥离 frontmatter 再注册。) + +- [ ] **Step 3: 装配 apply** + +在 `packages/tool-bailian-kb/src/index.ts` 追加(Config 声明保持不变): + +```ts +import type { Context } from '@deepseek-ai/cordis' +import { credentialRef } from '@deepseek-ai/dsh-credentials' +import { KbClient } from './client.js' +import { registerSkill } from './skill.js' +import { createKbTools } from './tools.js' + +/** + * Register the three knowledge tools over one shared client, plus the + * management skill when a skills registry is composed. + * @param ctx - registrant context carrying tools and credentials. + * @param config - deployment's workspace, host, pinning, and timeout choices. + */ +export function apply(ctx: Context, config: Config): void { + const client = new KbClient({ + workspaceId: config.workspaceId, + 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).', + ) + } + return resolved.value + }, + }) + for (const tool of createKbTools({ + client, + ...(config.defaultAgentId ? { defaultAgentId: config.defaultAgentId } : {}), + chatTimeoutMs: config.chatTimeoutMs, + })) { + ctx.tools.register(tool) + } + registerSkill(ctx) +} +``` + +- [ ] **Step 4: 全量测试 + 构建验证 & Commit** + +```bash +pnpm run test && pnpm run typecheck && pnpm run build +git add -A && git commit -m "feat: plugin apply wiring with credential-backed client and skill registration" +``` + +Expected: 测试全 PASS;`packages/tool-bailian-kb/lib/` 产出 `index.js` 等。 + +### Task 9: bundle 分发包 + +**Files:** +- Create: `packages/bundle/package.json`、`packages/bundle/cordis.patch.yml` + +- [ ] **Step 1: 写 bundle 包** + +`packages/bundle/package.json`: + +```json +{ + "name": "bailian-kb-bundle", + "version": "0.1.0", + "description": "Installable dsh bundle for Bailian knowledge-base tools: kb_service_list, kb_search, kb_chat plus the kscli management skill.", + "type": "module", + "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, + "exports": { + "./cordis.patch.yml": "./cordis.patch.yml", + "./package.json": "./package.json" + }, + "files": ["cordis.patch.yml"], + "dependencies": { "dsh-tool-bailian-kb": "workspace:^" } +} +``` + +(发布时 `workspace:^` 由 pnpm publish 自动替换为版本号;git spec 安装场景改为固定版本依赖——实现时二选一并在 README 记录。) + +`packages/bundle/cordis.patch.yml`: + +```yaml +# bailian-kb-bundle: 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. + +- insert: + - id: tool-bailian-kb + name: dsh-tool-bailian-kb + config: + workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID +``` + +- [ ] **Step 2: Commit** + +```bash +git add -A && git commit -m "feat: installable dsh bundle package" +``` + +### Task 10: README 文档 + +**Files:** +- Create: `README.md`(根)、`packages/tool-bailian-kb/README.md`、`packages/bundle/README.md` + +- [ ] **Step 1: 写三份 README** + +根 `README.md`:项目一句话定位、两包结构表、安装(`dsh plugin --profile web add bailian-kb-bundle`)、配置(`BAILIAN_WORKSPACE_ID` + `DASHSCOPE_API_KEY` 写入 `~/.dsh/.env`)、验证(`dsh --profile web --dump-config`)、开发命令(install/test/typecheck/build)。 + +`packages/tool-bailian-kb/README.md`:Config 字段表(五个字段与默认值,含 defaultAgentId 的静态 schema 语义)、三个工具的参数/返回摘要、错误语义(4xx 附服务清单、凭证缺失指引、chat 超时)、Known Limitations(chat 执行期无进展显示——进展流式见 spec 附录 A;服务清单单页 100 上限靠 name_filter 收窄)。 + +`packages/bundle/README.md`:patch row 说明、用户覆盖示例(id-targeted patch 需 restate 整个 config)、卸载方式。 + +- [ ] **Step 2: Commit** + +```bash +git add -A && git commit -m "docs: package and repository READMEs" +``` + +### Task 11: dsh 本地集成验收(手动 smoke) + +前置:本机可运行 dsh(deepseek-harness checkout 或全局安装);`~/.dsh/.env` 写入真实 `DASHSCOPE_API_KEY` 与 `BAILIAN_WORKSPACE_ID`。 + +- [ ] **Step 1: 安装进本地 profile** + +```bash +cd /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle && pnpm run build +cd /Users/zeyufeng/Documents/Code/workspace/deepseek-harness +pnpm dsh plugin --profile headless add /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle/packages/bundle +``` + +Expected: pnpm 安装成功,输出提示 bundle 加入 `dsh.profile.bundles`。 + +- [ ] **Step 2: 验证组合树** + +```bash +pnpm dsh --profile headless --dump-config | grep -A 4 tool-bailian-kb +``` + +Expected: 能看到 `tool-bailian-kb` row 及解析后的 workspaceId。 + +- [ ] **Step 3: 真实任务 smoke(需要 DEEPSEEK_API_KEY)** + +```bash +pnpm dsh --profile headless "列出可用的知识库检索服务" +pnpm dsh --profile headless "用知识库检索:<你知识库里确定存在的主题>,给出来源" +``` + +Expected: 模型调用 `kb_service_list` / `kb_search` 并给出带来源的回答。将实际 transcript 记录到验收笔记。 + +- [ ] **Step 4: 负例验证(fail loud)** + +```bash +# 临时移除 BAILIAN_WORKSPACE_ID 后: +pnpm dsh --profile headless --dump-config +``` + +Expected: 加载期报错指向 workspaceId 缺失(schemastery required),而不是静默跳过。验证后恢复环境变量。 + +- [ ] **Step 5: 打 tag** + +```bash +cd /Users/zeyufeng/Documents/Code/workspace/bailian-kb-bundle && git tag v0.1.0 +``` + +--- + +## 与 spec 的偏差记录 + +- **测试策略降级(spec §10)**:spec 要求 mock HTTP fixture 的 keyless snapshot 与真实 API e2e;首版以单元测试(Task 2-7)+ 手动集成验收(Task 11)覆盖,snapshot/e2e 基建依赖 dsh snapshot harness 对 out-of-tree bundle 的支持情况,作为 v0.2 跟进项记入根 README 的 Known Limitations。其余均与 2026-08-15 spec 最终版一致。 + +## 明确不在本计划内(spec §2) + +`retrieve` 工具、MCP 通道、chat 进展流式 UI、`run_in_background`、skills 生态独立分发、snapshot/e2e 测试基建(依赖 dsh snapshot harness 对 out-of-tree bundle 的支持情况,首版以单元测试 + 手动集成验收覆盖,作为 v0.2 跟进项记录在根 README 的 Known Limitations)。 diff --git a/docs/specs/2026-08-15-bailian-kb-bundle-design.md b/docs/specs/2026-08-15-bailian-kb-bundle-design.md new file mode 100644 index 00000000..e4f634b2 --- /dev/null +++ b/docs/specs/2026-08-15-bailian-kb-bundle-design.md @@ -0,0 +1,195 @@ +# 百炼知识库 dsh 插件(out-of-tree bundle)设计 + +> 状态:设计已确认并实现(v0.1.0 待真实 API smoke 后打 tag)。 + +## 1. 背景与目标 + +为 DeepSeek Harness(dsh)提供阿里云百炼知识库(Knowledge Studio / RAG)垂类能力。经对比 MCP、CLI、API 三种接入通道后确定分层混合架构: + +- **高频检索面走 API 直连原生工具**:结构化 schema、进程内 HTTP、结果可 snapshot、体验可打磨; +- **低频管理长尾走 kscli + skill**:`knowledge-studio-cli`(与 `bl knowledge` 同源实现的轻量发行面)覆盖建库/上传/部署等 34 个子命令,渐进式披露,零插件维护成本; +- **不做 MCP 通道**:托管 rag MCP 面向不拥有 API/CLI 的第三方宿主,非本方案投入点。 + +## 2. 范围 + +**做:** + +- 三个模型面工具:`kb_service_list`、`kb_search`、`kb_chat`(API 直连); +- 一个管理面 skill(引导 agent 使用 kscli); +- bundle 分发形态与配置、凭证、错误、测试设计。 + +**不做(含理由):** + +| 项 | 理由 | +|---|---| +| `retrieve` 工具 | 服务端已弃用(`search` 取代);新表面不携带 deprecated 能力,避免近义工具混淆 | +| MCP 通道 | 见 §1 | +| chat 进展流式 UI(模式 4) | 一期用缓冲式 + 期望管理,看真实使用反馈再决定(见 §7 与附录 A) | +| `run_in_background` 后台模式 | dsh jobs 机制已备好,出现真实需求再加 | +| skills 生态独立分发(B-3) | 一期 skill 随 bundle 注册;跨宿主分发留待后续 | +| 运行时 API/CLI fallback | 每个操作固定一条通道;双实现漂移与故障掩盖的代价大于收益 | + +## 3. 总体形态 + +独立仓库维护的 **out-of-tree bundle**:`package.json` 声明 `dsh.bundle` 指向 patch 文件,安装进 dsh profile 的 patch 层;不进入 deepseek-harness 主仓库,不改变 modelstudioai/cli 仓库的定位。 + +命名: + +- bundle 包:`bailian-kb-bundle` +- 插件包:`dsh-tool-bailian-kb` + +仓库为独立 pnpm workspace(目录 `workspace/bailian-kb-bundle`,独立 git 仓库),两包结构:`packages/tool-bailian-kb`(插件本体:Config、client、三个工具、随包打包的 `skills/bailian-kb-management/SKILL.md`)与 `packages/bundle`(分发面:`dsh.bundle` 声明、`cordis.patch.yml`,`dependencies` 含插件包)。拆分依据:patch row 的 bare 插件名必须出现在 bundle 的 `dependencies`,插件包保持纯净(仅 `@deepseek-ai/cordis` peer + dsh 能力包依赖)。 + +插件为函数插件形态(`name` / `inject: ['tools']` / `Config` / `apply`),在 `apply(ctx, config)` 中构建共享 API client 并注册三个工具。 + +### 3.1 接入与配置流程 + +**bundle 侧接入契约**(`@deepseek-ai/dsh-base` 为模板): + +- `package.json` 声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,并在 `exports` 暴露 `./cordis.patch.yml`; +- `cordis.patch.yml` 用 `insert` 插入插件 row 与 skill 注册 row;row 中的 bare 插件名必须出现在 bundle 自身的 `dependencies`; +- 发布到 npm,或直接以 git spec 分发(`github:/`)。 + +**用户安装**: + +```sh +dsh plugin --profile web add bailian-kb-bundle +``` + +CLI 转发 pnpm 将 bundle 装为 profile dependency;安装后自动 reconcile——检测到 `dsh.bundle` 声明即加入 `dsh.profile.bundles` 层栈,无需手改 YAML。boot 层序为 `dsh-base` → … → 本 bundle patch → profile `cordis.patch.yml` → 家目录 `cordis.patch.yml`,用户 patch 层在本 bundle 之上,插入的任何 row 均可被按 id 覆盖或禁用。卸载 `dsh plugin --profile web remove bailian-kb-bundle` 自动收回层栈。 + +**配置落点**:用户 patch 是整 config 替换(无 deep-merge),因此 bundle row 的 config 默认从环境读取: + +```yaml +config: + workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID +``` + +用户将 `BAILIAN_WORKSPACE_ID` 写入 `~/.dsh/.env` 即可运行(`DASHSCOPE_API_KEY` 放同处或 `.credentials.yaml`);需要精细控制的部署再以 id-targeted patch 覆盖整个 config。`workspaceId` 缺失时按 §6 在加载期 fail loud,错误信息指向 `.env` 配置方式。验证入口:`dsh --profile web --dump-config` 可见本 bundle 的 row。 + +**本地开发迭代**:checkout 内 `dsh plugin --profile dev add .`(相对路径锚定调用目录);patch 文件受 HMR 监听,编辑后自动 recompose。 + +## 4. 模型面工具 + +### 4.1 `kb_service_list` + +发现当前 workspace 的检索/问答服务(百炼"检索服务",即 `agent_id` 的来源)。 + +| 参数 | 类型 | 必填 | 语义 | +|---|---|---|---| +| `scene` | enum `chat` \| `search` | 否 | 省略时插件内部对两个 scene 各查一次并合并;每个条目携带 scene 标记(指明该服务配 `kb_chat` 还是 `kb_search` 使用) | +| `name_filter` | string | 否 | 服务名模糊匹配,透传服务端 `agent_name` | + +返回:服务条目数组(`agent_id`、名称、描述、scene、status、绑定的知识库)+ `total`。 + +**分页内部消化**:固定 `page_size=100, page_number=1`(服务端上限 100)。`total > 100` 时结果末尾附提示 `listed first 100 of N services; narrow with name_filter`。不向模型暴露翻页参数——模型的导航原语是名字过滤,不是页码。 + +**status 不作为参数**:条目携带 `status` 字段,description 提示优先使用 `deployed`;draft 服务仅在 `agentVersion: beta` 的调试部署下可调(部署期概念,不占模型参数面)。 + +### 4.2 `kb_search` + +语义检索,返回原始知识片段供 agent 综合与引用。 + +| 参数 | 类型 | 必填 | 语义 | +|---|---|---|---| +| `query` | string | 是 | 检索文本 | +| `agent_id` | string | 见 §5 | 检索服务 id(scene=search 的服务);检索范围与策略(多库加权、路由、重排)由服务端配置决定 | +| `top_k` | integer | 否,默认 5 | 返回片段数上限。服务端 search API 无此参数(条数由检索服务配置决定),插件对按 score 降序的 `nodes` 做客户端截断;description 写明该语义 | +| `images` | string[] | 否 | 多模态检索的图片 URL | + +返回:chunks 数组(内容 + 来源引用)。 + +### 4.3 `kb_chat` + +知识库成品问答。服务端为 agentic loop(分析 → 多轮检索 → 生成),耗时可达分钟级。 + +| 参数 | 类型 | 必填 | 语义 | +|---|---|---|---| +| `message` | string | 是 | 问题 | +| `agent_id` | string | 见 §5 | 问答服务 id(scene=chat) | + +返回:完整答案文本(含 API 提供的引用信息时一并返回)。 + +### 4.4 description 路由策略 + +`kb_search` 与 `kb_chat` 的 description **互相指名分界**,把"该用谁"写成可判断条件而非形容词: + +- `kb_search`:returns raw knowledge chunks with source references;用于需要核实、引用、或与其他上下文结合推理的场景; +- `kb_chat`:a complete, domain-tuned answer produced by a specialized RAG pipeline (retrieval + reranking + grounded generation);知识问答场景通常优于自行检索综合(typically outperforms searching and synthesizing yourself when the question can be answered by the knowledge base alone);并注明 may take a few minutes。 + +## 5. `agent_id` 的三种场景覆盖 + +| 场景 | 机制 | 插件成本 | +|---|---|---| +| 发现式 | `kb_service_list` → 选服务 → search/chat;`agent_id` 缺失或无效时,错误信息直接附当前服务清单,模型一步纠正 | 发现工具 + 错误增强 | +| 用户习惯固定 | 宿主 memory / 项目指令记住常用 `agent_id` | 零(skill 写入最佳实践) | +| 场景/部署固定 | Config 可选 `defaultAgentId`;配置后注册时将 `agent_id` 参数降为可选,description 注明缺省服务 | 一个可选配置字段 | + +**Schema 形态在加载期由配置静态决定**(未配 `defaultAgentId` 则 `agent_id` 必填),不是运行时 fallback;每个部署只有一条清晰规则,KV cache 前缀与 snapshot 均稳定。与 dsh preset 组合可实现按场景绑定(如客服 preset 固定客服库)。 + +## 6. 配置与凭证 + +```ts +interface Config { + /** 百炼工作空间 id。知识库 API 的 host 为 workspace 子域名:`https://.`。必填。 */ + workspaceId: string + /** 知识库 API 的 host 后缀。默认 `cn-beijing.maas.aliyuncs.com`;其他 region/私有化部署时替换。 */ + endpointHost: string + /** 场景固定式部署绑定的检索服务 id。可选。 */ + defaultAgentId?: string + /** 调用的服务版本:beta(草稿调试)或已发布版本号。可选,缺省最新发布版。不暴露给模型。 */ + agentVersion?: string + /** kb_chat 超时毫秒数。chat 为分钟级 loop,部署必须可调。默认 300000(5 分钟)。 */ + chatTimeoutMs: number +} +``` + +- schemastery 校验;缺失/非法配置在**加载期 fail loud**; +- API Key 走 `ctx.credentials` 引用(`DASHSCOPE_API_KEY`,env/.env provider),不进 Config、不进会话日志、不被 `--dump-config` 打印; +- URL 拼接是 `(endpointHost, workspaceId, path) → endpoint` 的纯函数(`https://${workspaceId}.${endpointHost}${path}`,与 kscli 的 `ragEndpoint` 同构),与请求构造、错误翻译一起收在插件内部的共享 client 中(协议路径为代码常量,不进配置)。 + +## 7. 执行语义 + +- **`kb_chat` 缓冲式**(与 bash 前台/subagent 同构的仓库惯例:dsh 中没有工具向模型或 UI 中途推流):`execute` 内部消费完 SSE,一次性返回完整答案。UI 呈现为 `presentCall` pending 卡片 → `presentResult` 完成卡片(`generic` 卡,纯函数、replay-safe); +- **期望管理**:description 与 pending 卡片标题注明 may take a few minutes; +- **超时**:`chatTimeoutMs` 显式可配(dsh tool-timeout guard 可另行部署级配置); +- **超长输出**:声明依赖 dsh spill 子系统兜底,插件不自造截断。 + +## 8. 管理面 skill + +- SKILL.md 随插件包打包;插件在 skills 服务可用时通过 `ctx.inject(['skills'], …)` 以 `ctx.skills.register()` 运行时注册(`source: 'bundled'`,`resourceBase` 指向包内 skill 目录),无 skills 服务的组合不受影响;工具与 skill 同版本发布,互相引用不漂移; +- 内容:kscli 安装引导(`npm install -g knowledge-studio-cli`)、API Key 与 workspace 解析(flag > `BAILIAN_WORKSPACE_ID` > 配置文件)、典型工作流(建库 → 上传 → 等解析 → 部署服务 → 检索验证)、"常用 `agent_id` 写入项目指令/记忆"最佳实践、检索面与管理面的分工说明(search/chat 用原生工具,不走 kscli); +- kscli 未安装时管理操作 fail loud 并给出安装命令;检索面不受影响。 + +## 9. 错误处理 + +- HTTP 错误翻译为模型可操作的文本:无效 `agent_id` → 附当前服务清单;鉴权失败 → 指向 API Key 获取与配置方式;超时 → 说明 chat 可能耗时并建议重试或改用 search; +- 凭证缺失在首次可解析点大声失败,不静默降级; +- 服务端非 2xx 的响应体原样摘要进错误信息(截断至安全长度),便于模型与用户诊断。 + +## 10. 测试策略 + +| 层 | 内容 | +|---|---| +| 单元测试 | endpoint 拼接、请求体构造(scene 合并、分页内化、`defaultAgentId` 解析)、错误翻译 | +| snapshot | mock HTTP fixture 的可重放 keyless snapshot,macOS/Linux 均可回放;覆盖三工具的调用与渲染卡片 | +| e2e | 真实 DashScope API,无 `DASHSCOPE_API_KEY` 时自跳过 | + +## 附录 A:预留扩展(已设计方向,未排期) + +- **chat 进展流式(模式 4)**:`execute` 消费 SSE 时 append 工具自有会话事件(如 `bailian/chat-progress`,`ignorable: true`),Web 客户端注册 `ConversationNodeDefinition` 渲染器实时显示;模型面不变(logged ≠ model-visible)。触发条件:真实用户对 chat 等待体验的负反馈; +- **后台模式**:`kb_chat` 增加 `run_in_background`,挂 `ctx.jobs`,`job_output` 收取; +- **skills 生态分发(B-3)**:以 bundle 仓库的 SKILL.md 为唯一源,发布到 `npx skills add` 生态覆盖其他宿主; +- **能力缝升级**:出现第二种传输(如私有化内网网关)时,将共享 client 提为 `ctx.` 服务,按 Service Definition / Provider / Consumer 三角色拆分。 + +## 附录 B:关键决策记录 + +| 决策 | 结论 | 理由摘要 | +|---|---|---| +| 接入通道 | API(检索面)+ CLI(管理面),不做 MCP | 频率×能力深度×控制权分层;API/CLI 均为己方资产 | +| CLI 选型 | kscli 而非 bl | 同源实现零能力损失;命令面窄、鉴权单一、onboarding 短 | +| `retrieve` | 不做 | 已弃用,避免近义工具 | +| `kb_chat` 门控 | 不门控,常驻注册 | 服务端 RAG 管线在知识问答场景更专业,description 写明场景让模型路由 | +| `agent_id` 归属 | 模型参数 + 发现工具 + 可选 `defaultAgentId` | 检索服务是用户运行时资产,插件与部署配置不应假设 | +| chat 流式 | 一期缓冲式 | 仓库惯例(bash/subagent 同构);进展流式留待反馈 | +| 分页 | 内部消化(page_size=100 + 溢出提示) | 模型导航原语是 name 过滤,非页码 | From 50b59f8640fd740ee66ee303b5031563f5ca67cc Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 19:36:00 +0800 Subject: [PATCH 14/45] =?UTF-8?q?refactor(bundle):=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E5=88=86=E5=8F=91=E5=8C=85=E4=B8=BA=20bailian-kb-dsh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将分发包名称从 bailian-kb-bundle 改为 bailian-kb-dsh - 同步更新 README.md 中的包名和卸载命令描述 - 更新 package.json 中的 name 字段 - 修改 cordis.patch.yml 中的注释以匹配新名称 - 确保所有文档和配置引用一致性 --- README.md | 6 +++--- packages/bundle/README.md | 4 ++-- packages/bundle/cordis.patch.yml | 2 +- packages/bundle/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 65e1afd5..50a50ccf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# bailian-kb-bundle +# bailian-kb-dsh 阿里云百炼知识库能力的 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件 bundle:三个 API 直连模型工具(`kb_service_list` / `kb_search` / `kb_chat`)+ kscli 管理面 skill。 @@ -14,7 +14,7 @@ ## 安装(dsh 用户) ```sh -dsh plugin --profile web add bailian-kb-bundle # npm 发布后;本地开发用绝对/相对路径 +dsh plugin --profile web add bailian-kb-dsh # npm 发布后;本地开发用绝对/相对路径 ``` 安装后 CLI 自动把 bundle 加入 profile 的层栈,无需手改 YAML。 @@ -28,7 +28,7 @@ DASHSCOPE_API_KEY=sk-xxx # 必填:也可放 ~/.dsh/.credentials.yaml 验证:`dsh --profile web --dump-config` 应能看到 `tool-bailian-kb` row。缺 `BAILIAN_WORKSPACE_ID` 时加载期直接报错(fail loud),不会静默跳过。 -卸载:`dsh plugin --profile web remove bailian-kb-bundle`。 +卸载:`dsh plugin --profile web remove bailian-kb-dsh`。 ## 开发 diff --git a/packages/bundle/README.md b/packages/bundle/README.md index 66ffd0ad..126b173f 100644 --- a/packages/bundle/README.md +++ b/packages/bundle/README.md @@ -1,4 +1,4 @@ -# bailian-kb-bundle(分发包) +# bailian-kb-dsh(分发包) dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.patch.yml`](cordis.patch.yml),向 profile 插入 `tool-bailian-kb` row。 @@ -32,5 +32,5 @@ dsh bundle 分发面:`package.json` 的 `dsh.bundle.patch` 声明 + [`cordis.p ## 卸载 ```sh -dsh plugin --profile remove bailian-kb-bundle +dsh plugin --profile remove bailian-kb-dsh ``` diff --git a/packages/bundle/cordis.patch.yml b/packages/bundle/cordis.patch.yml index b822b52a..3fce72a7 100644 --- a/packages/bundle/cordis.patch.yml +++ b/packages/bundle/cordis.patch.yml @@ -1,4 +1,4 @@ -# bailian-kb-bundle: inserts the Bailian knowledge-base consumer over dsh-base. +# 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. diff --git a/packages/bundle/package.json b/packages/bundle/package.json index 71a8bb58..58dc7dc6 100644 --- a/packages/bundle/package.json +++ b/packages/bundle/package.json @@ -1,5 +1,5 @@ { - "name": "bailian-kb-bundle", + "name": "bailian-kb-dsh", "version": "0.1.0", "description": "Installable dsh bundle for Bailian knowledge-base tools: kb_service_list, kb_search, kb_chat plus the kscli management skill.", "type": "module", From 589e3a4f14890fbaddb2bb2fef877b52b39c0481 Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Sat, 15 Aug 2026 22:44:31 +0800 Subject: [PATCH 15/45] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=20TypeS?= =?UTF-8?q?cript=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 98b59a3d..1ecfd7b1 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 126b173f..c53b02a9 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 3fce72a7..3cf4d605 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 853c5d5f..32a1e26d 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 ee91c6cd..ddae77ab 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 b88aae2e..4c41d47c 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 01f2d642..7a3253cf 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 00000000..f9800a8e --- /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 00000000..98dac717 --- /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 00000000..0965acb1 --- /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 00000000..6da4b80d --- /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 00000000..ec752bad --- /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 00000000..f4b7cf74 --- /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 c1aa0038..5b486337 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 f43a6981..0030c003 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 1ac7d1f3..14f96325 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 c7148159..7dca09bc 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 00000000..5c3bedff --- /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 00000000..c86f53d1 --- /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