mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75699966d4 | |||
| 2220f932b5 | |||
| d9134d6f0a | |||
| 5c390fad74 | |||
| 6218637f7c | |||
| 0c7ba721a7 | |||
| 0dbf071368 | |||
| 75a7100702 | |||
| 16e30686f0 | |||
| 7803d91e3f | |||
| c2d17707c0 | |||
| 1dfb4400cc | |||
| fd2077b373 | |||
| 2090293f85 | |||
| a245359792 | |||
| 19bd5a5ea8 | |||
| e262f2e574 | |||
| 180aac9f28 | |||
| 3477a08dad | |||
| aa1484264e | |||
| 2767491ce6 | |||
| efb832815a | |||
| 902516bbad | |||
| f44fb2e95f | |||
| c6b9931d6f | |||
| a92c58420b | |||
| bcf68a31f3 | |||
| 8e5dd127a9 | |||
| 4f41eaabc2 | |||
| e1634c40ed | |||
| 4c5688ad4a | |||
| 176961dc56 | |||
| 888031562f | |||
| d017954ae8 | |||
| 9d19558645 | |||
| a9200a3f85 | |||
| 83d7982ecb | |||
| d33cb71cc7 | |||
| 737f8db359 | |||
| a402026cce | |||
| da6e1313e5 | |||
| 2d2ec1c628 | |||
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| fd96fd664c | |||
| 719c0b68bb | |||
| 9e6f5ca9cb | |||
| 3f78de2dc1 | |||
| 38926aeb22 | |||
| e53daf05eb | |||
| 67ae182ec3 | |||
| e3f2996291 | |||
| 59250c4f44 | |||
| 676b6c2ece | |||
| 4f59ca5118 | |||
| 0872ff6a20 | |||
| 98ebeb398c | |||
| 5c96077d9f | |||
| 8a7674e7c3 | |||
| afb547e0c8 | |||
| 4fdd8389f8 | |||
| bd8b08b617 | |||
| 449eede237 | |||
| 8906108744 | |||
| 2fbb6efe67 | |||
| 47e9bcc634 | |||
| 00bcee36a6 | |||
| e9479e4084 | |||
| 4c7ef0c7a5 | |||
| 17ffde4ac2 | |||
| 9600f06205 | |||
| fc3ac36d00 | |||
| fe11227789 | |||
| 9469556671 | |||
| 804a43f8ed | |||
| d343dc3265 | |||
| 0d24f46f73 | |||
| cc209ce0b1 | |||
| 8360a9f584 | |||
| 90d1ef9dc5 | |||
| 124c425133 | |||
| 670c15532b | |||
| b93e0d586d | |||
| 4067b2c2aa | |||
| adb1ced45c | |||
| 69c1fb9f08 | |||
| e023af5be3 | |||
| a28d95a763 | |||
| 491858be4a | |||
| 18fd2ed8e5 | |||
| 4c51c95f31 | |||
| 3f21f55541 | |||
| eb8b0892d6 | |||
| a1023ce9a4 | |||
| 7bc1c49cb6 | |||
| a393bdc56b | |||
| f29f5fe632 | |||
| d24104f7dc | |||
| 9112a88c60 | |||
| b11adcc6fe | |||
| 90dd038a2f | |||
| e76ebee681 | |||
| 78a1c547d3 | |||
| a95ad7242b | |||
| b830a14e11 | |||
| a6291e00e1 | |||
| 5f44cb4402 | |||
| 70dbc2110d | |||
| 8714f61496 | |||
| e1793e1f3a | |||
| 0a63115aed | |||
| abad3a6643 | |||
| d69f73f1bc | |||
| 28fc1b6056 | |||
| a8774cc143 | |||
| fea86dc5aa | |||
| 06210a4e33 | |||
| e31addf0d6 | |||
| 0d8d354ddc | |||
| cd955211e3 | |||
| 6e3fdeafc0 | |||
| 0d28a35e26 | |||
| af95a9ec67 | |||
| 30c2be67a0 | |||
| efcb1d90ed | |||
| f36e044119 | |||
| 0ef7c829de | |||
| 6cedca21d9 | |||
| 7aa6aab7d7 | |||
| 6811ec619d | |||
| f0427be625 | |||
| 328739b91a | |||
| 4b3ea28a2d | |||
| 9000a5efbf | |||
| dbf83c3e16 | |||
| cda1e326f3 | |||
| ad5c44d746 | |||
| 9450895a06 | |||
| 8a09881626 | |||
| e681263049 | |||
| ffc460156c | |||
| 70b50060cf | |||
| 3909a17da1 | |||
| 589e3a4f14 | |||
| 50b59f8640 | |||
| 73233658bf | |||
| 188090af39 | |||
| a9ac7c0bea | |||
| acccca9e2f | |||
| a6ee267071 | |||
| b4129c6cbd | |||
| f2a3775516 | |||
| 5800222ea4 | |||
| 95708ccf32 | |||
| cdc1f10882 | |||
| bbe96d35a5 | |||
| fba98bf65e | |||
| 3aedb7b7fb | |||
| 7461189007 | |||
| 4086da572f | |||
| 8b7956d547 | |||
| 7e21573793 | |||
| f6cf2b999a | |||
| 2fe50f59a4 | |||
| 418dcffc53 | |||
| 9ddb8dab53 | |||
| 23f1ab7fd4 | |||
| d5c4bd3572 | |||
| f67ca55ec6 | |||
| 6b2f49de71 | |||
| eb4f9af3e7 | |||
| b7a4efe619 | |||
| 29ce8990b9 | |||
| a770cbe787 | |||
| 30f7525d50 | |||
| aa38d5c670 | |||
| cc51164c2f | |||
| 6b685964f3 | |||
| d74d4efcd0 | |||
| e7422bd2e5 | |||
| 2d5c49b02e | |||
| 9bd8b60c22 | |||
| 0369bd36b0 | |||
| 92a978af3c | |||
| ea7b0f016f | |||
| 9749a11d76 | |||
| 2965080cb7 | |||
| 81959145d7 | |||
| 4343fc87af | |||
| 1c76749ee5 | |||
| 1b568e8d37 | |||
| 5d1b7aac3a | |||
| e292b20d4b | |||
| 12e7a22195 | |||
| 219d8be80a | |||
| ab766d44d3 | |||
| 1d9852805f | |||
| 99a3dbae2d | |||
| 5007b9b574 | |||
| 8286a74fb6 | |||
| 9eb2acbb65 | |||
| 1d35326c86 | |||
| eb6c2b8e2a | |||
| ebd6226a9f | |||
| e25d3b0b8e | |||
| 3c64461cca | |||
| f30fff9065 | |||
| a7245c0f62 | |||
| d9e8601a50 | |||
| e3bb5a7fa0 | |||
| 54da9aa29a | |||
| ef463e8d5d | |||
| 8ee2c378f5 | |||
| 9fc6434a26 | |||
| 43abf0aca5 |
@@ -0,0 +1,27 @@
|
||||
# Poke the FC publish-skills flow after skills/ changes land.
|
||||
# The FC side reconciles this repo's skills/ directory against OSS
|
||||
# (bailian-wiki/skills/) using the repo HEAD snapshot as the only
|
||||
# source of truth — the request itself carries no content. Both the
|
||||
# repo and branch params are validated against FC-side whitelists
|
||||
# (PUBLISH_REPOS / PUBLISH_BRANCHES).
|
||||
#
|
||||
# feat/cli-skill-sync is temporary for end-to-end testing; remove it
|
||||
# (here and from the FC PUBLISH_BRANCHES whitelist) once the sync
|
||||
# link is verified on main.
|
||||
name: Publish skills to OSS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- feat/cli-skill-sync
|
||||
paths:
|
||||
- "skills/**"
|
||||
|
||||
jobs:
|
||||
poke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger FC publish-skills
|
||||
run: |
|
||||
curl -sf -X POST "${{ vars.FC_TRIGGER_URL }}/publish-skills?repo=modelstudioai/cli&branch=${{ github.ref_name }}"
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
options:
|
||||
- bailian-cli
|
||||
- knowledge-studio-cli
|
||||
- bailian-kb-dsh
|
||||
mode:
|
||||
description: "Publish mode"
|
||||
required: true
|
||||
@@ -18,7 +19,7 @@ on:
|
||||
- channel
|
||||
- stable
|
||||
channel:
|
||||
description: "Required when mode=channel. npm dist-tag only (lowercase, digits, dashes), e.g. mcp / plugin / sync-release. bailian-cli binary CDN always overwrites sync-release.json; knowledge-studio-cli is npm-only."
|
||||
description: "Required when mode=channel. npm dist-tag only (lowercase, digits, dashes), e.g. mcp / plugin / sync-release. bailian-cli binary CDN always overwrites sync-release.json; knowledge-studio-cli and bailian-kb-dsh are npm-only."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -28,7 +29,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
if: inputs.mode == 'stable'
|
||||
if: inputs.mode == 'stable' && inputs.package != 'bailian-kb-dsh'
|
||||
name: publish stable (${{ inputs.package }}) to npm + binary + tag
|
||||
runs-on: ubuntu-latest
|
||||
environment: production # Required Reviewers gate
|
||||
@@ -83,7 +84,7 @@ jobs:
|
||||
run: node tools/release/publish-stable.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }}
|
||||
|
||||
publish-channel:
|
||||
if: inputs.mode == 'channel'
|
||||
if: inputs.mode == 'channel' && inputs.package != 'bailian-kb-dsh'
|
||||
name: publish channel (${{ inputs.package }}) to npm + binary
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -138,3 +139,49 @@ jobs:
|
||||
BAILIAN_RELEASE_PREFIX: ${{ secrets.BAILIAN_RELEASE_PREFIX }}
|
||||
BAILIAN_STATIC_PREFIX: ${{ secrets.BAILIAN_STATIC_PREFIX }}
|
||||
run: node tools/release/publish-channel.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }} --channel "${{ inputs.channel }}"
|
||||
|
||||
# bailian-kb-dsh is the dsh plugin (downstream host adapter): independent version,
|
||||
# tsc + tsdown build, npm-only. It shares this workflow's entry UI and setup steps
|
||||
# but NOT publish-stable.mjs / publish-channel.mjs — those broadcast one version
|
||||
# across the locked bl package set and produce binary artifacts, neither of which
|
||||
# applies here. See docs/agents/dsh-plugin.md.
|
||||
publish-kb-dsh:
|
||||
if: inputs.package == 'bailian-kb-dsh'
|
||||
name: publish ${{ inputs.mode }} (bailian-kb-dsh) to npm
|
||||
runs-on: ubuntu-latest
|
||||
# stable goes through the Required Reviewers gate, same as the bl stable job;
|
||||
# channel stays ungated so dist-tag drops need no approval.
|
||||
environment: ${{ inputs.mode == 'stable' && 'production' || '' }}
|
||||
permissions:
|
||||
contents: write # push the bailian-kb-dsh-v<version> tag (stable only)
|
||||
id-token: write # OIDC for npm Trusted Publishing + provenance
|
||||
steps:
|
||||
- name: Require channel input
|
||||
if: ${{ inputs.mode == 'channel' && inputs.channel == '' }}
|
||||
run: |
|
||||
echo "::error::mode=channel requires the workflow input \"channel\" (npm dist-tag, e.g. mcp / plugin). Leave mode=stable if you do not need a dist-tag."
|
||||
exit 1
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install gitleaks
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GITLEAKS_VERSION=8.21.2
|
||||
curl -sSfL \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
| sudo tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: publish-kb-dsh
|
||||
run: node tools/release/publish-kb-dsh.mjs ${{ inputs.mode == 'channel' && format('--channel "{0}"', inputs.channel) || '' }}
|
||||
|
||||
@@ -14,6 +14,7 @@ dist-bin
|
||||
dist-ssr
|
||||
tools/generated
|
||||
.node-version
|
||||
*.tsbuildinfo
|
||||
|
||||
*.local
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ git add \
|
||||
skills/bailian-finetune/SKILL.md \
|
||||
skills/bailian-finetune/reference \
|
||||
skills/bailian-managed-agent/SKILL.md \
|
||||
skills/bailian-managed-agent/reference
|
||||
skills/bailian-managed-agent/reference \
|
||||
skills/bailian-web-search/SKILL.md
|
||||
|
||||
vp staged
|
||||
|
||||
@@ -11,6 +11,7 @@ monorepo 现在按"纯逻辑 → 运行时框架 → 命令库 → 产品入口"
|
||||
- `packages/commands` — `bailian-cli-commands`,可复用命令实现库,只导出 command,不决定产品路径
|
||||
- `packages/cli` — `bailian-cli`,完整 `bl` 产品入口;`src/commands.ts` 组装 `bl` 暴露的命令路径
|
||||
- `packages/kscli` — `knowledge-studio-cli`,Knowledge Studio 专用入口;`src/main.ts` 复用 commands 并重映射为 `kscli` 路径
|
||||
- `packages/bailian-kb-dsh` — `bailian-kb-dsh`,**下游宿主适配层**(依赖方向朝外):百炼知识库的 DeepSeek Harness (dsh) 插件,消费 `bl` CLI 与知识库 API,不在上面这条分层链上;版本、构建、发布都独立,见 [docs/agents/dsh-plugin.md](docs/agents/dsh-plugin.md)
|
||||
|
||||
### 关键文件
|
||||
|
||||
@@ -35,7 +36,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
|
||||
packages/core/src/client/ # HTTP client / endpoints / console gateway
|
||||
```
|
||||
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
|
||||
|
||||
约定:
|
||||
|
||||
@@ -56,25 +57,26 @@ Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐
|
||||
|
||||
按当前任务从下表挑一条进入对应文档:
|
||||
|
||||
| 场景 | 何时进入 | 详见 |
|
||||
| ----------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
|
||||
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
|
||||
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
|
||||
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
|
||||
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
|
||||
| Skill 文案 / 路由 | 改 SKILL 路由、安装约定、hand-off、hub/领域边界 | [docs/agents/skill-change.md](docs/agents/skill-change.md) |
|
||||
| 错误文案变更 | 改 `BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
|
||||
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
|
||||
| 埋点变更 | 改 AEM 命令事件、后端渠道 header、User-Agent | [docs/agents/telemetry-change.md](docs/agents/telemetry-change.md) |
|
||||
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
|
||||
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
|
||||
| Profile / 激活 | 改命名 Profile、预设或 `active_config` | [docs/agents/config-profile-change.md](docs/agents/config-profile-change.md) |
|
||||
| 安装文档 | 改安装、鉴权、验证流程或线上 install 页面 | [docs/agents/install-doc-change.md](docs/agents/install-doc-change.md) |
|
||||
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
|
||||
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
|
||||
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
|
||||
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
|
||||
| 场景 | 何时进入 | 详见 |
|
||||
| ----------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
|
||||
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
|
||||
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
|
||||
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
|
||||
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
|
||||
| Skill 文案 / 路由 | 改 SKILL 路由、安装约定、hand-off、hub/领域边界 | [docs/agents/skill-change.md](docs/agents/skill-change.md) |
|
||||
| 错误文案变更 | 改 `BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
|
||||
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
|
||||
| 埋点变更 | 改 AEM 命令事件、后端渠道 header、User-Agent | [docs/agents/telemetry-change.md](docs/agents/telemetry-change.md) |
|
||||
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
|
||||
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
|
||||
| Profile / 激活 | 改命名 Profile、预设或 `active_config` | [docs/agents/config-profile-change.md](docs/agents/config-profile-change.md) |
|
||||
| 安装文档 | 改安装、鉴权、验证流程或线上 install 页面 | [docs/agents/install-doc-change.md](docs/agents/install-doc-change.md) |
|
||||
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
|
||||
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
|
||||
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
|
||||
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
|
||||
| dsh 插件 | 改 `packages/bailian-kb-dsh`、dsh 依赖、插件发布 | [docs/agents/dsh-plugin.md](docs/agents/dsh-plugin.md) |
|
||||
|
||||
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增 `docs/agents/<scenario>.md`,把清单沉淀下来。
|
||||
|
||||
@@ -84,12 +86,15 @@ Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐
|
||||
|
||||
源码包的 `version` 当前保持一致: `packages/core`、`packages/runtime`、`packages/commands`、`packages/cli`、`packages/kscli`。做版本 bump 时一动多动。release 工具当前强校验 / 发布范围以 `tools/release/lib/packages.mjs` 为准;把新包纳入发布前必须同步该清单和 [publish.md](docs/agents/publish.md)。
|
||||
|
||||
**例外**: `packages/bailian-kb-dsh` 不参与这个锁步(独立 `0.1.x`,跟随 dsh rc 节奏),也不在 release 白名单里;它走 `publish.yml` 里 `package=bailian-kb-dsh` 的独立 job(`tools/release/publish-kb-dsh.mjs`)。
|
||||
|
||||
### 2. 分层边界
|
||||
|
||||
- `core` 是纯库:不依赖 `runtime` / `commands` / 产品入口;不调 `process.exit`;新增/改动时不硬编码 `bl` / `kscli` 命令名、控制台 URL 或渠道追踪参数。当前遗留项见 [error-hint-change.md](docs/agents/error-hint-change.md) 与 [url-change.md](docs/agents/url-change.md),触碰相关代码时顺手收敛
|
||||
- `runtime` 是通用 CLI 框架:可以处理 TTY、help、错误输出、middleware,但不写具体业务命令逻辑
|
||||
- `commands` 是命令实现库:不决定产品路径;不在 `usageArgs` / `exampleArgs` / hint 里硬编码产品 bin 前缀
|
||||
- `cli` / `kscli` 是产品层:负责命令路径 map、产品 identity、README、技能 reference、发版入口
|
||||
- `bailian-kb-dsh` 在这条链之外:它是别的宿主(dsh)里的插件,只允许依赖 `core`(且当前刻意零依赖),反过来 `core` / `runtime` / `commands` / 产品层**永远不许**依赖它
|
||||
- URL 集中在 `packages/runtime/src/urls.ts`(用户面控制台)和 `packages/core/src/config/schema.ts` / client 层(API)
|
||||
|
||||
### 3. 错误处理边界:CLI 不翻译服务端错误
|
||||
@@ -122,6 +127,10 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的
|
||||
|
||||
例外: 仅当作用域极小(≤3 行)且语义从上下文完全明确时,可使用 `k`/`v`(Object.entries 的 key/value)。
|
||||
|
||||
### 6. 用户可见 CLI 文案必须支持中英文
|
||||
|
||||
新增或修改用户可见的 CLI 文案时必须同时提供 `en-US` / `zh-CN`;runtime 公共文案遵循同一规则,服务端错误仍按第 3 节原样透传。命令文案的具体检查项见 [command-add-remove.md](docs/agents/command-add-remove.md)。
|
||||
|
||||
## 完成改动后的快速验证
|
||||
|
||||
```sh
|
||||
|
||||
+152
@@ -6,6 +6,158 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Profile-level watermark control** — configure `watermark` with `bl config set --key watermark --value true|false` to control the default watermark behavior for image generation and editing, video generation and editing, and reference-to-video commands.
|
||||
- **ASR accuracy controls** — `bl speech recognize` now supports instant hot words with `--vocabulary`, contextual word enhancement with `--context`, and reusable pre-built vocabularies with `--vocabulary-id` for supported ASR models.
|
||||
- **Speech vocabulary management** — added `bl speech vocabulary create|list|get|update|delete` to manage reusable pre-built hot-word vocabularies.
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
|
||||
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
|
||||
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
|
||||
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
|
||||
|
||||
### Internal
|
||||
|
||||
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
|
||||
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
|
||||
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
|
||||
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent API commands** — added direct list, get, search, version, upload, download, run, pause, archive, event, and diagnostic operations for Agents, Environments, Skills, Vaults, Deployments, Sessions, and Files.
|
||||
- **Scoped YAML-backed resource creation** — `agent create`, `environment create`, `skill create`, `vault create`, `vault credential create`, and `deployment create` update `agents.yaml` and apply only the target resource without unrelated drift blocking the operation.
|
||||
- **Agent Skill attachment** — Agent creation supports existing custom or official Skill IDs as well as local Skill directories and ZIP archives.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bailian-only Managed Agent CLI** — `bl managed-agent` now targets the Bailian provider exclusively; provider-selection flags were removed and configurations containing other providers are rejected.
|
||||
- **Runtime mutation confirmation** — Deployment run/pause/unpause, Session archive/delete, and File delete operations require explicit high-risk confirmation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Managed Agent error reporting** — Apply and scoped-create failures preserve the underlying provider diagnostic instead of ending with only `Apply failed.`.
|
||||
|
||||
### Security
|
||||
|
||||
- Credentials are resolved in memory and removed from the process environment; Vault credential declarations reference environment variables without persisting plaintext secrets.
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl quota delete`** — clears all custom QPM/TPM rate limits for a model.
|
||||
|
||||
### Changed
|
||||
|
||||
- **High-risk operation confirmation** — high-risk commands show risk details in `--help` and Skill command references. Without `--yes`, the high-risk operation is not executed; JSON output returns exit code `7` with `error.type: "requires_confirmation"`. After confirmation, re-run with `--yes`; `--dry-run` does not require confirmation.
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed API Key validation from `bl auth login`.
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### Added
|
||||
|
||||
- **Automatic Profile API Key fallback** — unsupported commands use the API Key and endpoint from `default` without requiring a Profile switch; explicit credentials still take priority.
|
||||
- **Independent speech defaults** — Profiles support separate default models for TTS and ASR.
|
||||
- **Wan3.0 file-to-video** — `bl video generate --file` accepts local files or URLs and validates incompatible inputs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Wan3.0 is now the default video model** — regular API Key text, image, and reference video tasks default to `wan3.0-video`, with first/last-frame and reference-audio support. Token Plan defaults remain unchanged.
|
||||
- **Expanded Token Plan preset** — login adds speech defaults and missing capabilities while preserving existing user configuration.
|
||||
- **Improved speech and Skill installation experience** — streaming synthesis defaults to PCM; installation docs add a Node 18-compatible fallback and clarify the Node.js and Git prerequisites.
|
||||
|
||||
## [1.17.1] - 2026-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`knowledge create` now requires `--description`** — aligns with the server's required-description validation: the new `--description` flag is mandatory and its 1-500 character limit is checked locally before the request goes out. `bl knowledge create` / `kscli kb create` calls need to pass it.
|
||||
- **`knowledge service update` warned about config fields the server itself returned** — updating the draft config through scalar flags such as `--policy` reads the full draft and merges before writing back; the draft's `user_system_prompt`, `anti_leak_prompt`, `refusal_prompt`, `credibility_prompt`, `session_file_parse_mode`, and `enable_thinking` / `enable_temperature` / `enable_credibility` / `enable_max_completion_tokens` were not recognized by the CLI, so every update printed a run of `unknown agent_config field passed through` warnings. The config itself was always written correctly; the spurious warnings are gone.
|
||||
|
||||
### Added
|
||||
|
||||
- **`bailian-web-search` routing skill** — `bl skill init` now also installs a dedicated web-search routing skill, so agents pick the right search entry point instead of guessing.
|
||||
- **Knowledge Studio CLI command manual** — full `kscli` reference docs covering knowledge bases, documents, chunks, collections/categories, files, retrieval/Q&A services, and search/chat, with runnable examples for every command.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Description flags explain what to write** — help text for the collection and service `--description` flags now states what the field is for (telling similar items apart in lists; for services, agents read it to pick the right one) rather than just repeating "required".
|
||||
- **`knowledge retrieve --rerank-model` documents its precondition** — help now states that the target knowledge base must already have a rerank model configured, otherwise every value is rejected.
|
||||
|
||||
## [1.17.0] - 2026-08-18
|
||||
|
||||
### Added
|
||||
|
||||
- **Native Bailian Managed Agent Deployments** — `deployments` declared in `agents.yaml` now materialize as native AgentStudio resources, with server-side cron schedules, local file resource uploads, archival through `destroy`, and migration of legacy emulated state on the next `apply`.
|
||||
- **Bilingual CLI experience** — Set `language` to `en-US` or `zh-CN` through `bl config set` or Config UI to switch CLI Help, Quick Start, command examples, and Config UI between English and Chinese. The selected language follows the active config.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Free Tier Auto-Stop controls** — `bl usage freetier --off` can now disable Auto-Stop even when free quota remains; status rendering reflects the actual switch state, and filtered model queries avoid server-side batch-limit failures.
|
||||
|
||||
## [1.16.0] - 2026-08-17
|
||||
|
||||
> Full knowledge-base lifecycle management arrives in the CLI: create and configure knowledge bases, upload documents, tune chunks, and deploy retrieval/Q&A services — all from `bl knowledge` and `kscli`.
|
||||
|
||||
### Added
|
||||
|
||||
- **Knowledge base management** — `bl knowledge create` / `list` / `info` / `update` / `delete` manage knowledge bases end to end; `bl knowledge stats` reports document counts and usage over a past time range.
|
||||
- **Document management** — `bl knowledge doc upload` uploads local files or whole directories (recursive scan, skips unsupported formats and tool directories like `node_modules`); `doc list` / `status` / `tag` / `delete` cover the rest of the document lifecycle, and `doc import-oss` imports documents from OSS.
|
||||
- **Retrieval / Q&A service management** — `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` manage retrieval and Q&A service configurations, including deploying a draft to a published version.
|
||||
- **Chunk management** — `bl knowledge chunk add` / `list` / `update` / `delete` inspect and fine-tune document chunks.
|
||||
- **Data-center management** — `bl knowledge category list` / `add` / `delete`, `bl knowledge file list` / `get` / `delete`, and `bl knowledge collection create` / `get` manage categories, raw files, and data collections.
|
||||
- **Service version selection for retrieval and chat** — `bl knowledge search` and `bl knowledge chat` accept `--agent-version` to call the beta (draft) config for debugging or a specific published version.
|
||||
- **`kscli` parity** — all new knowledge commands are also available in Knowledge Studio CLI under shorter paths, e.g. `kscli kb list`, `kscli doc upload`, `kscli service deploy`.
|
||||
|
||||
### Removed
|
||||
|
||||
- **`bl knowledge search --query-history` removed** — the parameter never took effect; use `bl knowledge chat` with `--message` history for multi-turn scenarios.
|
||||
|
||||
### Internal
|
||||
|
||||
- Requests now carry a static OpenAPI source identification header for backend channel attribution.
|
||||
- Added knowledge-base E2E suites, including five user-journey scenarios covering cold start, content ops, chunk tuning, service tuning, and the data plane.
|
||||
|
||||
## [1.15.1] - 2026-08-17
|
||||
|
||||
### Added
|
||||
|
||||
+152
@@ -6,6 +6,158 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.23.0] - 2026-09-10
|
||||
|
||||
### 新增
|
||||
|
||||
- **Profile 级水印控制** —— 可通过 `bl config set --key watermark --value true|false` 设置图片生成与编辑、视频生成与编辑以及参考生视频命令的默认水印行为。
|
||||
- **ASR 准确率增强** —— `bl speech recognize` 现支持通过 `--vocabulary` 传入即时热词、通过 `--context` 增强上下文词表,以及通过 `--vocabulary-id` 使用适用于对应 ASR 模型的预编译热词表。
|
||||
- **语音热词表管理** —— 新增 `bl speech vocabulary create|list|get|update|delete`,用于管理可复用的预编译热词表。
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### 变更
|
||||
|
||||
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
|
||||
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。
|
||||
|
||||
### 修复
|
||||
|
||||
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
|
||||
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
|
||||
|
||||
### 内部
|
||||
|
||||
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
|
||||
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
|
||||
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent API 命令** —— 新增 Agent、Environment、Skill、Vault、Deployment、Session 和 File 的列表、详情、搜索、版本、上传、下载、运行、暂停、归档、事件及诊断等操作。
|
||||
- **基于 YAML 的单资源创建** —— `agent create`、`environment create`、`skill create`、`vault create`、`vault credential create` 和 `deployment create` 会更新 `agents.yaml`,并且只 Apply 目标资源,不受无关资源 Drift 阻塞。
|
||||
- **Agent Skill 挂载** —— 创建 Agent 时支持引用已有的自定义或官方 Skill ID,也支持本地 Skill 目录和 ZIP 文件。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Managed Agent CLI 限定为百炼 Provider** —— `bl managed-agent` 现在只面向百炼,移除 Provider 选择参数,并拒绝包含其他 Provider 的配置。
|
||||
- **运行时变更增加确认** —— Deployment 运行/暂停/恢复、Session 归档/删除以及 File 删除操作需要显式进行高风险确认。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Managed Agent 错误输出** —— Apply 和单资源创建失败时会保留底层 Provider 的具体诊断,不再只显示 `Apply failed.`。
|
||||
|
||||
### 安全
|
||||
|
||||
- 凭证仅在内存中解析并从进程环境清除;Vault Credential 声明通过环境变量引用 Secret,不会持久化明文。
|
||||
|
||||
## [1.19.0] - 2026-09-01
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl quota delete`** — 清除指定模型的全部自定义 QPM/TPM 限流配置。
|
||||
|
||||
### 变更
|
||||
|
||||
- **高风险操作确认** — 高风险命令会在 `--help` 和 Skill 命令参考中展示风险说明。未传入 `--yes` 时,高风险操作不会执行;JSON 输出会返回退出码 `7` 和 `error.type: "requires_confirmation"`。确认后可添加 `--yes` 重新执行;`--dry-run` 无需确认。
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### 变更
|
||||
|
||||
- **删除与清除操作增加确认** —— `bl finetune delete`、`bl deploy delete`、`bl dataset delete` 和 `bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
- 移除 `bl auth login` 的 API Key 校验。
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### 新增
|
||||
|
||||
- **Profile API Key 自动回退** —— 当前 Profile 不支持某条命令时,自动使用 `default` 中的 API Key 和接入地址,无需手动切换 Profile;显式凭证仍然优先。
|
||||
- **语音合成与识别支持独立默认模型** —— Profile 支持分别配置 TTS 和 ASR 默认模型。
|
||||
- **Wan3.0 文件生视频** —— `bl video generate --file` 支持本地文件或 URL,并校验互斥输入。
|
||||
|
||||
### 变更
|
||||
|
||||
- **视频默认模型升级至 Wan3.0** —— 普通 API Key 的文生、图生和参考生视频默认使用 `wan3.0-video`,并支持首尾帧与参考音频;Token Plan 默认模型保持不变。
|
||||
- **扩展 Token Plan 预设** —— 登录后自动补充语音默认模型和缺失能力,同时保留用户已有配置。
|
||||
- **优化语音与 Skill 安装体验** —— 流式语音合成默认输出 PCM;Skill 安装文档补充兼容 Node 18 的降级方式及 Node.js、Git 前置条件。
|
||||
|
||||
## [1.17.1] - 2026-08-22
|
||||
|
||||
### 修复
|
||||
|
||||
- **`knowledge create` 的 `--description` 更新为必填** —— 对齐服务端对知识库描述的必填校验:新增 `--description` 参数并设为必填,在发出请求前于本地校验 1–500 个字符的长度限制。`bl knowledge create` / `kscli kb create` 调用需带上该参数。
|
||||
- **`knowledge service update` 对服务端自己返回的配置字段误报警告** —— 通过 `--policy` 等标量参数更新草稿配置时,CLI 会先读取完整草稿再合并回写;草稿中的 `user_system_prompt`、`anti_leak_prompt`、`refusal_prompt`、`credibility_prompt`、`session_file_parse_mode` 以及 `enable_thinking` / `enable_temperature` / `enable_credibility` / `enable_max_completion_tokens` 此前不被 CLI 识别,导致每次更新都刷出一串 `unknown agent_config field passed through` 警告。配置本身始终被正确写入,现在不再误报。
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bailian-web-search` 路由技能** —— `bl skill init` 现在会一并安装专门的联网搜索路由技能,让 agent 直接选中正确的搜索入口,不再靠猜。
|
||||
- **Knowledge Studio CLI 命令手册** —— 完整的 `kscli` 参考文档,覆盖知识库、文档、切片、集合/类目、文件、检索/问答服务以及 search/chat,每条命令均附可运行示例。
|
||||
|
||||
### 变更
|
||||
|
||||
- **描述类参数说明写清该填什么** —— 数据集合与服务的 `--description` 帮助文案现在会说明该字段的用途(在列表中区分同类项;服务描述供 agent 判断该调用哪个服务),不再只是重复「必填」。
|
||||
- **`knowledge retrieve --rerank-model` 补充前置条件说明** —— 帮助文案现在会说明目标知识库必须已配置重排序模型,否则任何取值都会被拒绝。
|
||||
|
||||
## [1.17.0] - 2026-08-18
|
||||
|
||||
### 新增
|
||||
|
||||
- **百炼原生 Managed Agent Deployment** —— `agents.yaml` 中声明的 `deployments` 现在会创建原生 AgentStudio 资源,支持服务端 Cron 调度、本地文件资源上传、通过 `destroy` 归档,以及在下次 `apply` 时迁移旧版模拟 Deployment state。
|
||||
- **CLI 中英文体验** —— 可通过 `bl config set` 或 Config UI 将 `language` 设置为 `en-US` 或 `zh-CN`,在英文和中文的 CLI Help、Quick Start、命令示例及 Config UI 之间切换;所选语言跟随当前激活的配置。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Free Tier Auto-Stop 控制** —— `bl usage freetier --off` 现在可在免费额度尚有剩余时关闭 Auto-Stop;状态展示会反映实际开关状态,并仅查询筛选后的模型,避免触发服务端批量查询上限。
|
||||
|
||||
## [1.16.0] - 2026-08-17
|
||||
|
||||
> CLI 迎来知识库全生命周期管理:从创建配置知识库、上传文档、调优切片,到部署检索/问答服务,均可通过 `bl knowledge` 与 `kscli` 完成。
|
||||
|
||||
### 新增
|
||||
|
||||
- **知识库管理** —— `bl knowledge create` / `list` / `info` / `update` / `delete` 覆盖知识库的完整生命周期;`bl knowledge stats` 查询指定过去时间段内的文档数量与用量统计。
|
||||
- **文档管理** —— `bl knowledge doc upload` 支持上传本地文件或整个目录(递归扫描,自动跳过不支持的格式及 `node_modules` 等工具目录);`doc list` / `status` / `tag` / `delete` 覆盖文档生命周期其余环节,`doc import-oss` 支持从 OSS 导入文档。
|
||||
- **检索 / 问答服务管理** —— `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` 管理检索与问答服务配置,支持将草稿部署为正式版本。
|
||||
- **切片管理** —— `bl knowledge chunk add` / `list` / `update` / `delete` 查看并精调文档切片。
|
||||
- **数据中心管理** —— `bl knowledge category list` / `add` / `delete`、`bl knowledge file list` / `get` / `delete`、`bl knowledge collection create` / `get` 管理类目、原始文件与数据集。
|
||||
- **检索与问答支持指定服务版本** —— `bl knowledge search` 和 `bl knowledge chat` 新增 `--agent-version`,可调用 beta(草稿)配置进行调试,或指定已发布的版本号。
|
||||
- **`kscli` 同步支持** —— 全部新知识库命令在 Knowledge Studio CLI 中以更短路径提供,如 `kscli kb list`、`kscli doc upload`、`kscli service deploy`。
|
||||
|
||||
### 移除
|
||||
|
||||
- **移除 `bl knowledge search --query-history`** —— 该参数此前并未实际生效;多轮场景请改用 `bl knowledge chat` 并通过 `--message` 传入对话历史。
|
||||
|
||||
### 内部
|
||||
|
||||
- 请求现在携带静态的 OpenAPI 来源标识请求头,用于后端渠道归因。
|
||||
- 新增知识库 E2E 测试套件,含冷启动、内容运营、切片调优、服务调优、数据面五条用户旅程场景。
|
||||
|
||||
## [1.15.1] - 2026-08-17
|
||||
|
||||
### 新增
|
||||
|
||||
+11
-5
@@ -16,11 +16,17 @@ Developer guide for `bailian-cli` — the official CLI for Aliyun Model Studio (
|
||||
```
|
||||
bailian-cli/
|
||||
├── packages/
|
||||
│ ├── cli/ # `bailian-cli` — CLI entry, commands, UI
|
||||
│ └── core/ # `bailian-cli-core` — auth, HTTP, types
|
||||
├── docs/agents/ # Scenario-based maintenance guides
|
||||
├── tools/ # Release automation & reference generation
|
||||
├── AGENTS.md # Contract for AI agents
|
||||
│ ├── core/ # `bailian-cli-core` — auth, config, HTTP client, errors, types
|
||||
│ ├── runtime/ # `bailian-cli-runtime` — CLI framework: parsing, help, middleware, output
|
||||
│ ├── commands/ # `bailian-cli-commands` — reusable command implementations
|
||||
│ ├── cli/ # `bailian-cli` — the full `bl` product entry
|
||||
│ ├── kscli/ # `knowledge-studio-cli` — `kscli` entry, reuses commands/
|
||||
│ ├── e2e/ # Shared e2e harness utilities (private)
|
||||
│ └── bailian-kb-dsh/ # `bailian-kb-dsh` — DeepSeek Harness plugin (independent version & release)
|
||||
├── skills/ # Agent skills installed by `bl skill init`
|
||||
├── docs/agents/ # Scenario-based maintenance guides
|
||||
├── tools/ # Release automation & reference generation
|
||||
├── AGENTS.md # Contract for AI agents
|
||||
└── README.md
|
||||
```
|
||||
|
||||
|
||||
+11
-5
@@ -16,11 +16,17 @@
|
||||
```
|
||||
bailian-cli/
|
||||
├── packages/
|
||||
│ ├── cli/ # `bailian-cli` —— CLI 入口、命令、UI
|
||||
│ └── core/ # `bailian-cli-core` —— 鉴权、HTTP、类型
|
||||
├── docs/agents/ # 场景化维护文档
|
||||
├── tools/ # 发版自动化与命令手册生成
|
||||
├── AGENTS.md # AI agent 维护契约
|
||||
│ ├── core/ # `bailian-cli-core` —— 鉴权、配置、HTTP client、错误、类型
|
||||
│ ├── runtime/ # `bailian-cli-runtime` —— CLI 运行时:参数解析、help、middleware、输出
|
||||
│ ├── commands/ # `bailian-cli-commands` —— 可复用命令实现库
|
||||
│ ├── cli/ # `bailian-cli` —— 完整 `bl` 产品入口
|
||||
│ ├── kscli/ # `knowledge-studio-cli` —— `kscli` 入口,复用 commands/
|
||||
│ ├── e2e/ # e2e 共享工具(不发布)
|
||||
│ └── bailian-kb-dsh/ # `bailian-kb-dsh` —— DeepSeek Harness 插件(版本与发布独立)
|
||||
├── skills/ # `bl skill init` 安装的 Agent skill
|
||||
├── docs/agents/ # 场景化维护文档
|
||||
├── tools/ # 发版自动化与命令手册生成
|
||||
├── AGENTS.md # AI agent 维护契约
|
||||
└── README.md
|
||||
```
|
||||
|
||||
|
||||
+48
-16
@@ -13,22 +13,25 @@ npm install -g bailian-cli
|
||||
```
|
||||
|
||||
4. 校验:`bl --version`。
|
||||
5. 按下方「安装 skills」安装。
|
||||
|
||||
安装 skills(CLI 内置,无需 Git / npx skills):
|
||||
### 安装 skills
|
||||
|
||||
**Supported(优先):** 用 CLI 内置命令一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`):
|
||||
|
||||
```bash
|
||||
bl skill init
|
||||
```
|
||||
|
||||
**Supported:** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
|
||||
|
||||
**Advanced / 按需子集:**
|
||||
**降级:** 若 `bl skill init` 失败,先确认本机同时满足两个条件:`node -v` / `npm -v` 成功,**且** `git --version` 成功。两个条件都成立时,再回退到:
|
||||
|
||||
```bash
|
||||
bl skill add --name bailian-protocol,bailian-gen
|
||||
npx skills@1.5.18 add modelstudioai/cli --all -g
|
||||
```
|
||||
|
||||
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
|
||||
> 必须使用 `skills@1.5.18`:这是最后兼容 Node ≥18 的版本(与 Bailian CLI 的 Node ≥18.17.0 一致);
|
||||
|
||||
安装成功后,按第 5 节向用户说明语言切换与能力 tip(若也装了 skills,一并简要说明)。
|
||||
|
||||
## 2. 备选:二进制安装(无需 Node)
|
||||
|
||||
@@ -67,8 +70,6 @@ bl --version
|
||||
which bl # Windows: where.exe bl
|
||||
```
|
||||
|
||||
若自动 skill 安装失败,再手动执行:`bl skill init`。
|
||||
|
||||
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。
|
||||
|
||||
---
|
||||
@@ -99,12 +100,43 @@ which bl # Windows: where.exe bl
|
||||
bl auth status --output json
|
||||
```
|
||||
|
||||
## 5. 常见问题
|
||||
## 5. 安装成功后向用户说明(必做)
|
||||
|
||||
| 现象 | 可能原因 | 建议动作 |
|
||||
| ------------------------ | ---------------------------- | ------------------------------------------------ |
|
||||
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin` 或 `npm prefix -g` |
|
||||
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
|
||||
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
|
||||
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
|
||||
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
|
||||
安装与校验完成后,**主动**用中文向用户说明下列两项;不要等用户追问。可执行 `bl`(已登录时)对照终端 tip,但以下列表为准,勿臆造。
|
||||
|
||||
### 5.1 界面语言
|
||||
|
||||
Bailian CLI 支持中英文界面(help / 欢迎语 / Quick Start 主次文案)。默认 `en-US`,可切换为 `zh-CN`:
|
||||
|
||||
```bash
|
||||
bl config set --key language --value zh-CN
|
||||
bl config set --key language --value en-US
|
||||
```
|
||||
|
||||
向用户说明:可随时用上述命令切换语言。
|
||||
|
||||
### 5.2 能力 tip(Quick Start)
|
||||
|
||||
向用户展示「试试使用Bailian CLI完成这些任务」及下列 5 条(中英各一行,与 `bl` 根帮助 tip 一致):
|
||||
|
||||
1. 帮我创建一个能够生成短片分镜和视频的 Managed Agent。
|
||||
Help me create a Managed Agent that can generate short-film storyboards and videos.
|
||||
2. 生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。
|
||||
Generate an image of a cat in a spacesuit standing on Mars, then turn it into a video.
|
||||
3. 查看最近的模型用量、免费额度和限流情况。
|
||||
Check my recent model usage, free quota, and rate limits.
|
||||
4. 推荐一个适合图片理解和智能客服的模型。
|
||||
Recommend a model suitable for image understanding and intelligent customer service.
|
||||
5. 介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。
|
||||
Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
| 现象 | 可能原因 | 建议动作 |
|
||||
| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin` 或 `npm prefix -g` |
|
||||
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
|
||||
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
|
||||
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node,或改用 npm 版 CLI |
|
||||
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
|
||||
| `bl skill init` 失败 | 网络 / registry 不可达等 | 有 Node 且有 Git 时降级 `npx skills@1.5.18 add modelstudioai/cli --all -g`;否则重试或告知用户 |
|
||||
|
||||
@@ -115,6 +115,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
|
||||
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
|
||||
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
|
||||
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
|
||||
| Model selection | "Recommend a model for image understanding and customer support." |
|
||||
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
|
||||
@@ -166,6 +167,9 @@ bl config list
|
||||
|
||||
# Switch config profile
|
||||
bl config use --name token-plan
|
||||
|
||||
# Switch the CLI interface to Chinese
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
Config file location: `~/.bailian/config.json`
|
||||
|
||||
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
@@ -165,6 +166,9 @@ bl config list
|
||||
|
||||
# 切换配置档
|
||||
bl config use --name token-plan
|
||||
|
||||
# 将 CLI 界面切换为中文
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
配置文件位置:`~/.bailian/config.json`
|
||||
|
||||
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
|
||||
|
||||
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
|
||||
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
|
||||
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
|
||||
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
|
||||
- `bl auth logout --console` 只清 `access_token`
|
||||
@@ -44,8 +44,12 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
- `resolveApiKey()` — `auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
|
||||
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
|
||||
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
|
||||
- `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
|
||||
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
|
||||
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
|
||||
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
|
||||
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile;未传
|
||||
`--config` 时继续写当前激活项,失败和 dry-run 不切换
|
||||
- `resolveConsole()` — `auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
|
||||
- `resolveOpenApi()` — `auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
|
||||
@@ -58,17 +62,19 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
`bl managed-agent *` 按调用链分两层:
|
||||
|
||||
- **离线命令** — `init`、`validate`、`state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
|
||||
- **联网命令** — `plan`、`apply`、`destroy`、`state import`、`skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁,authStage 都经 `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run` 传 `credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
|
||||
- **联网命令** — `plan`、`apply`、`destroy`、`state import`、`skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁,authStage 经 `resolveApiKey(sources)` 解析 Bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层再断言 Bailian key 非空。例外:`plan --no-refresh` / `plan --dry-run` 传 `credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
|
||||
|
||||
`bl managed-agent` 是 Bailian-only 产品入口:命令不暴露 `--provider`,`init` 只生成 `providers.bailian`,所有远端调用固定传 `provider: "bailian"`。`resolveAgentProjectConfig` 在创建 SDK runtime 前通过 `assertBailianOnlyProviders` 拒绝包含非 Bailian Provider 的手写配置;共享 `@openagentpack/sdk` 仍可保留多 Provider 能力。
|
||||
|
||||
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道五步:
|
||||
|
||||
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
|
||||
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
|
||||
1. `prepareProviderEnv()` — 调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`,含兼容别名)中仍为 undefined 的项占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
|
||||
2. `resolveProjectConfig` — 完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`,避免空 key 在 SDK zod 层提前报 "received null"
|
||||
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
|
||||
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
|
||||
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
|
||||
5. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider;随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint;离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
|
||||
|
||||
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
|
||||
禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help;根 help、鉴权域等代表性子进程冒烟 |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version`、`search --help` 等代表性子进程冒烟 |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
@@ -27,8 +28,9 @@
|
||||
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
|
||||
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
|
||||
@@ -36,10 +38,12 @@
|
||||
|
||||
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
|
||||
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
|
||||
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
|
||||
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
|
||||
|
||||
### 共享
|
||||
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
|
||||
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
@@ -47,9 +51,12 @@
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
test("<subcommand> --help 正常退出", async () => {
|
||||
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
@@ -64,20 +71,36 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
| 场景 | 条件 |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
|
||||
| 文本/搜索/配置 | `isDashScopeE2EReady()` |
|
||||
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
|
||||
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
|
||||
| OpenAPI AK/SK | `isOpenApiE2EReady()`(`.env` 中必须同时提供完整 AK/SK) |
|
||||
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
|
||||
| 知识库 chat/search live | `isChatE2EReady()` / `isSearchE2EReady()`(`knowledge chat/search`,需 `BAILIAN_WORKSPACE_ID` + agent ID) |
|
||||
| 记忆 live | `isMemoryE2EReady()`(另需 `BAILIAN_E2E_MEMORY_LIBRARY_ID`;记忆服务需账号单独开通) |
|
||||
|
||||
## 用例类型
|
||||
|
||||
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
|
||||
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
|
||||
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
|
||||
高风险命令额外要求:
|
||||
|
||||
- `--help` 展示 runtime 注入的 `--yes`
|
||||
- 无 `--yes` 返回 exit code 7 和 JSON `type: "requires_confirmation"`
|
||||
- `--dry-run` 无需 `--yes`,且必须证明在任何远端请求或本地写入之前返回
|
||||
- runtime 的离线 high-risk fixture 必须覆盖带 `--yes` 确实进入 `run()`,并断言 `yes` 不进入 command 自有 flags
|
||||
|
||||
## Journey 层(用户旅程全链路)
|
||||
|
||||
- **定位**:命令 E2E 验单命令契约;journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
|
||||
- **闭环断言**:fixture 埋独特标记词,以“标记词能否被召回”判定回路闭合;硬断言 fail,软断言 `recordSoft` 落报告人工复核
|
||||
- **日志产物**:`createJourneyReporter` 在 `test/output/<session>/` 落盘 `journey-report.md`、分步 stdout/stderr、`resources.json`(未清理资源警示)
|
||||
- **入口**:`pnpm run test:journey`;旅程清单与约定见 [journeys/README.md](../../packages/commands/tests/e2e/knowledge/journeys/README.md)
|
||||
- **新增命令时**:评估是否属于某条旅程的环节,是则纳入对应 journey 并更新 README 映射表
|
||||
|
||||
## 增删命令同步
|
||||
|
||||
- **commands export** + **topic 路由**(`topic-routes.ts` 或测试文件内 `ROUTES`)+ **产品 map**(`cli/commands.ts` / `kscli/commands.ts`)
|
||||
@@ -85,8 +108,8 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 安全与例外
|
||||
|
||||
- **禁止破坏真实用户配置**:`auth logout` 默认只用 `--dry-run`;需要验证实际落盘时,必须通过
|
||||
`BAILIAN_CONFIG_DIR` 指向隔离 fixture;`config set` 只用 `--dry-run`
|
||||
- **禁止破坏真实用户配置**:`auth logout` 和 `config set` 默认只用 `--dry-run`;只有验证持久化契约时,才允许通过
|
||||
`BAILIAN_CONFIG_DIR` 指向每个用例独占的临时目录实际落盘,并必须在 `finally` 中清理;禁止写入或复用真实 `~/.bailian`
|
||||
- **不加 dry-run**:`dryRun` 在 `resolveFileUrl` / `resolveCredential` / 上传**之后**的命令(如 `image edit`、`speech recognize` 带 `--url`)
|
||||
- **`--list-voices` 等旁路**:先于 `--text` 校验的 flag,缺参用例勿带该 flag
|
||||
- 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言
|
||||
@@ -96,7 +119,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
|
||||
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
|
||||
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
|
||||
|
||||
@@ -71,9 +71,12 @@ packages/commands/src/index.ts
|
||||
- `usageArgs`(不含 bin/path 前缀)
|
||||
- `exampleArgs`(不含 bin/path 前缀)
|
||||
- `validate`(跨 flag 校验)
|
||||
- 高风险命令必须声明 `risk: { level: "high", message: <双语文案> }`;`--yes` 由 runtime 注入,命令不得自行声明
|
||||
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
|
||||
- 声明 `risk` 的 `run(ctx)` 必须在任何远端请求或本地写入之前处理 `ctx.settings.dryRun` 并返回预览;runtime 只负责确认闸门,不替命令实现 dry-run
|
||||
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
|
||||
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
|
||||
- [ ] 用户可见 Help 文案在命令文件中就近提供 `en-US` / `zh-CN`:命令 `description`、flag `description`、`notes` 和包含自然语言的 `exampleArgs`;纯命令语法示例可保留为字符串,服务端错误不翻译
|
||||
- [ ] `packages/commands/src/index.ts`:新增或移除对应 export
|
||||
- [ ] 如果命令调用 Console Gateway,设置 `auth: "console"`;不要重复声明 console 凭证域 flags
|
||||
- [ ] 如果命令不需要网络或自己管理配置/登录,设置 `auth: "none"`;不要绕过 runtime auth stage
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
|
||||
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
|
||||
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
|
||||
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。
|
||||
|
||||
### B. 鉴权 / 全局选项
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
|
||||
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
|
||||
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`。
|
||||
- [ ] API Key capability ID 由 host 按实际叶子命令路径生成(例如 `agent credential` → `agent.credential`);Command Pack 不声明额外元数据,Profile allowlist 对 pack 命令同样 fail closed。
|
||||
- [ ] `ctx.credentials.apiKey()` 必须返回 `authStage` 已注入 `ctx.client` 的实际凭证,不能重新读取原 Profile 绕过 capability fallback。
|
||||
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore` 或 `configStore`。
|
||||
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager,避免 pack 安装或删除其他 pack。
|
||||
- [ ] 单包失败必须 fail-open:保留内置命令和其他合法 pack。
|
||||
@@ -40,6 +42,7 @@
|
||||
## 测试与文档
|
||||
|
||||
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
|
||||
- [ ] Command Pack 测试覆盖 capability 格式/鉴权域、adapter 保留字段,以及 raw API Key 委托使用 fallback 后的实际 Base URL。
|
||||
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
|
||||
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
|
||||
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
|
||||
|
||||
@@ -29,6 +29,7 @@ config 文件 ─┘
|
||||
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
|
||||
- `parseConfigFile()` 解析新字段
|
||||
- 如果是 enum 字段,加校验
|
||||
- 如果是数组字段,明确“缺失 / 空数组 / 非法值”的不同语义;安全策略字段的非法值必须 fail closed
|
||||
|
||||
### B. 加载逻辑
|
||||
|
||||
@@ -51,6 +52,7 @@ config 文件 ─┘
|
||||
- [ ] 配置展示 / 修改命令同步:
|
||||
- `packages/commands/src/commands/config/show.ts` 显示新字段
|
||||
- `packages/commands/src/commands/config/set.ts` 的 `VALID_KEYS` / `KEY_ALIASES` / description 允许 set
|
||||
- `packages/commands/src/commands/config/ui.ts` / `ui-html.ts` 能按原类型往返数组字段,不能把 `[]` 保存成字段缺失
|
||||
|
||||
### E. 文档
|
||||
|
||||
@@ -61,6 +63,7 @@ config 文件 ─┘
|
||||
- [ ] 单测覆盖优先级:flag > env > file
|
||||
- [ ] 校验失败抛错(非法值)
|
||||
- [ ] 默认值正确
|
||||
- [ ] 数组配置覆盖 CLI 逗号/JSON 输入、Config UI 往返、去重和显式空数组
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -82,3 +85,4 @@ cat ~/.bailian/config.json
|
||||
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
|
||||
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
|
||||
- ✗ `config show` 不显示新字段,用户改了无法回查
|
||||
- ✗ UI 用 `String([])` 把显式空数组渲染为空串,保存后意外关闭安全策略
|
||||
|
||||
@@ -18,15 +18,19 @@
|
||||
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
|
||||
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
|
||||
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
|
||||
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
|
||||
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
|
||||
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
|
||||
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
|
||||
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
|
||||
- 对命中内置 preset 的 Profile,API Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
|
||||
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
|
||||
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
|
||||
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
|
||||
|
||||
## 3. 保持读写命令交互一致
|
||||
|
||||
- `auth login`、`config set` 等写命令未传 `--config` 时修改当前激活项。
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
|
||||
创建和激活;`config set --config <name>` 可创建但不自动激活。
|
||||
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
|
||||
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
|
||||
@@ -55,6 +59,8 @@
|
||||
|
||||
- 旧配置无 `active_config` -> `default`。
|
||||
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
|
||||
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url` 和 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
|
||||
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`)按文件值生效。
|
||||
- 显式命名 `--config` 和 `--config default` 均覆盖激活项且不修改磁盘状态。
|
||||
- 激活不存在的 Profile 失败且不写盘。
|
||||
- 悬空 `active_config` 明确失败。
|
||||
@@ -63,6 +69,9 @@
|
||||
- 显式 `auth login --config <name>` 成功后激活该 Profile,失败或 dry-run 不创建、不切换;
|
||||
`--config default` 成功后切回 `default`。
|
||||
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
|
||||
- Console/OpenAPI/none 命令不参与 API Key capability fallback;fallback 后的 Client 特殊端点行为必须跟随最终解析的 `base_url`,不能根据原 `settings.configName` 推断端点类型。
|
||||
- Fallback 反馈只描述 CLI 能权威确认的本地行为:当前 Profile 不支持空格分隔的用户可见叶子命令,本次将从 `default` 读取 API Key 配置;不得声称整个 Profile 已切换,也不得展示 capability ID 或 `<undeclared>` 等内部值。存在 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式覆盖时必须跳过 fallback 且不输出反馈。
|
||||
- Fallback 反馈写 stderr,`--quiet` 抑制;text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象。dry-run 和后续鉴权失败仍保留反馈;JSON 模式下多个 diagnostics 以空行分隔,任何模式都不得输出凭证值。
|
||||
- `config list/show/use/ui`、`auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
|
||||
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
|
||||
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# dsh 插件维护(packages/bailian-kb-dsh)
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 改 `packages/bailian-kb-dsh` 的工具(`kb_search` / `kb_chat`)、服务缓存、settings / 凭据解析
|
||||
- 改 web 半(Settings 配置页 React 组件、CSS Modules)
|
||||
- 升级 `@deepseek-ai/dsh-*` peer 依赖
|
||||
- 改插件包名、bundle 声明或产物布局
|
||||
- 发布插件到 npm
|
||||
|
||||
## 这个包和其他 packages 不一样的地方
|
||||
|
||||
它是**下游宿主适配层**:依赖方向朝外(消费 `bl` CLI 与百炼 API,装进 DeepSeek Harness 运行),不是 `core → runtime → commands → 产品入口` 这条链上的一环。由此带来四条与 `packages/*` 通行约定的**故意偏离**:
|
||||
|
||||
| 项 | 本包 | 其他包 | 原因 |
|
||||
| -------- | ---------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 版本 | 独立 `0.1.x` | core/runtime/commands/cli/kscli 锁步 | 跟随 dsh 的 rc 节奏,与 `bl` 发版无关;不在 `tools/release/lib/packages.mjs` 白名单里 |
|
||||
| 构建 | `tsc` + `tsdown` | `vp pack` | 浏览器半需要 `__ModuleLoader__` banner/footer 与 lightningcss CSS Modules 内联,`vp pack` 产不出 |
|
||||
| 发布 | `publish.yml` 里 `package=bailian-kb-dsh` job,走 `publish-kb-dsh.mjs` | `publish.yml` 里 `publish-stable/channel.mjs` | 不在 `bailian-cli` 依赖闭包内,版本与构建都不同,不能与 `bl` 共用同一条 script |
|
||||
| tsconfig | 三个 | 一个 | 见下 |
|
||||
|
||||
## tsconfig 三件套(改动前先读)
|
||||
|
||||
| 文件 | 谁在用 | 作用 |
|
||||
| --------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `tsconfig.json` | oxlint / `vp check` 自动发现 | **纯类型检查**,覆盖 `src` + `tests` 两半:`noEmit` + `jsx: react-jsx` + DOM lib + `allowImportingTsExtensions` |
|
||||
| `tsconfig.build.json` | `build` script(`tsc -b`) | **产出** node 半到 `dist/`,`exclude: src/web` |
|
||||
| `tsconfig.web.json` | `build` / `typecheck` script | web 半的**隔离检查**:`types: []`,确保浏览器代码不误用 node 全局 |
|
||||
|
||||
- 不要把 `tsconfig.json` 改成产出配置:`allowImportingTsExtensions` 与 emit 互斥,一改 oxlint 就再也检查不了 `.tsx`(报 TS17004 `--jsx` not set)。
|
||||
- web 半的隔离检查挂在 `build` script 里,因为 CI 只跑 `build` / 根 `check` / 根 `test`,`typecheck` script 没有调用点。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 包身份(改包名时三处必须一起改)
|
||||
|
||||
- [ ] `package.json` 的 `name`
|
||||
- [ ] `cordis.patch.yml` 的 `insert[].name`(profile 层栈按这个名字解析插件)
|
||||
- [ ] `tsdown.config.ts` 的 `PLUGIN_ID`(进 `window.__ModuleLoader__.load({ id })` 与 `<style data-plugin>`)
|
||||
|
||||
漏任何一处都不会在构建期报错,只会在 dsh 里运行时崩。验证:`grep -rn "<新包名>" package.json cordis.patch.yml tsdown.config.ts` 三处齐全,且 `dist/web/client.js` 首行的 `id` 是新名。
|
||||
|
||||
### B. 产物布局
|
||||
|
||||
- [ ] 产物落 `dist/`(node 半)与 `dist/web/client.js`(浏览器半);根 `.gitignore` 忽略 `dist` 与 `*.tsbuildinfo`,**不要**改回 `lib/`(那会把产物提交进库)
|
||||
- [ ] `package.json` 的 `main` / `types` / `exports["."]` / `exports["./client"]` / `files` 与实际产物一致
|
||||
- [ ] tsdown 的 `clean` 保持 `false`:默认 clean 会清掉 `tsc` 刚产出的 node 半
|
||||
|
||||
### C. web 半的模块边界
|
||||
|
||||
- [ ] 只 import tsdown `CLIENT_EXTERNALS` 名单里的 `@deepseek-ai/*`(宿主 frozen module table 只能应答这些)——构建期由 `dsh-client-bundle-purity` 插件把关
|
||||
- [ ] 不 import `node:*` 与本仓 CLI 包(`bailian-cli-core` 等)——根 `vite.config.ts` 的 `no-restricted-imports` override 在 lint 期把关
|
||||
- [ ] 跨插件协作走 cordis service,不做 value import(type-only import 会被擦除,不受限制)
|
||||
|
||||
### D. skill 资产
|
||||
|
||||
- [ ] `skills/bailian-kb/` 留在**包内**,不要挪到仓库顶层 `skills/`:`.github/workflows/publish-skills.yml` 把 `skills/**` 全量对账到 OSS registry,`bl skill init` 会装给所有 `bl` 用户,而这个 skill 讲的 `kb_search` / `kb_chat` 原生工具只在 dsh 里存在
|
||||
- [ ] skill 只有一个手写 `SKILL.md`,**不带 `reference/`**:它不是 CLI,没有义务维护一份 `bl` 参数手册。`bl` 命令的 flag 详情交给 `bl <命令> --help`(权威的 `bl` reference 由 `tools/generate-reference.ts` 写到 `skills/bailian-cli/reference/`,与本包无关)。SKILL.md 里写到的 `bl` 命令/flag 修改时手动核对 `packages/commands/src/commands/`,不要锚版本号
|
||||
|
||||
### E. 文档(README 是 npm 主页,双语)
|
||||
|
||||
- [ ] 用户可见行为变了(工具参数、配置字段、环境变量、设置页、安装/卸载命令)→ `README.md` 与 `README.zh.md` **一起改**;两份互为镜像,头部互链
|
||||
- [ ] README 只写用户要知道的事;"为什么这么设计"(上下文注入载体、缓存布局、刷新触发点、桥接路由)写进 [../kb-dsh/runtime-behavior.md](../kb-dsh/runtime-behavior.md),不要回流 README
|
||||
- [ ] 包根保留 `LICENSE`(npm 无条件打包),与 `package.json` 的 `license` 一致
|
||||
|
||||
### F. 依赖与测试约定
|
||||
|
||||
- [ ] `@deepseek-ai/dsh-*` 同时列在 `peerDependencies`(运行时由 dsh 安装闭包提供)和 `devDependencies`(本地类型检查)——升级时两处同步
|
||||
- [ ] 测试从 `vite-plus/test` 导入(仓库统一约定),不要用 `vitest`
|
||||
- [ ] 忽略的 catch 绑定与 mock 签名参数用 `_` 前缀(根 `vite.config.ts` 已为本包放开 `no-unused-vars` 的对应 pattern)
|
||||
|
||||
### G. 改完跑
|
||||
|
||||
```sh
|
||||
pnpm --filter bailian-kb-dsh run build # tsc 出 dist/ + web 半隔离检查 + tsdown 出 client.js
|
||||
pnpm run check # 根 lint + 格式 + 类型
|
||||
npx vp test packages/bailian-kb-dsh
|
||||
```
|
||||
|
||||
手动集成(改了 bundle 声明 / web 半 / 工具 schema 时必做):
|
||||
|
||||
```sh
|
||||
dsh plugin --profile dev add <本仓库>/packages/bailian-kb-dsh
|
||||
dsh --profile dev --dump-config # 应能看到 tool-bailian-kb row
|
||||
```
|
||||
|
||||
## 发布
|
||||
|
||||
入口与 `bl` 共用:Actions → **Publish** → `package=bailian-kb-dsh` + `mode=stable|channel`。共享的只有 workflow 入口与 checkout/pnpm/node/gitleaks/install 几步 setup;它走自己的 `tools/release/publish-kb-dsh.mjs`,**不**复用 `publish-stable.mjs` / `publish-channel.mjs`。
|
||||
|
||||
| | stable | channel |
|
||||
| --------- | ------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 版本 | `package.json` 当前值(先手动 bump 并提交) | 临时 `0.0.0-beta-<sha>-<stamp>`,`finally` 还原,不提交 |
|
||||
| npm tag | `latest` | 你传的 `channel`(dist-tag) |
|
||||
| preflight | 工作区干净 + 必须在 `main` | 无 |
|
||||
| git tag | `bailian-kb-dsh-v<version>`(与 `bl` 的 `v<version>` 错开命名空间) | 不打 tag |
|
||||
| 审批 | `environment: production`(Required Reviewers) | 无 |
|
||||
| 产物 | npm only | npm only |
|
||||
|
||||
两种模式都跑:`build`(tsc + web 隔离检查 + tsdown)→ 幂等性查重 → `pnpm pack` → `publint` + `gitleaks` → `pnpm publish --provenance`。
|
||||
|
||||
本地验证(不真发,需本地装 gitleaks):
|
||||
|
||||
```sh
|
||||
node tools/release/publish-kb-dsh.mjs --dry-run
|
||||
node tools/release/publish-kb-dsh.mjs --dry-run --channel dsh-beta
|
||||
```
|
||||
|
||||
### 首发前的 npm 侧前置(仓外操作,一次性)
|
||||
|
||||
1. 占住包名 `bailian-kb-dsh`(OIDC Trusted Publishing 无法给不存在的包首发)
|
||||
2. 在 npm 包设置里绑 Trusted Publisher:仓库 `modelstudioai/cli`、workflow `publish.yml`;stable 走 environment `production`,channel 无 environment
|
||||
|
||||
未完成这两步前触发 workflow 会在 `pnpm publish --provenance` 这一步失败(前面的 build / scan 仍会正常跑完)。
|
||||
|
||||
### 已知待办
|
||||
|
||||
- `publint` 会报一条 warning:`exports["./client"]` 是 CJS 但包为 `type: module`,建议改 `.cjs` 扩展名。这是迁移前就存在的状态,warning 不阻断发布;真要改得同时动宿主按 `exports["./client"]` 解析 `/plugins/<id>/client.js` 的 URL 契约,需 dsh 侧一起验证,不要单方面改
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 设计与实现计划归档:[docs/kb-dsh/](../kb-dsh/)
|
||||
- 运行时行为与设计取舍(内部):[docs/kb-dsh/runtime-behavior.md](../kb-dsh/runtime-behavior.md)
|
||||
- 用户面说明:[packages/bailian-kb-dsh/README.md](../../packages/bailian-kb-dsh/README.md) / [README.zh.md](../../packages/bailian-kb-dsh/README.zh.md)
|
||||
- skill 文案与路由约定:[skill-change.md](skill-change.md)
|
||||
@@ -46,6 +46,7 @@ runtime/error-handler.ts:
|
||||
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
|
||||
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
|
||||
- Node fs errno → 翻成 GENERAL + errno hint
|
||||
- `--output json` → stderr 输出两空格缩进的多行 error JSON;前面已有结构化 warning 时,两个 diagnostics 以空行分隔
|
||||
- 其它 Error → 默认走 cause 链
|
||||
↓
|
||||
process.exit(err.exitCode)
|
||||
|
||||
@@ -24,6 +24,10 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口
|
||||
|
||||
`tools/release/lib/binary-release.mjs` 等是实现,一般不要单独当发版入口(调试可用)。
|
||||
|
||||
### bailian-kb-dsh(独立版本、npm-only)
|
||||
|
||||
同一个 Publish 入口,`package=bailian-kb-dsh`。它走单独的 `tools/release/publish-kb-dsh.mjs`,不复用 `publish-stable.mjs` / `publish-channel.mjs`(版本独立、无 binary、无 OSS CDN)。详见 [dsh-plugin.md](dsh-plugin.md#发布)。
|
||||
|
||||
两种模式:
|
||||
|
||||
| 模式 | 用途 | 触发方式 |
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
bailian-protocol ← 共享协议(consent / 鉴权 / 版本 / 错误上报)
|
||||
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
|
||||
│
|
||||
┌───────┴────────┬────────────────┬──────────────────┐
|
||||
bailian-gen bailian-finetune bailian-managed-agent
|
||||
(领域路由表) (领域工作流) (IaC 安全闸)
|
||||
│ │ │
|
||||
└────────────────┼──────────────────┘
|
||||
┌───────┴────────┬────────────────┬──────────────────┬───────────────────┐
|
||||
bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
(领域路由表) (领域工作流) (IaC 安全闸) (搜索路由+兜底)
|
||||
│ │ │ │
|
||||
└────────────────┼──────────────────┴─────────────────────┘
|
||||
▼ 软 hand-off(按 skill 名)
|
||||
bailian-cli(hub)
|
||||
hub 路由表:本职命令 + 领域 hand-off 行
|
||||
@@ -40,6 +40,8 @@ bailian-gen bailian-finetune bailian-managed-agent
|
||||
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
|
||||
- [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
|
||||
- [ ] **高风险确认**:统一由 `bailian-protocol` 定义;reference / leaf help 以 `risk: high` 明示风险,业务 skill 不得引导 Agent 自动补 `--yes`。遇到 exit code 7 / `requires_confirmation` 时停止执行并请求确认;目标或范围变化后重新确认
|
||||
- [ ] **正常控制流**:`requires_confirmation` 不是 CLI bug,`assets/issue-reporting.md` 必须将 exit code 7 保持在 EXCLUDE 范围
|
||||
- [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read,未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提
|
||||
- [ ] **Hub vs 领域**:`bailian-cli` 的「When to use which command」只列 hub 拥有的意图;媒体 / 精调 / managed-agent 各留 hand-off 行,**不抄**领域默认模型与子命令明细
|
||||
- [ ] **渐进披露**:SKILL 写意图路由与领域硬规则;flags / usage / examples 以 `reference/` 或 `bl <command> --help` 为准,表后保留「勿猜 flag」指向句
|
||||
@@ -55,6 +57,7 @@ bailian-gen bailian-finetune bailian-managed-agent
|
||||
|
||||
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
|
||||
- [ ] 跑 `pnpm run sync:skill-assets`(或 commit 走 pre-commit),提交生成的 `reference/` 与 version 同步结果
|
||||
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
|
||||
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
│ ├─ ~/.bailian/telemetry.jsonl
|
||||
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
|
||||
│
|
||||
└─ authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
└─ confirmationStage
|
||||
└─ versionCheckStage → authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
```
|
||||
|
||||
### 1. 三套鉴权与埋点标识
|
||||
@@ -29,7 +30,7 @@
|
||||
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
|
||||
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 无 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
|
||||
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
|
||||
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 仅对阿里云 host 注入
|
||||
- DashScope 上传策略 `getPolicy` 只有 `x-dashscope-source-config`,没有显式 CLI `User-Agent`
|
||||
- Console Gateway 用户数据由后端直接区分,不依赖 `x-dashscope-source-config`;AEM 的 `authMethod=console` 是独立的命令侧统计
|
||||
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product`、`action`、`api` 是鉴权或路由字段,不计为埋点标识
|
||||
|
||||
### 2. 后端渠道参数
|
||||
@@ -77,7 +79,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
### 3. 全命令 AEM 客户端埋点
|
||||
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹 `authStage` 与命令执行,因此成功、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹确认闸门、`authStage` 与命令执行,因此成功、确认未通过、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。确认闸门仍位于版本检查、鉴权和业务执行之前,不会因为埋点而放行高风险操作。
|
||||
|
||||
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`:
|
||||
|
||||
@@ -92,7 +94,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
- `command`、`timestamp`、`durationMs`、`success`
|
||||
- `cliVersion`、`nodeVersion`、`os`
|
||||
- `authMethod`
|
||||
- 失败时的 `errorMessage`、`httpStatus`、`requestId`
|
||||
- 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId`
|
||||
- 安全 allowlist 过滤后的 `params`
|
||||
|
||||
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
|
||||
@@ -108,16 +110,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
|
||||
|
||||
AEM 映射:
|
||||
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段 |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
| AEM 字段 | 内容 |
|
||||
| ---------- | ------------------------------------------------------------------ |
|
||||
| event name | 命令路径 |
|
||||
| `et` | `EXP` |
|
||||
| `ext` | 除 `command`、`params` 外的结构化事件字段,包含失败时的 `exitCode` |
|
||||
| `c1` | allowlist 参数 |
|
||||
| `c2` | `success` / `failure` |
|
||||
| `c3` | HTTP status |
|
||||
| `c4` | 错误文案,最多 500 字符 |
|
||||
| `c5` | request ID |
|
||||
|
||||
远端发送是 best-effort,不得阻塞命令或改变退出码。正常退出最多等待 1 秒,SIGINT 最多等待 500 ms。
|
||||
|
||||
@@ -144,6 +146,7 @@ AEM 映射:
|
||||
- [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射
|
||||
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
|
||||
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
|
||||
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5`
|
||||
- [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口
|
||||
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
|
||||
|
||||
|
||||
@@ -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:<org>/<repo>`)。
|
||||
|
||||
**用户安装**:
|
||||
|
||||
```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>.<endpointHost>`。必填。 */
|
||||
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.<key>` 服务,按 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 过滤,非页码 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
# bailian-kb-dsh 运行时行为(内部说明)
|
||||
|
||||
面向维护者:记录 `packages/bailian-kb-dsh` 里那些**为什么这么做**的选择。用户面文档在 [packages/bailian-kb-dsh/README.md](../../packages/bailian-kb-dsh/README.md)(中文版 `README.zh.md`),改动清单在 [docs/agents/dsh-plugin.md](../agents/dsh-plugin.md)。
|
||||
|
||||
## Bundle 声明与配置分层
|
||||
|
||||
`package.json` 的 `dsh.bundle.patch` 指向 `cordis.patch.yml`,向 profile 插入插件行:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: tool-bailian-kb
|
||||
name: "bailian-kb-dsh"
|
||||
config:
|
||||
workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID
|
||||
```
|
||||
|
||||
`workspaceId` 只是解析链的一层,不是唯一来源:Config 同时注册为 `bailian-kb` settings namespace,patch entry 作 base 层,设置页 / 设置文档的用户层叠在其上;都未设置时 per-call 回退到 `BAILIAN_WORKSPACE_ID` credential。同样的回退覆盖 `defaultRetrieveAgentId`(`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`)、`defaultChatAgentId`(`BAILIAN_DEFAULT_CHAT_AGENT_ID`)与 API key(`DASHSCOPE_API_KEY`,无 settings 面)。
|
||||
|
||||
settings 注册是**手写**的,没有用 `installSettingsSection`:需要两个它不带的东西 —— `expose` opt-in(设置页从浏览器改这个 section)和凭据迁移要写入的 scope handle。所有值每次调用经 source thunk 读取,因此设置改动无需重启或重注册工具。
|
||||
|
||||
### 四个值的解析链
|
||||
|
||||
| 值 | 1️⃣ settings 用户层(设置页可编辑、回显) | 2️⃣ entry config(patch 或用户覆盖,作 base 层) | 3️⃣ credential(`~/.dsh/.credentials.yaml` / env) | 4️⃣ 都缺失时 |
|
||||
| ----------------------------------- | ---------------------------------------- | ----------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | —(无 settings 面) | —(无 config 面) | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_WORKSPACE_ID` | ✅ `workspaceId` | ✅ `workspaceId` | ✅ | 工具调用报错并引导配置 |
|
||||
| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | 该 scene 只有一个已部署服务时取它;否则省略 `agent_id` 的调用报错 |
|
||||
| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | 同上 |
|
||||
|
||||
"唯一服务即默认"这一层是 2C 部署的零配置路径:只有一个服务时没有可选项,逼用户在设置里点一次名字买不到任何东西。注意 `agent_id` 在两个工具的 schema 中**恒必填**,模型路径不会触发默认服务回退;回退保留是为程序化调用与 credential 热切换。
|
||||
|
||||
行为参数(`endpointHost` / `agentVersion` / `chatTimeoutMs`)只在 config/settings 层(设置文档可改,实时生效)。
|
||||
|
||||
解析后的 workspaceId 还经 `shellEnv` 注册导出为 `BAILIAN_WORKSPACE_ID`,否则 settings 文档里的值对 bash 子进程(`bl knowledge …`)不可见。
|
||||
|
||||
### 用户覆盖
|
||||
|
||||
用户 patch 层在本 bundle 之上,按 id 覆盖时**替换整个 config(无 deep-merge)**:
|
||||
|
||||
```yaml
|
||||
# ~/.dsh/cordis.patch.yml 或 profile 的 cordis.patch.yml
|
||||
- id: tool-bailian-kb
|
||||
config:
|
||||
defaultRetrieveAgentId: aid-search-service
|
||||
defaultChatAgentId: aid-chat-service
|
||||
chatTimeoutMs: 600000
|
||||
```
|
||||
|
||||
禁用:`- id: tool-bailian-kb` + `disabled: true`。
|
||||
|
||||
## Web UI 配置页
|
||||
|
||||
装进 profile 后,Settings 左侧导航出现"百炼知识库"页(`settings.section` 槽位):
|
||||
|
||||
- **DashScope API Key** — write-only,`type=password` 遮罩输入草稿,仅显示 configured / 来自环境变量 徽标;写 `~/.dsh/.credentials.yaml`
|
||||
- **Bailian Workspace ID / 默认检索服务 ID / 默认对话服务 ID** — 回显:读写 `bailian-kb` settings 用户层,预填当前解析值;清空保存 = 移除用户层,回退 entry config → credential。两个服务 ID 可从服务缓存里选
|
||||
- **自动获取** — 按钮调 Host 桥接路由 `/bailian-kb/autofill`:Host 在宿主机拉起浏览器登录百炼控制台(不经 `bl` 命令),回调落到本机 loopback 端口后直接把 API 密钥写入凭据存储、工作空间 ID 写入 settings,明文 key 不过浏览器;面板轮询到完成后自动刷新(无需再次点击)。登录 URL 始终请求签发新 key,因此每次都与当前账号配对,切换账号直接点一次即可
|
||||
- **检索服务缓存** — 经 `/bailian-kb/services` 读缓存诊断(上次拉取时间、各 scene 条数、是否截断)并提供强制刷新按钮:面板存在的意义就是"开发者认为缓存不对"的那一刻
|
||||
|
||||
桥接路由(`/bailian-kb/settings`、`/bailian-kb/services`、`/bailian-kb/autofill`)而不是 settings wire:wire 需要 apiproxy 白名单,而 composition 不给树外 namespace 授权。GET 和 POST 共用一次 exact-route 注册 —— webServer map 按 (kind, path) 建键,同路径注册两次会抛 "duplicate route"。
|
||||
|
||||
首次接入 seed:启动时若 API key / workspaceId 从未被设置过(settings、credential、env 均无值),自动从 `~/.bailian/config.json` 采纳一次;`seededFields` 字段(settings 文档内,面板不可编辑)记账已消费 / 已由用户管理的字段,用户主动清空的值永不会被重新填回。
|
||||
|
||||
降级:远程浏览器(非 loopback,settings RPC 不可达)或未组合 settings 服务时,ID 字段退回旧的 write-only credential 控件,页面顶部显示提示。
|
||||
|
||||
## 检索服务缓存与上下文注入
|
||||
|
||||
模型要判断"该不该检索",靠的是看到本 workspace 部署了哪些检索服务。插件内部经 `/api/v1/indices/rag/app/list` 拉取该清单并缓存,**不对模型暴露服务发现工具**(`kb_service_list` 不会回归:它会把"先 list 再 search"的额外一轮重新引入);管理面仍用 bl。
|
||||
|
||||
补查型工具("只按关键词查、不列全部"的 `service_find`)也评估过,同样不做。理由不是成本而是**收益已被占掉**:唯一能支撑它的论据是"兜底走 bash + bl 不一定可用",而管理面本来就以 bl 为前提;catalog 那条通道是零轮次、无条件注入、且带截断告知与默认收敛策略,再开第三个入口只会跟它抢事实源。真正要补的是兜底路径本身——见下面两小节。
|
||||
|
||||
### 载体:上下文消息,不是工具描述
|
||||
|
||||
两个工具的 **description 保持静态**(不含任何服务 id)。清单经 `agent/pre-step` 注入为一条带 source 的 `UserMessage`(`{ kind: 'plugin', plugin: 'tool-bailian-kb/services', form: 'catalog' }`),而不是烘进 tool description。两个原因:
|
||||
|
||||
1. **插件加载是每进程一次,不是每会话一次。** 描述在 `apply()` 时定型,长驻宿主里 TTL 只会被评估一次,用户在控制台新建的服务要等重启才能被感知;
|
||||
2. **重注册工具会废掉 prompt 前缀缓存**(从第一个变化的 schema token 起)。走上下文消息则让 schema 永久稳定。
|
||||
|
||||
**变化抑制是正确性要求,不是优化**:`pre-step` 每个"步"(= 一次模型请求)触发一次,一轮里调 5 次工具就触发 6 次。只有清单内容变化时才重发,且判定叠加**可见性**(`session.surface.nodes`)—— 压缩把清单消息裁掉后会自动重新注入,否则模型会静默失去清单。
|
||||
|
||||
### 清单内容策略
|
||||
|
||||
| 情形 | 注入内容 |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------ |
|
||||
| 配了默认服务 | 只列该服务 + "另有 N 个" 提示 |
|
||||
| 未配默认,deployed ≤ 10 | 全量 `agent_id` + 名称 |
|
||||
| 未配默认,deployed > 10 | 按 `modify_time` 倒序取 10 条,**显式标明截断**与总数 |
|
||||
| 缓存里 0 个服务 | 注入 `buildNoServiceNotice()`:禁止编 id,并给出建服务/部署服务的路径(控制台或 bl) |
|
||||
| 无缓存文档(含拉取失败) | 不注入(工具仍可用) |
|
||||
|
||||
英文框架 + 服务名原样保留;空 scene 整节省略;截断必须告知(静默截断会让模型把清单当全集,进而断言"没有对应知识库")。
|
||||
|
||||
最后两行的差别是**能不能下断言**:缓存里有文档但 0 条,是权威的"这个 workspace 没有可调服务",说出来比沉默好——`agent_id` 恒必填,沉默只会让模型编一个 id(换来服务端裸报错)或静默放弃检索,两者在用户看来都像插件坏了。没有文档则意味着首拉还没落地或一直失败,此时任何断言都是猜,交给后台刷新在后续 step 自愈。
|
||||
|
||||
注意这与"空 scene 整节省略"不冲突:空的**节**是噪音(另一节仍在给 id),空的**清单**是模型手里一个 id 都没有。
|
||||
|
||||
### bl 引导只出现在动态载体上
|
||||
|
||||
工具描述是静态的,每次请求都付;而"怎么进一步查"取决于当下部署了什么。所以 `agent_id` 的参数描述**只指向上下文清单**,不写 `bl` 命令;`bl` 出现在两处动态文本里,且**每次出现都带安装方式**(`npm install -g bailian-cli`):
|
||||
|
||||
- catalog 中真正推荐了命令的分支(截断、收敛到默认服务);
|
||||
- 4xx 之后追加的服务清单(`buildRefreshedSceneList`)。
|
||||
|
||||
带安装行是必需的:插件走 API 直连、从不 shell out,所以一个凭据齐全的部署完全可能没装 `bl`。而安装引导原本只写在 `skills/bailian-kb/SKILL.md` 的前置检查里,那是二阶决策——要模型先决定加载 skill 才读到,恰好漏掉走工具描述兜底的那个模型。
|
||||
|
||||
### 缓存与刷新
|
||||
|
||||
落点:`${DSH_HOME:-~/.dsh}/cache/bailian-kb/services-<workspaceId>.json`(临时文件 + `rename()` 原子发布,目录 `0o700`)。按 workspace 分文件是必需的:api key 只能访问自己的 workspace,而"自动获取"按钮就是为了切账号。
|
||||
|
||||
存:`agent_id` / `agent_name` / `scene` / `status` / `modify_time`,预留 `description`(待列表接口返回)。**不存 `pipeline_list`** —— 它不稳定携带 `pipeline_name`,做不了知识库标签。
|
||||
|
||||
| 刷新触发点 | 模型何时看见 |
|
||||
| ---------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| pre-step 间隔调度(超 TTL 30 分钟,后台异步,**不阻塞**) | 下一步 |
|
||||
| 控制台登录成功(`/bailian-kb/autofill` 回调) | 下一步 |
|
||||
| agent 跑了 `bl knowledge service create/deploy/delete/copy`(`tools/result` 观察) | 下一步 |
|
||||
| 面板强制刷新(`POST /bailian-kb/services`) | 下一步 |
|
||||
| 调用撞 4xx(agent_id 已失效) | **本步**,刷新后的列表追加进错误消息 |
|
||||
| workspaceId / apiKey 变更 | 下一步 |
|
||||
|
||||
`tools/result` 的匹配是在序列化参数里找命令串,而不是认某个具体工具名:agent 可能用 bash、终端工具或 run_code 跑 `bl`。宽匹配是故意的 —— 误判只多花一次 list 请求,漏判则退回 TTL。
|
||||
|
||||
刷新失败只 warn,保留旧文档;并发刷新共享一个请求(pre-step 每步都会检查)。pre-step 监听器**永不抛异常** —— 抛出会使用户当前这一步失败。未组合 `agents` 的 headless 装配只是没有清单,工具照常可用。
|
||||
|
||||
## 错误语义
|
||||
|
||||
- HTTP 错误:4xx 时刷新服务缓存并把当前可用服务追加进错误消息(这两个接口上 `agent_id` 是唯一的调用方标识符,所以 4xx 大多是 id 已失效);刷新后该 scene **一个服务都没有**时也照样追加说明(明确"别换 id 重试"+ 建服务路径),而不是放裸错误过去——裸的 `invalid agent_id` 读起来就是"再试一个";5xx 与刷新本身失败则原错误透传;
|
||||
- 凭证缺失:指向 `~/.dsh/.env` / `.credentials.yaml` 配置方式与控制台取 key 页面;
|
||||
- chat 超时:说明服务端多轮检索特性,建议重试或改用 `kb_search`;
|
||||
- 服务端错误体截断至 500 字符进入错误信息(优先 `code: message`)。
|
||||
|
||||
## 管理面 skill
|
||||
|
||||
`skills/bailian-kb/SKILL.md` 随包分发,插件通过 `ctx.inject(['skills'])` 在 skills 服务可用时以 `source: 'bundled'` 运行时注册;无 skills 服务的组合(headless 最小装配)不受影响。文件的 YAML frontmatter 是 name / description 的**单一事实源**,注册时会被剥离(`SkillDefinition.content` 契约上是已去元数据的正文,而 runtime 注册路径不做任何解析)。
|
||||
|
||||
内容:bl CLI 安装 / 鉴权 / workspace 解析、建库 → 上传 → 部署工作流、服务清单的行为语义。**skill 不承担"该不该检索"的引导**(那是工具描述与上下文清单的事:skill 正文要模型先决定加载才能读到,是二阶决策);它反过来承担一件工具做不到的事:**引导 agent 在 `service create` 时把服务名写清楚**。无 desc 时服务名是唯一语义来源,管理面的动作直接决定检索面的效果。
|
||||
|
||||
## 已知限制的成因
|
||||
|
||||
- kb_chat 执行期无进展显示(缓冲式消费 SSE)。
|
||||
- `top_k` 是客户端截断:请求体不含该参数,服务端返回条数由检索服务配置决定,截断只影响进入模型上下文的量。
|
||||
- **服务画像的质量上限取决于服务名**:`service list` 接口当前不返回描述字段,所以模型只能靠 `agent_name` 判断一个服务能查什么。名字模糊的部署引导能力接近于零。列表接口补齐描述字段后只需改三处(`api-types` 补字段名 → `services.ts` 解析 → `buildServiceCatalog` 追加并截断到 200 字符),缓存已预留 `description` 键,无需迁移。
|
||||
- 拉取每个 scene 最多 2 页,超出时标 `truncated` 并在清单里告知。
|
||||
@@ -0,0 +1,248 @@
|
||||
# Chunk 管理命令手册
|
||||
|
||||
Chunk 是知识库中最小的检索单元。文档导入后自动切分为 chunk,也可以手动添加。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否² | 所属文档 ID;表格/图片知识库必填,文档型可选 |
|
||||
| `--content <text>` | string | 否¹ | Chunk 正文,最多 6000 字符(文档型);与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取正文(`.md`/`.txt` 等);与 `--content` 互斥 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,最多 50 字符(文档型) |
|
||||
| `--image-url <url>` | array | 否 | Chunk 图片 URL(可重复,最多 10 个;文档型) |
|
||||
| `--field <key=value>` | array | 否¹ | 任意字段键值对(可重复),用于表格/图片知识库,键为 Excel 列名;与 content/title/image 互斥 |
|
||||
|
||||
> ¹ `--content`/`--content-file`/`--title`/`--image-url` 与 `--field` 互斥,必须提供其一。
|
||||
> ² 表格/图片知识库必须提供 `--doc-id`。文档型知识库可选。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--field` 与 `--content`/`--content-file`/`--title`/`--image-url` 互斥
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--content` 最多 6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
- `--image-url` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
chunk created (pipeline: idx-xxx)
|
||||
List chunks to find the new chunk id.
|
||||
```
|
||||
|
||||
quiet 模式:无输出(成功退出码 0)。
|
||||
|
||||
json 模式:返回 API 原始响应(不含 chunk ID)。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 支持文档/表格/图片知识库;音视频知识库不支持。
|
||||
- API 响应不含 chunk ID,需用 `chunk list` 查找新 chunk。
|
||||
- API 幂等但限流 10 次/秒,批量脚本需自行节流。
|
||||
- 表格/图片知识库用 `--field`,键为 Excel 列名,值为字符串。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 添加文本 chunk
|
||||
bl knowledge chunk add --index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx
|
||||
|
||||
# 添加表格行(字段方式)
|
||||
bl knowledge chunk add --index-id idx-xxx --field 列A=v1 --field 列B=v2
|
||||
|
||||
# 从文件读取内容
|
||||
bl knowledge chunk add --index-id idx-xxx --content-file ./chunk.md --doc-id doc-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否 | 只显示属于此文档的 chunk |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
[chunk] chunk-xxx (doc: intro.md, doc_id: file-xxx) status: COMPLETED
|
||||
chunk content preview (truncated at 200 chars)…
|
||||
total: 1
|
||||
```
|
||||
|
||||
> 如果 chunk 被排除检索,行尾会显示 `[excluded from retrieval]`。
|
||||
|
||||
quiet 模式:每行一个 `metadata._id`(chunk ID),用于管道传给 update/delete。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 含完整 chunk 数据。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用 `metadata._id` 作为 chunk ID,`metadata.doc_id` 作为文档 ID,在 chunk update/delete 中使用。
|
||||
- 页大小默认 20,最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有 chunk
|
||||
bl knowledge chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看某文档的 chunk
|
||||
bl knowledge chunk list --index-id idx-xxx --doc-id file-xxx --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | string | 是 | Chunk ID(`metadata._id`,来自 chunk list 输出) |
|
||||
| `--doc-id <id>` | string | 是 | 所属文档 ID(`metadata.doc_id`,来自 chunk list 输出) |
|
||||
| `--content <text>` | string | 否¹ | 新内容,10-6000 字符;与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取新内容 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,0-50 字符(空字符串清除标题;不传则不变) |
|
||||
| `--exclude` | switch | 否² | 将此 chunk 排除出检索 |
|
||||
| `--include` | switch | 否² | 将此 chunk 恢复检索(默认行为) |
|
||||
|
||||
> ¹ `--content` 与 `--content-file` 互斥。
|
||||
> ² `--exclude` 与 `--include` 互斥。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--exclude` 与 `--include` 互斥
|
||||
- 至少提供一个更新项(`--content`/`--content-file`/`--title`/`--exclude`/`--include`)
|
||||
- `--content` 长度 10-6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: chunk-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 内容必须 10-6000 字符,且不超过知识库的 max chunk size。
|
||||
- `--content-file` 期望 UTF-8 纯文本文件,不解析 `.docx`/`.pdf` 等文档格式。
|
||||
- 仅切换 `--exclude`/`--include` 而不提供新内容时,CLI 自动读回当前内容并重新提交(API 要求 content 字段必填,CLI 隐藏了此限制)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 修改内容
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text" --workspace-id ws-xxx
|
||||
|
||||
# 排除 chunk 不参与检索
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude
|
||||
|
||||
# 恢复检索
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --include
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 chunk(s) in 1 batch(es)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 `{ deleted_count, batches }`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端每次最多接受 10 个 chunk ID,CLI 自动分批。
|
||||
- 如果某批失败,操作停止,已删除的批次会在错误 hint 中列出。
|
||||
- Chunk 被永久移除,不可恢复。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除多个 chunk
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,268 @@
|
||||
# 数据中心集合与分类命令手册
|
||||
|
||||
集合(collection)是数据中心的顶层容器,对应服务端的 connector。分类(category)用于组织集合内的文件,支持多级嵌套。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 集合名称(1-20 字符) |
|
||||
| `--description <text>` | string | 是 | 集合描述 |
|
||||
| `--store-type <type>` | string | 否 | 存储类型:`platform`(托管,默认)或 `custom`(自有 OSS bucket) |
|
||||
| `--oss-region <id>` | string | 否 | OSS region ID(`--store-type custom` 时必填) |
|
||||
| `--oss-bucket <name>` | string | 否 | OSS bucket 名称(`--store-type custom` 时必填) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--store-type` 只能是 `platform` 或 `custom`
|
||||
- `--store-type custom` 时 `--oss-region` 和 `--oss-bucket` 必填
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: conn-xxx (my-collection, PLATFORM)
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `platform` 使用平台托管存储;`custom` 使用已授权的 OSS bucket。
|
||||
- 自定义 bucket 必须携带标签 `bailian-connector-access=ReadAndWrite`(百炼的标签访问控制),否则服务端报 `setBucketCORS failed` 误导性错误。
|
||||
- **无集合删除 API**,创建需谨慎。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建平台托管的集合
|
||||
bl knowledge collection create --name my-collection --description "team docs" --workspace-id ws-xxx
|
||||
|
||||
# 创建使用自有 OSS bucket 的集合
|
||||
bl knowledge collection create --name oss-coll --description "own bucket" --store-type custom --oss-region cn-beijing --oss-bucket my-bucket
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------- |
|
||||
| `--collection-id <id>` | string | 否¹ | 集合 ID |
|
||||
| `--name <text>` | string | 否¹ | 集合名称 |
|
||||
|
||||
> ¹ `--collection-id` 和 `--name` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--collection-id` 和 `--name` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: conn-xxx
|
||||
name: my-collection
|
||||
description: team docs
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- getConnector 不返回 `fileConnectorConfig`(`storeType`/`regionId`/`bucketName`),这些字段仅在创建时通过请求体传入,查询时不可读回。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 按 ID 查询
|
||||
bl knowledge collection get --collection-id conn-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称查询
|
||||
bl knowledge collection get --name my-collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--collection-id <id>` | string | 否 | 按集合 ID 过滤 |
|
||||
| `--parent-id <id>` | string | 否 | 列出此分类的子分类 |
|
||||
| `--name <text>` | string | 否 | 按分类名称过滤(精确匹配,与知识库列表的模糊匹配不同) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌 |
|
||||
| `--max-result <n>` | number | 否 | 每页条数(默认:20) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
cate-xxx product-docs
|
||||
cate-yyy system-docs [default]
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
> 标记 `[default]` 的是文件未指定分类时的默认归属。
|
||||
|
||||
quiet 模式:每行一个 `categoryId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有分类
|
||||
bl knowledge category list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
bl knowledge category list --name my-category
|
||||
|
||||
# 翻页
|
||||
bl knowledge category list --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category add --name <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------- |
|
||||
| `--name <text>` | string | 是 | 分类名称(1-20 字符) |
|
||||
| `--parent-id <id>` | string | 否 | 创建为指定分类的子分类 |
|
||||
| `--collection-id <id>` | string | 否 | 创建在此集合下(默认:平台集合) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: cate-xxx (product-docs)
|
||||
```
|
||||
|
||||
quiet 模式:输出分类 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用分类按业务域组织数据中心文件。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建分类
|
||||
bl knowledge category add --name product-docs --workspace-id ws-xxx
|
||||
|
||||
# 创建子分类
|
||||
bl knowledge category add --name sub --parent-id cate-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: cate-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 含文件或子分类的分类的删除行为由服务端定义——服务端错误原样透传。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除分类(交互确认)
|
||||
bl knowledge category delete --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge category delete --category-id cate-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,344 @@
|
||||
# 文档管理命令手册
|
||||
|
||||
文档管理覆盖文件上传、OSS 导入、解析状态跟踪、文档删除和标签管理。文档导入知识库后自动解析为 chunk。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个文档,`FAILED` 状态的文档红色高亮。
|
||||
|
||||
```
|
||||
doc-xxx COMPLETED intro.md md 1024
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `doc_id` 与 `file_id` 的关系:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;通过 `knowledge doc upload --index-id` 导入的,`doc_id` 可能包含 workspace 后缀。
|
||||
- 页大小默认 10(服务端默认),最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出文档
|
||||
bl knowledge doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 每页 100 条
|
||||
bl knowledge doc list --index-id idx-xxx --page-size 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--job-id <id>` | string | 是 | 导入任务 ID(`ingestionId`,由 create/upload 返回) |
|
||||
| `--page-number <n>` | number | 否 | 页码 |
|
||||
| `--page-size <n>` | number | 否 | 每页条数 |
|
||||
| `--wait` | switch | 否 | 轮询直到任务到达终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
status: COMPLETED
|
||||
doc-xxx COMPLETED intro.md
|
||||
```
|
||||
|
||||
quiet 模式:输出任务状态(`PENDING`/`RUNNING`/`COMPLETED`)。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含每个文档的状态。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--index-id` 和 `--job-id` 服务端均要求必传,只传一个会返回 `SystemError`。
|
||||
- 整体任务状态为 `PENDING` / `RUNNING` / `COMPLETED`(无 `FAILED` 值)。
|
||||
- 单个文档可能解析失败(如 `PARSE_FAILED`),此时 CLI 以非零退出码报错,服务端消息原样透传。
|
||||
- 如果服务端对空闲知识库返回 `SystemError`,说明该 job 可能不存在。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看任务状态
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
|
||||
# 轮询等待完成,10 秒间隔
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --wait --poll-interval 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--file <path>` | array | 是 | 本地文件或目录路径(可重复)。目录递归扫描,不支持的格式自动跳过 |
|
||||
| `--index-id <id>` | string | 否 | 上传后导入到此知识库(所有文件合并为一个导入任务) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:工作区默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复),应用到每个上传的文件 |
|
||||
| `--wait` | switch | 否 | 轮询导入任务直到终态(需要 `--index-id`) |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--wait` 要求同时指定 `--index-id`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
intro.md file-xxx registered
|
||||
job: job-xxx
|
||||
status: COMPLETED
|
||||
|
||||
Uploaded 1 file.
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回自定义结构,包含 `files`(路径和 fileId)、`skipped`、`index_id`、`ingestion_id`、`final_status`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 上传管道:申请 lease → PUT 到 OSS → 注册文件 →(可选)创建导入任务。
|
||||
- 目录递归扫描,`node_modules`、`.git` 等自动跳过。
|
||||
- 多文件按顺序处理(无并发),避免 OSS 限流。
|
||||
- 支持的文件格式:`.pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`
|
||||
- 部分文件上传失败时,已注册的 fileId 会在错误 hint 中列出。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 上传单个文件
|
||||
bl knowledge doc upload --file ./a.md --workspace-id ws-xxx
|
||||
|
||||
# 上传多个文件并导入到知识库,等待完成
|
||||
bl knowledge doc upload --file ./a.md --file ./b.pdf --index-id idx-xxx --wait
|
||||
|
||||
# 上传整个目录
|
||||
bl knowledge doc upload --file ./docs/ --workspace-id ws-xxx
|
||||
|
||||
# 干跑预览(查看将上传和跳过的文件)
|
||||
bl knowledge doc upload --file ./docs/ --dry-run --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 document(s)
|
||||
doc-a
|
||||
doc-b
|
||||
```
|
||||
|
||||
quiet 模式:每行一个已删除的 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID 列表。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 只从知识库索引中移除文档,数据中心源文件不受影响(用 `file delete` 删除源文件)。
|
||||
- `doc_id` 应从 `knowledge doc list --quiet` 获取,而非 `doc upload` 返回的 `fileId`。
|
||||
- 删除是异步的:服务端立即返回 Success,但 `doc list` 中可能仍显示该文档(约 30 秒后传播完成)。
|
||||
- 输出的是服务端实际删除的 ID 列表,可能与请求的数量不一致(会在 stderr 警告)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除单个文档
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 用户明确确认后批量删除
|
||||
bl knowledge doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--doc-id <id>` | array | 是 | 数据中心文件 ID(可重复,最多 20 个/次) |
|
||||
| `--tag <text>` | array | 是 | 标签(可重复),应用到每个 `--doc-id` |
|
||||
| `--mode <mode>` | string | 否 | 更新模式:`append`(默认,追加)或 `overwrite`(覆盖) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--doc-id` 最多 20 个/次
|
||||
- `--tag` 最多 100 个
|
||||
- 每个标签最多 32 字符
|
||||
- 标签总长度最多 700 字符
|
||||
- `--mode` 只能是 `append` 或 `overwrite`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
tagged: 2 file(s) with [project-a, draft]
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 同一组标签应用到所有 `--doc-id`;不同标签集需多次执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 追加标签
|
||||
bl knowledge doc tag --doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx
|
||||
|
||||
# 覆盖标签
|
||||
bl knowledge doc tag --doc-id file-a --doc-id file-b --tag final --mode overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------------------------------- |
|
||||
| `--bucket <name>` | string | 是 | 已授权的 OSS bucket 名称 |
|
||||
| `--region <id>` | string | 是 | OSS region ID(如 `cn-beijing`) |
|
||||
| `--oss-key <key>` | array | 是 | OSS 对象 key(可重复,最多 10 个/次) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复,最多 10 个) |
|
||||
| `--overwrite` | switch | 否 | 覆盖之前从相同 OSS key 导入的文件 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--oss-key` 最多 10 个/次
|
||||
- `--tag` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
imported: 2 file(s)
|
||||
file-a SUCCESS docs/a.pdf
|
||||
file-b SUCCESS docs/b.docx
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.addFileResultList[]` 包含每个文件的 fileId、status 和 ossKey。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- bucket 必须事先授权给平台服务角色(RAM 中的 `AliyunServiceRoleForBailian`)。
|
||||
- 文件名取自 OSS key 的 basename。
|
||||
- `--overwrite` 会替换之前导入的文件并生成**新的 fileId**(旧 fileId 失效)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 导入单个文件
|
||||
bl knowledge doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx
|
||||
|
||||
# 导入多个文件并覆盖
|
||||
bl knowledge doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,157 @@
|
||||
# 数据中心文件管理命令手册
|
||||
|
||||
数据中心是知识库文件的存储层。文件通过 `doc upload` 或 `doc import-oss` 进入数据中心,再导入到知识库。数据中心文件可被多个知识库引用。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID(通过 `category list` 或 `file get` 获取) |
|
||||
| `--name <text>` | string | 否 | 按文件名过滤 |
|
||||
| `--file-id <id>` | array | 否 | 按文件 ID 过滤(可重复) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌(从上次输出获取) |
|
||||
| `--max-result <n>` | number | 否 | 每页条数 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
file-xxx SUCCESS intro.md 1024
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--category-id` 必须是真实的分类 ID。与上传 API 不同,字面量 `default` 在此不被解析,传入会返回空列表。通过 `file get` 的 category 字段或 `category list` 获取真实 ID。
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出分类下文件
|
||||
bl knowledge file list --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
bl knowledge file list --category-id cate-xxx --name report
|
||||
|
||||
# 翻页
|
||||
bl knowledge file list --category-id cate-xxx --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: file-xxx
|
||||
name: intro.md
|
||||
type: md
|
||||
size: 1024
|
||||
status: SUCCESS
|
||||
parser: AUTO_SELECT
|
||||
category: cate-xxx
|
||||
uploaded: 2026-01-01T00:00:00Z
|
||||
tags: project-a, draft
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 无特殊注意事项。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看文件详情
|
||||
bl knowledge file get --file-id file-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: file-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:如果知识库引用了此文件,相关文档索引会失效。
|
||||
- 与 `doc delete` 的区别:`doc delete` 只从单个知识库索引中移除文档,数据中心源文件保留;`file delete` 删除源文件本身,影响所有引用它的知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除文件(交互确认)
|
||||
bl knowledge file delete --file-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge file delete --file-id file-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,342 @@
|
||||
# 知识库管理命令手册
|
||||
|
||||
知识库(Knowledge Base / pipeline / index)是 RAG 的核心载体,存储文档解析后的向量索引。本组命令覆盖知识库的创建、查看、更新、删除和监控。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | --------------------------------- |
|
||||
| `--name <text>` | string | 否 | 按知识库名称模糊过滤(1-20 字符) |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个知识库,字段以双空格分隔,末尾显示总数。
|
||||
|
||||
```
|
||||
idx-xxx my-kb text-embedding-v4 600 product docs
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 返回的 `id` 字段作为后续命令的 `--index-id` 使用。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有知识库
|
||||
bl knowledge list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤,第二页
|
||||
bl knowledge list --name demo --page-number 2 --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:按诊断维度分组展示。
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: idx-xxx
|
||||
name: my-kb
|
||||
description: product docs
|
||||
dataType: ...
|
||||
Indexing: [immutable — recreate required to change]
|
||||
embeddingModelName: text-embedding-v4
|
||||
embeddingDimension: 1024
|
||||
chunkSize: 600
|
||||
overlapSize: ...
|
||||
chunkMode: ...
|
||||
separator: ...
|
||||
Retrieval:
|
||||
rerankModelName: ...
|
||||
rerankMinScore: ...
|
||||
rerankTopN: ...
|
||||
rerankMode: ...
|
||||
enableRewrite: ...
|
||||
denseSimilarityTopK: ...
|
||||
sparseSimilarityTopK: ...
|
||||
Data:
|
||||
sourceType: ...
|
||||
connectorId: ...
|
||||
```
|
||||
|
||||
quiet 模式:输出知识库 ID。
|
||||
|
||||
json 模式:返回知识库完整配置 JSON。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看知识库详情
|
||||
bl knowledge info --index-id idx-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 知识库名称(1-20 字符,工作区内唯一) |
|
||||
| `--description <text>` | string | 是 | 知识库装了什么内容、给谁用(1-500 字符) |
|
||||
| `--doc-id <id>` | array | 否¹ | 数据中心文件 ID(可重复);与 `--category-id` 互斥 |
|
||||
| `--category-id <id>` | array | 否¹ | 按分类导入该分类下所有文件(可重复);与 `--doc-id` 互斥 |
|
||||
| `--embedding-model <name>` | string | 否 | 向量模型名称(默认:`text-embedding-v4`) |
|
||||
| `--chunk-size <n>` | number | 否 | 切片大小,字符数(默认:600,建议 300-800) |
|
||||
| `--wait` | switch | 否 | 轮询初始导入任务直到终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
> ¹ `--doc-id` 和 `--category-id` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--description` 长度 1-500 字符,缺失或超长会在本地被拦截
|
||||
- `--doc-id` 和 `--category-id` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
index_id: idx-xxx
|
||||
ingestion_id: job-xxx
|
||||
status: COMPLETED
|
||||
Next: check the import job status, then search against this knowledge base.
|
||||
```
|
||||
|
||||
quiet 模式:只输出知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `pipelineId`(知识库 ID)和 `ingestionId`(导入任务 ID)。`--wait` 时追加 `final_status` 字段。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 结构/存储类型固定为默认文档知识库(非结构化,BUILT_IN 存储)。
|
||||
- 返回知识库 ID(`pipelineId`)和初始导入任务 ID(`ingestionId`)。
|
||||
- 使用 `doc status` 或 `--wait` 跟踪导入进度。
|
||||
- 如果 `--wait` 后部分文档解析失败,CLI 以非零退出码报错,知识库已创建成功的事实会在 hint 中提示。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 从指定文件创建知识库
|
||||
bl knowledge create --name demo --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 从分类导入并等待导入完成
|
||||
bl knowledge create --name demo --description '产品文档' --category-id cate-xxx --wait
|
||||
|
||||
# 指定向量模型和切片大小
|
||||
bl knowledge create --name my-kb --description '产品文档 v2' --doc-id file-a --doc-id file-b --embedding-model text-embedding-v4 --chunk-size 400 --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--name <text>` | string | 否 | 新名称(1-20 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述 |
|
||||
| `--rerank-min-score <score>` | number | 否 | rerank 最低分数阈值,范围 0-1(低于此分的 chunk 被过滤) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供 `--name`、`--description`、`--rerank-min-score` 之一,否则报错 "Nothing to update"
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--rerank-min-score` 范围 0-1
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 更新描述
|
||||
bl knowledge update --index-id idx-xxx --description "product docs v2" --workspace-id ws-xxx
|
||||
|
||||
# 调整 rerank 阈值
|
||||
bl knowledge update --index-id idx-xxx --rerank-min-score 0.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
bl knowledge delete --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge delete --index-id idx-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--start <time>` | string | 否 | 范围起始:Unix 秒或 ISO 日期(默认:24 小时前) |
|
||||
| `--end <time>` | string | 否 | 范围结束:Unix 秒或 ISO 日期(默认:当前时间) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
plan: ...
|
||||
storage: 100 / 1000
|
||||
peak qps: 5
|
||||
qps windows: 24 data point(s)
|
||||
```
|
||||
|
||||
quiet 模式:输出 json 格式。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `storageMonitorData` 和 `qpsMonitorData`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 默认查询最近 24 小时数据。
|
||||
- 时间戳自动转换为 epoch 秒(API 要求秒级字符串)。13 位毫秒时间戳会自动降为秒。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看最近 24 小时监控
|
||||
bl knowledge stats --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 指定日期范围
|
||||
bl knowledge stats --index-id idx-xxx --start 2026-07-30 --end 2026-07-31
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,822 @@
|
||||
# `bl knowledge` 命令完整用法指南
|
||||
|
||||
> `bl knowledge` / `kscli` 知识库 CLI 命令总览,覆盖全部 34 个子命令。完整参数与示例请参阅各子域手册。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [核心概念与实体关系](#核心概念与实体关系)
|
||||
3. [通用约定](#通用约定)
|
||||
4. [典型工作流](#典型工作流)
|
||||
5. [命令手册](#命令手册)
|
||||
- [知识库管理](#知识库管理) → [完整手册](knowledge/kb.md)
|
||||
- [文档管理](#文档管理) → [完整手册](knowledge/doc.md)
|
||||
- [检索服务管理](#检索服务管理) → [完整手册](knowledge/service.md)
|
||||
- [Chunk 管理](#chunk-管理) → [完整手册](knowledge/chunk.md)
|
||||
- [数据中心文件管理](#数据中心文件管理) → [完整手册](knowledge/file.md)
|
||||
- [数据中心集合与分类](#数据中心集合与分类) → [完整手册](knowledge/collection-category.md)
|
||||
- [检索与对话](#检索与对话) → [完整手册](knowledge/search-chat.md)
|
||||
6. [常见错误与排查](#常见错误与排查)
|
||||
7. [附录:命令速查表](#附录命令速查表)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
`bl knowledge` 是阿里云百炼 CLI 的知识库命令组,覆盖 RAG(检索增强生成)全链路能力:
|
||||
|
||||
- **知识库全生命周期管理**:创建、查看、更新、删除、监控
|
||||
- **文档管理**:上传本地文件、从 OSS 批量导入、查看解析状态、删除、打标签
|
||||
- **Chunk 级运维**:直接增删改查知识库中的内容切片
|
||||
- **检索服务管理**:创建/部署/复制/删除 Q&A 和检索服务(agent),管理 draft 与发布版本
|
||||
- **数据中心管理**:文件、集合(connector)、分类的增删查
|
||||
- **检索与对话**:语义检索(search)、多轮对话(chat)、兼容旧检索(retrieve)
|
||||
|
||||
共 34 个子命令,按功能域分为 7 组。所有命令均使用 DashScope API Key 鉴权。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念与实体关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据中心 (Data Center) │
|
||||
│ │
|
||||
│ 集合 (Collection) ──┬── 分类 (Category) ── 文件 (File) │
|
||||
│ │ "connector" 可多级嵌套 │
|
||||
│ └── 默认分类 │
|
||||
│ │
|
||||
│ 文件来源:doc upload(本地上传) / doc import-oss(OSS导入) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 导入 (import job)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 知识库 (Knowledge Base) │
|
||||
│ │
|
||||
│ 知识库 (KB / pipeline / index) │
|
||||
│ ├── 文档 (Doc) ── 解析状态: PENDING/RUNNING/COMPLETED │
|
||||
│ │ └── Chunk ── 内容切片,可增删改查、排除/恢复检索 │
|
||||
│ └── 索引设置 (immutable): 向量模型、切片大小等 │
|
||||
│ │
|
||||
│ 知识库管理命令: create / list / info / update / delete / stats │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 绑定 (agent_config.kb_search_configs)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 检索服务 (Service / Agent) │
|
||||
│ │
|
||||
│ Service (agent) │
|
||||
│ ├── scene: chat (Q&A) 或 search (检索) │
|
||||
│ ├── 版本: beta (草稿) → 1, 2, 3... (已发布) │
|
||||
│ ├── 状态: draft → deployed → edited → deleted │
|
||||
│ └── 配置: 模型、温度、策略、rerank 等 │
|
||||
│ │
|
||||
│ 消费方式: search (语义检索) / chat (多轮对话) │
|
||||
│ 管理命令: create / update / deploy / copy / delete / list / get │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键关系**:
|
||||
|
||||
- **数据中心文件 → 知识库**:通过 `knowledge create --doc-id` 或 `knowledge doc upload --index-id` 导入,文件解析后自动生成 chunk
|
||||
- **知识库 → 检索服务**:一个服务可绑定多个知识库,服务配置中 `kb_search_configs` 指定关联的知识库 ID
|
||||
- **检索服务 → 检索/对话**:`search` 和 `chat` 命令通过 `--agent-id` 指定服务来执行检索或对话
|
||||
|
||||
---
|
||||
|
||||
## 通用约定
|
||||
|
||||
### 鉴权
|
||||
|
||||
所有 `bl knowledge` 命令均使用 **DashScope API Key**(Bearer token)鉴权。获取方式:百炼控制台 API Key 页面。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--api-key <key>` 命令行参数
|
||||
2. `DASHSCOPE_API_KEY` 环境变量
|
||||
3. 配置文件中的 `api_key`(`bl config set api_key <key>`)
|
||||
|
||||
### Workspace ID
|
||||
|
||||
知识库 API 使用 workspace 级域名(`{workspaceId}.cn-beijing.maas.aliyuncs.com`),因此 **几乎所有 knowledge 命令都需要 workspace ID**。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--workspace-id <id>` 命令行参数
|
||||
2. `BAILIAN_WORKSPACE_ID` 环境变量
|
||||
3. 配置文件中的 `workspace_id`(`bl config set workspace_id <id>`)
|
||||
|
||||
缺失时报错:`Workspace ID is required.`
|
||||
|
||||
### 全局通用参数
|
||||
|
||||
以下参数在所有 `bl knowledge` 子命令中通用,后续命令手册中不再逐条列出:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------------------- | ------ | ----------------------------------------------------------- |
|
||||
| `--output <format>` | string | 输出格式:`text`(默认,人类友好)或 `json`(API 原始响应) |
|
||||
| `--api-key <key>` | string | DashScope API Key |
|
||||
| `--base-url <url>` | string | API 基地址(一般不需要指定) |
|
||||
| `--timeout <seconds>` | number | 请求超时秒数 |
|
||||
| `--quiet` | switch | 静默模式,只输出关键结果(如 ID 列表) |
|
||||
| `--verbose` | switch | 详细模式,打印 HTTP 请求/响应详情到 stderr |
|
||||
| `--dry-run` | switch | 干跑模式,预览将发送的请求结构,不实际调用 API |
|
||||
| `--config <name>` | string | 使用指定配置 profile 执行命令 |
|
||||
|
||||
> **注意**:命令手册中每个命令的参数表只列出该命令**特有**的参数。上述全局参数对所有命令有效。
|
||||
|
||||
### 输出格式约定
|
||||
|
||||
- **text 模式**(默认):人类友好的表格/结构化文本,适合终端查看。不同命令的输出格式见各命令的「输出」部分。
|
||||
- **json 模式**(`--output json`):返回 API 原始 JSON 响应,适合程序化处理和 agent 解析。
|
||||
- **quiet 模式**(`--quiet`):只输出最精简的结果(通常只有 ID),适合管道串联。
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
`--dry-run` 模式下,命令会输出将发送的 endpoint 和 request body,但**不实际发起网络请求**。部分命令在 dry-run 下仍会执行本地校验(如文件扩展名检查、参数约束检查)。
|
||||
|
||||
---
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 场景 A:从零搭建知识库并检索
|
||||
|
||||
```bash
|
||||
# 1. 上传本地文件到数据中心,同时导入到新知识库
|
||||
bl knowledge doc upload --file ./docs/intro.md --workspace-id ws-xxx
|
||||
# → 返回 file-id
|
||||
|
||||
# 2. 用文件创建知识库
|
||||
bl knowledge create --name my-kb --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx --wait
|
||||
# → 返回 index-id (pipelineId) 和导入任务状态
|
||||
|
||||
# 3. 创建检索服务(search 场景)
|
||||
bl knowledge service create --name my-search --scene search --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 agent-id
|
||||
|
||||
# 4. 部署服务
|
||||
bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx --yes
|
||||
|
||||
# 5. 执行检索
|
||||
bl knowledge search --query "什么是RAG" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 B:上传目录并导入到已有知识库
|
||||
|
||||
```bash
|
||||
# 1. 上传整个目录到数据中心并直接导入到知识库(一步到位)
|
||||
bl knowledge doc upload --file ./docs/ --index-id idx-xxx --workspace-id ws-xxx --wait
|
||||
# → 文件逐个上传到 OSS → 注册到数据中心 → 创建合并导入任务 → 轮询到完成
|
||||
|
||||
# 2. 检查文档状态
|
||||
bl knowledge doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 查看 doc_id 和解析状态
|
||||
|
||||
# 3. 如果有文档解析失败,查看导入任务详情
|
||||
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 C:创建并部署 Q&A 服务
|
||||
|
||||
```bash
|
||||
# 1. 创建 chat 场景的检索服务
|
||||
bl knowledge service create --name my-qa --scene chat --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 初始状态: draft, 版本: beta
|
||||
|
||||
# 2. 调整配置(如修改模型、温度)
|
||||
bl knowledge service update --agent-id aid-xxx --model qwen-max --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 3. 用 beta 版本测试
|
||||
bl knowledge chat --message "什么是RAG?" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
|
||||
# 4. 测试通过后发布
|
||||
bl knowledge service deploy --agent-id aid-xxx --version-desc "首版" --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 D:知识库内容运维
|
||||
|
||||
```bash
|
||||
# 1. 查看 chunk 列表
|
||||
bl knowledge chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 metadata._id (chunk id) 和 metadata.doc_id (document id)
|
||||
|
||||
# 2. 修改 chunk 内容
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --content "修正后的内容" --workspace-id ws-xxx
|
||||
|
||||
# 3. 排除某个 chunk 不参与检索(不删除内容)
|
||||
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --exclude --workspace-id ws-xxx
|
||||
|
||||
# 4. 手动添加新 chunk
|
||||
bl knowledge chunk add --index-id idx-xxx --content "新增的知识片段" --title "补充说明" --workspace-id ws-xxx
|
||||
|
||||
# 5. 删除 chunk(批量,自动分批每 10 个一组)
|
||||
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --yes --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 E:服务迁移/复用
|
||||
|
||||
```bash
|
||||
# 1. 复制现有服务为新草稿
|
||||
bl knowledge service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
# → 返回新的 agent-id,名称加 copy_ 前缀
|
||||
|
||||
# 2. 修改新服务配置
|
||||
bl knowledge service update --agent-id aid-new --name "改进版" --temperature 0.5 --workspace-id ws-xxx
|
||||
|
||||
# 3. 测试并发布
|
||||
bl knowledge chat --message "测试" --agent-id aid-new --agent-version beta --workspace-id ws-xxx
|
||||
bl knowledge service deploy --agent-id aid-new --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 F:从 OSS 批量导入文件
|
||||
|
||||
```bash
|
||||
# 1. 从已授权的 OSS bucket 批量导入文件到数据中心
|
||||
bl knowledge doc import-oss \
|
||||
--bucket my-bucket --region cn-beijing \
|
||||
--oss-key docs/a.pdf --oss-key docs/b.docx \
|
||||
--workspace-id ws-xxx
|
||||
# → 返回各文件的 fileId
|
||||
|
||||
# 2. 创建知识库并导入这些文件
|
||||
bl knowledge create --name oss-kb --description 'OSS 导入文档' --doc-id file-a --doc-id file-b --workspace-id ws-xxx --wait
|
||||
|
||||
# 3. 检索
|
||||
bl knowledge search --query "相关内容" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 命令手册
|
||||
|
||||
以下按功能域分组,覆盖全部 34 个子命令。每个条目包含功能说明、用法签名(kscli 前缀)和详细手册链接。
|
||||
|
||||
> 完整参数表、参数约束、输出说明、注意事项与示例请参阅各子域手册。子域手册中的用法签名使用 `bl knowledge` 前缀。
|
||||
|
||||
---
|
||||
|
||||
### 知识库管理
|
||||
|
||||
> 📖 [完整手册](knowledge/kb.md) — 6 个命令
|
||||
|
||||
#### `kscli kb list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
```bash
|
||||
kscli kb list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
```bash
|
||||
kscli kb info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-info)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
```bash
|
||||
kscli kb create --name <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
```bash
|
||||
kscli kb update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
```bash
|
||||
kscli kb delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
```bash
|
||||
kscli kb stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-stats)
|
||||
|
||||
---
|
||||
|
||||
### 文档管理
|
||||
|
||||
> 📖 [完整手册](knowledge/doc.md) — 6 个命令
|
||||
|
||||
#### `kscli doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
```bash
|
||||
kscli doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
```bash
|
||||
kscli doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-status)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
```bash
|
||||
kscli doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-upload)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
```bash
|
||||
kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
```bash
|
||||
kscli doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-tag)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
```bash
|
||||
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-import-oss)
|
||||
|
||||
---
|
||||
|
||||
### 检索服务管理
|
||||
|
||||
> 📖 [完整手册](knowledge/service.md) — 7 个命令
|
||||
|
||||
#### `kscli service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
```bash
|
||||
kscli service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
```bash
|
||||
kscli service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
```bash
|
||||
kscli service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
```bash
|
||||
kscli service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
```bash
|
||||
kscli service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-deploy)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
```bash
|
||||
kscli service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
```bash
|
||||
kscli service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-copy)
|
||||
|
||||
---
|
||||
|
||||
### Chunk 管理
|
||||
|
||||
> 📖 [完整手册](knowledge/chunk.md) — 4 个命令
|
||||
|
||||
#### `kscli chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
```bash
|
||||
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
```bash
|
||||
kscli chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
```bash
|
||||
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
```bash
|
||||
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心文件管理
|
||||
|
||||
> 📖 [完整手册](knowledge/file.md) — 3 个命令
|
||||
|
||||
#### `kscli file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
```bash
|
||||
kscli file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
```bash
|
||||
kscli file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
```bash
|
||||
kscli file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心集合与分类
|
||||
|
||||
> 📖 [完整手册](knowledge/collection-category.md) — 5 个命令
|
||||
|
||||
#### `kscli collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
```bash
|
||||
kscli collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
```bash
|
||||
kscli collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category add --name <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-delete)
|
||||
|
||||
---
|
||||
|
||||
### 检索与对话
|
||||
|
||||
> 📖 [完整手册](knowledge/search-chat.md) — 3 个命令
|
||||
|
||||
#### `kscli retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
```bash
|
||||
kscli retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-retrieve)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
```bash
|
||||
kscli search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-search)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
```bash
|
||||
kscli chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-chat)
|
||||
|
||||
---
|
||||
|
||||
## 常见错误与排查
|
||||
|
||||
### Workspace ID 缺失
|
||||
|
||||
**报错**:`Workspace ID is required.`
|
||||
|
||||
**原因**:所有 knowledge 管理命令都需要 workspace ID 来构造 API 端点(`{workspaceId}.cn-beijing.maas.aliyuncs.com`)。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 方式1:命令行参数
|
||||
bl knowledge list --workspace-id ws-xxx
|
||||
|
||||
# 方式2:环境变量
|
||||
export BAILIAN_WORKSPACE_ID=ws-xxx
|
||||
|
||||
# 方式3:配置文件
|
||||
bl config set workspace_id ws-xxx
|
||||
```
|
||||
|
||||
### 知识库 ID 不存在
|
||||
|
||||
**报错**:`Knowledge base not found: idx-xxx`
|
||||
|
||||
**原因**:`--index-id` 指定的知识库在当前 workspace 中不存在。
|
||||
|
||||
**解决**:先 `bl knowledge list` 确认知识库 ID。
|
||||
|
||||
### 导入任务 SystemError
|
||||
|
||||
**报错**:服务端返回 `SystemError`
|
||||
|
||||
**原因**:`doc status` 传入了不存在的 job ID,或知识库空闲无任务。
|
||||
|
||||
**解决**:检查 `doc list` 输出中的 `ingestionId`,或从 `doc upload`/`knowledge create` 的返回值获取。
|
||||
|
||||
### doc_id 与 fileId 混淆
|
||||
|
||||
**问题**:`doc delete` 时用了 `doc upload` 返回的 `fileId` 而非 `doc list` 返回的 `doc_id`。
|
||||
|
||||
**原因**:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。
|
||||
|
||||
**解决**:始终用 `doc list --quiet` 获取 `doc_id`。
|
||||
|
||||
### retrieve 已废弃
|
||||
|
||||
**问题**:`retrieve` 命令输出废弃警告。
|
||||
|
||||
**解决**:改用 `search` 命令。`search` 通过 `--agent-id` 驱动检索策略,支持多知识库、路由、rerank 等高级特性。`retrieve` 直接操作 `--index-id`,功能受限且不再迭代。
|
||||
|
||||
### OSS 导入权限错误
|
||||
|
||||
**报错**:服务端返回权限相关错误。
|
||||
|
||||
**原因**:OSS bucket 未授权给平台服务角色。
|
||||
|
||||
**解决**:检查 RAM 控制台中的 `AliyunServiceRoleForBailian` 角色是否已正确授权。
|
||||
|
||||
### Chat SSE error
|
||||
|
||||
**报错**:`Chat API error` + API error code。
|
||||
|
||||
**原因**:流式对话过程中服务端返回 error 事件。
|
||||
|
||||
**解决**:检查 `--agent-id` 是否存在、服务是否已部署、API Key 是否有效。错误消息和 code 原样透传,不二次包装。
|
||||
|
||||
### file list 返回空
|
||||
|
||||
**问题**:`file list --category-id default` 返回空列表。
|
||||
|
||||
**原因**:与上传 API 不同,`file list` 不解析字面量 `default`,需要真实分类 ID。
|
||||
|
||||
**解决**:通过 `file get` 的 category 字段或 `category list` 获取真实分类 ID。
|
||||
|
||||
### 集合无法删除
|
||||
|
||||
**问题**:没有 `collection delete` 命令。
|
||||
|
||||
**原因**:暂不支持通过 CLI 删除。
|
||||
|
||||
**解决**:创建集合需谨慎。如需隔离,创建新集合并迁移文件。
|
||||
|
||||
---
|
||||
|
||||
## 附录:命令速查表
|
||||
|
||||
| 命令 | 功能 | 关键参数 |
|
||||
| ------------------------- | ------------ | ----------------------------------------------------------- |
|
||||
| `kscli kb list` | 列出知识库 | `--name` |
|
||||
| `kscli kb info` | 知识库详情 | `--index-id` |
|
||||
| `kscli kb create` | 创建知识库 | `--name`, `--doc-id`/`--category-id` |
|
||||
| `kscli kb update` | 更新知识库 | `--index-id`, `--name`/`--description`/`--rerank-min-score` |
|
||||
| `kscli kb delete` | 删除知识库 | `--index-id`, `--yes` |
|
||||
| `kscli kb stats` | 监控数据 | `--index-id`, `--start`/`--end` |
|
||||
| `kscli doc list` | 文档列表 | `--index-id` |
|
||||
| `kscli doc status` | 导入任务状态 | `--index-id`, `--job-id`, `--wait` |
|
||||
| `kscli doc upload` | 上传文件 | `--file`, `--index-id`, `--wait` |
|
||||
| `kscli doc delete` | 删除文档 | `--index-id`, `--doc-id` |
|
||||
| `kscli doc tag` | 文件打标签 | `--doc-id`, `--tag`, `--mode` |
|
||||
| `kscli doc import-oss` | OSS 导入 | `--bucket`, `--region`, `--oss-key` |
|
||||
| `kscli service list` | 服务列表 | `--scene` |
|
||||
| `kscli service get` | 服务详情 | `--agent-id` |
|
||||
| `kscli service create` | 创建服务 | `--name`, `--scene`, `--index-id` |
|
||||
| `kscli service update` | 更新服务 | `--agent-id`, 配置参数 |
|
||||
| `kscli service deploy` | 发布服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service delete` | 删除服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service copy` | 复制服务 | `--agent-id` |
|
||||
| `kscli chunk add` | 添加 chunk | `--index-id`, `--content`/`--field` |
|
||||
| `kscli chunk list` | chunk 列表 | `--index-id`, `--doc-id` |
|
||||
| `kscli chunk update` | 更新 chunk | `--index-id`, `--chunk-id`, `--doc-id` |
|
||||
| `kscli chunk delete` | 删除 chunk | `--index-id`, `--chunk-id`, `--yes` |
|
||||
| `kscli file list` | 文件列表 | `--category-id` |
|
||||
| `kscli file get` | 文件详情 | `--file-id` |
|
||||
| `kscli file delete` | 删除文件 | `--file-id`, `--yes` |
|
||||
| `kscli collection create` | 创建集合 | `--name`, `--description` |
|
||||
| `kscli collection get` | 集合详情 | `--collection-id`/`--name` |
|
||||
| `kscli category list` | 分类列表 | `--collection-id`, `--parent-id` |
|
||||
| `kscli category add` | 创建分类 | `--name`, `--parent-id` |
|
||||
| `kscli category delete` | 删除分类 | `--category-id`, `--yes` |
|
||||
| `kscli retrieve` | 检索(废弃) | `--index-id`, `--query` |
|
||||
| `kscli search` | 语义检索 | `--query`, `--agent-id` |
|
||||
| `kscli chat` | RAG 对话 | `--message`, `--agent-id` |
|
||||
@@ -0,0 +1,218 @@
|
||||
# 检索与对话命令手册
|
||||
|
||||
以下命令通过检索服务(agent)消费知识库。`search` 用于语义检索,`chat` 用于多轮对话。`retrieve` 已废弃。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--query <text>` | string | 是 | 检索查询文本 |
|
||||
| `--dense-similarity-top-k <n>` | number | 否 | 稠密检索 top K |
|
||||
| `--sparse-similarity-top-k <n>` | number | 否 | 稀疏检索 top K |
|
||||
| `--rerank` | switch | 否 | 启用 rerank |
|
||||
| `--rerank-top-n <n>` | number | 否 | rerank 返回 top N 结果 |
|
||||
| `--rerank-model <name>` | string | 否 | rerank 模型名,如 `qwen3-rerank-hybrid` |
|
||||
| `--rerank-mode <mode>` | string | 否 | rerank 模式:`qa`、`similar` 或 `custom` |
|
||||
| `--rerank-instruct <text>` | string | 否 | 自定义 rerank 指令(`--rerank-mode custom` 时使用) |
|
||||
| `--top-k <n>` | number | 否 | 返回结果数(已废弃,用 `--rerank-top-n` 替代) |
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **已废弃**,推荐使用 `search` 命令。`search` 通过 agent_id 驱动检索策略,支持更多高级特性。
|
||||
- `--top-k` 已废弃,使用 `--rerank-top-n` 替代,传入 `--top-k` 会输出 stderr 警告。
|
||||
- 此命令直接用 `--index-id` 检索,不需要创建检索服务。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
bl knowledge retrieve --index-id idx-xxx --query "How to use Alibaba Cloud Bailian" --workspace-id ws-xxx
|
||||
|
||||
# 启用 rerank
|
||||
bl knowledge retrieve --index-id idx-xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------- |
|
||||
| `--query <text>` | string | 是 | 检索查询文本(不可为空) |
|
||||
| `--agent-id <id>` | string | 是 | 检索服务 ID(在控制台知识检索页面获取,或通过 `service list` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复),用于多模态检索 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--query` 不可为空(API 要求 `minLength: 1`)
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 包含检索结果。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 检索范围和策略(多知识库加权、路由、rerank 等)由 `--agent-id` 对应的服务配置驱动。只需 `--query` 和 `--agent-id` 即可调用。
|
||||
- `--agent-version beta` 调试草稿配置进行调试,部署前验证效果。
|
||||
- 与 `retrieve` 的区别:`search` 通过 agent_id 间接驱动检索策略(支持多知识库、路由、rerank 等),`retrieve` 直接操作 index_id 且功能较少。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
bl knowledge search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态检索(带图片)
|
||||
bl knowledge search --query "describe this image" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg
|
||||
|
||||
# 调试草稿版本
|
||||
bl knowledge search --query "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--message <text>` | array | 是¹ | 消息文本(可重复)。支持 `role:content` 前缀设置角色(如 `user:hello`),默认角色为 `user`。也支持完整 JSON 对象传递结构化消息 |
|
||||
| `--agent-id <id>` | string | 是 | Q&A 服务 ID(在控制台知识问答页面获取,或通过 `service list --scene chat` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复)。附加到最后一条 user 消息作为多模态内容 |
|
||||
|
||||
> ¹ `--message` 或 `--image` 至少提供其一。纯图片查询可以只传 `--image`(CLI 会自动创建空 user 消息承载图片)。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--message` 或 `--image` 至少提供一个
|
||||
- `--image` 不能与已包含 `image_url` 内容部分的消息同时使用
|
||||
|
||||
**输出**
|
||||
|
||||
**TTY text 模式**(实时流式):
|
||||
|
||||
```
|
||||
🔍 Retrieving...
|
||||
✍️ Generating...
|
||||
这是AI生成的回答内容,逐字流式输出...
|
||||
```
|
||||
|
||||
> 进度标签由 SSE `step_change` 事件驱动:`tool_calling`(检索中)→ `plan_start`(规划中)→ `generation_start`(生成中)。
|
||||
|
||||
**非 TTY text 模式**(缓冲输出):
|
||||
|
||||
```
|
||||
完整的回答文本...
|
||||
```
|
||||
|
||||
**json 模式**(`--output json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "完整的回答文本...",
|
||||
"request_id": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
quiet 模式:输出完整的回答文本。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- API 仅支持 SSE 流式响应。TTY 环境下实时打印 token;非 TTY 环境缓冲后输出完整文本。
|
||||
- SSE 事件生命周期:`tool_calling` → `tool_return` → `plan_start` → `planning` → `plan_end` → `generation_start` → `generating` → `generation_end`。`tool_calling` → `tool_return` 可能循环多次。
|
||||
- 多轮对话:用 `--message "user:..."` 和 `--message "assistant:..."` 传递对话历史。
|
||||
- `--agent-version beta` 调用草稿配置进行调试。
|
||||
- `--image` 附加到最后一条 user 消息上。如果消息中已包含 `image_url` 内容部分,则不能再用 `--image`。
|
||||
- `--verbose` 模式下,所有 SSE 事件详情会输出到 stderr。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 单轮对话
|
||||
bl knowledge chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多轮对话(带历史)
|
||||
bl knowledge chat \
|
||||
--message "user:What is RAG?" \
|
||||
--message "assistant:RAG is retrieval-augmented generation..." \
|
||||
--message "How does it work?" \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态对话(带图片)
|
||||
bl knowledge chat \
|
||||
--message "Describe these images" \
|
||||
--image https://example.com/a.png \
|
||||
--image https://example.com/b.png \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 调试草稿版本
|
||||
bl knowledge chat --message "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,401 @@
|
||||
# 检索服务管理命令手册
|
||||
|
||||
检索服务(也称 agent)是知识库的检索入口。通过 `--agent-id` 在 search/chat 命令中使用。服务有 `chat`(问答)和 `search`(检索)两种场景。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------------- |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--status <status>` | string | 否 | 按状态过滤:`draft`、`deployed`(含 edited)、`deleted` |
|
||||
| `--name <text>` | string | 否 | 按服务名称模糊过滤 |
|
||||
| `--agent-id <id>` | string | 否 | 按精确 agent ID 过滤 |
|
||||
| `--index-id <id>` | string | 否 | 按关联知识库 ID 过滤 |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--status` 只能是 `draft`、`deployed`、`deleted`
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
aid-xxx deployed 2 my-qa (kb: my-kb)
|
||||
total: 1
|
||||
Use an agent_id above with the knowledge chat command.
|
||||
```
|
||||
|
||||
> 最后一行根据 scene 自动提示用 `search` 还是 `chat` 命令消费。
|
||||
|
||||
quiet 模式:每行一个 `agent_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端要求 `--scene` 必填,要查看两种场景的服务需分别执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出 chat 服务
|
||||
bl knowledge service list --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 只看已部署的检索服务
|
||||
bl knowledge service list --scene search --status deployed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--agent-version <version>` | string | 否 | 指定版本查看(`beta` 或已发布版本号);不传则返回所有版本 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: aid-xxx
|
||||
name: my-qa
|
||||
desc: product Q&A
|
||||
scene: chat
|
||||
status: deployed
|
||||
Version beta:
|
||||
desc: draft
|
||||
policy: turbo
|
||||
model: qwen-max
|
||||
temperature: 0.7
|
||||
kb: idx-xxx (my-kb)
|
||||
Version 1:
|
||||
published: 2026-01-01
|
||||
...
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不传 `--agent-version` 时返回所有版本(beta 草稿 + 已发布版本号)。
|
||||
- 版本值原样传递,有效值集合由服务端维护。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看服务完整详情
|
||||
bl knowledge service get --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看 beta 草稿配置
|
||||
bl knowledge service get --agent-id aid-xxx --agent-version beta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 服务名称(最多 200 字符,同一场景下工作区内唯一) |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--description <text>` | string | 建议 | 这个服务能回答什么、给谁用(最多 1000 字符) |
|
||||
| `--index-id <id>` | string | 否 | 绑定此知识库;其他配置使用服务端默认值 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 最多 200 字符
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--description` 最多 1000 字符;建议填写 —— agent 靠它判断该调用哪个服务
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: aid-xxx (status: draft, version: beta)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不指定 `--index-id` 时,服务端使用默认 agent 配置。
|
||||
- beta 草稿可通过 search/chat 的 `--agent-version beta` 测试,部署后才生效。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建 Q&A 服务
|
||||
bl knowledge service create --name my-qa --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 创建检索服务并绑定知识库
|
||||
bl knowledge service create --name my-search --scene search --index-id idx-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------ | ------ | ---- | ---------------------------------------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--name <text>` | string | 否 | 新名称(最多 200 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述(最多 1000 字符) |
|
||||
| `--agent-version <version>` | string | 否 | 目标版本(默认:beta 草稿。已发布版本只接受 `--version-desc`) |
|
||||
| `--version-desc <text>` | string | 否 | 版本描述 |
|
||||
| `--policy <policy>` | string | 否 | Agent 策略:`turbo`(快速)或 `agentic`(多轮) |
|
||||
| `--model <name>` | string | 否 | 生成模型代码(须在平台白名单中) |
|
||||
| `--temperature <n>` | number | 否 | 采样温度,范围 0-2 |
|
||||
| `--max-llm-calls <n>` | number | 否 | 单次请求最大 LLM 调用次数,范围 1-30 |
|
||||
| `--enable-session-file <bool>` | string | 否 | 启用会话文件:`true` 或 `false` |
|
||||
| `--enable-refusal <bool>` | string | 否 | 启用拒答:`true` 或 `false` |
|
||||
| `--enable-anti-leak <bool>` | string | 否 | 启用防泄漏:`true` 或 `false` |
|
||||
| `--enable-rich-text <bool>` | string | 否 | 启用富文本输出:`true` 或 `false` |
|
||||
| `--enable-citation <bool>` | string | 否 | 启用引用标注:`true` 或 `false` |
|
||||
| `--config-file <path>` | string | 否 | JSON 文件替换整个 `agent_config`(含嵌套设置如 `kb_search_configs`);与标量配置参数互斥 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供一个更新项(`--name`/`--description`/`--version-desc`/`--config-file`/标量配置参数),否则报错 "Nothing to update"
|
||||
- `--config-file` 与标量配置参数(`--policy`/`--model`/`--temperature` 等)互斥
|
||||
- 已发布版本 + 配置变更 → 报错(已发布版本只接受 `--version-desc`)
|
||||
- `--name` 最多 200 字符;`--description` 最多 1000 字符
|
||||
- `--policy` 只能是 `turbo` 或 `agentic`
|
||||
- `--temperature` 范围 0-2
|
||||
- `--max-llm-calls` 范围 1-30
|
||||
- 布尔参数(`--enable-*`)只能是 `true` 或 `false`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: aid-xxx
|
||||
Draft config changed — verify with --agent-version beta, then deploy.
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 配置变更只作用于 beta 草稿;已发布版本只接受 `--version-desc`。
|
||||
- 标量配置参数采用 read-merge-write:CLI 先读取当前 beta 配置,再合并变更后整体提交(API 是整替换语义)。
|
||||
- `--config-file` 替换整个配置,适合设置嵌套字段(如 `kb_search_configs`)。
|
||||
- 修改草稿后用 `--agent-version beta` 在 search/chat 上测试,通过后 `service deploy` 发布。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 调整温度
|
||||
bl knowledge service update --agent-id aid-xxx --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 用 JSON 文件替换整个配置
|
||||
bl knowledge service update --agent-id aid-xxx --config-file ./agent-config.json
|
||||
|
||||
# 给已发布版本 1 加描述
|
||||
bl knowledge service update --agent-id aid-xxx --agent-version 1 --version-desc "first stable release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deployed: aid-xxx version 2
|
||||
```
|
||||
|
||||
quiet 模式:输出新版本号。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 版本号自动递增,状态变为 `deployed`。
|
||||
- 发布影响线上调用方,确认提示会警告。
|
||||
- 如果当前状态为 `edited`(已发布后又改了草稿),确认提示会额外警告「发布会覆盖线上行为」。
|
||||
- 需要工作区的知识库修改权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 发布(交互确认)
|
||||
bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 带描述并跳过确认
|
||||
bl knowledge service deploy --agent-id aid-xxx --version-desc "tuned rerank params" --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: aid-xxx (status: deleted)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 删除不可撤销,`agent_id` 不再可用于 search/chat 调用。
|
||||
- API 是幂等的:删除已删除的服务不会报错。
|
||||
- 如果服务状态为 `deployed` 或 `edited`,确认提示会额外警告「此服务正在线上运行」。
|
||||
- 需要工作区的知识库删除权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
bl knowledge service delete --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
bl knowledge service delete --agent-id aid-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `bl knowledge service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
bl knowledge service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--agent-id <id>` | string | 是 | 源服务(agent)ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
new agent_id: aid-new (name: copy_my-qa, status: draft)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出新 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 副本初始为 beta 草稿,测试后需 deploy 发布。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 复制服务
|
||||
bl knowledge service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](../knowledge-cli-guide.md)
|
||||
@@ -0,0 +1,248 @@
|
||||
# Chunk 管理命令手册
|
||||
|
||||
Chunk 是知识库中最小的检索单元。文档导入后自动切分为 chunk,也可以手动添加。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否² | 所属文档 ID;表格/图片知识库必填,文档型可选 |
|
||||
| `--content <text>` | string | 否¹ | Chunk 正文,最多 6000 字符(文档型);与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取正文(`.md`/`.txt` 等);与 `--content` 互斥 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,最多 50 字符(文档型) |
|
||||
| `--image-url <url>` | array | 否 | Chunk 图片 URL(可重复,最多 10 个;文档型) |
|
||||
| `--field <key=value>` | array | 否¹ | 任意字段键值对(可重复),用于表格/图片知识库,键为 Excel 列名;与 content/title/image 互斥 |
|
||||
|
||||
> ¹ `--content`/`--content-file`/`--title`/`--image-url` 与 `--field` 互斥,必须提供其一。
|
||||
> ² 表格/图片知识库必须提供 `--doc-id`。文档型知识库可选。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--field` 与 `--content`/`--content-file`/`--title`/`--image-url` 互斥
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--content` 最多 6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
- `--image-url` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
chunk created (pipeline: idx-xxx)
|
||||
List chunks to find the new chunk id.
|
||||
```
|
||||
|
||||
quiet 模式:无输出(成功退出码 0)。
|
||||
|
||||
json 模式:返回 API 原始响应(不含 chunk ID)。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 支持文档/表格/图片知识库;音视频知识库不支持。
|
||||
- API 响应不含 chunk ID,需用 `chunk list` 查找新 chunk。
|
||||
- API 幂等但限流 10 次/秒,批量脚本需自行节流。
|
||||
- 表格/图片知识库用 `--field`,键为 Excel 列名,值为字符串。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 添加文本 chunk
|
||||
kscli chunk add --index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx
|
||||
|
||||
# 添加表格行(字段方式)
|
||||
kscli chunk add --index-id idx-xxx --field 列A=v1 --field 列B=v2
|
||||
|
||||
# 从文件读取内容
|
||||
kscli chunk add --index-id idx-xxx --content-file ./chunk.md --doc-id doc-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | string | 否 | 只显示属于此文档的 chunk |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
[chunk] chunk-xxx (doc: intro.md, doc_id: file-xxx) status: COMPLETED
|
||||
chunk content preview (truncated at 200 chars)…
|
||||
total: 1
|
||||
```
|
||||
|
||||
> 如果 chunk 被排除检索,行尾会显示 `[excluded from retrieval]`。
|
||||
|
||||
quiet 模式:每行一个 `metadata._id`(chunk ID),用于管道传给 update/delete。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 含完整 chunk 数据。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用 `metadata._id` 作为 chunk ID,`metadata.doc_id` 作为文档 ID,在 chunk update/delete 中使用。
|
||||
- 页大小默认 20,最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有 chunk
|
||||
kscli chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看某文档的 chunk
|
||||
kscli chunk list --index-id idx-xxx --doc-id file-xxx --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | string | 是 | Chunk ID(`metadata._id`,来自 chunk list 输出) |
|
||||
| `--doc-id <id>` | string | 是 | 所属文档 ID(`metadata.doc_id`,来自 chunk list 输出) |
|
||||
| `--content <text>` | string | 否¹ | 新内容,10-6000 字符;与 `--content-file` 互斥 |
|
||||
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取新内容 |
|
||||
| `--title <text>` | string | 否 | Chunk 标题,0-50 字符(空字符串清除标题;不传则不变) |
|
||||
| `--exclude` | switch | 否² | 将此 chunk 排除出检索 |
|
||||
| `--include` | switch | 否² | 将此 chunk 恢复检索(默认行为) |
|
||||
|
||||
> ¹ `--content` 与 `--content-file` 互斥。
|
||||
> ² `--exclude` 与 `--include` 互斥。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--content` 与 `--content-file` 互斥
|
||||
- `--exclude` 与 `--include` 互斥
|
||||
- 至少提供一个更新项(`--content`/`--content-file`/`--title`/`--exclude`/`--include`)
|
||||
- `--content` 长度 10-6000 字符
|
||||
- `--title` 最多 50 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: chunk-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 内容必须 10-6000 字符,且不超过知识库的 max chunk size。
|
||||
- `--content-file` 期望 UTF-8 纯文本文件,不解析 `.docx`/`.pdf` 等文档格式。
|
||||
- 仅切换 `--exclude`/`--include` 而不提供新内容时,CLI 自动读回当前内容并重新提交(API 要求 content 字段必填,CLI 隐藏了此限制)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 修改内容
|
||||
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text" --workspace-id ws-xxx
|
||||
|
||||
# 排除 chunk 不参与检索
|
||||
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude
|
||||
|
||||
# 恢复检索
|
||||
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --include
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 chunk(s) in 1 batch(es)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 `{ deleted_count, batches }`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端每次最多接受 10 个 chunk ID,CLI 自动分批。
|
||||
- 如果某批失败,操作停止,已删除的批次会在错误 hint 中列出。
|
||||
- Chunk 被永久移除,不可恢复。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除多个 chunk
|
||||
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,268 @@
|
||||
# 数据中心集合与分类命令手册
|
||||
|
||||
集合(collection)是数据中心的顶层容器,对应服务端的 connector。分类(category)用于组织集合内的文件,支持多级嵌套。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 集合名称(1-20 字符) |
|
||||
| `--description <text>` | string | 是 | 集合描述 |
|
||||
| `--store-type <type>` | string | 否 | 存储类型:`platform`(托管,默认)或 `custom`(自有 OSS bucket) |
|
||||
| `--oss-region <id>` | string | 否 | OSS region ID(`--store-type custom` 时必填) |
|
||||
| `--oss-bucket <name>` | string | 否 | OSS bucket 名称(`--store-type custom` 时必填) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--store-type` 只能是 `platform` 或 `custom`
|
||||
- `--store-type custom` 时 `--oss-region` 和 `--oss-bucket` 必填
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: conn-xxx (my-collection, PLATFORM)
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `platform` 使用平台托管存储;`custom` 使用已授权的 OSS bucket。
|
||||
- 自定义 bucket 必须携带标签 `bailian-connector-access=ReadAndWrite`(百炼的标签访问控制),否则服务端报 `setBucketCORS failed` 误导性错误。
|
||||
- **无集合删除 API**,创建需谨慎。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建平台托管的集合
|
||||
kscli collection create --name my-collection --description "team docs" --workspace-id ws-xxx
|
||||
|
||||
# 创建使用自有 OSS bucket 的集合
|
||||
kscli collection create --name oss-coll --description "own bucket" --store-type custom --oss-region cn-beijing --oss-bucket my-bucket
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------- |
|
||||
| `--collection-id <id>` | string | 否¹ | 集合 ID |
|
||||
| `--name <text>` | string | 否¹ | 集合名称 |
|
||||
|
||||
> ¹ `--collection-id` 和 `--name` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--collection-id` 和 `--name` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: conn-xxx
|
||||
name: my-collection
|
||||
description: team docs
|
||||
```
|
||||
|
||||
quiet 模式:输出集合 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- getConnector 不返回 `fileConnectorConfig`(`storeType`/`regionId`/`bucketName`),这些字段仅在创建时通过请求体传入,查询时不可读回。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 按 ID 查询
|
||||
kscli collection get --collection-id conn-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称查询
|
||||
kscli collection get --name my-collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli category list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--collection-id <id>` | string | 否 | 按集合 ID 过滤 |
|
||||
| `--parent-id <id>` | string | 否 | 列出此分类的子分类 |
|
||||
| `--name <text>` | string | 否 | 按分类名称过滤(精确匹配,与知识库列表的模糊匹配不同) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌 |
|
||||
| `--max-result <n>` | number | 否 | 每页条数(默认:20) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
cate-xxx product-docs
|
||||
cate-yyy system-docs [default]
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
> 标记 `[default]` 的是文件未指定分类时的默认归属。
|
||||
|
||||
quiet 模式:每行一个 `categoryId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有分类
|
||||
kscli category list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
kscli category list --name my-category
|
||||
|
||||
# 翻页
|
||||
kscli category list --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli category add --name <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------- |
|
||||
| `--name <text>` | string | 是 | 分类名称(1-20 字符) |
|
||||
| `--parent-id <id>` | string | 否 | 创建为指定分类的子分类 |
|
||||
| `--collection-id <id>` | string | 否 | 创建在此集合下(默认:平台集合) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: cate-xxx (product-docs)
|
||||
```
|
||||
|
||||
quiet 模式:输出分类 ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 用分类按业务域组织数据中心文件。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建分类
|
||||
kscli category add --name product-docs --workspace-id ws-xxx
|
||||
|
||||
# 创建子分类
|
||||
kscli category add --name sub --parent-id cate-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ---------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: cate-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 含文件或子分类的分类的删除行为由服务端定义——服务端错误原样透传。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除分类(交互确认)
|
||||
kscli category delete --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
kscli category delete --category-id cate-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,344 @@
|
||||
# 文档管理命令手册
|
||||
|
||||
文档管理覆盖文件上传、OSS 导入、解析状态跟踪、文档删除和标签管理。文档导入知识库后自动解析为 chunk。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | ------------------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个文档,`FAILED` 状态的文档红色高亮。
|
||||
|
||||
```
|
||||
doc-xxx COMPLETED intro.md md 1024
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `doc_id` 与 `file_id` 的关系:通过 `kb create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;通过 `doc upload --index-id` 导入的,`doc_id` 可能包含 workspace 后缀。
|
||||
- 页大小默认 10(服务端默认),最大 100。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出文档
|
||||
kscli doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 每页 100 条
|
||||
kscli doc list --index-id idx-xxx --page-size 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--job-id <id>` | string | 是 | 导入任务 ID(`ingestionId`,由 create/upload 返回) |
|
||||
| `--page-number <n>` | number | 否 | 页码 |
|
||||
| `--page-size <n>` | number | 否 | 每页条数 |
|
||||
| `--wait` | switch | 否 | 轮询直到任务到达终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
status: COMPLETED
|
||||
doc-xxx COMPLETED intro.md
|
||||
```
|
||||
|
||||
quiet 模式:输出任务状态(`PENDING`/`RUNNING`/`COMPLETED`)。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含每个文档的状态。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--index-id` 和 `--job-id` 服务端均要求必传,只传一个会返回 `SystemError`。
|
||||
- 整体任务状态为 `PENDING` / `RUNNING` / `COMPLETED`(无 `FAILED` 值)。
|
||||
- 单个文档可能解析失败(如 `PARSE_FAILED`),此时 CLI 以非零退出码报错,服务端消息原样透传。
|
||||
- 如果服务端对空闲知识库返回 `SystemError`,说明该 job 可能不存在。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看任务状态
|
||||
kscli doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
|
||||
# 轮询等待完成,10 秒间隔
|
||||
kscli doc status --index-id idx-xxx --job-id job-xxx --wait --poll-interval 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ---------------------------------------------------------------- |
|
||||
| `--file <path>` | array | 是 | 本地文件或目录路径(可重复)。目录递归扫描,不支持的格式自动跳过 |
|
||||
| `--index-id <id>` | string | 否 | 上传后导入到此知识库(所有文件合并为一个导入任务) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:工作区默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复),应用到每个上传的文件 |
|
||||
| `--wait` | switch | 否 | 轮询导入任务直到终态(需要 `--index-id`) |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--wait` 要求同时指定 `--index-id`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
intro.md file-xxx registered
|
||||
job: job-xxx
|
||||
status: COMPLETED
|
||||
|
||||
Uploaded 1 file.
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回自定义结构,包含 `files`(路径和 fileId)、`skipped`、`index_id`、`ingestion_id`、`final_status`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 上传管道:申请 lease → PUT 到 OSS → 注册文件 →(可选)创建导入任务。
|
||||
- 目录递归扫描,`node_modules`、`.git` 等自动跳过。
|
||||
- 多文件按顺序处理(无并发),避免 OSS 限流。
|
||||
- 支持的文件格式:`.pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`
|
||||
- 部分文件上传失败时,已注册的 fileId 会在错误 hint 中列出。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 上传单个文件
|
||||
kscli doc upload --file ./a.md --workspace-id ws-xxx
|
||||
|
||||
# 上传多个文件并导入到知识库,等待完成
|
||||
kscli doc upload --file ./a.md --file ./b.pdf --index-id idx-xxx --wait
|
||||
|
||||
# 上传整个目录
|
||||
kscli doc upload --file ./docs/ --workspace-id ws-xxx
|
||||
|
||||
# 干跑预览(查看将上传和跳过的文件)
|
||||
kscli doc upload --file ./docs/ --dry-run --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------ |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
|
||||
| `--yes` | switch | 否 | 显式确认高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: 2 document(s)
|
||||
doc-a
|
||||
doc-b
|
||||
```
|
||||
|
||||
quiet 模式:每行一个已删除的 `doc_id`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID 列表。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 只从知识库索引中移除文档,数据中心源文件不受影响(用 `file delete` 删除源文件)。
|
||||
- `doc_id` 应从 `doc list --quiet` 获取,而非 `doc upload` 返回的 `fileId`。
|
||||
- 删除是异步的:服务端立即返回 Success,但 `doc list` 中可能仍显示该文档(约 30 秒后传播完成)。
|
||||
- 输出的是服务端实际删除的 ID 列表,可能与请求的数量不一致(会在 stderr 警告)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除单个文档
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
|
||||
|
||||
# 用户明确确认后批量删除
|
||||
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| `--doc-id <id>` | array | 是 | 数据中心文件 ID(可重复,最多 20 个/次) |
|
||||
| `--tag <text>` | array | 是 | 标签(可重复),应用到每个 `--doc-id` |
|
||||
| `--mode <mode>` | string | 否 | 更新模式:`append`(默认,追加)或 `overwrite`(覆盖) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--doc-id` 最多 20 个/次
|
||||
- `--tag` 最多 100 个
|
||||
- 每个标签最多 32 字符
|
||||
- 标签总长度最多 700 字符
|
||||
- `--mode` 只能是 `append` 或 `overwrite`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
tagged: 2 file(s) with [project-a, draft]
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 同一组标签应用到所有 `--doc-id`;不同标签集需多次执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 追加标签
|
||||
kscli doc tag --doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx
|
||||
|
||||
# 覆盖标签
|
||||
kscli doc tag --doc-id file-a --doc-id file-b --tag final --mode overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ------ | ---- | ------------------------------------- |
|
||||
| `--bucket <name>` | string | 是 | 已授权的 OSS bucket 名称 |
|
||||
| `--region <id>` | string | 是 | OSS region ID(如 `cn-beijing`) |
|
||||
| `--oss-key <key>` | array | 是 | OSS 对象 key(可重复,最多 10 个/次) |
|
||||
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:默认分类) |
|
||||
| `--tag <text>` | array | 否 | 文件标签(可重复,最多 10 个) |
|
||||
| `--overwrite` | switch | 否 | 覆盖之前从相同 OSS key 导入的文件 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--oss-key` 最多 10 个/次
|
||||
- `--tag` 最多 10 个
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
imported: 2 file(s)
|
||||
file-a SUCCESS docs/a.pdf
|
||||
file-b SUCCESS docs/b.docx
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.addFileResultList[]` 包含每个文件的 fileId、status 和 ossKey。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- bucket 必须事先授权给平台服务角色(RAM 中的 `AliyunServiceRoleForBailian`)。
|
||||
- 文件名取自 OSS key 的 basename。
|
||||
- `--overwrite` 会替换之前导入的文件并生成**新的 fileId**(旧 fileId 失效)。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 导入单个文件
|
||||
kscli doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx
|
||||
|
||||
# 导入多个文件并覆盖
|
||||
kscli doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,157 @@
|
||||
# 数据中心文件管理命令手册
|
||||
|
||||
数据中心是知识库文件的存储层。文件通过 `doc upload` 或 `doc import-oss` 进入数据中心,再导入到知识库。数据中心文件可被多个知识库引用。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------------------------- |
|
||||
| `--category-id <id>` | string | 是 | 分类 ID(通过 `category list` 或 `file get` 获取) |
|
||||
| `--name <text>` | string | 否 | 按文件名过滤 |
|
||||
| `--file-id <id>` | array | 否 | 按文件 ID 过滤(可重复) |
|
||||
| `--next-token <token>` | string | 否 | 游标分页令牌(从上次输出获取) |
|
||||
| `--max-result <n>` | number | 否 | 每页条数 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
file-xxx SUCCESS intro.md 1024
|
||||
next: --next-token eyJ...
|
||||
```
|
||||
|
||||
quiet 模式:每行一个 `fileId`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--category-id` 必须是真实的分类 ID。与上传 API 不同,字面量 `default` 在此不被解析,传入会返回空列表。通过 `file get` 的 category 字段或 `category list` 获取真实 ID。
|
||||
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出分类下文件
|
||||
kscli file list --category-id cate-xxx --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤
|
||||
kscli file list --category-id cate-xxx --name report
|
||||
|
||||
# 翻页
|
||||
kscli file list --category-id cate-xxx --next-token eyJ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | --------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
id: file-xxx
|
||||
name: intro.md
|
||||
type: md
|
||||
size: 1024
|
||||
status: SUCCESS
|
||||
parser: AUTO_SELECT
|
||||
category: cate-xxx
|
||||
uploaded: 2026-01-01T00:00:00Z
|
||||
tags: project-a, draft
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 无特殊注意事项。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看文件详情
|
||||
kscli file get --file-id file-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ---------------------- |
|
||||
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: file-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:如果知识库引用了此文件,相关文档索引会失效。
|
||||
- 与 `doc delete` 的区别:`doc delete` 只从单个知识库索引中移除文档,数据中心源文件保留;`file delete` 删除源文件本身,影响所有引用它的知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除文件(交互确认)
|
||||
kscli file delete --file-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
kscli file delete --file-id file-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,342 @@
|
||||
# 知识库管理命令手册
|
||||
|
||||
知识库(Knowledge Base / pipeline / index)是 RAG 的核心载体,存储文档解析后的向量索引。本组命令覆盖知识库的创建、查看、更新、删除和监控。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb list [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | ------ | ---- | --------------------------------- |
|
||||
| `--name <text>` | string | 否 | 按知识库名称模糊过滤(1-20 字符) |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:20,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:每行一个知识库,字段以双空格分隔,末尾显示总数。
|
||||
|
||||
```
|
||||
idx-xxx my-kb text-embedding-v4 600 product docs
|
||||
total: 1
|
||||
```
|
||||
|
||||
quiet 模式:每行一个知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 返回的 `id` 字段作为后续命令的 `--index-id` 使用。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出所有知识库
|
||||
kscli kb list --workspace-id ws-xxx
|
||||
|
||||
# 按名称过滤,第二页
|
||||
kscli kb list --name demo --page-number 2 --page-size 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:按诊断维度分组展示。
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: idx-xxx
|
||||
name: my-kb
|
||||
description: product docs
|
||||
dataType: ...
|
||||
Indexing: [immutable — recreate required to change]
|
||||
embeddingModelName: text-embedding-v4
|
||||
embeddingDimension: 1024
|
||||
chunkSize: 600
|
||||
overlapSize: ...
|
||||
chunkMode: ...
|
||||
separator: ...
|
||||
Retrieval:
|
||||
rerankModelName: ...
|
||||
rerankMinScore: ...
|
||||
rerankTopN: ...
|
||||
rerankMode: ...
|
||||
enableRewrite: ...
|
||||
denseSimilarityTopK: ...
|
||||
sparseSimilarityTopK: ...
|
||||
Data:
|
||||
sourceType: ...
|
||||
connectorId: ...
|
||||
```
|
||||
|
||||
quiet 模式:输出知识库 ID。
|
||||
|
||||
json 模式:返回知识库完整配置 JSON。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看知识库详情
|
||||
kscli kb info --index-id idx-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 知识库名称(1-20 字符,工作区内唯一) |
|
||||
| `--description <text>` | string | 是 | 知识库装了什么内容、给谁用(1-500 字符) |
|
||||
| `--doc-id <id>` | array | 否¹ | 数据中心文件 ID(可重复);与 `--category-id` 互斥 |
|
||||
| `--category-id <id>` | array | 否¹ | 按分类导入该分类下所有文件(可重复);与 `--doc-id` 互斥 |
|
||||
| `--embedding-model <name>` | string | 否 | 向量模型名称(默认:`text-embedding-v4`) |
|
||||
| `--chunk-size <n>` | number | 否 | 切片大小,字符数(默认:600,建议 300-800) |
|
||||
| `--wait` | switch | 否 | 轮询初始导入任务直到终态 |
|
||||
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数(默认:5) |
|
||||
|
||||
> ¹ `--doc-id` 和 `--category-id` 二选一,必须提供其一。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--description` 长度 1-500 字符,缺失或超长会在本地被拦截
|
||||
- `--doc-id` 和 `--category-id` 互斥,必须提供其一
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
index_id: idx-xxx
|
||||
ingestion_id: job-xxx
|
||||
status: COMPLETED
|
||||
Next: check the import job status, then search against this knowledge base.
|
||||
```
|
||||
|
||||
quiet 模式:只输出知识库 ID。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `pipelineId`(知识库 ID)和 `ingestionId`(导入任务 ID)。`--wait` 时追加 `final_status` 字段。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 结构/存储类型固定为默认文档知识库(非结构化,BUILT_IN 存储)。
|
||||
- 返回知识库 ID(`pipelineId`)和初始导入任务 ID(`ingestionId`)。
|
||||
- 使用 `doc status` 或 `--wait` 跟踪导入进度。
|
||||
- 如果 `--wait` 后部分文档解析失败,CLI 以非零退出码报错,知识库已创建成功的事实会在 hint 中提示。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 从指定文件创建知识库
|
||||
kscli kb create --name demo --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx
|
||||
|
||||
# 从分类导入并等待导入完成
|
||||
kscli kb create --name demo --description '产品文档' --category-id cate-xxx --wait
|
||||
|
||||
# 指定向量模型和切片大小
|
||||
kscli kb create --name my-kb --description '产品文档 v2' --doc-id file-a --doc-id file-b --embedding-model text-embedding-v4 --chunk-size 400 --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------------------- | ------ | ---- | -------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--name <text>` | string | 否 | 新名称(1-20 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述 |
|
||||
| `--rerank-min-score <score>` | number | 否 | rerank 最低分数阈值,范围 0-1(低于此分的 chunk 被过滤) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供 `--name`、`--description`、`--rerank-min-score` 之一,否则报错 "Nothing to update"
|
||||
- `--name` 长度 1-20 字符
|
||||
- `--rerank-min-score` 范围 0-1
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 更新描述
|
||||
kscli kb update --index-id idx-xxx --description "product docs v2" --workspace-id ws-xxx
|
||||
|
||||
# 调整 rerank 阈值
|
||||
kscli kb update --index-id idx-xxx --rerank-min-score 0.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: idx-xxx
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **不可逆操作**:知识库及所有索引内容被永久删除。
|
||||
- 数据中心中的源文件不受影响,仅删除知识库索引。
|
||||
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
kscli kb delete --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
kscli kb delete --index-id idx-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli kb stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--start <time>` | string | 否 | 范围起始:Unix 秒或 ISO 日期(默认:24 小时前) |
|
||||
| `--end <time>` | string | 否 | 范围结束:Unix 秒或 ISO 日期(默认:当前时间) |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
plan: ...
|
||||
storage: 100 / 1000
|
||||
peak qps: 5
|
||||
qps windows: 24 data point(s)
|
||||
```
|
||||
|
||||
quiet 模式:输出 json 格式。
|
||||
|
||||
json 模式:返回 API 原始响应,包含 `storageMonitorData` 和 `qpsMonitorData`。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 默认查询最近 24 小时数据。
|
||||
- 时间戳自动转换为 epoch 秒(API 要求秒级字符串)。13 位毫秒时间戳会自动降为秒。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看最近 24 小时监控
|
||||
kscli kb stats --index-id idx-xxx --workspace-id ws-xxx
|
||||
|
||||
# 指定日期范围
|
||||
kscli kb stats --index-id idx-xxx --start 2026-07-30 --end 2026-07-31
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,929 @@
|
||||
# `kscli` 命令完整用法指南
|
||||
|
||||
> Knowledge Studio CLI(`kscli`)命令总览,覆盖全部 37 个命令:34 个知识库命令 + 3 个配置/维护命令。完整参数与示例请参阅各子域手册。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [核心概念与实体关系](#核心概念与实体关系)
|
||||
3. [通用约定](#通用约定)
|
||||
4. [典型工作流](#典型工作流)
|
||||
5. [命令手册](#命令手册)
|
||||
- [知识库管理](#知识库管理) → [完整手册](kb.md)
|
||||
- [文档管理](#文档管理) → [完整手册](doc.md)
|
||||
- [检索服务管理](#检索服务管理) → [完整手册](service.md)
|
||||
- [Chunk 管理](#chunk-管理) → [完整手册](chunk.md)
|
||||
- [数据中心文件管理](#数据中心文件管理) → [完整手册](file.md)
|
||||
- [数据中心集合与分类](#数据中心集合与分类) → [完整手册](collection-category.md)
|
||||
- [检索与对话](#检索与对话) → [完整手册](search-chat.md)
|
||||
- [配置与维护](#配置与维护)
|
||||
6. [常见错误与排查](#常见错误与排查)
|
||||
7. [附录:命令速查表](#附录命令速查表)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
`kscli`(`knowledge-studio-cli`)是面向 RAG 开发者的知识库专用 CLI,把知识库能力铺平成一级命令组,覆盖 RAG(检索增强生成)全链路:
|
||||
|
||||
- **知识库全生命周期管理**:创建、查看、更新、删除、监控
|
||||
- **文档管理**:上传本地文件或目录、从 OSS 批量导入、查看解析状态、删除、打标签
|
||||
- **Chunk 级运维**:直接增删改查知识库中的内容切片
|
||||
- **检索服务管理**:创建/部署/复制/删除 Q&A 和检索服务(agent),管理 draft 与发布版本
|
||||
- **数据中心管理**:文件、集合(connector)、分类的增删查
|
||||
- **检索与对话**:语义检索(search)、多轮对话(chat)、兼容旧检索(retrieve)
|
||||
- **配置与维护**:查看/修改本地配置、自更新 CLI
|
||||
|
||||
共 37 个命令:34 个知识库命令(按功能域分为 7 组)+ `config show` / `config set` / `update`。所有知识库命令均使用 DashScope API Key 鉴权。
|
||||
|
||||
> **与 `bl` 的关系**:`kscli` 与 `bl knowledge` 复用同一套命令实现,flag 名、行为逻辑、校验规则完全一致,只有命令路径不同 —— `kscli` 把知识库能力铺平(`kscli kb list`、`kscli file list`),`bl` 则把它们收在 `bl knowledge` 之下。用 `bl` 的读者请参阅 [`bl knowledge` 指南](../knowledge/knowledge-cli-guide.md)。
|
||||
|
||||
安装与运行:
|
||||
|
||||
```bash
|
||||
# 免安装执行(推荐,版本可控)
|
||||
npx knowledge-studio-cli@latest --help
|
||||
|
||||
# 全局安装后使用 kscli
|
||||
npm install -g knowledge-studio-cli
|
||||
kscli --help
|
||||
```
|
||||
|
||||
> 后文示例统一写作 `kscli <command>`;若未全局安装,把 `kscli` 换成 `npx knowledge-studio-cli@latest` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念与实体关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据中心 (Data Center) │
|
||||
│ │
|
||||
│ 集合 (Collection) ──┬── 分类 (Category) ── 文件 (File) │
|
||||
│ │ "connector" 可多级嵌套 │
|
||||
│ └── 默认分类 │
|
||||
│ │
|
||||
│ 文件来源:doc upload(本地上传) / doc import-oss(OSS导入) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 导入 (import job)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 知识库 (Knowledge Base) │
|
||||
│ │
|
||||
│ 知识库 (KB / pipeline / index) │
|
||||
│ ├── 文档 (Doc) ── 解析状态: PENDING/RUNNING/COMPLETED │
|
||||
│ │ └── Chunk ── 内容切片,可增删改查、排除/恢复检索 │
|
||||
│ └── 索引设置 (immutable): 向量模型、切片大小等 │
|
||||
│ │
|
||||
│ 知识库管理命令: kb create / list / info / update / delete / stats │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│ 绑定 (agent_config.kb_search_configs)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 检索服务 (Service / Agent) │
|
||||
│ │
|
||||
│ Service (agent) │
|
||||
│ ├── scene: chat (Q&A) 或 search (检索) │
|
||||
│ ├── 版本: beta (草稿) → 1, 2, 3... (已发布) │
|
||||
│ ├── 状态: draft → deployed → edited → deleted │
|
||||
│ └── 配置: 模型、温度、策略、rerank 等 │
|
||||
│ │
|
||||
│ 消费方式: search (语义检索) / chat (多轮对话) │
|
||||
│ 管理命令: create / update / deploy / copy / delete / list / get │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键关系**:
|
||||
|
||||
- **数据中心文件 → 知识库**:通过 `kscli kb create --doc-id` 或 `kscli doc upload --index-id` 导入,文件解析后自动生成 chunk
|
||||
- **知识库 → 检索服务**:一个服务可绑定多个知识库,服务配置中 `kb_search_configs` 指定关联的知识库 ID
|
||||
- **检索服务 → 检索/对话**:`kscli search` 和 `kscli chat` 通过 `--agent-id` 指定服务来执行检索或对话
|
||||
|
||||
---
|
||||
|
||||
## 通用约定
|
||||
|
||||
### 鉴权
|
||||
|
||||
所有知识库命令均使用 **DashScope API Key**(Bearer token)鉴权。获取方式:百炼控制台 API Key 页面。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--api-key <key>` 命令行参数
|
||||
2. `DASHSCOPE_API_KEY` 环境变量
|
||||
3. 配置文件中的 `api_key`(`kscli config set --key api_key --value <key>`)
|
||||
|
||||
### Workspace ID
|
||||
|
||||
知识库 API 使用 workspace 级域名(`{workspaceId}.cn-beijing.maas.aliyuncs.com`),因此 **几乎所有知识库命令都需要 workspace ID**。
|
||||
|
||||
优先级(高 → 低):
|
||||
|
||||
1. `--workspace-id <id>` 命令行参数
|
||||
2. `BAILIAN_WORKSPACE_ID` 环境变量
|
||||
3. 配置文件中的 `workspace_id`(`kscli config set --key workspace_id --value <id>`)
|
||||
|
||||
缺失时报错:`Workspace ID is required.`
|
||||
|
||||
### 全局通用参数
|
||||
|
||||
以下参数在所有知识库命令中通用,后续命令手册中不再逐条列出:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------------------- | ------ | ----------------------------------------------------------- |
|
||||
| `--output <format>` | string | 输出格式:`text`(默认,人类友好)或 `json`(API 原始响应) |
|
||||
| `--api-key <key>` | string | DashScope API Key |
|
||||
| `--base-url <url>` | string | API 基地址(一般不需要指定) |
|
||||
| `--timeout <seconds>` | number | 请求超时秒数 |
|
||||
| `--quiet` | switch | 静默模式,只输出关键结果(如 ID 列表) |
|
||||
| `--verbose` | switch | 详细模式,打印 HTTP 请求/响应详情到 stderr |
|
||||
| `--dry-run` | switch | 干跑模式,预览将发送的请求结构,不实际调用 API |
|
||||
| `--config <name>` | string | 使用指定配置 profile 执行命令 |
|
||||
|
||||
> **注意**:命令手册中每个命令的参数表只列出该命令**特有**的参数。上述全局参数对所有命令有效。
|
||||
|
||||
### 输出格式约定
|
||||
|
||||
- **text 模式**(默认):人类友好的表格/结构化文本,适合终端查看。不同命令的输出格式见各命令的「输出」部分。
|
||||
- **json 模式**(`--output json`):返回 API 原始 JSON 响应,适合程序化处理和 agent 解析。
|
||||
- **quiet 模式**(`--quiet`):只输出最精简的结果(通常只有 ID),适合管道串联。
|
||||
|
||||
### 危险操作确认
|
||||
|
||||
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
|
||||
|
||||
### Dry-run 模式
|
||||
|
||||
`--dry-run` 模式下,命令会输出将发送的 endpoint 和 request body,但**不实际发起网络请求**。部分命令在 dry-run 下仍会执行本地校验(如文件扩展名检查、参数约束检查)。
|
||||
|
||||
---
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 场景 A:从零搭建知识库并检索
|
||||
|
||||
```bash
|
||||
# 1. 上传本地文件到数据中心
|
||||
kscli doc upload --file ./docs/intro.md --workspace-id ws-xxx
|
||||
# → 返回 file-id
|
||||
|
||||
# 2. 用文件创建知识库
|
||||
kscli kb create --name my-kb --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx --wait
|
||||
# → 返回 index-id (pipelineId) 和导入任务状态
|
||||
|
||||
# 3. 创建检索服务(search 场景)
|
||||
kscli service create --name my-search --scene search --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 agent-id
|
||||
|
||||
# 4. 部署服务
|
||||
kscli service deploy --agent-id aid-xxx --workspace-id ws-xxx --yes
|
||||
|
||||
# 5. 执行检索
|
||||
kscli search --query "什么是RAG" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 B:上传目录并导入到已有知识库
|
||||
|
||||
```bash
|
||||
# 1. 上传整个目录到数据中心并直接导入到知识库(一步到位)
|
||||
kscli doc upload --file ./docs/ --index-id idx-xxx --workspace-id ws-xxx --wait
|
||||
# → 文件逐个上传到 OSS → 注册到数据中心 → 创建合并导入任务 → 轮询到完成
|
||||
|
||||
# 2. 检查文档状态
|
||||
kscli doc list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 查看 doc_id 和解析状态
|
||||
|
||||
# 3. 如果有文档解析失败,查看导入任务详情
|
||||
kscli doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 C:创建并部署 Q&A 服务
|
||||
|
||||
```bash
|
||||
# 1. 创建 chat 场景的检索服务
|
||||
kscli service create --name my-qa --scene chat --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 初始状态: draft, 版本: beta
|
||||
|
||||
# 2. 调整配置(如修改模型、温度)
|
||||
kscli service update --agent-id aid-xxx --model qwen-max --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 3. 用 beta 版本测试
|
||||
kscli chat --message "什么是RAG?" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
|
||||
# 4. 测试通过后发布
|
||||
kscli service deploy --agent-id aid-xxx --version-desc "首版" --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 D:知识库内容运维
|
||||
|
||||
```bash
|
||||
# 1. 查看 chunk 列表
|
||||
kscli chunk list --index-id idx-xxx --workspace-id ws-xxx
|
||||
# → 返回 metadata._id (chunk id) 和 metadata.doc_id (document id)
|
||||
|
||||
# 2. 修改 chunk 内容
|
||||
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --content "修正后的内容" --workspace-id ws-xxx
|
||||
|
||||
# 3. 排除某个 chunk 不参与检索(不删除内容)
|
||||
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --exclude --workspace-id ws-xxx
|
||||
|
||||
# 4. 手动添加新 chunk
|
||||
kscli chunk add --index-id idx-xxx --content "新增的知识片段" --title "补充说明" --workspace-id ws-xxx
|
||||
|
||||
# 5. 删除 chunk(批量,自动分批每 10 个一组)
|
||||
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --yes --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
### 场景 E:服务迁移/复用
|
||||
|
||||
```bash
|
||||
# 1. 复制现有服务为新草稿
|
||||
kscli service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
# → 返回新的 agent-id,名称加 copy_ 前缀
|
||||
|
||||
# 2. 修改新服务配置
|
||||
kscli service update --agent-id aid-new --name "改进版" --temperature 0.5 --workspace-id ws-xxx
|
||||
|
||||
# 3. 测试并发布
|
||||
kscli chat --message "测试" --agent-id aid-new --agent-version beta --workspace-id ws-xxx
|
||||
kscli service deploy --agent-id aid-new --workspace-id ws-xxx --yes
|
||||
```
|
||||
|
||||
### 场景 F:从 OSS 批量导入文件
|
||||
|
||||
```bash
|
||||
# 1. 从已授权的 OSS bucket 批量导入文件到数据中心
|
||||
kscli doc import-oss \
|
||||
--bucket my-bucket --region cn-beijing \
|
||||
--oss-key docs/a.pdf --oss-key docs/b.docx \
|
||||
--workspace-id ws-xxx
|
||||
# → 返回各文件的 fileId
|
||||
|
||||
# 2. 创建知识库并导入这些文件
|
||||
kscli kb create --name oss-kb --description 'OSS 导入文档' --doc-id file-a --doc-id file-b --workspace-id ws-xxx --wait
|
||||
|
||||
# 3. 检索
|
||||
kscli search --query "相关内容" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 命令手册
|
||||
|
||||
以下按功能域分组,覆盖全部 37 个命令。每个条目包含功能说明、用法签名和详细手册链接。
|
||||
|
||||
> 完整参数表、参数约束、输出说明、注意事项与示例请参阅各子域手册。
|
||||
|
||||
---
|
||||
|
||||
### 知识库管理
|
||||
|
||||
> 📖 [完整手册](kb.md) — 6 个命令
|
||||
|
||||
#### `kscli kb list`
|
||||
|
||||
列出工作区中的知识库。
|
||||
|
||||
```bash
|
||||
kscli kb list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb info`
|
||||
|
||||
查看知识库配置详情。
|
||||
|
||||
```bash
|
||||
kscli kb info --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-info)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb create`
|
||||
|
||||
创建知识库并导入数据中心文件或分类。
|
||||
|
||||
```bash
|
||||
kscli kb create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb update`
|
||||
|
||||
更新知识库名称、描述或 rerank 阈值。
|
||||
|
||||
```bash
|
||||
kscli kb update --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb delete`
|
||||
|
||||
删除知识库及其所有文档和 chunk。
|
||||
|
||||
```bash
|
||||
kscli kb delete --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli kb stats`
|
||||
|
||||
查看知识库存储和 QPS 监控数据。
|
||||
|
||||
```bash
|
||||
kscli kb stats --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](kb.md#kscli-kb-stats)
|
||||
|
||||
---
|
||||
|
||||
### 文档管理
|
||||
|
||||
> 📖 [完整手册](doc.md) — 6 个命令
|
||||
|
||||
#### `kscli doc list`
|
||||
|
||||
列出知识库中的文档及其解析/索引状态。
|
||||
|
||||
```bash
|
||||
kscli doc list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc status`
|
||||
|
||||
查看知识库导入任务状态。
|
||||
|
||||
```bash
|
||||
kscli doc status --index-id <id> --job-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-status)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc upload`
|
||||
|
||||
上传本地文件或目录到数据中心,可选导入到知识库。
|
||||
|
||||
```bash
|
||||
kscli doc upload --file <path> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-upload)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc delete`
|
||||
|
||||
从知识库中删除文档及其 chunk。
|
||||
|
||||
```bash
|
||||
kscli doc delete --index-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc tag`
|
||||
|
||||
批量更新数据中心文件的标签。
|
||||
|
||||
```bash
|
||||
kscli doc tag --doc-id <id> --tag <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-tag)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli doc import-oss`
|
||||
|
||||
从已授权的 OSS bucket 批量导入文件到数据中心。
|
||||
|
||||
```bash
|
||||
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](doc.md#kscli-doc-import-oss)
|
||||
|
||||
---
|
||||
|
||||
### 检索服务管理
|
||||
|
||||
> 📖 [完整手册](service.md) — 7 个命令
|
||||
|
||||
#### `kscli service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
```bash
|
||||
kscli service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
```bash
|
||||
kscli service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
```bash
|
||||
kscli service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
```bash
|
||||
kscli service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
```bash
|
||||
kscli service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-deploy)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
```bash
|
||||
kscli service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-delete)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
```bash
|
||||
kscli service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](service.md#kscli-service-copy)
|
||||
|
||||
---
|
||||
|
||||
### Chunk 管理
|
||||
|
||||
> 📖 [完整手册](chunk.md) — 4 个命令
|
||||
|
||||
#### `kscli chunk add`
|
||||
|
||||
直接向知识库添加 chunk。
|
||||
|
||||
```bash
|
||||
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](chunk.md#kscli-chunk-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk list`
|
||||
|
||||
列出知识库中的 chunk,含内容和状态。
|
||||
|
||||
```bash
|
||||
kscli chunk list --index-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](chunk.md#kscli-chunk-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk update`
|
||||
|
||||
更新 chunk 内容或切换其检索可见性。
|
||||
|
||||
```bash
|
||||
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](chunk.md#kscli-chunk-update)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chunk delete`
|
||||
|
||||
从知识库中删除 chunk(不可逆)。
|
||||
|
||||
```bash
|
||||
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](chunk.md#kscli-chunk-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心文件管理
|
||||
|
||||
> 📖 [完整手册](file.md) — 3 个命令
|
||||
|
||||
#### `kscli file list`
|
||||
|
||||
列出数据中心分类下的文件。
|
||||
|
||||
```bash
|
||||
kscli file list --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](file.md#kscli-file-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file get`
|
||||
|
||||
查看数据中心文件详情。
|
||||
|
||||
```bash
|
||||
kscli file get --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](file.md#kscli-file-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli file delete`
|
||||
|
||||
从数据中心永久删除文件。
|
||||
|
||||
```bash
|
||||
kscli file delete --file-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](file.md#kscli-file-delete)
|
||||
|
||||
---
|
||||
|
||||
### 数据中心集合与分类
|
||||
|
||||
> 📖 [完整手册](collection-category.md) — 5 个命令
|
||||
|
||||
#### `kscli collection create`
|
||||
|
||||
创建 FILE 数据集合。
|
||||
|
||||
```bash
|
||||
kscli collection create --name <text> --description <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](collection-category.md#kscli-collection-create)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli collection get`
|
||||
|
||||
查看数据集合详情。
|
||||
|
||||
```bash
|
||||
kscli collection get (--collection-id <id> | --name <text>) [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](collection-category.md#kscli-collection-get)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category list`
|
||||
|
||||
列出数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category list [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](collection-category.md#kscli-category-list)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category add`
|
||||
|
||||
创建数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category add --name <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](collection-category.md#kscli-category-add)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli category delete`
|
||||
|
||||
删除数据中心分类。
|
||||
|
||||
```bash
|
||||
kscli category delete --category-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](collection-category.md#kscli-category-delete)
|
||||
|
||||
---
|
||||
|
||||
### 检索与对话
|
||||
|
||||
> 📖 [完整手册](search-chat.md) — 3 个命令
|
||||
|
||||
#### `kscli retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
```bash
|
||||
kscli retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](search-chat.md#kscli-retrieve)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
```bash
|
||||
kscli search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](search-chat.md#kscli-search)
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
```bash
|
||||
kscli chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
→ [完整参数与示例](search-chat.md#kscli-chat)
|
||||
|
||||
---
|
||||
|
||||
### 配置与维护
|
||||
|
||||
这 3 个命令不调用知识库 API,用于管理本地配置与 CLI 自身版本。配置文件默认位于 `~/.bailian/config.json`(可用 `BAILIAN_CONFIG_DIR` 改写目录)。
|
||||
|
||||
#### `kscli config show`
|
||||
|
||||
显示当前生效配置(含 base_url、output、timeout、profile 名和配置文件路径;密钥类字段自动脱敏)。
|
||||
|
||||
```bash
|
||||
kscli config show [--output json]
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 查看当前配置
|
||||
kscli config show
|
||||
|
||||
# JSON 输出,便于脚本解析
|
||||
kscli config show --output json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli config set`
|
||||
|
||||
写入一个配置项到配置文件。
|
||||
|
||||
```bash
|
||||
kscli config set --key <key> --value <value>
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--key <key>` | string | 是 | 配置项名称:`language`、`base_url`、`output`、`output_dir`、`timeout`、`api_key`、`access_token`、`access_key_id`、`access_key_secret`、`security_token`、`default_*_model`、`workspace_id` |
|
||||
| `--value <value>` | string | 是 | 要写入的值(按 key 类型校验并转换) |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 持久化 API Key
|
||||
kscli config set --key api_key --value sk-xxx
|
||||
|
||||
# 持久化 workspace,省去每次传 --workspace-id
|
||||
kscli config set --key workspace_id --value ws-xxx
|
||||
|
||||
# 默认输出 JSON
|
||||
kscli config set --key output --value json
|
||||
```
|
||||
|
||||
**注意事项**
|
||||
|
||||
- `--dry-run` 只打印将写入的键值和配置文件路径,不落盘。
|
||||
- 密钥类字段(`api_key`、`access_token` 等)在回显时被掩码。
|
||||
- 配合 `--config <name>` 可写入指定 profile。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli update`
|
||||
|
||||
将 CLI 自更新到最新版本,或用 `--to` 指定目标版本。
|
||||
|
||||
```bash
|
||||
kscli update [--to <version>]
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | ---- | ------------------------------------------------------------------------- |
|
||||
| `--to <version>` | string | 否 | 目标版本(semver,如 `1.13.0` / `v1.13.0` / `0.0.0-beta-<sha>-<时间戳>`) |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 更新到最新版
|
||||
kscli update
|
||||
|
||||
# 回滚/固定到指定版本
|
||||
kscli update --to 1.13.0
|
||||
```
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 更新方式按安装来源自动选择(npm 全局安装或二进制安装)。
|
||||
- `--to` 传入非法 semver 会在本地被拦截并报错。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误与排查
|
||||
|
||||
### Workspace ID 缺失
|
||||
|
||||
**报错**:`Workspace ID is required.`
|
||||
|
||||
**原因**:所有知识库管理命令都需要 workspace ID 来构造 API 端点(`{workspaceId}.cn-beijing.maas.aliyuncs.com`)。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 方式1:命令行参数
|
||||
kscli kb list --workspace-id ws-xxx
|
||||
|
||||
# 方式2:环境变量
|
||||
export BAILIAN_WORKSPACE_ID=ws-xxx
|
||||
|
||||
# 方式3:配置文件
|
||||
kscli config set --key workspace_id --value ws-xxx
|
||||
```
|
||||
|
||||
### 知识库 ID 不存在
|
||||
|
||||
**报错**:`Knowledge base not found: idx-xxx`
|
||||
|
||||
**原因**:`--index-id` 指定的知识库在当前 workspace 中不存在。
|
||||
|
||||
**解决**:先 `kscli kb list` 确认知识库 ID。
|
||||
|
||||
### 导入任务 SystemError
|
||||
|
||||
**报错**:服务端返回 `SystemError`
|
||||
|
||||
**原因**:`doc status` 传入了不存在的 job ID,或知识库空闲无任务。
|
||||
|
||||
**解决**:检查 `doc list` 输出中的 `ingestionId`,或从 `doc upload` / `kb create` 的返回值获取。
|
||||
|
||||
### doc_id 与 fileId 混淆
|
||||
|
||||
**问题**:`doc delete` 时用了 `doc upload` 返回的 `fileId` 而非 `doc list` 返回的 `doc_id`。
|
||||
|
||||
**原因**:通过 `kb create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。
|
||||
|
||||
**解决**:始终用 `kscli doc list --quiet` 获取 `doc_id`。
|
||||
|
||||
### retrieve 已废弃
|
||||
|
||||
**问题**:`retrieve` 命令输出废弃警告。
|
||||
|
||||
**解决**:改用 `search` 命令。`search` 通过 `--agent-id` 驱动检索策略,支持多知识库、路由、rerank 等高级特性。`retrieve` 直接操作 `--index-id`,功能受限且不再迭代。
|
||||
|
||||
### OSS 导入权限错误
|
||||
|
||||
**报错**:服务端返回权限相关错误。
|
||||
|
||||
**原因**:OSS bucket 未授权给平台服务角色。
|
||||
|
||||
**解决**:检查 RAM 控制台中的 `AliyunServiceRoleForBailian` 角色是否已正确授权。
|
||||
|
||||
### Chat SSE error
|
||||
|
||||
**报错**:`Chat API error` + API error code。
|
||||
|
||||
**原因**:流式对话过程中服务端返回 error 事件。
|
||||
|
||||
**解决**:检查 `--agent-id` 是否存在、服务是否已部署、API Key 是否有效。错误消息和 code 原样透传,不二次包装。
|
||||
|
||||
### file list 返回空
|
||||
|
||||
**问题**:`file list --category-id default` 返回空列表。
|
||||
|
||||
**原因**:与上传 API 不同,`file list` 不解析字面量 `default`,需要真实分类 ID。
|
||||
|
||||
**解决**:通过 `file get` 的 category 字段或 `category list` 获取真实分类 ID。
|
||||
|
||||
### 集合无法删除
|
||||
|
||||
**问题**:没有 `collection delete` 命令。
|
||||
|
||||
**原因**:暂不支持通过 CLI 删除。
|
||||
|
||||
**解决**:创建集合需谨慎。如需隔离,创建新集合并迁移文件。
|
||||
|
||||
---
|
||||
|
||||
## 附录:命令速查表
|
||||
|
||||
| 命令 | 功能 | 关键参数 |
|
||||
| ------------------------- | ------------ | ----------------------------------------------------------- |
|
||||
| `kscli kb list` | 列出知识库 | `--name` |
|
||||
| `kscli kb info` | 知识库详情 | `--index-id` |
|
||||
| `kscli kb create` | 创建知识库 | `--name`, `--description`, `--doc-id`/`--category-id` |
|
||||
| `kscli kb update` | 更新知识库 | `--index-id`, `--name`/`--description`/`--rerank-min-score` |
|
||||
| `kscli kb delete` | 删除知识库 | `--index-id`, `--yes` |
|
||||
| `kscli kb stats` | 监控数据 | `--index-id`, `--start`/`--end` |
|
||||
| `kscli doc list` | 文档列表 | `--index-id` |
|
||||
| `kscli doc status` | 导入任务状态 | `--index-id`, `--job-id`, `--wait` |
|
||||
| `kscli doc upload` | 上传文件 | `--file`, `--index-id`, `--wait` |
|
||||
| `kscli doc delete` | 删除文档 | `--index-id`, `--doc-id` |
|
||||
| `kscli doc tag` | 文件打标签 | `--doc-id`, `--tag`, `--mode` |
|
||||
| `kscli doc import-oss` | OSS 导入 | `--bucket`, `--region`, `--oss-key` |
|
||||
| `kscli service list` | 服务列表 | `--scene` |
|
||||
| `kscli service get` | 服务详情 | `--agent-id` |
|
||||
| `kscli service create` | 创建服务 | `--name`, `--scene`, `--index-id` |
|
||||
| `kscli service update` | 更新服务 | `--agent-id`, 配置参数 |
|
||||
| `kscli service deploy` | 发布服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service delete` | 删除服务 | `--agent-id`, `--yes` |
|
||||
| `kscli service copy` | 复制服务 | `--agent-id` |
|
||||
| `kscli chunk add` | 添加 chunk | `--index-id`, `--content`/`--field` |
|
||||
| `kscli chunk list` | chunk 列表 | `--index-id`, `--doc-id` |
|
||||
| `kscli chunk update` | 更新 chunk | `--index-id`, `--chunk-id`, `--doc-id` |
|
||||
| `kscli chunk delete` | 删除 chunk | `--index-id`, `--chunk-id`, `--yes` |
|
||||
| `kscli file list` | 文件列表 | `--category-id` |
|
||||
| `kscli file get` | 文件详情 | `--file-id` |
|
||||
| `kscli file delete` | 删除文件 | `--file-id`, `--yes` |
|
||||
| `kscli collection create` | 创建集合 | `--name`, `--description` |
|
||||
| `kscli collection get` | 集合详情 | `--collection-id`/`--name` |
|
||||
| `kscli category list` | 分类列表 | `--collection-id`, `--parent-id` |
|
||||
| `kscli category add` | 创建分类 | `--name`, `--parent-id` |
|
||||
| `kscli category delete` | 删除分类 | `--category-id`, `--yes` |
|
||||
| `kscli retrieve` | 检索(废弃) | `--index-id`, `--query` |
|
||||
| `kscli search` | 语义检索 | `--query`, `--agent-id` |
|
||||
| `kscli chat` | RAG 对话 | `--message`, `--agent-id` |
|
||||
| `kscli config show` | 查看配置 | `--output` |
|
||||
| `kscli config set` | 写入配置 | `--key`, `--value` |
|
||||
| `kscli update` | 自更新 CLI | `--to` |
|
||||
@@ -0,0 +1,218 @@
|
||||
# 检索与对话命令手册
|
||||
|
||||
以下命令通过检索服务(agent)消费知识库。`search` 用于语义检索,`chat` 用于多轮对话。`retrieve` 已废弃。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli retrieve`
|
||||
|
||||
从知识库检索(已废弃,请用 `search` 替代)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli retrieve --index-id <id> --query <text> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------- | ------ | ---- | --------------------------------------------------- |
|
||||
| `--index-id <id>` | string | 是 | 知识库 ID |
|
||||
| `--query <text>` | string | 是 | 检索查询文本 |
|
||||
| `--dense-similarity-top-k <n>` | number | 否 | 稠密检索 top K |
|
||||
| `--sparse-similarity-top-k <n>` | number | 否 | 稀疏检索 top K |
|
||||
| `--rerank` | switch | 否 | 启用 rerank |
|
||||
| `--rerank-top-n <n>` | number | 否 | rerank 返回 top N 结果 |
|
||||
| `--rerank-model <name>` | string | 否 | rerank 模型名,如 `qwen3-rerank-hybrid` |
|
||||
| `--rerank-mode <mode>` | string | 否 | rerank 模式:`qa`、`similar` 或 `custom` |
|
||||
| `--rerank-instruct <text>` | string | 否 | 自定义 rerank 指令(`--rerank-mode custom` 时使用) |
|
||||
| `--top-k <n>` | number | 否 | 返回结果数(已废弃,用 `--rerank-top-n` 替代) |
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- **已废弃**,推荐使用 `search` 命令。`search` 通过 agent_id 驱动检索策略,支持更多高级特性。
|
||||
- `--top-k` 已废弃,使用 `--rerank-top-n` 替代,传入 `--top-k` 会输出 stderr 警告。
|
||||
- 此命令直接用 `--index-id` 检索,不需要创建检索服务。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
kscli retrieve --index-id idx-xxx --query "How to use Alibaba Cloud Bailian" --workspace-id ws-xxx
|
||||
|
||||
# 启用 rerank
|
||||
kscli retrieve --index-id idx-xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli search`
|
||||
|
||||
对知识库执行语义检索(RAG 检索)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli search --query <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------- |
|
||||
| `--query <text>` | string | 是 | 检索查询文本(不可为空) |
|
||||
| `--agent-id <id>` | string | 是 | 检索服务 ID(在控制台知识检索页面获取,或通过 `service list` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复),用于多模态检索 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--query` 不可为空(API 要求 `minLength: 1`)
|
||||
|
||||
**输出**
|
||||
|
||||
text/quiet 模式:
|
||||
|
||||
```
|
||||
[1] (score: 0.9512)
|
||||
检索到的文本内容...
|
||||
|
||||
[2] (score: 0.8734)
|
||||
另一段文本内容...
|
||||
```
|
||||
|
||||
> 无结果时输出 `No results found.`
|
||||
|
||||
json 模式:返回 API 原始响应,`data.nodes[]` 包含检索结果。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 检索范围和策略(多知识库加权、路由、rerank 等)由 `--agent-id` 对应的服务配置驱动。只需 `--query` 和 `--agent-id` 即可调用。
|
||||
- `--agent-version beta` 调试草稿配置进行调试,部署前验证效果。
|
||||
- 与 `retrieve` 的区别:`search` 通过 agent_id 间接驱动检索策略(支持多知识库、路由、rerank 等),`retrieve` 直接操作 index_id 且功能较少。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 基础检索
|
||||
kscli search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态检索(带图片)
|
||||
kscli search --query "describe this image" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg
|
||||
|
||||
# 调试草稿版本
|
||||
kscli search --query "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli chat`
|
||||
|
||||
与知识库进行 RAG 对话(流式输出)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli chat --message <text> --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--message <text>` | array | 是¹ | 消息文本(可重复)。支持 `role:content` 前缀设置角色(如 `user:hello`),默认角色为 `user`。也支持完整 JSON 对象传递结构化消息 |
|
||||
| `--agent-id <id>` | string | 是 | Q&A 服务 ID(在控制台知识问答页面获取,或通过 `service list --scene chat` 查看) |
|
||||
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
|
||||
| `--image <url>` | array | 否 | 图片 URL(可重复)。附加到最后一条 user 消息作为多模态内容 |
|
||||
|
||||
> ¹ `--message` 或 `--image` 至少提供其一。纯图片查询可以只传 `--image`(CLI 会自动创建空 user 消息承载图片)。
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--message` 或 `--image` 至少提供一个
|
||||
- `--image` 不能与已包含 `image_url` 内容部分的消息同时使用
|
||||
|
||||
**输出**
|
||||
|
||||
**TTY text 模式**(实时流式):
|
||||
|
||||
```
|
||||
🔍 Retrieving...
|
||||
✍️ Generating...
|
||||
这是AI生成的回答内容,逐字流式输出...
|
||||
```
|
||||
|
||||
> 进度标签由 SSE `step_change` 事件驱动:`tool_calling`(检索中)→ `plan_start`(规划中)→ `generation_start`(生成中)。
|
||||
|
||||
**非 TTY text 模式**(缓冲输出):
|
||||
|
||||
```
|
||||
完整的回答文本...
|
||||
```
|
||||
|
||||
**json 模式**(`--output json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "完整的回答文本...",
|
||||
"request_id": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
quiet 模式:输出完整的回答文本。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- API 仅支持 SSE 流式响应。TTY 环境下实时打印 token;非 TTY 环境缓冲后输出完整文本。
|
||||
- SSE 事件生命周期:`tool_calling` → `tool_return` → `plan_start` → `planning` → `plan_end` → `generation_start` → `generating` → `generation_end`。`tool_calling` → `tool_return` 可能循环多次。
|
||||
- 多轮对话:用 `--message "user:..."` 和 `--message "assistant:..."` 传递对话历史。
|
||||
- `--agent-version beta` 调用草稿配置进行调试。
|
||||
- `--image` 附加到最后一条 user 消息上。如果消息中已包含 `image_url` 内容部分,则不能再用 `--image`。
|
||||
- `--verbose` 模式下,所有 SSE 事件详情会输出到 stderr。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 单轮对话
|
||||
kscli chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多轮对话(带历史)
|
||||
kscli chat \
|
||||
--message "user:What is RAG?" \
|
||||
--message "assistant:RAG is retrieval-augmented generation..." \
|
||||
--message "How does it work?" \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 多模态对话(带图片)
|
||||
kscli chat \
|
||||
--message "Describe these images" \
|
||||
--image https://example.com/a.png \
|
||||
--image https://example.com/b.png \
|
||||
--agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 调试草稿版本
|
||||
kscli chat --message "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -0,0 +1,401 @@
|
||||
# 检索服务管理命令手册
|
||||
|
||||
检索服务(也称 agent)是知识库的检索入口。通过 `--agent-id` 在 search/chat 命令中使用。服务有 `chat`(问答)和 `search`(检索)两种场景。
|
||||
|
||||
> **通用约定**(鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service list`
|
||||
|
||||
列出工作区中的检索/Q&A 服务。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service list --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------------- |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--status <status>` | string | 否 | 按状态过滤:`draft`、`deployed`(含 edited)、`deleted` |
|
||||
| `--name <text>` | string | 否 | 按服务名称模糊过滤 |
|
||||
| `--agent-id <id>` | string | 否 | 按精确 agent ID 过滤 |
|
||||
| `--index-id <id>` | string | 否 | 按关联知识库 ID 过滤 |
|
||||
| `--page-number <n>` | number | 否 | 页码(默认:1) |
|
||||
| `--page-size <n>` | number | 否 | 每页条数(默认:10,最大 100) |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--status` 只能是 `draft`、`deployed`、`deleted`
|
||||
- `--page-size` 范围 1-100
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
aid-xxx deployed 2 my-qa (kb: my-kb)
|
||||
total: 1
|
||||
Use an agent_id above with the chat command.
|
||||
```
|
||||
|
||||
> 最后一行根据 scene 自动提示用 `search` 还是 `chat` 命令消费。
|
||||
|
||||
quiet 模式:每行一个 `agent_id`。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 服务端要求 `--scene` 必填,要查看两种场景的服务需分别执行。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 列出 chat 服务
|
||||
kscli service list --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 只看已部署的检索服务
|
||||
kscli service list --scene search --status deployed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service get`
|
||||
|
||||
查看服务详情,含各版本配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service get --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------------------------- | ------ | ---- | --------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--agent-version <version>` | string | 否 | 指定版本查看(`beta` 或已发布版本号);不传则返回所有版本 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
Basic:
|
||||
id: aid-xxx
|
||||
name: my-qa
|
||||
desc: product Q&A
|
||||
scene: chat
|
||||
status: deployed
|
||||
Version beta:
|
||||
desc: draft
|
||||
policy: turbo
|
||||
model: qwen-max
|
||||
temperature: 0.7
|
||||
kb: idx-xxx (my-kb)
|
||||
Version 1:
|
||||
published: 2026-01-01
|
||||
...
|
||||
```
|
||||
|
||||
quiet 模式:输出 JSON 格式。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不传 `--agent-version` 时返回所有版本(beta 草稿 + 已发布版本号)。
|
||||
- 版本值原样传递,有效值集合由服务端维护。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 查看服务完整详情
|
||||
kscli service get --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 只看 beta 草稿配置
|
||||
kscli service get --agent-id aid-xxx --agent-version beta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service create`
|
||||
|
||||
创建检索/Q&A 服务,初始状态为 draft,版本为 beta。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service create --name <text> --scene <chat|search> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | ------ | ---- | ------------------------------------------------- |
|
||||
| `--name <text>` | string | 是 | 服务名称(最多 200 字符,同一场景下工作区内唯一) |
|
||||
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`(Q&A)或 `search`(检索) |
|
||||
| `--description <text>` | string | 建议 | 这个服务能回答什么、给谁用(最多 1000 字符) |
|
||||
| `--index-id <id>` | string | 否 | 绑定此知识库;其他配置使用服务端默认值 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- `--name` 最多 200 字符
|
||||
- `--scene` 只能是 `chat` 或 `search`
|
||||
- `--description` 最多 1000 字符;建议填写 —— agent 靠它判断该调用哪个服务
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
created: aid-xxx (status: draft, version: beta)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 不指定 `--index-id` 时,服务端使用默认 agent 配置。
|
||||
- beta 草稿可通过 search/chat 的 `--agent-version beta` 测试,部署后才生效。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 创建 Q&A 服务
|
||||
kscli service create --name my-qa --scene chat --workspace-id ws-xxx
|
||||
|
||||
# 创建检索服务并绑定知识库
|
||||
kscli service create --name my-search --scene search --index-id idx-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service update`
|
||||
|
||||
更新服务名称、描述或草稿配置。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service update --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------------------------ | ------ | ---- | ---------------------------------------------------------------------------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--name <text>` | string | 否 | 新名称(最多 200 字符) |
|
||||
| `--description <text>` | string | 否 | 新描述(最多 1000 字符) |
|
||||
| `--agent-version <version>` | string | 否 | 目标版本(默认:beta 草稿。已发布版本只接受 `--version-desc`) |
|
||||
| `--version-desc <text>` | string | 否 | 版本描述 |
|
||||
| `--policy <policy>` | string | 否 | Agent 策略:`turbo`(快速)或 `agentic`(多轮) |
|
||||
| `--model <name>` | string | 否 | 生成模型代码(须在平台白名单中) |
|
||||
| `--temperature <n>` | number | 否 | 采样温度,范围 0-2 |
|
||||
| `--max-llm-calls <n>` | number | 否 | 单次请求最大 LLM 调用次数,范围 1-30 |
|
||||
| `--enable-session-file <bool>` | string | 否 | 启用会话文件:`true` 或 `false` |
|
||||
| `--enable-refusal <bool>` | string | 否 | 启用拒答:`true` 或 `false` |
|
||||
| `--enable-anti-leak <bool>` | string | 否 | 启用防泄漏:`true` 或 `false` |
|
||||
| `--enable-rich-text <bool>` | string | 否 | 启用富文本输出:`true` 或 `false` |
|
||||
| `--enable-citation <bool>` | string | 否 | 启用引用标注:`true` 或 `false` |
|
||||
| `--config-file <path>` | string | 否 | JSON 文件替换整个 `agent_config`(含嵌套设置如 `kb_search_configs`);与标量配置参数互斥 |
|
||||
|
||||
**参数约束**
|
||||
|
||||
- 至少提供一个更新项(`--name`/`--description`/`--version-desc`/`--config-file`/标量配置参数),否则报错 "Nothing to update"
|
||||
- `--config-file` 与标量配置参数(`--policy`/`--model`/`--temperature` 等)互斥
|
||||
- 已发布版本 + 配置变更 → 报错(已发布版本只接受 `--version-desc`)
|
||||
- `--name` 最多 200 字符;`--description` 最多 1000 字符
|
||||
- `--policy` 只能是 `turbo` 或 `agentic`
|
||||
- `--temperature` 范围 0-2
|
||||
- `--max-llm-calls` 范围 1-30
|
||||
- 布尔参数(`--enable-*`)只能是 `true` 或 `false`
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
updated: aid-xxx
|
||||
Draft config changed — verify with --agent-version beta, then deploy.
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 配置变更只作用于 beta 草稿;已发布版本只接受 `--version-desc`。
|
||||
- 标量配置参数采用 read-merge-write:CLI 先读取当前 beta 配置,再合并变更后整体提交(API 是整替换语义)。
|
||||
- `--config-file` 替换整个配置,适合设置嵌套字段(如 `kb_search_configs`)。
|
||||
- 修改草稿后用 `--agent-version beta` 在 search/chat 上测试,通过后 `service deploy` 发布。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 调整温度
|
||||
kscli service update --agent-id aid-xxx --temperature 0.7 --workspace-id ws-xxx
|
||||
|
||||
# 用 JSON 文件替换整个配置
|
||||
kscli service update --agent-id aid-xxx --config-file ./agent-config.json
|
||||
|
||||
# 给已发布版本 1 加描述
|
||||
kscli service update --agent-id aid-xxx --agent-version 1 --version-desc "first stable release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service deploy`
|
||||
|
||||
发布 beta 草稿为新版本。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service deploy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deployed: aid-xxx version 2
|
||||
```
|
||||
|
||||
quiet 模式:输出新版本号。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 版本号自动递增,状态变为 `deployed`。
|
||||
- 发布影响线上调用方,确认提示会警告。
|
||||
- 如果当前状态为 `edited`(已发布后又改了草稿),确认提示会额外警告「发布会覆盖线上行为」。
|
||||
- 需要工作区的知识库修改权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 发布(交互确认)
|
||||
kscli service deploy --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 带描述并跳过确认
|
||||
kscli service deploy --agent-id aid-xxx --version-desc "tuned rerank params" --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service delete`
|
||||
|
||||
删除检索/Q&A 服务(软删除,幂等)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service delete --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
deleted: aid-xxx (status: deleted)
|
||||
```
|
||||
|
||||
quiet 模式:无输出。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 删除不可撤销,`agent_id` 不再可用于 search/chat 调用。
|
||||
- API 是幂等的:删除已删除的服务不会报错。
|
||||
- 如果服务状态为 `deployed` 或 `edited`,确认提示会额外警告「此服务正在线上运行」。
|
||||
- 需要工作区的知识库删除权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 删除(交互确认)
|
||||
kscli service delete --agent-id aid-xxx --workspace-id ws-xxx
|
||||
|
||||
# 跳过确认
|
||||
kscli service delete --agent-id aid-xxx --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `kscli service copy`
|
||||
|
||||
复制服务为新草稿(名称自动加 `copy_` 前缀)。
|
||||
|
||||
**用法**
|
||||
|
||||
```bash
|
||||
kscli service copy --agent-id <id> [flags]
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------- |
|
||||
| `--agent-id <id>` | string | 是 | 源服务(agent)ID |
|
||||
|
||||
**输出**
|
||||
|
||||
text 模式:
|
||||
|
||||
```
|
||||
new agent_id: aid-new (name: copy_my-qa, status: draft)
|
||||
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
|
||||
```
|
||||
|
||||
quiet 模式:输出新 agent ID。
|
||||
|
||||
json 模式:返回 API 原始响应。
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 副本初始为 beta 草稿,测试后需 deploy 发布。
|
||||
- 需要工作区的知识库创建权限。
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 复制服务
|
||||
kscli service copy --agent-id aid-source --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [返回总览](./kscli-cli-guide.md)
|
||||
@@ -21,6 +21,7 @@
|
||||
"bl": "pnpm -F bailian-cli dev",
|
||||
"kscli": "pnpm -F knowledge-studio-cli dev",
|
||||
"test": "vp test",
|
||||
"test:journey": "vp test packages/commands/tests/e2e/knowledge/journeys",
|
||||
"release:check": "node tools/release/check.mjs",
|
||||
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
|
||||
"test:stress": "node packages/cli/tests/stress/run.mjs"
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Aliyun Model Studio (DashScope) AI Platform
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,159 @@
|
||||
<div align="center">
|
||||
|
||||
# Bailian Knowledge Base for DeepSeek Harness
|
||||
|
||||
**Knowledge-base retrieval tools for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), backed by Aliyun Model Studio (Bailian).**
|
||||
|
||||
[](https://www.npmjs.com/package/bailian-kb-dsh)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](LICENSE)
|
||||
|
||||
[Bailian Console](https://bailian.console.aliyun.com/) · [中文文档](README.zh.md) · [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) · [API Documentation](https://help.aliyun.com/zh/model-studio/)
|
||||
|
||||
</div>
|
||||
|
||||
## What is this?
|
||||
|
||||
`bailian-kb-dsh` is a plugin (and dsh bundle) that gives a DeepSeek Harness agent access to knowledge bases hosted on [Aliyun Model Studio](https://bailian.console.aliyun.com/) (Bailian). It registers two model-facing tools — `kb_search` for raw evidence and `kb_chat` for grounded answers — and ships a settings page plus a management skill for the [`bl` CLI](https://www.npmjs.com/package/bailian-cli).
|
||||
|
||||
Retrieval happens through **retrieval services** you deploy on Bailian: a service binds one or more knowledge bases to an embedding/rerank configuration and is addressed by its `agent_id`. The plugin keeps the list of deployed services in front of the model, so it can decide whether the question is answerable from your knowledge at all.
|
||||
|
||||
## Features
|
||||
|
||||
- **Two model-facing tools** — `kb_search` returns scored chunks with source references; `kb_chat` returns a complete grounded answer
|
||||
- **Service awareness** — the workspace's deployed retrieval services are injected into the conversation, so the model knows what it can look up instead of guessing service ids
|
||||
- **Low-friction setup** — sign in to the Bailian console from the settings page to fill in the API key and workspace id; an existing `bl` CLI login is adopted automatically
|
||||
- **Settings page** — a "Bailian KB" section in the web UI for credentials, default services, and service-cache inspection
|
||||
- **Management skill** — bundled `bailian-kb` skill teaching the agent the `bl` CLI workflow for creating knowledge bases, ingesting documents, and deploying services
|
||||
|
||||
## Requirements
|
||||
|
||||
- DeepSeek Harness with its plugin runtime (`@deepseek-ai/dsh-*`), Node.js >= 22.12
|
||||
- An Aliyun Model Studio account: a **workspace id** and a **DashScope API key** ([get one](https://bailian.console.aliyun.com/?tab=app#/api-key))
|
||||
- At least one **deployed** retrieval or Q&A service in that workspace — create one in the [console](https://bailian.console.aliyun.com/) or with `bl knowledge service create` / `bl knowledge service deploy`
|
||||
- The [`bl` CLI](https://www.npmjs.com/package/bailian-cli) (`npm install -g bailian-cli`) for anything on the management side — creating knowledge bases, ingesting documents, deploying services. Retrieval itself calls the API directly and never shells out, so `kb_search` / `kb_chat` work without it
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add bailian-kb-dsh
|
||||
```
|
||||
|
||||
The CLI adds the bundle to the profile's layer stack; no manual YAML editing required. To remove it:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web remove bailian-kb-dsh
|
||||
```
|
||||
|
||||
Verify the plugin is composed — `dsh --profile web --dump-config` should list a `tool-bailian-kb` row.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Option 1 — Settings page (recommended)
|
||||
|
||||
After installation, **Settings → Bailian KB** appears in the web UI:
|
||||
|
||||
- **Fetch from console login** — opens the Bailian console in a browser on the host machine; when you finish signing in, the API key and workspace id of that account are stored on the host (the key never travels to the browser). Each login requests a freshly issued key, so switching accounts is one click.
|
||||
- **API key** — write-only: the stored value is never echoed back, only reported as configured or not.
|
||||
- **Workspace id / default retrieval service / default Q&A service** — editable with echo; the service ids can be picked from the cached service list. Clearing a value falls back to the layers below.
|
||||
- **Retrieval service cache** — shows when the injected service list was last fetched, how many services it holds, and offers a manual refresh for a service you just created.
|
||||
|
||||
If you have already run `bl auth login`, the API key and workspace id are adopted once from `~/.bailian/config.json` at startup. A value you deliberately clear is never re-filled.
|
||||
|
||||
### Option 2 — Environment and credential files
|
||||
|
||||
```sh
|
||||
# ~/.dsh/.env, or the credential store at ~/.dsh/.credentials.yaml
|
||||
DASHSCOPE_API_KEY=sk-xxx # required
|
||||
BAILIAN_WORKSPACE_ID=ws-xxx # required
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID=aid-xxx # optional
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID=aid-xxx # optional
|
||||
```
|
||||
|
||||
### Option 3 — Profile patch
|
||||
|
||||
The bundle inserts its own entry into the profile; you can override it by id in `~/.dsh/cordis.patch.yml` or the profile's patch file. An override **replaces the whole config object** (no deep merge):
|
||||
|
||||
```yaml
|
||||
- id: tool-bailian-kb
|
||||
config:
|
||||
defaultRetrieveAgentId: aid-search-service
|
||||
defaultChatAgentId: aid-chat-service
|
||||
chatTimeoutMs: 600000
|
||||
```
|
||||
|
||||
Disable the plugin with `- id: tool-bailian-kb` plus `disabled: true`.
|
||||
|
||||
### Config fields
|
||||
|
||||
The config doubles as the `bailian-kb` settings section, so edits in the settings page or settings document apply to the next call without a restart.
|
||||
|
||||
| Field | Type | Default | Meaning |
|
||||
| ------------------------ | ------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `workspaceId` | string? | — | Bailian workspace id; the API host is the workspace subdomain `https://<workspaceId>.<endpointHost>` |
|
||||
| `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | Host suffix; replace for another region or a private deployment |
|
||||
| `defaultRetrieveAgentId` | string? | — | Service `kb_search` falls back to when the caller omits `agent_id` |
|
||||
| `defaultChatAgentId` | string? | — | Service `kb_chat` falls back to when the caller omits `agent_id` |
|
||||
| `agentVersion` | string? | — | `beta` (draft) or a published version number; defaults to the latest published version. Not exposed to the model |
|
||||
| `chatTimeoutMs` | number | `300000` | `kb_chat` timeout — the server side is a minutes-scale agentic loop |
|
||||
|
||||
### Resolution order
|
||||
|
||||
| Value | Settings layer (settings page) | Entry config (profile patch) | Credential store / env |
|
||||
| ------------------------- | ------------------------------ | ---------------------------- | ----------------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | write-only control | — | `DASHSCOPE_API_KEY` |
|
||||
| workspace id | ✅ `workspaceId` | ✅ `workspaceId` | `BAILIAN_WORKSPACE_ID` |
|
||||
| default retrieval service | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` |
|
||||
| default Q&A service | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | `BAILIAN_DEFAULT_CHAT_AGENT_ID` |
|
||||
|
||||
Every value is re-read per call, so a rotated key or a switched workspace takes effect immediately. The API key and workspace id are mandatory: without them the tools fail with a message pointing at these configuration paths. Default services are optional — when the workspace has exactly one deployed service for a scene, that one is used.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Parameters | Returns |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `kb_search` | `query`, `agent_id` (required), `top_k?` (default 5, applied client-side), `images?` (image URLs for multimodal) | Scored chunks with source references, plus a total |
|
||||
| `kb_chat` | `message`, `agent_id` (required) | The complete answer plus a `request_id` |
|
||||
|
||||
`agent_id` is required in both schemas: the schema cannot tell the model whether this deployment pins a default, and discovering a missing default at call time wastes a round-trip. The configured default is still honoured for programmatic calls that omit it.
|
||||
|
||||
The list of deployed services — id, name, and scene — is injected into the conversation as a context message, refreshed periodically and whenever a `bl knowledge service` command changes the inventory. Long lists are truncated with the total stated, so the model never mistakes a partial list for the full inventory.
|
||||
|
||||
## Errors
|
||||
|
||||
- **HTTP 4xx** — most often an `agent_id` that no longer exists, so the service list is refreshed and appended to the error message for immediate recovery
|
||||
- **HTTP 5xx** — passed through unchanged
|
||||
- **Missing credentials** — the message names the configuration paths (`~/.dsh/.env`, `~/.dsh/.credentials.yaml`, the settings page) and links to the console key page
|
||||
- **`kb_chat` timeout** — the message explains the server-side multi-turn retrieval and suggests retrying or switching to `kb_search`
|
||||
|
||||
## Known limitations
|
||||
|
||||
- `kb_chat` buffers the server stream, so there is no progress output while it runs.
|
||||
- `top_k` is a client-side cut: the request body carries no such parameter, and how many chunks the server returns is decided by the service configuration.
|
||||
- **Service names carry the routing signal.** The service list API does not return a description field yet, so the model judges what a service covers from its name alone. Name your services after their content (`Product docs retrieval`, not `Service 1`).
|
||||
- At most two pages per scene are fetched; beyond that the injected list is marked as truncated.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
pnpm --filter bailian-kb-dsh run build # tsc → dist/ (node half) + tsdown → dist/web/client.js (browser half)
|
||||
pnpm --filter bailian-kb-dsh run typecheck # node and web tsconfigs
|
||||
pnpm --filter bailian-kb-dsh run test
|
||||
```
|
||||
|
||||
For local integration, add the working copy to a dev profile (the patch file is watched by HMR):
|
||||
|
||||
```sh
|
||||
dsh plugin --profile dev add <this-repo>/packages/bailian-kb-dsh
|
||||
```
|
||||
|
||||
Internal design notes — context injection strategy, service cache layout, refresh triggers — live in [docs/kb-dsh/runtime-behavior.md](https://github.com/modelstudioai/cli/blob/main/docs/kb-dsh/runtime-behavior.md); the maintenance checklist is [docs/agents/dsh-plugin.md](https://github.com/modelstudioai/cli/blob/main/docs/agents/dsh-plugin.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup and the contribution workflow.
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
@@ -0,0 +1,159 @@
|
||||
<div align="center">
|
||||
|
||||
# 百炼知识库 for DeepSeek Harness
|
||||
|
||||
**基于阿里云百炼(Aliyun Model Studio)的知识库检索工具,供 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 使用。**
|
||||
|
||||
[](https://www.npmjs.com/package/bailian-kb-dsh)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](LICENSE)
|
||||
|
||||
[百炼控制台](https://bailian.console.aliyun.com/) · [English](README.md) · [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) · [API 文档](https://help.aliyun.com/zh/model-studio/)
|
||||
|
||||
</div>
|
||||
|
||||
## 这是什么?
|
||||
|
||||
`bailian-kb-dsh` 是一个 DeepSeek Harness 插件(同时是 dsh bundle),让 agent 能检索托管在[阿里云百炼](https://bailian.console.aliyun.com/)上的知识库。它注册两个面向模型的工具 —— `kb_search` 取原始证据、`kb_chat` 出成品答案 —— 并附带一个设置页和一份面向 [`bl` CLI](https://www.npmjs.com/package/bailian-cli) 的管理 skill。
|
||||
|
||||
检索经由你在百炼上部署的**检索服务**完成:一个服务把一个或多个知识库绑定到指定的向量 / 排序配置上,通过 `agent_id` 寻址。插件会把已部署服务的清单持续呈现给模型,让它能判断用户的问题是否落在你的知识范围内。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **两个面向模型的工具** — `kb_search` 返回带分数和来源的知识切片;`kb_chat` 返回基于知识的完整答案
|
||||
- **服务感知** — 工作空间里已部署的检索服务会注入到会话上下文,模型据此知道自己能查什么,不必猜 `agent_id`
|
||||
- **低门槛配置** — 在设置页登录百炼控制台即可自动填入 API 密钥与工作空间 ID;已有的 `bl` CLI 登录会被自动采纳
|
||||
- **设置页** — Web UI 中的"百炼知识库"页,管理凭据、默认服务,并可查看服务缓存状态
|
||||
- **管理 skill** — 随包分发的 `bailian-kb` skill,教 agent 用 `bl` CLI 完成建库、文档导入、服务部署
|
||||
|
||||
## 环境要求
|
||||
|
||||
- DeepSeek Harness 及其插件运行时(`@deepseek-ai/dsh-*`),Node.js >= 22.12
|
||||
- 阿里云百炼账号:一个**工作空间 ID** 和一个 **DashScope API 密钥**([去获取](https://bailian.console.aliyun.com/?tab=app#/api-key))
|
||||
- 该工作空间下至少有一个**已部署**的检索或问答服务 —— 可在[控制台](https://bailian.console.aliyun.com/)创建,或用 `bl knowledge service create` / `bl knowledge service deploy`
|
||||
- 管理面的事(建库、导入文档、部署服务)需要 [`bl` CLI](https://www.npmjs.com/package/bailian-cli)(`npm install -g bailian-cli`)。检索本身直连 API、从不起子进程,所以 `kb_search` / `kb_chat` 不装也能用
|
||||
|
||||
## 安装
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add bailian-kb-dsh
|
||||
```
|
||||
|
||||
CLI 会自动把 bundle 加入 profile 的层栈,无需手改 YAML。卸载:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web remove bailian-kb-dsh
|
||||
```
|
||||
|
||||
验证插件已装配:`dsh --profile web --dump-config` 应能看到 `tool-bailian-kb` row。
|
||||
|
||||
## 配置
|
||||
|
||||
### 方式一 — 设置页(推荐)
|
||||
|
||||
安装后,Web UI 的 **Settings → 百炼知识库** 页出现:
|
||||
|
||||
- **自动获取** — 在宿主机浏览器中拉起百炼控制台登录;登录完成后,该账号的 API 密钥与工作空间 ID 直接落到宿主机(明文密钥不经过浏览器)。每次登录都会请求签发新密钥,因此切换账号点一次即可。
|
||||
- **API 密钥** — 只写不回显:存下的值不会再次显示,只显示"已配置 / 未配置"。
|
||||
- **工作空间 ID / 默认检索服务 / 默认对话服务** — 可编辑且回显;两个服务 ID 可从缓存的服务清单里选。清空保存则回退到下层来源。
|
||||
- **检索服务缓存** — 展示注入清单的上次拉取时间、各场景服务条数、是否被截断,并提供手动刷新(刚新建完服务想立刻生效时用)。
|
||||
|
||||
如果此前已运行过 `bl auth login`,启动时会从 `~/.bailian/config.json` 一次性采纳 API 密钥与工作空间 ID。被你主动清空的值不会被重新填回。
|
||||
|
||||
### 方式二 — 环境变量与凭据文件
|
||||
|
||||
```sh
|
||||
# ~/.dsh/.env,或凭据存储 ~/.dsh/.credentials.yaml
|
||||
DASHSCOPE_API_KEY=sk-xxx # 必填
|
||||
BAILIAN_WORKSPACE_ID=ws-xxx # 必填
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID=aid-xxx # 选填
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID=aid-xxx # 选填
|
||||
```
|
||||
|
||||
### 方式三 — Profile patch
|
||||
|
||||
bundle 会向 profile 插入自己的 entry,你可以在 `~/.dsh/cordis.patch.yml` 或 profile 的 patch 文件里按 id 覆盖。覆盖时**替换整个 config 对象(无 deep-merge)**:
|
||||
|
||||
```yaml
|
||||
- id: tool-bailian-kb
|
||||
config:
|
||||
defaultRetrieveAgentId: aid-search-service
|
||||
defaultChatAgentId: aid-chat-service
|
||||
chatTimeoutMs: 600000
|
||||
```
|
||||
|
||||
禁用插件:`- id: tool-bailian-kb` 加 `disabled: true`。
|
||||
|
||||
### 配置字段
|
||||
|
||||
Config 同时注册为 `bailian-kb` settings section,因此在设置页或设置文档里的修改会在下一次调用生效,无需重启。
|
||||
|
||||
| 字段 | 类型 | 默认值 | 语义 |
|
||||
| ------------------------ | ------- | ------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| `workspaceId` | string? | — | 百炼工作空间 ID;API host 为工作空间子域名 `https://<workspaceId>.<endpointHost>` |
|
||||
| `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | host 后缀,其他 region 或私有化部署时替换 |
|
||||
| `defaultRetrieveAgentId` | string? | — | 调用方省略 `agent_id` 时 `kb_search` 使用的服务 |
|
||||
| `defaultChatAgentId` | string? | — | 调用方省略 `agent_id` 时 `kb_chat` 使用的服务 |
|
||||
| `agentVersion` | string? | — | `beta`(草稿调试)或已发布版本号;默认调用最新发布版本。不暴露给模型 |
|
||||
| `chatTimeoutMs` | number | `300000` | `kb_chat` 超时时间 —— 服务端是分钟级的多轮检索循环 |
|
||||
|
||||
### 解析优先级
|
||||
|
||||
| 值 | settings 用户层(设置页) | entry config(profile patch) | 凭据存储 / 环境变量 |
|
||||
| ------------------- | --------------------------- | ----------------------------- | ----------------------------------- |
|
||||
| `DASHSCOPE_API_KEY` | 只写控件 | — | `DASHSCOPE_API_KEY` |
|
||||
| 工作空间 ID | ✅ `workspaceId` | ✅ `workspaceId` | `BAILIAN_WORKSPACE_ID` |
|
||||
| 默认检索服务 | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` |
|
||||
| 默认对话服务 | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | `BAILIAN_DEFAULT_CHAT_AGENT_ID` |
|
||||
|
||||
所有值每次调用重新解析,因此轮换密钥或切换工作空间即时生效。API 密钥与工作空间 ID 是必填项:缺失时工具调用会报错并指出上述配置路径。默认服务是选填的 —— 当某个场景下工作空间只有一个已部署服务时,直接用它。
|
||||
|
||||
## 工具
|
||||
|
||||
| 工具 | 参数 | 返回 |
|
||||
| ----------- | ---------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `kb_search` | `query`、`agent_id`(必填)、`top_k?`(默认 5,客户端截断)、`images?`(多模态图片 URL) | 带来源引用的评分切片,以及总数 |
|
||||
| `kb_chat` | `message`、`agent_id`(必填) | 完整答案,以及 `request_id` |
|
||||
|
||||
两个工具的 schema 中 `agent_id` 均为必填:schema 无法告诉模型这套部署是否配了默认服务,而"调用时才发现没有默认值"会白费一轮。配置的默认服务仍对省略该参数的程序化调用生效。
|
||||
|
||||
已部署服务的清单(ID、名称、场景)以上下文消息的形式注入会话,周期性刷新,`bl knowledge service` 命令改动服务清单时也会刷新。清单过长时会截断并注明总数,避免模型把部分清单当成全部。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- **HTTP 4xx** — 多数情况是 `agent_id` 已失效,因此会刷新服务清单并追加到错误信息里,便于立即纠正
|
||||
- **HTTP 5xx** — 原样透传
|
||||
- **凭据缺失** — 错误信息指出配置路径(`~/.dsh/.env`、`~/.dsh/.credentials.yaml`、设置页)并给出控制台取密钥的链接
|
||||
- **`kb_chat` 超时** — 错误信息说明服务端多轮检索的特性,建议重试或改用 `kb_search`
|
||||
|
||||
## 已知限制
|
||||
|
||||
- `kb_chat` 会缓冲服务端流式输出,执行期间没有进展显示。
|
||||
- `top_k` 是客户端截断:请求体不含该参数,服务端返回多少切片由检索服务配置决定。
|
||||
- **服务名承载了路由信号。** 服务列表接口目前不返回描述字段,模型只能靠服务名判断一个服务能查什么。请按内容命名(`产品文档检索`,而不是`检索服务1`)。
|
||||
- 每个场景最多拉取两页,超出时注入的清单会标明已截断。
|
||||
|
||||
## 开发
|
||||
|
||||
```sh
|
||||
pnpm --filter bailian-kb-dsh run build # tsc 出 dist/(node 半)+ tsdown 出 dist/web/client.js(浏览器半)
|
||||
pnpm --filter bailian-kb-dsh run typecheck # node 与 web 两套 tsconfig
|
||||
pnpm --filter bailian-kb-dsh run test
|
||||
```
|
||||
|
||||
本地联调时把工作副本装进 dev profile(patch 文件受 HMR 监听):
|
||||
|
||||
```sh
|
||||
dsh plugin --profile dev add <本仓库>/packages/bailian-kb-dsh
|
||||
```
|
||||
|
||||
内部设计说明(上下文注入策略、服务缓存布局、刷新触发点)见 [docs/kb-dsh/runtime-behavior.md](https://github.com/modelstudioai/cli/blob/main/docs/kb-dsh/runtime-behavior.md);维护清单见 [docs/agents/dsh-plugin.md](https://github.com/modelstudioai/cli/blob/main/docs/agents/dsh-plugin.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提交 Bug 报告、功能建议和 PR。开发环境搭建与贡献流程见 [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md)。
|
||||
|
||||
## 许可证
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
@@ -0,0 +1,13 @@
|
||||
# bailian-kb-dsh: inserts the Bailian knowledge-base consumer over dsh-base.
|
||||
# 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 defaultRetrieveAgentId
|
||||
# via BAILIAN_DEFAULT_RETRIEVE_AGENT_ID, defaultChatAgentId via
|
||||
# BAILIAN_DEFAULT_CHAT_AGENT_ID, and the API key via DASHSCOPE_API_KEY.
|
||||
|
||||
- insert:
|
||||
- id: tool-bailian-kb
|
||||
name: "bailian-kb-dsh"
|
||||
config:
|
||||
workspaceId: !!js process.env.BAILIAN_WORKSPACE_ID
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"name": "bailian-kb-dsh",
|
||||
"version": "0.1.19",
|
||||
"description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the bl CLI management skill.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
"aliyun",
|
||||
"bailian",
|
||||
"dashscope",
|
||||
"deepseek-harness",
|
||||
"dsh",
|
||||
"knowledge-base",
|
||||
"rag"
|
||||
],
|
||||
"homepage": "https://github.com/modelstudioai/cli/tree/main/packages/bailian-kb-dsh",
|
||||
"bugs": {
|
||||
"url": "https://github.com/modelstudioai/cli/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Aliyun Model Studio",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/modelstudioai/cli.git",
|
||||
"directory": "packages/bailian-kb-dsh"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"skills",
|
||||
"cordis.patch.yml"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"default": "./dist/web/client.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json && tsc -p tsconfig.web.json && tsdown",
|
||||
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.web.json",
|
||||
"test": "vp test",
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"yaml": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/schemastery": "^3.18.1",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "~18.3.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"react": "^18.2.0",
|
||||
"tsdown": "^0.22.2",
|
||||
"typescript": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"yaml": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
||||
"@deepseek-ai/schemastery": "^3.18.1",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"platform": "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: bailian-kb
|
||||
description: >-
|
||||
管理阿里云百炼知识库(建库、上传文档、部署检索服务、Chunk 运维、数据中心文件管理),命令行工具为 bl(bailian-cli)。
|
||||
当用户要创建/更新/删除知识库、上传或导入文档(本地/OSS)、创建/部署/调参检索或问答服务、
|
||||
增删改查 Chunk、管理数据中心类目/文件/集合时使用本 skill。
|
||||
检索与问答不走本 skill——用原生工具 kb_search(取证据)/ kb_chat(成品问答);
|
||||
bl knowledge search / chat 仅用于部署后的验证调试(如 --agent-version beta 调试草稿版)。
|
||||
kb_search / kb_chat 的凭据与工作空间由插件自动解析(~/.dsh/settings.yaml 的 bailian-kb 段、
|
||||
~/.dsh/.credentials.yaml 的 DASHSCOPE_API_KEY),不要自己去读或传。
|
||||
普通问答、编程、写作、翻译、泛搜索不触发本 skill。
|
||||
---
|
||||
|
||||
# 百炼知识库管理(bl)
|
||||
|
||||
检索面与管理面的分工:**查知识用 `kb_search`(取证据)/ `kb_chat`(成品问答)原生工具;本 skill 只覆盖管理长尾**——知识库全生命周期、文档、检索服务、Chunk、数据中心。
|
||||
|
||||
本 skill **不负责**判断何时该检索。可用检索服务的清单(含 agent_id)由插件自动注入到会话上下文里,`kb_search` / `kb_chat` 直接取用;不需要为了检索先加载本 skill。
|
||||
|
||||
## 检索服务清单的行为语义
|
||||
|
||||
- 清单由插件从百炼 API 拉取后缓存,按会话周期性刷新(约 30 分钟),只含 **deployed** 状态的服务;
|
||||
- **刚用 `bl` 新建或部署的服务不会立刻出现在清单里**。不用等刷新——命令输出里刚拿到的 `agent_id` 直接可用;
|
||||
- 服务很多时清单只列最近修改的若干条并标明总数。要找特定服务用 `bl knowledge service list --scene search --name <关键词>`;
|
||||
- 清单里确实没有能回答用户问题的服务时,如实告知用户,**不要挑一个最像的 agent_id 去试**。
|
||||
|
||||
## 前置检查
|
||||
|
||||
1. 安装校验:运行 `bl knowledge list --help`。若报 `Unknown command` 或 bl 未安装,执行
|
||||
`npm install -g bailian-cli`(需 Node.js ≥ 18.17);已安装但命令缺失时先 `bl update` 升级。
|
||||
安装失败时把错误原样报告给用户,不要静默跳过。
|
||||
2. 鉴权:需要 `DASHSCOPE_API_KEY`(环境变量,或 `bl auth login --api-key sk-xxx`,或 `bl config set --key api_key --value sk-xxx`)。
|
||||
3. workspace 解析优先级:`--workspace-id` 参数 > 环境变量 `BAILIAN_WORKSPACE_ID` > `bl config set --key workspace_id --value ws-xxx`。
|
||||
|
||||
## 何时用哪个命令
|
||||
|
||||
| 用户意图 | 命令 | 备注 |
|
||||
| ---------------------------------------- | --------------------------------------------------- | -------------------------------- |
|
||||
| 查知识 / 问答(日常检索) | 原生工具 `kb_search` / `kb_chat` | 不走 bl |
|
||||
| 建库 / 查看 / 改名 / 删库 / 监控 | `bl knowledge create/list/info/update/delete/stats` | `bl knowledge create --help` |
|
||||
| 上传本地文档、看解析状态、删文档、打标签 | `bl knowledge doc upload/list/status/delete/tag` | `bl knowledge doc upload --help` |
|
||||
| 从 OSS 批量导入 | `bl knowledge doc import-oss` | Bucket 需预先授权服务角色 |
|
||||
| 创建 / 部署 / 调参检索(问答)服务 | `bl knowledge service create/update/deploy/…` | `bl knowledge service --help` |
|
||||
| 修正错误切片、屏蔽某段内容 | `bl knowledge chunk add/list/update/delete` | `bl knowledge chunk --help` |
|
||||
| 数据中心类目 / 文件 / 集合管理 | `bl knowledge category/file/collection …` | `bl knowledge category --help` |
|
||||
| CLI 配置、升级 | `bl config show/set`、`bl update` | `bl config --help` |
|
||||
| 部署后验证、调试草稿版服务 | `bl knowledge search/chat --agent-version beta` | `bl knowledge search --help` |
|
||||
|
||||
## 核心工作流:建库到可检索
|
||||
|
||||
```bash
|
||||
bl knowledge doc upload --file ./docs/ --workspace-id ws-xxx # 1. 上传本地文件/目录 → 得 fileId
|
||||
bl knowledge create --name my-kb --description '产品文档' --doc-id <fileId> --wait # 2. 建库并导入 → 得 index-id (pipelineId)
|
||||
bl knowledge service create --name my-search --scene search --index-id <index-id> # 3. 建检索服务 → 得 agent-id(draft)
|
||||
bl knowledge service deploy --agent-id <agent-id> --yes # 4. 发布服务(此后可被默认版本调用)
|
||||
bl knowledge service list --scene search --status deployed # 5. 确认服务可见
|
||||
```
|
||||
|
||||
部署完成后用原生工具 `kb_search` 带该 `agent_id` 验证检索;若要在部署前调试草稿配置,用 `bl knowledge search --agent-id <id> --agent-version beta`。
|
||||
|
||||
已有文件再入库的简写:`bl knowledge doc upload --file ./a.md --index-id <index-id> --wait`(上传+导入一步完成)。
|
||||
|
||||
## ID 速查(极易混淆)
|
||||
|
||||
| ID | 来源 | 用在哪 |
|
||||
| ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `index-id` | `knowledge create` 返回的 pipelineId / `knowledge list` | 所有 knowledge/doc/chunk 命令的 `--index-id` |
|
||||
| `fileId` | `doc upload` / `doc import-oss` 返回 | 数据中心命令(`file get/delete`、`knowledge create --doc-id`、`doc tag`) |
|
||||
| `doc_id`(库内文档 ID) | `doc list` 输出 | `doc delete`、`chunk add/update` 的 `--doc-id`;**可能带 workspace 后缀,≠ fileId** |
|
||||
| `job-id` | 导入命令返回的 ingestionId | `doc status`(必须同时给 `--index-id` 和 `--job-id`) |
|
||||
| chunk id | `chunk list` 输出的 `metadata._id` | `chunk update/delete` 的 `--chunk-id` |
|
||||
| `agent-id` | `service create/list` | `service *`、`kb_search`/`kb_chat`、`bl knowledge search/chat` |
|
||||
|
||||
## 命令参考
|
||||
|
||||
执行不熟悉的命令前,跑 `bl <命令> --help` 查看完整 Usage / Flags / Notes / Examples。**不要猜 flag。**
|
||||
全部命令支持 `--output json`(结构化输出)、`--dry-run`(预览请求)、`--quiet`、`--verbose`。
|
||||
|
||||
## 危险与不可逆操作
|
||||
|
||||
执行以下操作前须向用户确认,脚本化时才用 `--yes` 跳过交互确认:
|
||||
|
||||
- `knowledge delete`:不可逆,库和全部索引内容永久删除(数据中心源文件保留)。
|
||||
- `file delete`:不可逆,且引用该文件的知识库文档索引会失效;只想从单个库移除用 `doc delete`。
|
||||
- `chunk delete`:不可逆。
|
||||
- `service deploy`:发布影响线上调用方;`service delete` 后 agent_id 不可再用(软删、幂等)。
|
||||
- `collection create`:**没有删除 API**,创建集合要慎重。
|
||||
- 索引配置(embedding 模型、chunk size 等)建库后不可改,只能重建。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- **建服务时必须把名字写清楚**:`service create --name` 的名称是模型判断"这个服务能查什么"的主要依据(服务描述暂未随列表接口返回)。`检索服务1` 这类无语义的名字会让后续检索无法路由;写成 `产品文档检索`、`HR制度问答` 这种能看出覆盖内容的名字。同时填 `--description`(≤1000 字符),列表接口返回该字段后即可自动生效。
|
||||
- 服务有 draft/deployed 两种状态:只有 deployed 可被默认版本调用,也只有 deployed 会进入模型看到的服务清单;draft 调试用 `--agent-version beta`。改已发布版本的配置:先改 beta 草稿(`service update`),验证后 `service deploy` 发新版本。
|
||||
- 导入类命令(`knowledge create`、`doc upload --index-id`、`doc status`)优先带 `--wait` 轮询到终态,避免手工轮询;文档解析失败(如 PARSE_FAILED)会以非零退出码透传错误。
|
||||
- `chunk add` 有 10 QPS 限流,批量脚本注意节流;响应不带 chunk id,需要 `chunk list` 反查。
|
||||
- `service list` 必须带 `--scene chat|search`,两个场景要分别查询。
|
||||
@@ -0,0 +1,72 @@
|
||||
/** Request/response fields of the DashScope search and chat endpoints. */
|
||||
|
||||
/** Retrieval-service scenes; the server requires one per list query. */
|
||||
export type ServiceScene = "chat" | "search";
|
||||
|
||||
export interface ServiceListRequest {
|
||||
agent_scene: ServiceScene;
|
||||
/** Filter to deployed services (spelling required by the server). */
|
||||
agent_status?: "deployed";
|
||||
agent_name?: string;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row of the service list.
|
||||
*
|
||||
* `description` is not included: the listing endpoint does not currently return
|
||||
* it, even for services created with a description. Add it here once it starts
|
||||
* appearing on the wire.
|
||||
*
|
||||
* `pipeline_list` is typed but deliberately never consumed: it can omit
|
||||
* `pipeline_name` or come back empty, so it cannot serve as a knowledge-base
|
||||
* label.
|
||||
*/
|
||||
export interface ServiceListRow {
|
||||
agent_id?: string;
|
||||
agent_name?: string;
|
||||
agent_scene?: string;
|
||||
agent_status?: string;
|
||||
agent_version?: string;
|
||||
create_time?: string;
|
||||
modify_time?: 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<string, unknown> }[];
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Host-side bridge to the locally installed `bl` CLI (bailian-cli): reads the
|
||||
* credentials an earlier `bl auth login` already stored, so a user who has
|
||||
* already set the CLI up does not have to configure this plugin a second time.
|
||||
*
|
||||
* Reading the CLI's credential file (`~/.bailian/config.json`) is the only way
|
||||
* to obtain the value programmatically — CLI commands mask stored keys on
|
||||
* output, so there is nothing to parse from stdout.
|
||||
*
|
||||
* Starting a login is NOT done through the CLI; see `console-login.ts`.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** The two values this plugin can adopt from the bl CLI credential file. */
|
||||
export interface BlCliConfig {
|
||||
/** DashScope api key (`api_key`, top-level default profile). */
|
||||
apiKey?: string;
|
||||
/** Bailian workspace id (`workspace_id`), present when the console login callback carried one. */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
/** Default location of the bl CLI credential file (default profile at top level). */
|
||||
export function blCliConfigPath(): string {
|
||||
return join(homedir(), ".bailian", "config.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the api key and workspace id from the bl CLI credential file.
|
||||
* Best-effort: a missing, unreadable, or malformed file reads as empty —
|
||||
* callers treat that the same as "the CLI has not logged in yet".
|
||||
* @param configPath - override for tests; defaults to `~/.bailian/config.json`.
|
||||
* @returns the values found; fields are absent rather than blank.
|
||||
*/
|
||||
export function readBlCliConfig(configPath = blCliConfigPath()): BlCliConfig {
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(configPath, "utf8")) as unknown;
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
||||
const record = parsed as Record<string, unknown>;
|
||||
const apiKey =
|
||||
typeof record.api_key === "string" && record.api_key.trim() !== ""
|
||||
? record.api_key.trim()
|
||||
: undefined;
|
||||
const workspaceId =
|
||||
typeof record.workspace_id === "string" && record.workspace_id.trim() !== ""
|
||||
? record.workspace_id.trim()
|
||||
: undefined;
|
||||
return {
|
||||
...(apiKey !== undefined ? { apiKey } : {}),
|
||||
...(workspaceId !== undefined ? { workspaceId } : {}),
|
||||
};
|
||||
} catch (_unreadable) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/** 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<ChatResult> {
|
||||
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 };
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/** 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 {
|
||||
/** Resolves the current workspace id per call (patch config or credential); throws with guidance when unconfigured. */
|
||||
resolveWorkspaceId: () => Promise<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<string>;
|
||||
/** 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<Response> {
|
||||
const [apiKey, workspaceId] = await Promise.all([
|
||||
this.opts.resolveApiKey(),
|
||||
this.opts.resolveWorkspaceId(),
|
||||
]);
|
||||
const fetchImpl = this.opts.fetchImpl ?? fetch;
|
||||
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,
|
||||
},
|
||||
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<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
|
||||
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<Response> {
|
||||
return await this.post(path, body, "text/event-stream", signal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Self-contained Bailian console login: the plugin drives the console's browser
|
||||
* login itself and persists what comes back, rather than shelling out to the
|
||||
* `bl` CLI.
|
||||
*
|
||||
* Driving the flow here lets it always request a freshly issued api key, so the
|
||||
* key and the workspace id both come from the account that just signed in, and
|
||||
* the values land straight in the dsh stores.
|
||||
*
|
||||
* Shape of the flow: bind a loopback-only port, open the console login page
|
||||
* pointed at that port, then accept one callback carrying the credentials as
|
||||
* query parameters or a JSON / form-encoded body.
|
||||
*/
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import http from "node:http";
|
||||
|
||||
/** Console web origins by site, keyed as the CLI's `--console-site` values. */
|
||||
const CONSOLE_ORIGINS: Record<string, string> = {
|
||||
domestic: "https://bailian.console.aliyun.com",
|
||||
international: "https://modelstudio.console.alibabacloud.com",
|
||||
};
|
||||
|
||||
/** How long the loopback listener waits for the browser callback. */
|
||||
const LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
|
||||
/** Upper bound on a callback body. */
|
||||
const MAX_CALLBACK_BODY = 65536;
|
||||
|
||||
/** Credentials the console callback can carry. */
|
||||
export interface ConsoleLoginCredentials {
|
||||
/** Freshly issued DashScope api key. */
|
||||
apiKey?: string;
|
||||
/** Workspace id of the account that signed in. */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the login flow stands. Deliberately carries no secret: the plain key
|
||||
* is handed to the completion callback and never retained here, so polling
|
||||
* this state from the browser cannot leak it.
|
||||
*/
|
||||
export type ConsoleLoginState =
|
||||
| { phase: "idle" }
|
||||
| { phase: "waiting"; loginUrl: string }
|
||||
| { phase: "done"; fields: string[] }
|
||||
| { phase: "failed"; reason: string };
|
||||
|
||||
/** The single in-flight flow: one browser login at a time. */
|
||||
let active: { server: http.Server } | undefined;
|
||||
let state: ConsoleLoginState = { phase: "idle" };
|
||||
|
||||
/** Read the current flow state (safe to expose to the panel). */
|
||||
export function consoleLoginState(): ConsoleLoginState {
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Pick the first non-blank string among the given keys. */
|
||||
function stringField(source: Record<string, unknown>, ...keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Read a bounded UTF-8 request body; an oversized body reads as empty. */
|
||||
function readBody(req: http.IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
size += chunk.length;
|
||||
if (size > MAX_CALLBACK_BODY) {
|
||||
req.destroy();
|
||||
resolve("");
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
resolve(Buffer.concat(chunks).toString("utf8"));
|
||||
});
|
||||
req.on("error", () => {
|
||||
resolve("");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a callback body as JSON (optionally wrapped in `data`) or as form
|
||||
* encoding. Content-type is a hint only: both shapes occur in practice, so both
|
||||
* are attempted.
|
||||
* @param raw - the raw request body.
|
||||
* @returns the flattened fields; an unparseable body yields no fields.
|
||||
*/
|
||||
export function parseCallbackBody(raw: string): Record<string, unknown> {
|
||||
const text = raw.replace(/^\uFEFF/, "").trim();
|
||||
if (text === "") return {};
|
||||
let json: unknown;
|
||||
let parsedAsJson = false;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
parsedAsJson = true;
|
||||
} catch (_notJson) {
|
||||
/* fall through to form parsing */
|
||||
}
|
||||
if (parsedAsJson) {
|
||||
// Valid JSON that is not an object carries no fields. Returning here rather
|
||||
// than falling through matters: form parsing would turn the whole payload
|
||||
// into one junk key.
|
||||
if (json === null || typeof json !== "object" || Array.isArray(json)) return {};
|
||||
const record = json as Record<string, unknown>;
|
||||
const inner = record.data;
|
||||
if (inner !== null && typeof inner === "object" && !Array.isArray(inner)) {
|
||||
// Merge the envelope's `data` under the top level, top level winning.
|
||||
return { ...(inner as Record<string, unknown>), ...record };
|
||||
}
|
||||
return record;
|
||||
}
|
||||
try {
|
||||
return Object.fromEntries(new URLSearchParams(text));
|
||||
} catch (_notForm) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the api key and workspace id out of a callback's fields, query
|
||||
* parameters taking priority over the body.
|
||||
* @param query - the callback URL's query parameters.
|
||||
* @param body - the parsed callback body.
|
||||
* @returns the credentials found; fields are absent rather than blank.
|
||||
*/
|
||||
export function pickCallbackCredentials(
|
||||
query: Record<string, unknown>,
|
||||
body: Record<string, unknown>,
|
||||
): ConsoleLoginCredentials {
|
||||
const apiKey = stringField(query, "api_key", "apiKey") ?? stringField(body, "api_key", "apiKey");
|
||||
const workspaceId =
|
||||
stringField(query, "workspace_id", "workspaceId") ??
|
||||
stringField(body, "workspace_id", "workspaceId");
|
||||
return {
|
||||
...(apiKey !== undefined ? { apiKey } : {}),
|
||||
...(workspaceId !== undefined ? { workspaceId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Extract the credentials from a callback, query parameters taking priority. */
|
||||
async function extractCredentials(
|
||||
req: http.IncomingMessage,
|
||||
url: URL,
|
||||
): Promise<ConsoleLoginCredentials> {
|
||||
const method = req.method ?? "GET";
|
||||
const body =
|
||||
method === "POST" || method === "PUT" || method === "PATCH"
|
||||
? parseCallbackBody(await readBody(req))
|
||||
: {};
|
||||
return pickCallbackCredentials(Object.fromEntries(url.searchParams), body);
|
||||
}
|
||||
|
||||
/** Open a URL with the OS default handler; never routed through a shell. */
|
||||
function openInBrowser(url: string): Promise<void> {
|
||||
const cmd =
|
||||
process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
||||
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(cmd, args, { windowsHide: true }, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Bind an http server to a loopback-only port chosen by the OS. */
|
||||
function listenLoopback(server: http.Server): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onError = (err: Error): void => {
|
||||
reject(err);
|
||||
};
|
||||
server.once("error", onError);
|
||||
server.listen({ port: 0, host: "127.0.0.1", exclusive: true }, () => {
|
||||
server.off("error", onError);
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string") {
|
||||
reject(new Error("expected a TCP address"));
|
||||
return;
|
||||
}
|
||||
resolve(address.port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Outcome of asking the host to start a console login. */
|
||||
export type ConsoleLoginStart =
|
||||
| { status: "started"; loginUrl: string }
|
||||
| { status: "already-running"; loginUrl: string }
|
||||
| { status: "failed"; reason: string };
|
||||
|
||||
/**
|
||||
* Start a console login on the host: binds a loopback listener, opens the
|
||||
* console login page in the host's default browser, and hands the credentials
|
||||
* from the callback to `onComplete` (which persists them). Fire-and-forget —
|
||||
* this resolves once the browser has been opened; poll {@link consoleLoginState}
|
||||
* for the outcome.
|
||||
* @param opts.site - console site, `domestic` (default) or `international`.
|
||||
* @param opts.onComplete - persists the received credentials; its resolved
|
||||
* field names become the `done` state's `fields`.
|
||||
* @returns whether the flow started, plus the URL to open manually if needed.
|
||||
*/
|
||||
export async function startConsoleLogin(opts: {
|
||||
site?: string;
|
||||
onComplete: (credentials: ConsoleLoginCredentials) => Promise<string[]>;
|
||||
}): Promise<ConsoleLoginStart> {
|
||||
if (active !== undefined) {
|
||||
return {
|
||||
status: "already-running",
|
||||
loginUrl: state.phase === "waiting" ? state.loginUrl : "",
|
||||
};
|
||||
}
|
||||
const expectedState = randomBytes(16).toString("hex");
|
||||
let settled = false;
|
||||
const server = http.createServer((req, res) => {
|
||||
void (async () => {
|
||||
if (req.method === "OPTIONS") {
|
||||
// The console page posts cross-origin; answer its preflight.
|
||||
res.writeHead(204, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
||||
if (url.searchParams.get("state") !== expectedState) {
|
||||
// Not our callback (or a forged one): refuse without ending the flow.
|
||||
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("bad state\n");
|
||||
return;
|
||||
}
|
||||
const credentials = await extractCredentials(req, url);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
res.end("OK\n");
|
||||
if (credentials.apiKey === undefined && credentials.workspaceId === undefined) {
|
||||
// A callback with neither value (e.g. a bare probe): keep waiting.
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
try {
|
||||
const fields = await opts.onComplete(credentials);
|
||||
state = { phase: "done", fields };
|
||||
} catch (err) {
|
||||
state = { phase: "failed", reason: err instanceof Error ? err.message : "persist failed" };
|
||||
}
|
||||
server.close();
|
||||
})().catch(() => {
|
||||
res.statusCode = 500;
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
let port: number;
|
||||
try {
|
||||
port = await listenLoopback(server);
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : "could not bind a local port";
|
||||
state = { phase: "failed", reason };
|
||||
return { status: "failed", reason };
|
||||
}
|
||||
|
||||
// Ask for a freshly issued key, so the key and the workspace id cannot end up
|
||||
// belonging to two different accounts.
|
||||
const origin =
|
||||
(opts.site !== undefined ? CONSOLE_ORIGINS[opts.site] : undefined) ?? CONSOLE_ORIGINS.domestic!;
|
||||
const loginUrl =
|
||||
`${origin}/console-login?notice=127.0.0.1:${port}` +
|
||||
`?state=${encodeURIComponent(expectedState)}&needapikey=true`;
|
||||
|
||||
active = { server };
|
||||
state = { phase: "waiting", loginUrl };
|
||||
const timer = setTimeout(() => {
|
||||
server.close();
|
||||
}, LOGIN_TIMEOUT_MS);
|
||||
timer.unref?.();
|
||||
server.once("close", () => {
|
||||
clearTimeout(timer);
|
||||
active = undefined;
|
||||
if (!settled && state.phase === "waiting") {
|
||||
state = { phase: "failed", reason: "the login timed out before the console called back" };
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await openInBrowser(loginUrl);
|
||||
} catch (_browserRefused) {
|
||||
// Headless or locked-down host: the panel shows `loginUrl` to open by hand.
|
||||
}
|
||||
return { status: "started", loginUrl };
|
||||
}
|
||||
|
||||
/** Abandon an in-flight login (closes the listener). */
|
||||
export function cancelConsoleLogin(): void {
|
||||
active?.server.close();
|
||||
active = undefined;
|
||||
state = { phase: "idle" };
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Protocol path constants and the workspace-subdomain URL builder (external API spec; not configurable). */
|
||||
|
||||
/**
|
||||
* DashScope knowledge API paths. `serviceList` backs the plugin's internal
|
||||
* service cache only — it is deliberately NOT exposed as a model tool (a
|
||||
* discovery tool reintroduces the "list before you search" round trip this
|
||||
* design exists to remove); the management surface uses the bl CLI instead.
|
||||
*/
|
||||
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}`;
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* Bailian knowledge-base consumer plugin: registers kb_search and kb_chat over the DashScope RAG API,
|
||||
* plus the bl management skill.
|
||||
* @module dsh-tool-bailian-kb
|
||||
*/
|
||||
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
||||
import {
|
||||
settingsNamespace,
|
||||
SettingsProvider,
|
||||
type SettingsRegisterOptions,
|
||||
type SettingsScope,
|
||||
} from "@deepseek-ai/dsh-settings";
|
||||
import { readBlCliConfig } from "./bl-cli.js";
|
||||
import { consoleLoginState, startConsoleLogin } from "./console-login.js";
|
||||
import { KbClient } from "./client.js";
|
||||
import { registerSkill } from "./skill.js";
|
||||
import { ServiceCache } from "./service-cache.js";
|
||||
import { buildRefreshedSceneList } from "./service-catalog.js";
|
||||
import { installServiceContext } from "./service-context.js";
|
||||
import type { ServiceScene } from "./api-types.js";
|
||||
import { createKbTools } from "./tools.js";
|
||||
|
||||
/** Minimal webServer route shape (declared inline to avoid a host-package dependency). */
|
||||
interface WebRoute {
|
||||
kind: "exact" | "prefix";
|
||||
path: string;
|
||||
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
||||
}
|
||||
/** Shell-environment registration shape (declared inline to avoid a host-package dependency). */
|
||||
interface ShellEnvRegistration {
|
||||
name: string;
|
||||
variables: Record<string, { description: string }>;
|
||||
resolve: () => Record<string, string | undefined>;
|
||||
}
|
||||
declare module "@deepseek-ai/cordis" {
|
||||
interface Context {
|
||||
webServer: {
|
||||
register(route: WebRoute): () => void;
|
||||
};
|
||||
shellEnv: {
|
||||
register(registration: ShellEnvRegistration): void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const name = "tool-bailian-kb";
|
||||
export const inject = ["tools", "credentials"];
|
||||
|
||||
/** Settings namespace this plugin registers when a settings service is composed. */
|
||||
const SETTINGS_NS = settingsNamespace("bailian-kb");
|
||||
|
||||
/** Settings fields seeded once from their credential references ({@link seedFromCredentials}). */
|
||||
const CREDENTIAL_SEEDS = [
|
||||
["workspaceId", "BAILIAN_WORKSPACE_ID"],
|
||||
["defaultRetrieveAgentId", "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID"],
|
||||
["defaultChatAgentId", "BAILIAN_DEFAULT_CHAT_AGENT_ID"],
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Every {@link Config} field the bridge route accepts. A static allowlist,
|
||||
* NOT `key in current()`: optional fields with no default and no base
|
||||
* (the default service ids) vanish from the resolved config once cleared,
|
||||
* and a membership test against it would silently drop their next write.
|
||||
*/
|
||||
const CONFIG_FIELDS = new Set<string>([
|
||||
"workspaceId",
|
||||
"endpointHost",
|
||||
"defaultRetrieveAgentId",
|
||||
"defaultChatAgentId",
|
||||
"agentVersion",
|
||||
"chatTimeoutMs",
|
||||
]);
|
||||
|
||||
/**
|
||||
* One-time migration: before this section existed, the workspace and
|
||||
* default-service ids lived only as credentials, which the wire never echoes.
|
||||
* Seed each field the resolved section does not answer from the WRITABLE
|
||||
* credential layer (`file`), so the page shows the value the deployment
|
||||
* already runs with; env-sourced values stay where they are — freezing one
|
||||
* into the document would shadow later environment changes.
|
||||
* @param ctx - registrant context carrying credentials.
|
||||
* @param scope - the registered `bailian-kb` scope the seed writes through.
|
||||
*/
|
||||
async function seedFromCredentials(ctx: Context, scope: SettingsScope<Config>): Promise<void> {
|
||||
try {
|
||||
const seeds: Partial<Record<(typeof CREDENTIAL_SEEDS)[number][0], string>> = {};
|
||||
for (const [field, ref] of CREDENTIAL_SEEDS) {
|
||||
if (scope.get()[field]) continue;
|
||||
const resolved = await ctx.credentials.resolve(credentialRef(ref));
|
||||
if (resolved?.source !== "file") continue;
|
||||
seeds[field] = resolved.value;
|
||||
}
|
||||
if (Object.keys(seeds).length > 0) await scope.update(seeds);
|
||||
} catch (_migrationFailure) {
|
||||
// Best-effort: a failed seed leaves the credential fallback in place, so
|
||||
// resolution still answers — the page merely starts blank.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time adoption of the bl CLI's stored login (`~/.bailian/config.json`):
|
||||
* fields never set anywhere are filled from the CLI's credential file, so a
|
||||
* `bl auth login --console` done before installing the plugin "just works".
|
||||
* `seededFields` is the consumed-once ledger — a field is marked when it was
|
||||
* seeded here, or when it already had a value (user-managed elsewhere) — so
|
||||
* a value the user later clears deliberately is never resurrected.
|
||||
* @param ctx - registrant context carrying credentials.
|
||||
* @param scope - the registered `bailian-kb` scope the seed writes through.
|
||||
*/
|
||||
async function seedFromBlCli(ctx: Context, scope: SettingsScope<Config>): Promise<void> {
|
||||
try {
|
||||
const done = new Set(scope.get().seededFields ?? []);
|
||||
if (done.has("apiKey") && done.has("workspaceId")) return;
|
||||
const bl = readBlCliConfig();
|
||||
const marks: string[] = [];
|
||||
if (!done.has("apiKey")) {
|
||||
const existing = await ctx.credentials.resolve(credentialRef("DASHSCOPE_API_KEY"));
|
||||
if (existing) {
|
||||
// Already configured (env or file): user-managed, never seed later.
|
||||
marks.push("apiKey");
|
||||
} else if (bl.apiKey !== undefined) {
|
||||
try {
|
||||
await ctx.credentials.set(credentialRef("DASHSCOPE_API_KEY"), bl.apiKey);
|
||||
marks.push("apiKey");
|
||||
} catch (_readOnlyShadow) {
|
||||
// A read-only source refuses the write; leave unmarked so a later
|
||||
// startup (once the shadow is gone) can still seed.
|
||||
}
|
||||
}
|
||||
// Neither configured nor available from the CLI: leave unmarked so a
|
||||
// later startup (after `bl auth login --console`) can seed.
|
||||
}
|
||||
if (!done.has("workspaceId")) {
|
||||
const configured =
|
||||
scope.get().workspaceId ||
|
||||
(await ctx.credentials.resolve(credentialRef("BAILIAN_WORKSPACE_ID"))) !== undefined;
|
||||
if (configured) {
|
||||
marks.push("workspaceId");
|
||||
} else if (bl.workspaceId !== undefined) {
|
||||
await scope.update({ workspaceId: bl.workspaceId });
|
||||
marks.push("workspaceId");
|
||||
}
|
||||
}
|
||||
if (marks.length > 0) {
|
||||
await scope.update({
|
||||
seededFields: [...new Set([...(scope.get().seededFields ?? []), ...marks])],
|
||||
});
|
||||
}
|
||||
} catch (_seedFailure) {
|
||||
// Best-effort: an unseeded field still resolves through the normal
|
||||
// credential fallback chain, and the panel's autofill button remains.
|
||||
}
|
||||
}
|
||||
|
||||
/** Bailian knowledge-base plugin configuration. */
|
||||
export interface Config {
|
||||
/** Bailian workspace id; the API host is the workspace subdomain `https://<workspaceId>.<endpointHost>`. Optional here: an unset value falls back per call to the BAILIAN_WORKSPACE_ID credential (env/.env or ~/.dsh/.credentials.yaml). Editable with echo on the Settings → 百炼知识库 page (settings layer). */
|
||||
workspaceId?: string;
|
||||
/** API host suffix; replace for other regions or private deployments. */
|
||||
endpointHost: string;
|
||||
/** Retrieval-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_RETRIEVE_AGENT_ID credential. */
|
||||
defaultRetrieveAgentId?: string;
|
||||
/** Q&A-service id pinned by this deployment; when unset, the per-call fallback reads the BAILIAN_DEFAULT_CHAT_AGENT_ID credential. */
|
||||
defaultChatAgentId?: 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;
|
||||
/** Consumed-once ledger of {@link seedFromBlCli}: fields listed here are never auto-seeded again, so a deliberately cleared value stays cleared. Maintained by the host; not editable from the panel. */
|
||||
seededFields?: string[];
|
||||
}
|
||||
|
||||
/** Schemastery validation for {@link Config}; workspaceId and default agent ids are optional — both resolve per call with a credentials fallback. */
|
||||
export const Config: z<Config> = z.object({
|
||||
workspaceId: z.string(),
|
||||
endpointHost: z.string().default("cn-beijing.maas.aliyuncs.com"),
|
||||
defaultRetrieveAgentId: z.string(),
|
||||
defaultChatAgentId: z.string(),
|
||||
agentVersion: z.string(),
|
||||
chatTimeoutMs: z.number().default(300_000),
|
||||
seededFields: z.array(z.string()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Register the two knowledge tools over one shared client, plus the
|
||||
* management skill when a skills registry is composed. The Config doubles as
|
||||
* the `bailian-kb` settings section (entry config as the base layer), so
|
||||
* every value is read through the live source thunk per call — tool schemas
|
||||
* are static (agent_id stays required regardless), so a settings edit needs
|
||||
* no re-registration.
|
||||
* @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 {
|
||||
// The active configuration source: the composition entry until a settings
|
||||
// service attaches, then the resolved section (schema defaults → entry
|
||||
// base → user layer). Detach falls back to the entry automatically.
|
||||
// Hand-rolled instead of `installSettingsSection` for two extras it does
|
||||
// not carry: the `expose` opt-in (this page edits the section from the
|
||||
// browser) and the scope handle the credential migration writes through.
|
||||
let current: () => Config = () => config;
|
||||
let scope: SettingsScope<Config> | undefined;
|
||||
/** The settings provider, captured for `mutate` (path-level unset) access. */
|
||||
let settings: SettingsProvider | undefined;
|
||||
/**
|
||||
* Mark fields as consumed in the {@link seedFromBlCli} ledger — called on
|
||||
* every user-driven write or clear, so a managed field is never re-seeded.
|
||||
* Best-effort: a failed mark only risks one extra seed attempt.
|
||||
*/
|
||||
const markSeeded = async (fields: readonly string[]): Promise<void> => {
|
||||
if (!scope) return;
|
||||
try {
|
||||
const done = new Set(scope.get().seededFields ?? []);
|
||||
const added = fields.filter((field) => !done.has(field));
|
||||
if (added.length === 0) return;
|
||||
await scope.update({ seededFields: [...done, ...added] });
|
||||
} catch (_markFailure) {
|
||||
/* best-effort */
|
||||
}
|
||||
};
|
||||
ctx.inject(["settings"], (sctx) => {
|
||||
settings = sctx.settings;
|
||||
// `expose` is the wire opt-in the harness documents as deferred work; the
|
||||
// assertion keeps this compiling against pristine upstream types, which do
|
||||
// not declare it yet. Until upstream lands it the option is ignored and
|
||||
// the browser page degrades to its credentials-only fallback.
|
||||
const options = { base: config, expose: true } as SettingsRegisterOptions<Config>;
|
||||
scope = sctx.settings.register(SETTINGS_NS, Config, options);
|
||||
current = () => scope!.get();
|
||||
sctx.effect(
|
||||
() => () => {
|
||||
current = () => config;
|
||||
},
|
||||
"tool-bailian-kb: settings source fallback",
|
||||
);
|
||||
void seedFromCredentials(ctx, scope).then(() => seedFromBlCli(ctx, scope!));
|
||||
// Any api-key write or clear — this panel, the Models page, an external
|
||||
// file edit — means the user manages the credential: consume the seed so
|
||||
// a deliberately cleared key is never resurrected at the next startup.
|
||||
sctx.on("credentials/updated", (ref) => {
|
||||
if (ref === "DASHSCOPE_API_KEY") void markSeeded(["apiKey"]);
|
||||
});
|
||||
});
|
||||
|
||||
const client = new KbClient({
|
||||
resolveWorkspaceId: async () => {
|
||||
const pinned = current().workspaceId;
|
||||
if (pinned) return pinned;
|
||||
const resolved = await ctx.credentials.resolve(credentialRef("BAILIAN_WORKSPACE_ID"));
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
"BAILIAN_WORKSPACE_ID is not configured. Set the workspace id in the web UI (Settings → 百炼知识库) " +
|
||||
"or in ~/.dsh/.credentials.yaml; it appears as the subdomain of your Bailian endpoints.",
|
||||
);
|
||||
}
|
||||
return resolved.value;
|
||||
},
|
||||
// Live settings reads: the client keeps no copy, so a committed edit to
|
||||
// the section applies on the next call.
|
||||
get endpointHost() {
|
||||
return current().endpointHost;
|
||||
},
|
||||
get agentVersion() {
|
||||
return current().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 the web UI (Settings → 百炼知识库) " +
|
||||
"or in ~/.dsh/.credentials.yaml (create a key at https://bailian.console.aliyun.com/?tab=app#/api-key).",
|
||||
);
|
||||
}
|
||||
return resolved.value;
|
||||
},
|
||||
});
|
||||
|
||||
/** The workspace id if configured, without the client's guidance throw. */
|
||||
const resolveWorkspaceIdOrUndefined = async (): Promise<string | undefined> => {
|
||||
const pinned = current().workspaceId;
|
||||
if (pinned) return pinned;
|
||||
const resolved = await ctx.credentials.resolve(credentialRef("BAILIAN_WORKSPACE_ID"));
|
||||
return resolved?.value;
|
||||
};
|
||||
|
||||
const serviceCache = new ServiceCache({
|
||||
client,
|
||||
resolveWorkspaceId: async () => {
|
||||
const workspaceId = await resolveWorkspaceIdOrUndefined();
|
||||
if (workspaceId === undefined) throw new Error("workspace id is not configured");
|
||||
return workspaceId;
|
||||
},
|
||||
get endpointHost() {
|
||||
return current().endpointHost;
|
||||
},
|
||||
warn: (message) => {
|
||||
ctx.logger.warn(message);
|
||||
},
|
||||
});
|
||||
|
||||
/** The user's explicitly configured default for one scene: settings layer, then credential. */
|
||||
const configuredDefaultAgentId = async (scene: ServiceScene): Promise<string | undefined> => {
|
||||
const pinned =
|
||||
scene === "search" ? current().defaultRetrieveAgentId : current().defaultChatAgentId;
|
||||
if (pinned) return pinned;
|
||||
const ref =
|
||||
scene === "search" ? "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" : "BAILIAN_DEFAULT_CHAT_AGENT_ID";
|
||||
const resolved = await ctx.credentials.resolve(credentialRef(ref));
|
||||
return resolved?.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* The default service for one scene, falling back to the sole deployed service
|
||||
* when the workspace has exactly one. That last layer is the zero-configuration
|
||||
* path for the common 2C deployment: with one service there is nothing to
|
||||
* choose, so making the user name it in settings buys nothing.
|
||||
*/
|
||||
const resolveDefaultAgentId = async (scene: ServiceScene): Promise<string | undefined> => {
|
||||
const configured = await configuredDefaultAgentId(scene);
|
||||
if (configured !== undefined) return configured;
|
||||
const workspaceId = await resolveWorkspaceIdOrUndefined();
|
||||
if (workspaceId === undefined) return undefined;
|
||||
const forScene =
|
||||
serviceCache.peek(workspaceId)?.entries.filter((entry) => entry.scene === scene) ?? [];
|
||||
return forScene.length === 1 ? forScene[0]?.agent_id : undefined;
|
||||
};
|
||||
for (const tool of createKbTools({
|
||||
client,
|
||||
resolveDefaultRetrieveAgentId: async () => await resolveDefaultAgentId("search"),
|
||||
resolveDefaultChatAgentId: async () => await resolveDefaultAgentId("chat"),
|
||||
// Self-heal for a cached id the server has since rejected: refresh once and
|
||||
// put the current list in the error, which reaches the model this step. An
|
||||
// empty result is reported too rather than dropped — a bare "invalid
|
||||
// agent_id" reads as "try another one", and when nothing is deployed no id
|
||||
// can work.
|
||||
describeServicesAfterRefresh: async (scene) => {
|
||||
await serviceCache.refresh();
|
||||
const workspaceId = await resolveWorkspaceIdOrUndefined();
|
||||
if (workspaceId === undefined) return undefined;
|
||||
return buildRefreshedSceneList(scene, serviceCache.entriesFor(workspaceId, scene));
|
||||
},
|
||||
get chatTimeoutMs() {
|
||||
return current().chatTimeoutMs;
|
||||
},
|
||||
})) {
|
||||
ctx.tools.register(tool);
|
||||
}
|
||||
registerSkill(ctx);
|
||||
|
||||
// A management command that changes the service inventory invalidates the
|
||||
// cache immediately, so the next session sees the new service instead of
|
||||
// waiting out the TTL. `tools/result` is observe-only (it returns undefined and
|
||||
// sits after the pipeline), so listening here cannot affect tool execution.
|
||||
//
|
||||
// The command string is matched inside the serialized arguments rather than
|
||||
// against a specific tool name: the agent may run `bl` through bash, a
|
||||
// terminal tool, or a run_code program. A loose match is deliberate — a false
|
||||
// positive costs one list request, while a miss falls back to the TTL.
|
||||
ctx.on("tools/result", (_exec, result) => {
|
||||
if (result.isError) return;
|
||||
const args = JSON.stringify((_exec as { arguments?: unknown }).arguments ?? "");
|
||||
if (!/bl\s+knowledge\s+service\s+(create|deploy|delete|copy)/.test(args)) return;
|
||||
serviceCache.invalidate();
|
||||
void serviceCache.refresh();
|
||||
});
|
||||
|
||||
// The service catalog rides an `agent/pre-step` context message rather than the
|
||||
// tool descriptions: descriptions freeze at plugin load, and a plugin loads
|
||||
// once per process, so in a long-running host a service created elsewhere
|
||||
// would never be seen. Optional inject — a headless assembly without `agents`
|
||||
// simply gets no catalog, and both tools keep working.
|
||||
ctx.inject(["agents"], (actx) => {
|
||||
installServiceContext(actx, {
|
||||
cache: serviceCache,
|
||||
resolveWorkspaceId: resolveWorkspaceIdOrUndefined,
|
||||
resolveDefaultRetrieveAgentId: async () => await configuredDefaultAgentId("search"),
|
||||
resolveDefaultChatAgentId: async () => await configuredDefaultAgentId("chat"),
|
||||
warn: (message) => {
|
||||
actx.logger.warn(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Export the resolved workspace id as a shell environment variable so
|
||||
// management CLI commands (`bl knowledge list`, `bl knowledge service list`, etc.)
|
||||
// running in bash can see the value the settings service resolved.
|
||||
// Without this, the settings.yaml value is invisible to child processes.
|
||||
ctx.inject(["shellEnv"], (envCtx) => {
|
||||
envCtx.shellEnv.register({
|
||||
name: "bailian-kb",
|
||||
variables: {
|
||||
BAILIAN_WORKSPACE_ID: {
|
||||
description:
|
||||
"Bailian workspace id resolved from settings (Settings → 百炼知识库) or credentials.",
|
||||
},
|
||||
},
|
||||
resolve: () => {
|
||||
const wsId = current().workspaceId;
|
||||
return wsId ? { BAILIAN_WORKSPACE_ID: wsId } : {};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Bridge routes let the browser settings page read and write the resolved
|
||||
// section without riding the settings wire (which requires an apiproxy
|
||||
// allowlist entry the composition does not grant out-of-tree namespaces).
|
||||
// GET and POST share one exact-route registration: the webServer map keys
|
||||
// on (kind, path), so two registrations for the same path throw
|
||||
// "duplicate route" and the second handler silently replaces the first.
|
||||
ctx.inject(["webServer"], (wctx) => {
|
||||
wctx.effect(
|
||||
() =>
|
||||
wctx.webServer.register({
|
||||
kind: "exact",
|
||||
path: "/bailian-kb/settings",
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method === "GET" || req.method === "HEAD") {
|
||||
sendJson(res, 200, current());
|
||||
return;
|
||||
}
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "use GET or POST" });
|
||||
return;
|
||||
}
|
||||
if (!scope) {
|
||||
sendJson(res, 503, { error: "settings service unavailable" });
|
||||
return;
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await readJsonBody(req);
|
||||
} catch (err) {
|
||||
sendJson(res, 400, { error: err instanceof Error ? err.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
if (typeof body !== "object" || body === null) {
|
||||
sendJson(res, 400, { error: "expected JSON object" });
|
||||
return;
|
||||
}
|
||||
// Build a settings update patch. null-valued keys are removals (the
|
||||
// field falls back to the entry config and then the credential store).
|
||||
const patch: Record<string, unknown> = {};
|
||||
const removals = new Set<string>();
|
||||
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
|
||||
if (!CONFIG_FIELDS.has(key)) continue;
|
||||
if (value === null) {
|
||||
removals.add(key);
|
||||
continue;
|
||||
}
|
||||
patch[key] = value;
|
||||
}
|
||||
try {
|
||||
// Apply non-removal patches first (scope.update merges into the user
|
||||
// layer without disturbing other fields).
|
||||
if (Object.keys(patch).length > 0) await scope.update(patch);
|
||||
// Remove fields via path-level unset ops: this deletes the key from
|
||||
// the user layer so it re-inherits the entry base and schema defaults.
|
||||
// Using scope.replace() with the resolved config would bake defaults
|
||||
// (endpointHost, chatTimeoutMs) and entry values into the user layer,
|
||||
// shadowing future entry changes and polluting the stored document.
|
||||
if (removals.size > 0 && settings) {
|
||||
for (const key of removals) {
|
||||
await settings.mutate(SETTINGS_NS, [{ op: "unset", path: [key] }]);
|
||||
}
|
||||
}
|
||||
// A user-driven workspace write or clear consumes its bl-CLI seed:
|
||||
// a deliberately cleared value must never be resurrected at startup.
|
||||
if ("workspaceId" in patch || removals.has("workspaceId"))
|
||||
await markSeeded(["workspaceId"]);
|
||||
sendJson(res, 200, scope.get());
|
||||
} catch (err) {
|
||||
sendJson(res, 500, {
|
||||
error: err instanceof Error ? err.message : "settings write failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
"tool-bailian-kb: settings bridge route",
|
||||
);
|
||||
|
||||
// Service cache bridge: the panel's only window into cache freshness.
|
||||
// GET returns the diagnostic snapshot plus the pickable services; POST
|
||||
// forces a refresh and returns the same shape, so the numbers the developer
|
||||
// sees update in place.
|
||||
wctx.effect(
|
||||
() =>
|
||||
wctx.webServer.register({
|
||||
kind: "exact",
|
||||
path: "/bailian-kb/services",
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "use GET or POST" });
|
||||
return;
|
||||
}
|
||||
const workspaceId = await resolveWorkspaceIdOrUndefined();
|
||||
if (workspaceId === undefined) {
|
||||
sendJson(res, 200, { configured: false });
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST") {
|
||||
// Force a fetch regardless of TTL: the button exists precisely for the
|
||||
// case where the developer believes the cache is wrong.
|
||||
serviceCache.invalidate();
|
||||
await serviceCache.refresh();
|
||||
}
|
||||
sendJson(res, 200, {
|
||||
configured: true,
|
||||
status: serviceCache.status(workspaceId),
|
||||
search: serviceCache.entriesFor(workspaceId, "search"),
|
||||
chat: serviceCache.entriesFor(workspaceId, "chat"),
|
||||
});
|
||||
},
|
||||
}),
|
||||
"tool-bailian-kb: service cache bridge route",
|
||||
);
|
||||
|
||||
// Autofill bridge: fetch credentials by signing in to the Bailian console
|
||||
// (panel button). `login` drives the console's callback protocol on the
|
||||
// host and persists what comes back — the plain key never rides the wire
|
||||
// to the browser; `loginStatus` lets the panel poll for the outcome.
|
||||
wctx.effect(
|
||||
() =>
|
||||
wctx.webServer.register({
|
||||
kind: "exact",
|
||||
path: "/bailian-kb/autofill",
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "use POST" });
|
||||
return;
|
||||
}
|
||||
let action = "login";
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
if (
|
||||
typeof body === "object" &&
|
||||
body !== null &&
|
||||
(body as { action?: unknown }).action === "loginStatus"
|
||||
)
|
||||
action = "loginStatus";
|
||||
} catch (_emptyOrMalformedBody) {
|
||||
/* default to login */
|
||||
}
|
||||
if (action === "loginStatus") {
|
||||
sendJson(res, 200, consoleLoginState());
|
||||
return;
|
||||
}
|
||||
// Drive the console flow ourselves, requesting a freshly issued key,
|
||||
// so the key and the workspace id both belong to the account signing
|
||||
// in now. Persisting here keeps the plain key on the host.
|
||||
const started = await startConsoleLogin({
|
||||
onComplete: async (credentials) => {
|
||||
const written: string[] = [];
|
||||
if (credentials.apiKey !== undefined) {
|
||||
await ctx.credentials.set(credentialRef("DASHSCOPE_API_KEY"), credentials.apiKey);
|
||||
written.push("apiKey");
|
||||
}
|
||||
if (credentials.workspaceId !== undefined && scope) {
|
||||
await scope.update({ workspaceId: credentials.workspaceId });
|
||||
written.push("workspaceId");
|
||||
}
|
||||
if (written.length > 0) await markSeeded(written);
|
||||
// A completed console login is the one unambiguous signal that the
|
||||
// account may have changed. Without this the next session would
|
||||
// build its catalog from the previous account's services, which is
|
||||
// worse than having no cache at all.
|
||||
if (written.length > 0) {
|
||||
serviceCache.invalidate();
|
||||
void serviceCache.refresh();
|
||||
}
|
||||
return written;
|
||||
},
|
||||
});
|
||||
sendJson(res, 200, started);
|
||||
},
|
||||
}),
|
||||
"tool-bailian-kb: autofill bridge route",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Write a JSON response. */
|
||||
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/** Read a UTF-8 JSON body up to a size limit. */
|
||||
function readJsonBody(req: IncomingMessage, maxBytes = 16384): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
if (total > maxBytes) {
|
||||
req.destroy();
|
||||
reject(new Error("body too large"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* On-disk cache of the workspace's deployed retrieval services.
|
||||
*
|
||||
* Landing spot: `${DSH_HOME:-~/.dsh}/cache/bailian-kb/services-<workspaceId>.json`.
|
||||
* Per-workspace files are required: an api key is scoped to one workspace, and
|
||||
* the panel's "autofill" button exists to switch accounts, so one shared file
|
||||
* would blend services from different accounts.
|
||||
*
|
||||
* Why not `ctx.storage`: the storage hub is absent from every shipped agent
|
||||
* preset, so `inject(['storage'])` may never fire for a third-party plugin, and
|
||||
* the JSON backend's on-disk location is decided by its own `root` config — the
|
||||
* plugin could not tell anyone where the data went. Why not `settings.yaml`: that
|
||||
* document is the user's, and it hot-reloads, so writing machine-refreshed data
|
||||
* there both fights the user for the file and republishes configuration for no
|
||||
* reason.
|
||||
*
|
||||
* Every read is best-effort and total: a missing, malformed, foreign, or
|
||||
* stale-schema file reads as a miss. Callers run inside `agent/pre-step`, where a
|
||||
* throw fails the user's step.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { KbClient } from "./client.js";
|
||||
import { listServices, type ServiceEntry } from "./services.js";
|
||||
|
||||
/** Bumped whenever the stored shape changes; a mismatch reads as a miss (no migration). */
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
/** Refresh interval. Evaluated per `agent/pre-step`, so a short window genuinely takes effect. */
|
||||
export const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Refresh interval applied when the cached list is EMPTY.
|
||||
*
|
||||
* An empty list is almost never a settled fact — it is the intermediate state of
|
||||
* a workspace being set up. Caching that negative result for the full TTL breaks
|
||||
* the standard first-run path: configure the plugin against a fresh workspace (0
|
||||
* services) → create a knowledge base and a service → and then wait up to half an
|
||||
* hour before the catalog appears. Re-asking every minute while the answer is
|
||||
* "nothing yet" has a bounded cost and removes that trap.
|
||||
*/
|
||||
export const EMPTY_CACHE_TTL_MS = 60 * 1000;
|
||||
|
||||
/** The stored document. */
|
||||
export interface ServiceCacheDocument {
|
||||
version: number;
|
||||
/** Epoch millis of the fetch that produced `entries`. */
|
||||
fetchedAt: number;
|
||||
/** Guards against reading a file written for another account or region. */
|
||||
workspaceId: string;
|
||||
endpointHost: string;
|
||||
entries: ServiceEntry[];
|
||||
total: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** Resolve the harness home the same way `settings-file` does. */
|
||||
function dshHome(): string {
|
||||
const fromEnv = process.env.DSH_HOME;
|
||||
return fromEnv !== undefined && fromEnv !== "" ? fromEnv : join(homedir(), ".dsh");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache path for one workspace.
|
||||
* @param workspaceId - the resolved Bailian workspace id.
|
||||
* @param home - override for tests; defaults to `$DSH_HOME` or `~/.dsh`.
|
||||
* @returns the absolute file path.
|
||||
*/
|
||||
export function serviceCachePath(workspaceId: string, home: string = dshHome()): string {
|
||||
return join(home, "cache", "bailian-kb", `services-${workspaceId}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a cache document, validating it belongs to this workspace and schema.
|
||||
* @param path - the cache file path.
|
||||
* @param workspaceId - the workspace the caller is serving.
|
||||
* @param endpointHost - the host the caller is serving.
|
||||
* @returns the document, or undefined for any miss (absent, malformed, foreign, or wrong version).
|
||||
*/
|
||||
export function readServiceCache(
|
||||
path: string,
|
||||
workspaceId: string,
|
||||
endpointHost: string,
|
||||
): ServiceCacheDocument | undefined {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch (_unreadableOrMalformed) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
||||
const doc = parsed as Partial<ServiceCacheDocument>;
|
||||
if (doc.version !== CACHE_VERSION) return undefined;
|
||||
if (doc.workspaceId !== workspaceId || doc.endpointHost !== endpointHost) return undefined;
|
||||
if (typeof doc.fetchedAt !== "number" || !Array.isArray(doc.entries)) return undefined;
|
||||
return {
|
||||
version: CACHE_VERSION,
|
||||
fetchedAt: doc.fetchedAt,
|
||||
workspaceId,
|
||||
endpointHost,
|
||||
entries: doc.entries,
|
||||
total: typeof doc.total === "number" ? doc.total : doc.entries.length,
|
||||
truncated: doc.truncated === true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a cache document atomically: a reader either sees the previous file or
|
||||
* the complete new one, never a half-written mix.
|
||||
* @param path - the cache file path.
|
||||
* @param doc - the document to store.
|
||||
*/
|
||||
export function writeServiceCache(path: string, doc: ServiceCacheDocument): void {
|
||||
const dir = dirname(path);
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const temp = `${path}.${process.pid}.tmp`;
|
||||
writeFileSync(temp, `${JSON.stringify(doc, undefined, 2)}\n`, { mode: 0o600 });
|
||||
renameSync(temp, path);
|
||||
}
|
||||
|
||||
/** What the settings panel shows about the cache; see {@link ServiceCache.status}. */
|
||||
export interface ServiceCacheStatus {
|
||||
workspaceId: string;
|
||||
/** Epoch millis of the last successful fetch; absent when nothing is cached. */
|
||||
fetchedAt?: number;
|
||||
searchCount: number;
|
||||
chatCount: number;
|
||||
/** Server-reported total, which exceeds the counts above when the fetch was capped. */
|
||||
total: number;
|
||||
truncated: boolean;
|
||||
stale: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceCacheOptions {
|
||||
client: KbClient;
|
||||
/** Resolves the current workspace id; a failure means "not configured yet". */
|
||||
resolveWorkspaceId: () => Promise<string>;
|
||||
endpointHost: string;
|
||||
/** Reports refresh failures without escalating them. */
|
||||
warn: (message: string) => void;
|
||||
/** Test seams. */
|
||||
home?: string;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The service cache: a synchronous in-memory view over the on-disk document,
|
||||
* plus a deduplicated background refresh.
|
||||
*/
|
||||
export class ServiceCache {
|
||||
/** Last document read or written; undefined until one is available. */
|
||||
private document: ServiceCacheDocument | undefined;
|
||||
/** The in-flight refresh, if any. One per instance: `pre-step` asks on every model request. */
|
||||
private inFlight: Promise<void> | undefined;
|
||||
/** Workspace of {@link document}, so a workspace switch invalidates in memory too. */
|
||||
private loadedFor: string | undefined;
|
||||
|
||||
constructor(private readonly opts: ServiceCacheOptions) {}
|
||||
|
||||
private get now(): number {
|
||||
return (this.opts.now ?? Date.now)();
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached entries for a workspace, loading the file on first use.
|
||||
* Synchronous and total — safe to call from `agent/pre-step`.
|
||||
* @param workspaceId - the workspace being served.
|
||||
* @returns the document, or undefined when nothing usable is cached.
|
||||
*/
|
||||
peek(workspaceId: string): ServiceCacheDocument | undefined {
|
||||
if (this.loadedFor !== workspaceId) {
|
||||
this.document = readServiceCache(
|
||||
serviceCachePath(workspaceId, this.opts.home ?? dshHome()),
|
||||
workspaceId,
|
||||
this.opts.endpointHost,
|
||||
);
|
||||
this.loadedFor = workspaceId;
|
||||
}
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cached document is missing or older than its TTL.
|
||||
* An empty list expires on the much shorter {@link EMPTY_CACHE_TTL_MS}.
|
||||
* @param workspaceId - the workspace being served.
|
||||
* @returns true when a refresh is due.
|
||||
*/
|
||||
isStale(workspaceId: string): boolean {
|
||||
const doc = this.peek(workspaceId);
|
||||
if (doc === undefined) return true;
|
||||
const ttl = doc.entries.length === 0 ? EMPTY_CACHE_TTL_MS : CACHE_TTL_MS;
|
||||
return this.now - doc.fetchedAt >= ttl;
|
||||
}
|
||||
|
||||
/** Drop the in-memory view and force the next `peek` to re-read from disk. */
|
||||
invalidate(): void {
|
||||
this.document = undefined;
|
||||
this.loadedFor = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A diagnostic snapshot for the settings panel.
|
||||
*
|
||||
* The panel exists because this cache's staleness is otherwise invisible: a
|
||||
* developer whose agent silently stops retrieving cannot tell an empty
|
||||
* workspace from a stale list without reading the JSON file. `fetchedAt` plus
|
||||
* the per-scene counts answer that in one glance.
|
||||
* @param workspaceId - the workspace being served.
|
||||
* @returns the snapshot; `fetchedAt` is undefined when nothing is cached.
|
||||
*/
|
||||
status(workspaceId: string): ServiceCacheStatus {
|
||||
const doc = this.peek(workspaceId);
|
||||
if (doc === undefined) {
|
||||
return { workspaceId, searchCount: 0, chatCount: 0, total: 0, truncated: false, stale: true };
|
||||
}
|
||||
return {
|
||||
workspaceId,
|
||||
fetchedAt: doc.fetchedAt,
|
||||
searchCount: doc.entries.filter((entry) => entry.scene === "search").length,
|
||||
chatCount: doc.entries.filter((entry) => entry.scene === "chat").length,
|
||||
total: doc.total,
|
||||
truncated: doc.truncated,
|
||||
stale: this.isStale(workspaceId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached entries of one scene, most recently modified first.
|
||||
* Backs the panel's service picker, which exists so a default service can be
|
||||
* chosen by name instead of by pasting a 36-character hex id.
|
||||
* @param workspaceId - the workspace being served.
|
||||
* @param scene - `search` or `chat`.
|
||||
* @returns the entries, newest first.
|
||||
*/
|
||||
entriesFor(workspaceId: string, scene: ServiceEntry["scene"]): ServiceEntry[] {
|
||||
const doc = this.peek(workspaceId);
|
||||
if (doc === undefined) return [];
|
||||
return doc.entries
|
||||
.filter((entry) => entry.scene === scene)
|
||||
.sort((left, right) => (right.modify_time ?? "").localeCompare(left.modify_time ?? ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and store the current service list.
|
||||
* Never rejects: failures are warned and leave the previous document in place.
|
||||
* Concurrent calls share one request.
|
||||
* @returns a promise resolving once the attempt finishes.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
// Without this guard `pre-step` would start a fetch on every model request
|
||||
// while the first is still outstanding.
|
||||
this.inFlight ??= this.runRefresh().finally(() => {
|
||||
this.inFlight = undefined;
|
||||
});
|
||||
return await this.inFlight;
|
||||
}
|
||||
|
||||
private async runRefresh(): Promise<void> {
|
||||
try {
|
||||
const workspaceId = await this.opts.resolveWorkspaceId();
|
||||
const list = await listServices(this.opts.client);
|
||||
// Both scenes failing means the fetch produced nothing; keep the old file.
|
||||
if (list.failedScenes.length === 2) {
|
||||
this.opts.warn("bailian-kb service cache not refreshed: both scene queries failed");
|
||||
return;
|
||||
}
|
||||
if (list.failedScenes.length > 0) {
|
||||
this.opts.warn(
|
||||
`bailian-kb service cache refreshed without scene(s): ${list.failedScenes.join(", ")}`,
|
||||
);
|
||||
}
|
||||
const doc: ServiceCacheDocument = {
|
||||
version: CACHE_VERSION,
|
||||
fetchedAt: this.now,
|
||||
workspaceId,
|
||||
endpointHost: this.opts.endpointHost,
|
||||
entries: list.entries,
|
||||
total: list.total,
|
||||
truncated: list.truncated,
|
||||
};
|
||||
writeServiceCache(serviceCachePath(workspaceId, this.opts.home ?? dshHome()), doc);
|
||||
this.document = doc;
|
||||
this.loadedFor = workspaceId;
|
||||
} catch (failed) {
|
||||
this.opts.warn(`bailian-kb service cache refresh failed: ${String(failed)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Renders the deployed-service catalog the model reads before deciding whether
|
||||
* to retrieve, plus the notice for a workspace that deploys nothing at all.
|
||||
*
|
||||
* These are pure functions on purpose: the selection branches below are the
|
||||
* whole routing policy, and they are far easier to pin down in tests than
|
||||
* through a live pre-step.
|
||||
*
|
||||
* Rendering conventions (settled):
|
||||
* - English frame, service names verbatim — same language as the tool
|
||||
* descriptions, so the model is not switched between languages mid-prompt.
|
||||
* - Truncation is always stated. Silently cutting the list makes the model treat
|
||||
* it as complete and flatly answer "there is no such knowledge base".
|
||||
* - An empty scene omits its whole section. `no chat services` is pure noise and
|
||||
* invites the model to handle a case that does not exist. An empty CATALOG is
|
||||
* the opposite case and does get a message: see {@link buildNoServiceNotice}.
|
||||
* - `bl` is only ever recommended together with how to get it. This plugin talks
|
||||
* to the API directly and never shells out, so a fully configured deployment
|
||||
* can have no `bl` on PATH at all.
|
||||
*/
|
||||
|
||||
import type { ServiceScene } from "./api-types.js";
|
||||
import type { ServiceEntry } from "./services.js";
|
||||
|
||||
/** Entries rendered per scene before switching to "most recently modified" mode. */
|
||||
export const CATALOG_ENTRY_LIMIT = 10;
|
||||
|
||||
/** Truncation applied to a service description once the backend returns one. */
|
||||
const DESCRIPTION_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* Appended once whenever the text tells the model to look further with `bl`.
|
||||
*
|
||||
* The install line belongs next to the recommendation, not in the tool
|
||||
* descriptions: those are static and every token there is spent on every
|
||||
* request, while this is needed only in the branches that actually name the
|
||||
* command. A single-service catalog never carries it.
|
||||
*/
|
||||
const BL_AVAILABILITY_NOTE =
|
||||
"(`bl` is the Bailian CLI — install it with `npm install -g bailian-cli` if the command is not found.)";
|
||||
|
||||
export interface CatalogInput {
|
||||
entries: readonly ServiceEntry[];
|
||||
/** Server-reported total, which may exceed `entries` when the fetch itself was capped. */
|
||||
total: number;
|
||||
/** True when the fetch stopped before the server ran out of rows. */
|
||||
truncated: boolean;
|
||||
defaultRetrieveAgentId?: string;
|
||||
defaultChatAgentId?: string;
|
||||
}
|
||||
|
||||
const SCENE_LABEL: Record<ServiceScene, string> = {
|
||||
search: "kb_search (retrieval)",
|
||||
chat: "kb_chat (grounded Q&A)",
|
||||
};
|
||||
|
||||
/** Render one entry as a single line. */
|
||||
function renderEntry(entry: ServiceEntry): string {
|
||||
const name = entry.agent_name === "" ? "(unnamed)" : entry.agent_name;
|
||||
const description =
|
||||
entry.description === undefined || entry.description.trim() === ""
|
||||
? undefined
|
||||
: entry.description.trim().length > DESCRIPTION_LIMIT
|
||||
? `${entry.description.trim().slice(0, DESCRIPTION_LIMIT - 1)}…`
|
||||
: entry.description.trim();
|
||||
return `- ${entry.agent_id} — ${name}${description === undefined ? "" : `: ${description}`}`;
|
||||
}
|
||||
|
||||
/** Most recently modified first; entries without a timestamp sort last. */
|
||||
function byRecency(left: ServiceEntry, right: ServiceEntry): number {
|
||||
const l = left.modify_time ?? "";
|
||||
const r = right.modify_time ?? "";
|
||||
if (l === r) return 0;
|
||||
if (l === "") return 1;
|
||||
if (r === "") return -1;
|
||||
return l < r ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one scene's section, or undefined when the scene has no services.
|
||||
* @param entries - all cached entries (any scene).
|
||||
* @param scene - the scene to render.
|
||||
* @param defaultAgentId - this scene's configured default service, when set.
|
||||
* @param truncatedFetch - whether the fetch itself left rows unread.
|
||||
* @returns the section lines, or undefined to omit the section entirely.
|
||||
*/
|
||||
function renderScene(
|
||||
entries: readonly ServiceEntry[],
|
||||
scene: ServiceScene,
|
||||
defaultAgentId: string | undefined,
|
||||
truncatedFetch: boolean,
|
||||
): { lines: string[]; usedLookupHint: boolean } | undefined {
|
||||
const forScene = entries.filter((entry) => entry.scene === scene);
|
||||
if (forScene.length === 0) return undefined;
|
||||
const lines = [`${SCENE_LABEL[scene]}:`];
|
||||
|
||||
const configured =
|
||||
defaultAgentId === undefined
|
||||
? undefined
|
||||
: forScene.find((entry) => entry.agent_id === defaultAgentId);
|
||||
if (configured !== undefined) {
|
||||
// A configured default is the user's own pick: the highest-quality signal
|
||||
// available, so it is the only entry worth spending context on.
|
||||
lines.push(renderEntry(configured));
|
||||
const others = forScene.length - 1;
|
||||
if (others > 0) {
|
||||
lines.push(
|
||||
` (default service; ${others} other${others === 1 ? "" : "s"} exist — ` +
|
||||
`run \`bl knowledge service list --scene ${scene}\` to see them)`,
|
||||
);
|
||||
return { lines, usedLookupHint: true };
|
||||
}
|
||||
return { lines, usedLookupHint: false };
|
||||
}
|
||||
|
||||
if (forScene.length <= CATALOG_ENTRY_LIMIT && !truncatedFetch) {
|
||||
lines.push(...forScene.map(renderEntry));
|
||||
return { lines, usedLookupHint: false };
|
||||
}
|
||||
|
||||
const shown = [...forScene].sort(byRecency).slice(0, CATALOG_ENTRY_LIMIT);
|
||||
lines.push(...shown.map(renderEntry));
|
||||
// State the shortfall: the model must know this list is partial before it
|
||||
// concludes no service covers the question.
|
||||
const knownTotal = Math.max(forScene.length, shown.length);
|
||||
lines.push(
|
||||
` (showing ${shown.length} most recently modified of ${truncatedFetch ? "more than " : ""}` +
|
||||
`${knownTotal} deployed ${scene} services — run \`bl knowledge service list --scene ${scene} ` +
|
||||
"--name <keyword>` to look for others)",
|
||||
);
|
||||
return { lines, usedLookupHint: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* The notice injected when the workspace is reachable but deploys nothing the
|
||||
* tools can call.
|
||||
*
|
||||
* This deliberately contradicts the "omit what does not exist" rule above, and
|
||||
* the cases are not symmetric: an empty SECTION is noise because the other
|
||||
* section still hands the model ids, whereas an empty CATALOG leaves it with no
|
||||
* id at all while `agent_id` stays required. With nothing said, the model either
|
||||
* invents an id — earning a raw server rejection — or silently drops the
|
||||
* retrieval; both read to the user as a broken plugin when the real answer is
|
||||
* "deploy a service". Naming the fix is the only way out that does not require
|
||||
* the model to already have loaded the management skill.
|
||||
* @returns the model-facing notice text.
|
||||
*/
|
||||
export function buildNoServiceNotice(): string {
|
||||
return [
|
||||
"<system-reminder>",
|
||||
"The Bailian knowledge tools (kb_search / kb_chat) are configured, but this workspace has no " +
|
||||
"deployed knowledge service, so neither tool can be called: both require an `agent_id` and there " +
|
||||
"is none to use. Do not call them, and do not invent an id.",
|
||||
"",
|
||||
"If the user asks for something that should come from their knowledge base, tell them a service has " +
|
||||
"to be created and deployed first — in the Bailian console (https://bailian.console.aliyun.com/), " +
|
||||
"or with `bl knowledge service create` followed by `bl knowledge service deploy`. " +
|
||||
BL_AVAILABILITY_NOTE,
|
||||
"</system-reminder>",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* The service list appended to a call the server rejected, rendered from a
|
||||
* just-refreshed cache.
|
||||
*
|
||||
* Lives here rather than at the call site so every model-facing rendering obeys
|
||||
* the one set of conventions documented at the top of this file — in particular
|
||||
* that a shortfall is stated and that `bl` never appears without its install
|
||||
* line.
|
||||
* @param scene - the scene the failed call targeted.
|
||||
* @param entries - that scene's entries as of the refresh, newest first.
|
||||
* @returns the text to append to the failure.
|
||||
*/
|
||||
export function buildRefreshedSceneList(
|
||||
scene: ServiceScene,
|
||||
entries: readonly ServiceEntry[],
|
||||
): string {
|
||||
if (entries.length === 0) return buildNoSceneServiceNotice(scene);
|
||||
const shown = entries.slice(0, CATALOG_ENTRY_LIMIT);
|
||||
const more = entries.length - shown.length;
|
||||
return [
|
||||
`Deployed ${scene} services in this workspace, re-read just now:`,
|
||||
...shown.map(renderEntry),
|
||||
...(more > 0
|
||||
? [
|
||||
`(and ${more} more — run \`bl knowledge service list --scene ${scene}\` to see them) ` +
|
||||
BL_AVAILABILITY_NOTE,
|
||||
]
|
||||
: []),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* What a rejected call is told when the refreshed list holds no service for that
|
||||
* scene at all.
|
||||
*
|
||||
* The tool has already failed by this point, so this text is the model's only
|
||||
* instruction. Without it the bare server rejection ("invalid agent_id") invites
|
||||
* a retry with another guess, and no guess can succeed.
|
||||
* @param scene - the scene whose services came back empty.
|
||||
* @returns the text to append to the failure.
|
||||
*/
|
||||
function buildNoSceneServiceNotice(scene: ServiceScene): string {
|
||||
return (
|
||||
`This workspace has no deployed ${scene} services at all (re-checked just now), so no agent_id can ` +
|
||||
"work — do not retry with a different id. Tell the user one has to be created and deployed, in the " +
|
||||
"Bailian console (https://bailian.console.aliyun.com/) or with `bl knowledge service create` " +
|
||||
`followed by \`bl knowledge service deploy\`. ${BL_AVAILABILITY_NOTE}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the catalog text for one cached service list.
|
||||
* @param input - the cached entries plus the deployment's configured defaults.
|
||||
* @returns the model-facing text, or undefined when there is nothing worth injecting.
|
||||
*/
|
||||
export function buildServiceCatalog(input: CatalogInput): string | undefined {
|
||||
const search = renderScene(
|
||||
input.entries,
|
||||
"search",
|
||||
input.defaultRetrieveAgentId,
|
||||
input.truncated,
|
||||
);
|
||||
const chat = renderScene(input.entries, "chat", input.defaultChatAgentId, input.truncated);
|
||||
if (search === undefined && chat === undefined) return undefined;
|
||||
const usedLookupHint = search?.usedLookupHint === true || chat?.usedLookupHint === true;
|
||||
return [
|
||||
"<system-reminder>",
|
||||
// The header must not name the tools: a scene with no services omits its
|
||||
// section, and naming that tool anyway would invite passing an id from the
|
||||
// other scene, which the service rejects. Section labels carry the mapping.
|
||||
"Bailian knowledge services deployed in this workspace, grouped by the tool that accepts them. " +
|
||||
"Pass an id from the matching section as that tool's `agent_id` argument — it is required and " +
|
||||
"cannot be guessed.",
|
||||
"",
|
||||
...(search?.lines ?? []),
|
||||
...(search !== undefined && chat !== undefined ? [""] : []),
|
||||
...(chat?.lines ?? []),
|
||||
"",
|
||||
"If none of these services covers what the user is asking about, say so plainly rather than " +
|
||||
"trying the closest-looking id — an unrelated retrieval result is worse than none.",
|
||||
...(usedLookupHint ? [BL_AVAILABILITY_NOTE] : []),
|
||||
"</system-reminder>",
|
||||
].join("\n");
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Publishes the cached service catalog into each request as a sourced context
|
||||
* message, on the `agent/pre-step` waterfall.
|
||||
*
|
||||
* Why a context message rather than the tool descriptions: a tool description is
|
||||
* fixed when the plugin loads, and a plugin loads once per PROCESS, not once per
|
||||
* session. In a long-running host the TTL would be evaluated exactly once at
|
||||
* `apply()` and a service created elsewhere would never be noticed until a
|
||||
* restart. Re-registering tools to refresh a description instead invalidates the
|
||||
* prompt prefix cache from the first changed schema token. Injecting context
|
||||
* keeps the tool schemas byte-stable forever and still refreshes per step.
|
||||
*
|
||||
* Two hard constraints follow from `agent/pre-step` semantics:
|
||||
*
|
||||
* 1. `pre-step` fires once per STEP, and a step is one model request — a turn with
|
||||
* five tool calls fires it six times. Re-injecting each time would insert six
|
||||
* copies into one turn and void the KV cache from the first insertion onward,
|
||||
* so change suppression is a correctness requirement, not an optimization.
|
||||
* 2. A throwing listener fails the proposed step, i.e. the user's turn stalls.
|
||||
* Everything here is therefore wrapped: any failure degrades to "inject
|
||||
* nothing this step".
|
||||
*/
|
||||
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
|
||||
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
||||
import type { UserMessage } from "@deepseek-ai/dsh-session";
|
||||
import { buildNoServiceNotice, buildServiceCatalog } from "./service-catalog.js";
|
||||
import type { ServiceCache } from "./service-cache.js";
|
||||
|
||||
/** Marks this plugin's own injections in the durable log. */
|
||||
const SOURCE_PLUGIN = "tool-bailian-kb/services";
|
||||
|
||||
export interface ServiceContextOptions {
|
||||
cache: ServiceCache;
|
||||
/** Resolves the workspace being served; undefined means "not configured yet". */
|
||||
resolveWorkspaceId: () => Promise<string | undefined>;
|
||||
/** Resolves the configured default retrieval service (settings then credential). */
|
||||
resolveDefaultRetrieveAgentId: () => Promise<string | undefined>;
|
||||
/** Resolves the configured default chat service (settings then credential). */
|
||||
resolveDefaultChatAgentId: () => Promise<string | undefined>;
|
||||
warn: (message: string) => void;
|
||||
}
|
||||
|
||||
/** Whether one durable message came from this module. */
|
||||
function isOwnInjection(source: { kind: string; plugin?: string }): boolean {
|
||||
return source.kind === "plugin" && source.plugin === SOURCE_PLUGIN;
|
||||
}
|
||||
|
||||
/** Concatenate a message's text parts, which is what the model actually reads. */
|
||||
function messageText(message: UserMessage): string {
|
||||
return message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalog text this session last injected AND still shows the model.
|
||||
*
|
||||
* The visibility test is the subtle half. Scanning only for "did we ever publish
|
||||
* this" would make compaction permanent data loss: once the catalog message is
|
||||
* dropped from the surface, an identical digest would suppress every future
|
||||
* injection and the model would silently spend the rest of the session without a
|
||||
* service list.
|
||||
* @param agent - the subject agent.
|
||||
* @returns the visible catalog text, or undefined when none is currently visible.
|
||||
*/
|
||||
function visibleCatalogText(agent: Agent): string | undefined {
|
||||
const visible = new Set(agent.session.surface.nodes);
|
||||
const events = agent.session.events;
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const event = events[index];
|
||||
if (event === undefined) continue;
|
||||
if (event.type !== "user/message" || !isOwnInjection(event.data.source)) continue;
|
||||
return visible.has(event.seq) ? messageText(event.data) : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** This module's proposed-but-not-yet-entered message, if the batch already carries one. */
|
||||
function pendingCatalog(messages: readonly UserMessage[]): UserMessage | undefined {
|
||||
return messages.find((message) => isOwnInjection(message.source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the pre-step listener that keeps the catalog present and current.
|
||||
* @param ctx - a context with `agents` available.
|
||||
* @param opts - the cache plus the deployment's resolved workspace and defaults.
|
||||
*/
|
||||
export function installServiceContext(ctx: Context, opts: ServiceContextOptions): void {
|
||||
ctx.on(
|
||||
"agent/pre-step",
|
||||
async ({ agent, signal }, next): Promise<PreStepDecision> => {
|
||||
const decision = await next();
|
||||
if (decision.kind === "reject" || signal.aborted) return decision;
|
||||
try {
|
||||
const workspaceId = await opts.resolveWorkspaceId();
|
||||
// Nothing is configured yet: the tools themselves will explain that.
|
||||
if (workspaceId === undefined || workspaceId === "") return decision;
|
||||
|
||||
// Refresh scheduling lives here, not at plugin load, so a long-running
|
||||
// process still notices services created elsewhere. Never awaited: a slow
|
||||
// list request must not delay the user's request.
|
||||
if (opts.cache.isStale(workspaceId)) void opts.cache.refresh();
|
||||
|
||||
const document = opts.cache.peek(workspaceId);
|
||||
// No document at all means the first fetch has not landed (or is
|
||||
// failing): we know nothing, so claiming anything would be a guess. The
|
||||
// refresh above self-corrects this on a later step.
|
||||
if (document === undefined) return decision;
|
||||
const [defaultRetrieveAgentId, defaultChatAgentId] = await Promise.all([
|
||||
opts.resolveDefaultRetrieveAgentId(),
|
||||
opts.resolveDefaultChatAgentId(),
|
||||
]);
|
||||
if (signal.aborted) return decision;
|
||||
// A document with zero entries is the opposite: an authoritative "this
|
||||
// workspace deploys nothing callable". Saying so beats silence, which
|
||||
// leaves the model to invent a required agent_id.
|
||||
const text =
|
||||
buildServiceCatalog({
|
||||
entries: document.entries,
|
||||
total: document.total,
|
||||
truncated: document.truncated,
|
||||
...(defaultRetrieveAgentId !== undefined ? { defaultRetrieveAgentId } : {}),
|
||||
...(defaultChatAgentId !== undefined ? { defaultChatAgentId } : {}),
|
||||
}) ?? buildNoServiceNotice();
|
||||
|
||||
// Identical to what the model already sees: stay out of the way. This is
|
||||
// the branch that runs on nearly every step.
|
||||
if (visibleCatalogText(agent) === text) return decision;
|
||||
|
||||
const pending = pendingCatalog(decision.messages);
|
||||
if (pending !== undefined && messageText(pending) === text) return decision;
|
||||
|
||||
const catalog = createUserMessage({
|
||||
content: [{ type: "text", text }],
|
||||
source: { kind: "plugin", plugin: SOURCE_PLUGIN, form: "catalog" },
|
||||
});
|
||||
return {
|
||||
kind: "enter",
|
||||
messages:
|
||||
pending === undefined
|
||||
? [...decision.messages, catalog]
|
||||
: decision.messages.map((message) => (message.id === pending.id ? catalog : message)),
|
||||
};
|
||||
} catch (failed) {
|
||||
// A throw here would fail the user's step; a missing catalog is far cheaper.
|
||||
opts.warn(`bailian-kb service catalog not injected: ${String(failed)}`);
|
||||
return decision;
|
||||
}
|
||||
},
|
||||
{ prepend: true },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Retrieval-service discovery for the plugin's internal cache. Not a model tool:
|
||||
* see `KB_PATHS.serviceList`.
|
||||
*
|
||||
* Listing is paged, and a workspace can hold far more services than are useful
|
||||
* for routing, so this module stops after {@link MAX_PAGES} and reports the
|
||||
* shortfall rather than paging through everything. Only deployed services are
|
||||
* requested: anything else is not callable by the default service version, so it
|
||||
* would be noise in the catalog the model reads.
|
||||
*/
|
||||
|
||||
import type { ServiceListResponse, ServiceScene } from "./api-types.js";
|
||||
import type { KbClient } from "./client.js";
|
||||
import { KB_PATHS } from "./endpoints.js";
|
||||
|
||||
/** Page size requested per listing call. */
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
/** Pages fetched per scene before reporting truncation; well past the useful routing range. */
|
||||
const MAX_PAGES = 2;
|
||||
|
||||
/** One deployed retrieval or Q&A service, reduced to the fields that inform routing. */
|
||||
export interface ServiceEntry {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
scene: ServiceScene;
|
||||
/** Deployment status as reported for the service; only callable states are listed. */
|
||||
status: string;
|
||||
/** Last modification timestamp; the only signal for "which of these is in use". */
|
||||
modify_time?: string;
|
||||
/** Service description, when the listing provides one. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ServiceList {
|
||||
entries: ServiceEntry[];
|
||||
/** Server-reported total across the queried scenes, including rows never fetched. */
|
||||
total: number;
|
||||
/** True when a scene reported more rows than {@link MAX_PAGES} pages returned. */
|
||||
truncated: boolean;
|
||||
/** Scenes whose query failed; a partial list stays usable. */
|
||||
failedScenes: ServiceScene[];
|
||||
}
|
||||
|
||||
const SCENES: readonly ServiceScene[] = ["search", "chat"];
|
||||
|
||||
/**
|
||||
* Fetch the deployed services of one scene, stopping at the page cap.
|
||||
* @param client - the shared knowledge API client.
|
||||
* @param scene - `search` or `chat`.
|
||||
* @returns the scene's entries, its server-reported total, and whether rows were left unfetched.
|
||||
*/
|
||||
async function listScene(
|
||||
client: KbClient,
|
||||
scene: ServiceScene,
|
||||
): Promise<{ entries: ServiceEntry[]; total: number; truncated: boolean }> {
|
||||
const entries: ServiceEntry[] = [];
|
||||
let total = 0;
|
||||
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
||||
const res = await client.postJson<ServiceListResponse>(KB_PATHS.serviceList, {
|
||||
agent_scene: scene,
|
||||
agent_status: "deployed",
|
||||
page_number: page,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
total = res.data?.total_count ?? total;
|
||||
const rows = res.data?.rows ?? [];
|
||||
for (const row of rows) {
|
||||
const agentId = row.agent_id ?? "";
|
||||
// A row without an id cannot be called, so it has no reason to exist here.
|
||||
if (agentId === "") continue;
|
||||
entries.push({
|
||||
agent_id: agentId,
|
||||
agent_name: row.agent_name ?? "",
|
||||
scene,
|
||||
status: row.agent_status ?? "",
|
||||
...(typeof row.modify_time === "string" ? { modify_time: row.modify_time } : {}),
|
||||
});
|
||||
}
|
||||
// A short page is the last page; the server has nothing further to give.
|
||||
if (rows.length < PAGE_SIZE) return { entries, total, truncated: false };
|
||||
}
|
||||
return { entries, total, truncated: total > entries.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* List the deployed services of both scenes.
|
||||
* A scene that fails is recorded and skipped rather than failing the whole
|
||||
* refresh: half a list still routes better than none.
|
||||
* @param client - the shared knowledge API client.
|
||||
* @returns merged entries plus totals, truncation, and per-scene failures.
|
||||
*/
|
||||
export async function listServices(client: KbClient): Promise<ServiceList> {
|
||||
const entries: ServiceEntry[] = [];
|
||||
const failedScenes: ServiceScene[] = [];
|
||||
let total = 0;
|
||||
let truncated = false;
|
||||
for (const scene of SCENES) {
|
||||
try {
|
||||
const result = await listScene(client, scene);
|
||||
entries.push(...result.entries);
|
||||
total += result.total;
|
||||
truncated = truncated || result.truncated;
|
||||
} catch (_sceneFailed) {
|
||||
failedScenes.push(scene);
|
||||
}
|
||||
}
|
||||
return { entries, total, truncated, failedScenes };
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Runtime skill registration: the packaged bailian-kb SKILL.md joins the catalog
|
||||
* when a skills registry is composed.
|
||||
*
|
||||
* The file's YAML frontmatter is the single source of truth for the routing name
|
||||
* and description — duplicating them here drifts, and the copy that loses is the
|
||||
* one nobody reads. The frontmatter must also be STRIPPED from the registered
|
||||
* body: `SkillDefinition.content` is contractually the body a provider has
|
||||
* already cleaned of its own metadata, and `ctx.skills.register()` parses
|
||||
* nothing, so handing over the raw file ships the YAML block into the model's
|
||||
* context.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
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/", import.meta.url));
|
||||
|
||||
/** The registrable fields carried by one skill file. */
|
||||
export interface ParsedSkillFile {
|
||||
/** Kebab-case skill name from frontmatter. */
|
||||
name: string;
|
||||
/** Routing description from frontmatter (the catalog truncates at 500 chars). */
|
||||
description: string;
|
||||
/** Optional extra routing guidance. */
|
||||
whenToUse?: string;
|
||||
/** Optional frontmatter `metadata` object. */
|
||||
metadata?: Record<string, unknown>;
|
||||
/** Markdown body with the frontmatter block removed. */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the frontmatter block, mirroring the filesystem provider's delimiters
|
||||
* so a file that loads from disk behaves identically when bundled.
|
||||
* @param raw - the file's full text.
|
||||
* @returns the frontmatter YAML and the body after it, or undefined when unfenced.
|
||||
*/
|
||||
function splitFrontmatter(raw: string): { yaml: string; body: string } | undefined {
|
||||
const firstLineEnd = raw.indexOf("\n");
|
||||
if (firstLineEnd < 0) return undefined;
|
||||
if (raw.slice(0, firstLineEnd).replace(/\r$/, "") !== "---") return undefined;
|
||||
const start = firstLineEnd + 1;
|
||||
let lineStart = start;
|
||||
while (lineStart <= raw.length) {
|
||||
const nextNewline = raw.indexOf("\n", lineStart);
|
||||
const lineEnd = nextNewline < 0 ? raw.length : nextNewline;
|
||||
if (raw.slice(lineStart, lineEnd).replace(/\r$/, "") === "---") {
|
||||
return {
|
||||
yaml: raw.slice(start, lineStart),
|
||||
body: raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1),
|
||||
};
|
||||
}
|
||||
if (nextNewline < 0) return undefined;
|
||||
lineStart = nextNewline + 1;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one skill file into its registrable fields.
|
||||
* @param raw - the file's full text.
|
||||
* @returns the parsed fields, or undefined when the frontmatter is absent, unparsable, or missing name/description.
|
||||
*/
|
||||
export function parseSkillFile(raw: string): ParsedSkillFile | undefined {
|
||||
const split = splitFrontmatter(raw);
|
||||
if (split === undefined) return undefined;
|
||||
let data: unknown;
|
||||
try {
|
||||
data = parseYaml(split.yaml);
|
||||
} catch (_invalidYaml) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined;
|
||||
const record = data as Record<string, unknown>;
|
||||
const name = typeof record.name === "string" ? record.name.trim() : "";
|
||||
const description = typeof record.description === "string" ? record.description.trim() : "";
|
||||
// The registry rejects a blank description outright; failing here keeps the
|
||||
// diagnostic on the file instead of on the registration call.
|
||||
if (name === "" || description === "") return undefined;
|
||||
const whenToUse = typeof record.whenToUse === "string" ? record.whenToUse.trim() : "";
|
||||
const metadata =
|
||||
typeof record.metadata === "object" &&
|
||||
record.metadata !== null &&
|
||||
!Array.isArray(record.metadata)
|
||||
? (record.metadata as Record<string, unknown>)
|
||||
: undefined;
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
...(whenToUse !== "" ? { whenToUse } : {}),
|
||||
...(metadata !== undefined ? { metadata } : {}),
|
||||
content: split.body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the management skill when the skills registry is composed; headless
|
||||
* assemblies without the seam stay unaffected. An unreadable or malformed file
|
||||
* degrades to a warning — a broken bundled asset must not fail plugin load.
|
||||
* @param ctx - the plugin context.
|
||||
*/
|
||||
export function registerSkill(ctx: Context): void {
|
||||
ctx.inject(["skills"], (skillCtx) => {
|
||||
const path = join(SKILL_DIR, "SKILL.md");
|
||||
let raw: string;
|
||||
try {
|
||||
raw = readFileSync(path, "utf8");
|
||||
} catch (unreadable) {
|
||||
skillCtx.logger.warn(
|
||||
`bailian-kb skill not registered: cannot read ${path}: ${String(unreadable)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const parsed = parseSkillFile(raw);
|
||||
if (parsed === undefined) {
|
||||
skillCtx.logger.warn(
|
||||
`bailian-kb skill not registered: ${path} needs YAML frontmatter carrying name and description`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
skillCtx.skills.register({
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
...(parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {}),
|
||||
...(parsed.metadata !== undefined ? { metadata: parsed.metadata } : {}),
|
||||
content: parsed.content,
|
||||
source: "bundled",
|
||||
path,
|
||||
resourceBase: { kind: "directory", path: SKILL_DIR },
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<Uint8Array>): AsyncGenerator<SseEvent> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* The two model-facing knowledge tools (kb_search, kb_chat). agent_id is REQUIRED in the schema:
|
||||
* a model cannot know from the tool spec whether this deployment configures a default service, and a
|
||||
* missing default previously only surfaced at call time, forcing a wasted round-trip. The per-call
|
||||
* fallback to a configured default (settings/config or credential) is retained as defense-in-depth,
|
||||
* but note defineTool validates args against the schema before execute, so through that entry point
|
||||
* the fallback is inert; the model-facing contract is explicit.
|
||||
*
|
||||
* These descriptions are deliberately STATIC. The available service ids are
|
||||
* deployment state that changes while the process runs, and re-registering a tool
|
||||
* to refresh its description invalidates the prompt prefix cache from the first
|
||||
* changed schema token. The live catalog therefore rides an `agent/pre-step`
|
||||
* context message instead (see `service-context.ts`), leaving these schemas
|
||||
* byte-stable for the life of the process.
|
||||
*
|
||||
* Being static is also why recovery instructions do NOT live here: how to look
|
||||
* further depends on what is actually deployed, and a token spent in a
|
||||
* description is spent on every request. The dynamic carriers own that — the
|
||||
* catalog message states a shortfall where one exists, and a rejected call comes
|
||||
* back with the services that do exist (`service-catalog.ts`).
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
/** Client-side chunk cap applied when the model omits top_k. */
|
||||
const DEFAULT_TOP_K = 5;
|
||||
|
||||
export interface KbToolDeps {
|
||||
client: KbClient;
|
||||
/** Resolves the default retrieval agent id per call (settings/patch config or credential); omitted means no default for kb_search. */
|
||||
resolveDefaultRetrieveAgentId?: () => Promise<string | undefined>;
|
||||
/** Resolves the default chat agent id per call (settings/patch config or credential); omitted means no default for kb_chat. */
|
||||
resolveDefaultChatAgentId?: () => Promise<string | undefined>;
|
||||
/**
|
||||
* Refreshes the service cache and summarizes what the workspace currently
|
||||
* deploys for one scene. Called only after a client-side API failure, so a
|
||||
* stale cached id self-corrects within the same step instead of waiting for the
|
||||
* next scheduled refresh.
|
||||
*/
|
||||
describeServicesAfterRefresh?: (scene: "search" | "chat") => Promise<string | undefined>;
|
||||
/** Read per call (a live-settings deployment supplies a getter). */
|
||||
chatTimeoutMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rethrow an API failure, appending a freshly refreshed service list when the
|
||||
* server rejected the request.
|
||||
*
|
||||
* On these two endpoints `agent_id` is the only caller-supplied identifier, so a
|
||||
* 4xx is most often a service id that no longer exists — the recovery the model
|
||||
* needs is the current list, delivered in the error message. The message is
|
||||
* ordinary conversation text appended at the tail, so unlike a re-registered
|
||||
* description it does not disturb the request prefix.
|
||||
*/
|
||||
async function withServiceHint(
|
||||
err: unknown,
|
||||
scene: "search" | "chat",
|
||||
describe: KbToolDeps["describeServicesAfterRefresh"],
|
||||
): Promise<never> {
|
||||
if (describe === undefined || !(err instanceof KbApiError)) throw err;
|
||||
const status = err.status;
|
||||
if (status === undefined || status < 400 || status >= 500) throw err;
|
||||
// Best-effort enrichment: a failing refresh must not replace the real error.
|
||||
const summary = await describe(scene).catch(() => undefined);
|
||||
if (summary === undefined) throw err;
|
||||
throw new KbApiError(
|
||||
`${err.message}
|
||||
|
||||
${summary}`,
|
||||
status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the two 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) {
|
||||
// chatTimeoutMs is deliberately NOT destructured: reading it off deps at
|
||||
// execute time keeps a live-settings getter live.
|
||||
const { client, resolveDefaultRetrieveAgentId, resolveDefaultChatAgentId } = deps;
|
||||
const agentIdParam = {
|
||||
type: "string" as const,
|
||||
required: true as const,
|
||||
description:
|
||||
"Retrieval/Q&A service id. REQUIRED: the schema cannot know whether this deployment " +
|
||||
"configures a default service, so always pass one. This conversation carries a context message " +
|
||||
"listing the services deployed in the workspace — take the id from the section matching the tool " +
|
||||
"you are calling. When that list is absent, or none of its services covers the question, say so " +
|
||||
"rather than guessing: a rejected id is answered with the services that do exist, so a guess buys " +
|
||||
"nothing.",
|
||||
};
|
||||
const resolveRetrieveAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
if (supplied !== undefined) return supplied;
|
||||
const defaultId =
|
||||
resolveDefaultRetrieveAgentId === undefined
|
||||
? undefined
|
||||
: await resolveDefaultRetrieveAgentId();
|
||||
if (defaultId === undefined) {
|
||||
throw new Error(
|
||||
"agent_id is required: no default retrieval service is configured. Pass agent_id explicitly " +
|
||||
"(find ids: `bl knowledge service list --scene search --workspace-id <workspaceId>`), or configure a " +
|
||||
"default: bailian-kb.defaultRetrieveAgentId in ~/.dsh/settings.yaml or " +
|
||||
"BAILIAN_DEFAULT_RETRIEVE_AGENT_ID in ~/.dsh/.credentials.yaml.",
|
||||
);
|
||||
}
|
||||
return defaultId;
|
||||
};
|
||||
const resolveChatAgentId = async (supplied: string | undefined): Promise<string> => {
|
||||
if (supplied !== undefined) return supplied;
|
||||
const defaultId =
|
||||
resolveDefaultChatAgentId === undefined ? undefined : await resolveDefaultChatAgentId();
|
||||
if (defaultId === undefined) {
|
||||
throw new Error(
|
||||
"agent_id is required: no default chat service is configured. Pass agent_id explicitly " +
|
||||
"(find ids: `bl knowledge service list --scene chat --workspace-id <workspaceId>`), or configure a " +
|
||||
"default: bailian-kb.defaultChatAgentId in ~/.dsh/settings.yaml or " +
|
||||
"BAILIAN_DEFAULT_CHAT_AGENT_ID in ~/.dsh/.credentials.yaml.",
|
||||
);
|
||||
}
|
||||
return defaultId;
|
||||
};
|
||||
|
||||
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. " +
|
||||
"Credentials and workspace resolve automatically from DSH config " +
|
||||
"(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — " +
|
||||
"never read or pass them yourself. agent_id is REQUIRED (see its parameter description). " +
|
||||
"If no listed service covers what the user is asking about, say so plainly rather than trying " +
|
||||
"the closest-looking id: unrelated evidence is worse for the user than none.",
|
||||
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: await resolveRetrieveAgentId(args.agent_id),
|
||||
...(client.agentVersion ? { agent_version: client.agentVersion } : {}),
|
||||
...(args.images && args.images.length > 0 ? { images: args.images } : {}),
|
||||
};
|
||||
const res = await client
|
||||
.postJson<SearchResponse>(KB_PATHS.search, body)
|
||||
.catch(
|
||||
async (err) => await withServiceHint(err, "search", deps.describeServicesAfterRefresh),
|
||||
);
|
||||
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. " +
|
||||
"Credentials and workspace resolve automatically from DSH config " +
|
||||
"(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — " +
|
||||
"never read or pass them yourself. agent_id is REQUIRED (see its parameter description). " +
|
||||
"If no listed service covers what the user is asking about, say so plainly rather than trying " +
|
||||
"the closest-looking id.",
|
||||
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 chatTimeoutMs = deps.chatTimeoutMs;
|
||||
const body = {
|
||||
input: { messages: [{ role: "user" as const, content: args.message }] },
|
||||
parameters: {
|
||||
agent_options: {
|
||||
agent_id: await resolveChatAgentId(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(err, "chat", deps.describeServicesAfterRefresh);
|
||||
}
|
||||
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 [search, chat];
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/* Bailian settings page: title row, credential fields, clear control, and
|
||||
save footer, in the settings-panel design language (16/24 title, 14/22
|
||||
intro, `--dsw-alias-*` tokens — an out-of-tree bundle cannot value-import
|
||||
the host section components, only their platform primitives). */
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 720px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.headRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* The three controls grouped as one outlined object on the panel fill. */
|
||||
.form {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 12px;
|
||||
padding: 4px 16px 12px;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
}
|
||||
|
||||
/* Autofill control between the intro and the form: button + one-line status. */
|
||||
.autofillRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Carried beside the title so a scrolled page 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);
|
||||
}
|
||||
|
||||
/* Degraded-transport notice: the write-only fallback explains itself once. */
|
||||
.notice {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.labelWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.getLink {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-brand-primary);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.getLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.badgeSuccess {
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
background: var(--dsw-alias-state-success-tertiary);
|
||||
color: var(--dsw-alias-state-success-primary);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.autofillNoticeSuccess {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-state-success-primary);
|
||||
}
|
||||
|
||||
.advancedSection {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.field + .advancedSection,
|
||||
.notice + .advancedSection {
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.advancedToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.advancedToggle:hover {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.advancedToggle::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: translateY(-2px) rotate(45deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.advancedToggle[aria-expanded="true"]::after {
|
||||
transform: translateY(1px) rotate(-135deg);
|
||||
}
|
||||
|
||||
.advancedFields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.advancedFields .field:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 0 0;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* The Bailian knowledge-base settings page: one section page in the
|
||||
* Settings left nav. The workspace, default-retrieval-service and
|
||||
* default-chat-service ids echo from the `bailian-kb` settings section
|
||||
* while the scope is ready (clearing one falls back down the resolution
|
||||
* chain), and degrade to write-only credential controls otherwise; the
|
||||
* API key is always write-only — it drafts behind a password mask,
|
||||
* starts blank, and reports only configured/unconfigured.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
|
||||
import {
|
||||
dirtyOf,
|
||||
echoedValue,
|
||||
SETTINGS_FIELDS,
|
||||
type BailianCardFace,
|
||||
type BailianCardState,
|
||||
type BailianFieldKey,
|
||||
} from "./bailian-card-controller.ts";
|
||||
import type { BailianKbLocaleKey } from "./locales.ts";
|
||||
import css from "./BailianCard.module.css";
|
||||
|
||||
/** Bailian console URL that lets the user create or copy an API key. */
|
||||
const BAILIAN_CONSOLE_API_KEY_URL =
|
||||
"https://bailian.console.aliyun.com/cn-beijing?tab=globalset#/efm/api_key";
|
||||
|
||||
/** Props the renderer binds for the Bailian section page. */
|
||||
export type BailianCardProps = PropsRuntime<"settings.section"> &
|
||||
PropsLocale<"tool-bailian-kb"> &
|
||||
InjectFace<BailianCardFace>;
|
||||
|
||||
/** One field's render description. */
|
||||
interface FieldView {
|
||||
key: BailianFieldKey;
|
||||
labelKey: BailianKbLocaleKey;
|
||||
/** Locale key for the "get" link shown next to the label. */
|
||||
getKey?: BailianKbLocaleKey;
|
||||
/** Echo-mode explanation (settings-backed value, blank save = fall back). */
|
||||
hintKey: BailianKbLocaleKey;
|
||||
/** Write-only explanation (credential store, blank = keep the stored value). */
|
||||
fallbackHintKey: BailianKbLocaleKey;
|
||||
setKey: BailianKbLocaleKey;
|
||||
unsetKey: BailianKbLocaleKey;
|
||||
/** Password-masked drafting; only the API key is an actual secret. */
|
||||
secret: boolean;
|
||||
/** When true the field is hidden inside the collapsible advanced section. */
|
||||
advanced?: boolean;
|
||||
/** External console URL for the "get" link. */
|
||||
getUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The controls, in page order.
|
||||
*
|
||||
* The two default-service ids are NOT here: they render as pickers inside the
|
||||
* advanced section instead, driven by the cached service list. A free-text id
|
||||
* field beside a picker for the same setting is the same value twice, and the
|
||||
* pair drifts the moment one of them writes.
|
||||
*/
|
||||
const FIELDS: readonly FieldView[] = [
|
||||
{
|
||||
key: "DASHSCOPE_API_KEY",
|
||||
labelKey: "apiKey",
|
||||
getKey: "apiKeyGet",
|
||||
hintKey: "apiKeyHint",
|
||||
fallbackHintKey: "apiKeyHint",
|
||||
setKey: "apiKeySet",
|
||||
unsetKey: "apiKeyUnset",
|
||||
secret: true,
|
||||
advanced: true,
|
||||
getUrl: BAILIAN_CONSOLE_API_KEY_URL,
|
||||
},
|
||||
{
|
||||
key: "BAILIAN_WORKSPACE_ID",
|
||||
labelKey: "workspaceId",
|
||||
getKey: "workspaceIdGet",
|
||||
hintKey: "workspaceIdHint",
|
||||
fallbackHintKey: "workspaceIdHintFallback",
|
||||
setKey: "workspaceIdSet",
|
||||
unsetKey: "workspaceIdUnset",
|
||||
secret: false,
|
||||
advanced: true,
|
||||
getUrl: BAILIAN_CONSOLE_API_KEY_URL,
|
||||
},
|
||||
];
|
||||
|
||||
const ADVANCED_FIELDS = FIELDS.filter((field) => field.advanced);
|
||||
|
||||
/** Whether both the API key and the workspace id already hold values. */
|
||||
function fullyConfigured(state: BailianCardState): boolean {
|
||||
const apiKeyConfigured = state.credentials.DASHSCOPE_API_KEY.configured;
|
||||
const workspaceConfigured =
|
||||
state.credentials.BAILIAN_WORKSPACE_ID.configured ||
|
||||
echoedValue(state, "BAILIAN_WORKSPACE_ID") !== "";
|
||||
return apiKeyConfigured && workspaceConfigured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Bailian section page.
|
||||
* @param props - locale copy, the page snapshot, and its actions.
|
||||
* @returns the section page.
|
||||
*/
|
||||
export function BailianCard(props: BailianCardProps) {
|
||||
const { t } = props;
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const state = props.useBailianCard((snapshot) => snapshot);
|
||||
const dirty = dirtyOf(state);
|
||||
const busy = state.saving || state.clearing;
|
||||
|
||||
// The flow spans a browser login, so the button stays disabled until it settles.
|
||||
const autofillBusy = state.autofill === "running" || state.autofill === "awaitingLogin";
|
||||
|
||||
// Pick the notice under the autofill button: settled outcome first, then
|
||||
// a green "already configured" hint, then the default invitation.
|
||||
let autofillNotice: BailianKbLocaleKey | undefined;
|
||||
let autofillNoticeSuccess = false;
|
||||
if (state.autofill === "done") {
|
||||
autofillNotice = "autofillDone";
|
||||
autofillNoticeSuccess = true;
|
||||
} else if (state.autofill === "awaitingLogin") {
|
||||
autofillNotice = "autofillAwaitingLogin";
|
||||
} else if (state.autofill === "failed") {
|
||||
autofillNotice = "autofillFailed";
|
||||
} else if (fullyConfigured(state)) {
|
||||
autofillNotice = "autofillConfigured";
|
||||
autofillNoticeSuccess = true;
|
||||
}
|
||||
|
||||
const renderField = (field: FieldView) => {
|
||||
const credential = state.credentials[field.key];
|
||||
// Echo mode: the settings scope answers with the resolved value, so
|
||||
// the control is an ordinary pre-filled input. Otherwise the control
|
||||
// is write-only and the badge is all the state there is.
|
||||
const echo = SETTINGS_FIELDS[field.key] !== undefined && state.settings.status === "ready";
|
||||
const echoed = echoedValue(state, field.key);
|
||||
const value = state.drafts[field.key] ?? (echo ? echoed : "");
|
||||
const disabled = busy || (echo ? !state.settings.writable : !credential.writable);
|
||||
// The launch environment wins over the credential store and refuses
|
||||
// writes; in echo mode a non-empty settings value shadows both, so
|
||||
// the badge only reports the fallback under an empty input.
|
||||
const badge = echo
|
||||
? echoed !== ""
|
||||
? undefined
|
||||
: credential.configured
|
||||
? { label: t("fallbackConfigured"), set: true }
|
||||
: { label: t(field.unsetKey), set: false }
|
||||
: credential.configured
|
||||
? { label: credential.writable ? t(field.setKey) : t("fromEnv"), set: true }
|
||||
: { label: t(field.unsetKey), set: false };
|
||||
const showClear =
|
||||
(field.key === "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" ||
|
||||
field.key === "BAILIAN_DEFAULT_CHAT_AGENT_ID") &&
|
||||
(credential.configured || (echo && echoed !== ""));
|
||||
return (
|
||||
<div className={css.field} key={field.key}>
|
||||
<div className={css.head}>
|
||||
<span className={css.labelWrap}>
|
||||
<label className={css.label} htmlFor={`bailian-kb-${field.key}`}>
|
||||
{t(field.labelKey)}
|
||||
</label>
|
||||
{field.getUrl !== undefined && field.getKey !== undefined ? (
|
||||
<a className={css.getLink} href={field.getUrl} target="_blank" rel="noreferrer">
|
||||
{t(field.getKey)}
|
||||
</a>
|
||||
) : null}
|
||||
</span>
|
||||
<span className={css.badges}>
|
||||
{showClear ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.clear}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
void props.clearDefaultAgent(
|
||||
field.key as
|
||||
| "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID"
|
||||
| "BAILIAN_DEFAULT_CHAT_AGENT_ID",
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t(state.clearing ? "clearing" : "clear")}
|
||||
</button>
|
||||
) : null}
|
||||
{badge !== undefined ? (
|
||||
<span className={badge.set ? css.badgeSuccess : css.badgeMuted}>{badge.label}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id={`bailian-kb-${field.key}`}
|
||||
className={css.input}
|
||||
type={field.secret ? "password" : "text"}
|
||||
autoComplete="off"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
props.edit(field.key, event.target.value);
|
||||
}}
|
||||
/>
|
||||
<p className={css.hint}>{t(echo ? field.hintKey : field.fallbackHintKey)}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* One scene's default-service picker — the sole control for that setting.
|
||||
*
|
||||
* The cached list is the menu, but a value already pinned outside this list
|
||||
* (the fetch is capped, so an older service can be absent) is prepended as its
|
||||
* own option: dropping it would make the panel silently forget a live setting.
|
||||
*/
|
||||
function renderPicker(scene: "search" | "chat") {
|
||||
const cache = state.cache;
|
||||
const entries = scene === "search" ? cache.search : cache.chat;
|
||||
const pinned =
|
||||
scene === "search"
|
||||
? state.settings.values.defaultRetrieveAgentId
|
||||
: state.settings.values.defaultChatAgentId;
|
||||
const isPinned = pinned !== undefined && pinned !== "";
|
||||
const pinnedIsListed = isPinned && entries.some((entry) => entry.agent_id === pinned);
|
||||
return (
|
||||
<div className={css.field}>
|
||||
<div className={css.head}>
|
||||
<span className={css.label}>
|
||||
{t(scene === "search" ? "retrieveAgentId" : "chatAgentId")}
|
||||
</span>
|
||||
{isPinned ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.clear}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
void props.selectDefaultAgent(scene, undefined);
|
||||
}}
|
||||
>
|
||||
{t(state.clearing ? "clearing" : "pickerClear")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<select
|
||||
className={css.input}
|
||||
value={pinned ?? ""}
|
||||
disabled={busy || cache.status !== "ready"}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value;
|
||||
// The empty option is the clear path, which must remove the value
|
||||
// from both the settings layer and the credential store.
|
||||
void props.selectDefaultAgent(scene, next === "" ? undefined : next);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("pickerNone")}</option>
|
||||
{isPinned && !pinnedIsListed ? <option value={pinned}>{pinned}</option> : null}
|
||||
{entries.map((entry) => (
|
||||
<option key={entry.agent_id} value={entry.agent_id}>
|
||||
{entry.agent_name === "" ? entry.agent_id : entry.agent_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className={css.hint}>
|
||||
{entries.length === 0
|
||||
? t("cacheEmpty")
|
||||
: t(scene === "search" ? "retrieveAgentIdHint" : "chatAgentIdHint")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The service-cache diagnostics: last fetch, per-scene counts, refresh.
|
||||
*
|
||||
* This stays OUTSIDE the advanced fold on purpose. It is the answer to "why did
|
||||
* the agent stop retrieving" — an empty workspace and a stale list look
|
||||
* identical from the outside, and before this the only way to tell them apart
|
||||
* was reading the cache JSON off disk.
|
||||
*/
|
||||
function renderCacheStatus() {
|
||||
const cache = state.cache;
|
||||
if (cache.status === "loading") return <p className={css.hint}>{t("cacheLoading")}</p>;
|
||||
if (cache.status === "unconfigured")
|
||||
return <p className={css.notice}>{t("cacheUnconfigured")}</p>;
|
||||
if (cache.status === "unavailable")
|
||||
return <p className={css.notice}>{t("cacheUnavailable")}</p>;
|
||||
|
||||
const fetched =
|
||||
cache.fetchedAt === undefined ? t("cacheNever") : new Date(cache.fetchedAt).toLocaleString();
|
||||
|
||||
return (
|
||||
<div className={css.field}>
|
||||
<div className={css.head}>
|
||||
<span className={css.label}>{t("cacheTitle")}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={css.clear}
|
||||
disabled={cache.refreshing}
|
||||
onClick={() => {
|
||||
void props.refreshServices();
|
||||
}}
|
||||
>
|
||||
{t(cache.refreshing ? "cacheRefreshing" : "cacheRefresh")}
|
||||
</button>
|
||||
</div>
|
||||
<p className={css.hint}>
|
||||
{t("cacheFetchedAt")}: {fetched}
|
||||
{cache.stale ? ` (${t("cacheStale")})` : ""}
|
||||
{" · "}
|
||||
{t("cacheSearchCount")}: {cache.searchCount}
|
||||
{" · "}
|
||||
{t("cacheChatCount")}: {cache.chatCount}
|
||||
</p>
|
||||
{cache.truncated ? <p className={css.notice}>{t("cacheTruncated")}</p> : null}
|
||||
{cache.searchCount === 0 && cache.chatCount === 0 ? (
|
||||
<p className={css.notice}>{t("cacheEmpty")}</p>
|
||||
) : null}
|
||||
<p className={css.hint}>{t("cacheHint")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={css.section}>
|
||||
<div className={css.headRow}>
|
||||
<h2 className={css.title}>{t("title")}</h2>
|
||||
{dirty ? <span className={css.pending}>{t("unsaved")}</span> : null}
|
||||
</div>
|
||||
<p className={css.intro}>{t("description")}</p>
|
||||
<div className={css.autofillRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.discard}
|
||||
disabled={busy || autofillBusy}
|
||||
onClick={() => {
|
||||
void props.autofill();
|
||||
}}
|
||||
>
|
||||
{t(state.autofill === "running" ? "autofilling" : "autofill")}
|
||||
</button>
|
||||
<span className={autofillNoticeSuccess ? css.autofillNoticeSuccess : css.hint}>
|
||||
{autofillNotice !== undefined ? t(autofillNotice) : t("autofillHint")}
|
||||
{/* The host opens the page itself; this link is the fallback when it cannot. */}
|
||||
{state.autofillLoginUrl !== undefined && state.autofillLoginUrl !== "" ? (
|
||||
<>
|
||||
{" "}
|
||||
<a href={state.autofillLoginUrl} target="_blank" rel="noreferrer">
|
||||
{t("autofillOpenUrl")}
|
||||
</a>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.form}>
|
||||
{state.settings.status === "unavailable" ? (
|
||||
<p className={css.notice}>{t("settingsUnavailable")}</p>
|
||||
) : null}
|
||||
{renderCacheStatus()}
|
||||
<div className={css.advancedSection}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.advancedToggle}
|
||||
aria-expanded={advancedOpen}
|
||||
onClick={() => {
|
||||
setAdvancedOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
{t("advancedConfig")}
|
||||
</button>
|
||||
{advancedOpen ? (
|
||||
<div className={css.advancedFields}>
|
||||
{ADVANCED_FIELDS.map(renderField)}
|
||||
{renderPicker("search")}
|
||||
{renderPicker("chat")}
|
||||
<div className={css.footer}>
|
||||
{state.failed ? (
|
||||
<p className={css.failed} role="status">
|
||||
{t("saveFailed")}
|
||||
</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={css.discard}
|
||||
disabled={!dirty || busy}
|
||||
onClick={props.discard}
|
||||
>
|
||||
{t("discard")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.save}
|
||||
disabled={!dirty || busy}
|
||||
onClick={() => {
|
||||
void props.save();
|
||||
}}
|
||||
>
|
||||
{t(state.saving ? "saving" : "save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
/**
|
||||
* The Bailian page's controller: a hybrid form over two domains.
|
||||
*
|
||||
* The workspace, default-retrieval-service and default-chat-service ids live
|
||||
* in the `bailian-kb` settings section the Host half registers. The Host
|
||||
* exposes them over a bridge route (`/bailian-kb/settings`) so the page can
|
||||
* read and write without riding the settings wire (which requires an apiproxy
|
||||
* allowlist entry the composition does not grant out-of-tree namespaces).
|
||||
*
|
||||
* The API key always rides its credential reference (write-only by design:
|
||||
* the wire is structurally value-free), so that control starts blank and
|
||||
* reports only configured/unconfigured.
|
||||
*/
|
||||
|
||||
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 page addresses, keyed by their ref names. */
|
||||
export const BAILIAN_CARD_REFS = [
|
||||
"DASHSCOPE_API_KEY",
|
||||
"BAILIAN_WORKSPACE_ID",
|
||||
"BAILIAN_DEFAULT_RETRIEVE_AGENT_ID",
|
||||
"BAILIAN_DEFAULT_CHAT_AGENT_ID",
|
||||
] as const;
|
||||
|
||||
/** One page field, addressed by its credential reference. */
|
||||
export type BailianFieldKey = (typeof BAILIAN_CARD_REFS)[number];
|
||||
|
||||
/** Settings-section field names of the echoing controls. */
|
||||
export type BailianSettingsField = "workspaceId" | "defaultRetrieveAgentId" | "defaultChatAgentId";
|
||||
|
||||
/** Credential reference → settings-section field, for the hybrid controls. */
|
||||
export const SETTINGS_FIELDS: Partial<Record<BailianFieldKey, BailianSettingsField>> = {
|
||||
BAILIAN_WORKSPACE_ID: "workspaceId",
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: "defaultRetrieveAgentId",
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: "defaultChatAgentId",
|
||||
};
|
||||
|
||||
/** The section subset this page reads and writes (the namespace holds the whole plugin Config). */
|
||||
export interface BailianKbSection {
|
||||
workspaceId?: string;
|
||||
defaultRetrieveAgentId?: string;
|
||||
defaultChatAgentId?: string;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** The page's mirror of the settings scope. */
|
||||
export interface BailianSettingsView {
|
||||
/** `ready` enables echo; `unavailable` degrades to write-only credentials. */
|
||||
status: "loading" | "ready" | "unavailable";
|
||||
/** Whether the Host settings document accepts writes. */
|
||||
writable: boolean;
|
||||
/** Resolved section values (entry base + user layer) for the two hybrid controls. */
|
||||
values: BailianKbSection;
|
||||
}
|
||||
|
||||
/** Where the autofill flow (adopt a Bailian console login) currently stands. */
|
||||
export type BailianAutofillStatus = "idle" | "running" | "awaitingLogin" | "done" | "failed";
|
||||
|
||||
/** One cached retrieval or Q&A service, as the picker lists it. */
|
||||
export interface BailianServiceEntry {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
scene: "search" | "chat";
|
||||
status: string;
|
||||
modify_time?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The service cache as the panel shows it.
|
||||
*
|
||||
* This exists because cache staleness is otherwise invisible: an agent that
|
||||
* silently stops retrieving looks identical whether the workspace is empty or the
|
||||
* list is merely out of date. `fetchedAt` with the per-scene counts distinguishes
|
||||
* those two in one glance, which is the whole reason the panel earns its space.
|
||||
*/
|
||||
export interface BailianCacheView {
|
||||
/** `unconfigured` = no workspace id yet; `unavailable` = the bridge route failed. */
|
||||
status: "loading" | "ready" | "unconfigured" | "unavailable";
|
||||
/** Epoch millis of the last successful fetch; absent when nothing is cached. */
|
||||
fetchedAt?: number;
|
||||
searchCount: number;
|
||||
chatCount: number;
|
||||
/** Server-reported total, which exceeds the counts when the fetch was capped. */
|
||||
total: number;
|
||||
truncated: boolean;
|
||||
stale: boolean;
|
||||
search: BailianServiceEntry[];
|
||||
chat: BailianServiceEntry[];
|
||||
/** Whether a forced refresh is in flight. */
|
||||
refreshing: boolean;
|
||||
}
|
||||
|
||||
/** What the Bailian page renders. */
|
||||
export interface BailianCardState {
|
||||
/** Staged drafts; undefined = untouched (the control shows the echoed value). */
|
||||
drafts: Record<BailianFieldKey, string | undefined>;
|
||||
/** Last credentials-domain answer per reference; unknown refs read as writable. */
|
||||
credentials: Record<BailianFieldKey, BailianCredentialView>;
|
||||
/** Settings-scope echo state for the id fields. */
|
||||
settings: BailianSettingsView;
|
||||
/** Whether a save is in flight. */
|
||||
saving: boolean;
|
||||
/** Whether a default-service clear is in flight. */
|
||||
clearing: boolean;
|
||||
/** Whether the last save or clear was refused; drafts are kept for correction. */
|
||||
failed: boolean;
|
||||
/** The autofill flow's state; feeds the button label and its result notice. */
|
||||
autofill: BailianAutofillStatus;
|
||||
/** Console login URL while `awaitingLogin`, shown in case the host could not open a browser. */
|
||||
autofillLoginUrl?: string;
|
||||
/** Service cache diagnostics and the pickable services. */
|
||||
cache: BailianCacheView;
|
||||
}
|
||||
|
||||
/** The registration-side face the page's slot entry injects. */
|
||||
export interface BailianCardFace {
|
||||
hooks: {
|
||||
/** Page snapshot bound by the renderer as useBailianCard. */
|
||||
bailianCard: SnapshotStore<BailianCardState>;
|
||||
};
|
||||
/** Stage one draft. */
|
||||
edit: (key: BailianFieldKey, text: string) => void;
|
||||
/** Write every staged draft through its domain, then re-read. */
|
||||
save: () => Promise<void>;
|
||||
/** Drop every staged draft. */
|
||||
discard: () => void;
|
||||
/** Remove the stored default service from every writable layer, then re-read. */
|
||||
clearDefaultAgent: (
|
||||
key: "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" | "BAILIAN_DEFAULT_CHAT_AGENT_ID",
|
||||
) => Promise<void>;
|
||||
/** Adopt a Bailian console login (api key + workspace id) via the Host. */
|
||||
autofill: () => Promise<void>;
|
||||
/** Force a service-cache refresh, bypassing the TTL. */
|
||||
refreshServices: () => Promise<void>;
|
||||
/**
|
||||
* Pin one scene's default service, or clear it when `agentId` is undefined.
|
||||
* Clearing removes the value from the settings user layer AND the credential
|
||||
* store, so the fallback chain cannot resurrect what the user just cleared.
|
||||
*/
|
||||
selectDefaultAgent: (scene: "search" | "chat", agentId: string | undefined) => Promise<void>;
|
||||
}
|
||||
|
||||
/** The text a field's control shows when its draft is untouched. */
|
||||
export function echoedValue(state: BailianCardState, key: BailianFieldKey): string {
|
||||
const field = SETTINGS_FIELDS[key];
|
||||
if (field === undefined || state.settings.status !== "ready") return "";
|
||||
return state.settings.values[field] ?? "";
|
||||
}
|
||||
|
||||
/** Whether one field stages a change a save would write. */
|
||||
function staged(state: BailianCardState, key: BailianFieldKey): boolean {
|
||||
const draft = state.drafts[key];
|
||||
if (draft === undefined) return false;
|
||||
const field = SETTINGS_FIELDS[key];
|
||||
if (field !== undefined && state.settings.status === "ready") {
|
||||
return draft !== echoedValue(state, key);
|
||||
}
|
||||
// Write-only control: blank means untouched, never "erase the stored value".
|
||||
return draft !== "";
|
||||
}
|
||||
|
||||
/** Whether any field stages a change (enables Save/Discard). */
|
||||
export function dirtyOf(state: BailianCardState): boolean {
|
||||
return BAILIAN_CARD_REFS.some((key) => staged(state, key));
|
||||
}
|
||||
|
||||
/** Bridge the settings bridge route and the credentials domain onto the page. */
|
||||
export class BailianCardController {
|
||||
private readonly store: SnapshotStore<BailianCardState>;
|
||||
|
||||
/**
|
||||
* @param api - wire face used for the four credential references.
|
||||
*/
|
||||
constructor(private readonly api: Pick<IApiClient, "credentials">) {
|
||||
this.store = createSnapshotStore<BailianCardState>({
|
||||
drafts: {
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
BAILIAN_WORKSPACE_ID: undefined,
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: undefined,
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: undefined,
|
||||
},
|
||||
credentials: {
|
||||
DASHSCOPE_API_KEY: { configured: false, writable: true },
|
||||
BAILIAN_WORKSPACE_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_RETRIEVE_AGENT_ID: { configured: false, writable: true },
|
||||
BAILIAN_DEFAULT_CHAT_AGENT_ID: { configured: false, writable: true },
|
||||
},
|
||||
settings: { status: "loading", writable: false, values: {} },
|
||||
saving: false,
|
||||
clearing: false,
|
||||
failed: false,
|
||||
autofill: "idle",
|
||||
autofillLoginUrl: undefined,
|
||||
cache: {
|
||||
status: "loading",
|
||||
searchCount: 0,
|
||||
chatCount: 0,
|
||||
total: 0,
|
||||
truncated: false,
|
||||
stale: true,
|
||||
search: [],
|
||||
chat: [],
|
||||
refreshing: false,
|
||||
},
|
||||
});
|
||||
void this.fetchSettings();
|
||||
void this.fetchServices();
|
||||
void this.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current settings from the Host bridge route. Called at
|
||||
* construction and after every mutation (save, clear).
|
||||
*/
|
||||
async fetchSettings(): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch("/bailian-kb/settings");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const value = (await resp.json()) as Record<string, unknown>;
|
||||
this.store.update((draft) => {
|
||||
draft.settings = {
|
||||
status: "ready",
|
||||
writable: true,
|
||||
values: {
|
||||
...(typeof value.workspaceId === "string" ? { workspaceId: value.workspaceId } : {}),
|
||||
...(typeof value.defaultRetrieveAgentId === "string"
|
||||
? { defaultRetrieveAgentId: value.defaultRetrieveAgentId }
|
||||
: {}),
|
||||
...(typeof value.defaultChatAgentId === "string"
|
||||
? { defaultChatAgentId: value.defaultChatAgentId }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (_fetchFailure) {
|
||||
this.store.update((draft) => {
|
||||
draft.settings = { status: "unavailable", writable: false, values: {} };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the service cache snapshot from the Host bridge route.
|
||||
* @param force - POST instead of GET, making the Host refetch regardless of TTL.
|
||||
*/
|
||||
async fetchServices(force = false): Promise<void> {
|
||||
if (force)
|
||||
this.store.update((draft) => {
|
||||
draft.cache.refreshing = true;
|
||||
});
|
||||
try {
|
||||
const resp = await fetch("/bailian-kb/services", { method: force ? "POST" : "GET" });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const value = (await resp.json()) as {
|
||||
configured?: boolean;
|
||||
status?: {
|
||||
fetchedAt?: number;
|
||||
searchCount?: number;
|
||||
chatCount?: number;
|
||||
total?: number;
|
||||
truncated?: boolean;
|
||||
stale?: boolean;
|
||||
};
|
||||
search?: BailianServiceEntry[];
|
||||
chat?: BailianServiceEntry[];
|
||||
};
|
||||
this.store.update((draft) => {
|
||||
draft.cache.refreshing = false;
|
||||
if (value.configured !== true) {
|
||||
// No workspace id yet: the panel says so rather than showing zeros,
|
||||
// which would read as "the workspace has no services".
|
||||
draft.cache.status = "unconfigured";
|
||||
return;
|
||||
}
|
||||
draft.cache.status = "ready";
|
||||
draft.cache.fetchedAt = value.status?.fetchedAt;
|
||||
draft.cache.searchCount = value.status?.searchCount ?? 0;
|
||||
draft.cache.chatCount = value.status?.chatCount ?? 0;
|
||||
draft.cache.total = value.status?.total ?? 0;
|
||||
draft.cache.truncated = value.status?.truncated === true;
|
||||
draft.cache.stale = value.status?.stale === true;
|
||||
draft.cache.search = value.search ?? [];
|
||||
draft.cache.chat = value.chat ?? [];
|
||||
});
|
||||
} catch (_routeFailure) {
|
||||
this.store.update((draft) => {
|
||||
draft.cache.refreshing = false;
|
||||
draft.cache.status = "unavailable";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Force a refresh, bypassing the TTL, and show the updated numbers. */
|
||||
async refreshServices(): Promise<void> {
|
||||
if (this.store.getSnapshot().cache.refreshing) return;
|
||||
await this.fetchServices(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin or clear one scene's default service.
|
||||
*
|
||||
* Clearing delegates to {@link clearDefaultAgent}, which removes the value from
|
||||
* the settings user layer AND the credential store — without the second
|
||||
* removal the fallback chain would resurrect what the user just cleared.
|
||||
* @param scene - which tool's default to set.
|
||||
* @param agentId - the service id to pin, or undefined to clear.
|
||||
*/
|
||||
async selectDefaultAgent(scene: "search" | "chat", agentId: string | undefined): Promise<void> {
|
||||
const key =
|
||||
scene === "search" ? "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" : "BAILIAN_DEFAULT_CHAT_AGENT_ID";
|
||||
if (agentId === undefined) {
|
||||
await this.clearDefaultAgent(key);
|
||||
return;
|
||||
}
|
||||
const field: BailianSettingsField =
|
||||
scene === "search" ? "defaultRetrieveAgentId" : "defaultChatAgentId";
|
||||
this.store.update((draft) => {
|
||||
draft.saving = true;
|
||||
});
|
||||
try {
|
||||
const resp = await fetch("/bailian-kb/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ [field]: agentId }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
this.store.update((draft) => {
|
||||
draft.failed = false;
|
||||
});
|
||||
} catch (_writeFailure) {
|
||||
this.store.update((draft) => {
|
||||
draft.failed = true;
|
||||
});
|
||||
} finally {
|
||||
this.store.update((draft) => {
|
||||
draft.saving = false;
|
||||
});
|
||||
await this.fetchSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 staged draft through its domain: echoing fields go to the
|
||||
* settings user layer via the bridge route (blank = removal, falling back
|
||||
* to entry config and the credential store), write-only fields go to
|
||||
* `credentials.set`. A refused credential write keeps its draft; a refused
|
||||
* settings write self-heals by re-fetching the Host value.
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
const state = this.store.getSnapshot();
|
||||
if (state.saving || !dirtyOf(state)) return;
|
||||
this.store.update((draft) => {
|
||||
draft.saving = true;
|
||||
});
|
||||
let failed = false;
|
||||
const settingsPatch: Record<string, unknown> = {};
|
||||
let hasSettingsWrite = false;
|
||||
const credentialWrites: Promise<void>[] = [];
|
||||
const settled: BailianFieldKey[] = [];
|
||||
for (const key of BAILIAN_CARD_REFS) {
|
||||
if (!staged(state, key)) continue;
|
||||
const text = state.drafts[key] as string;
|
||||
const field = SETTINGS_FIELDS[key];
|
||||
if (field !== undefined && state.settings.status === "ready") {
|
||||
settingsPatch[field] = text === "" ? null : text;
|
||||
hasSettingsWrite = true;
|
||||
settled.push(key);
|
||||
continue;
|
||||
}
|
||||
credentialWrites.push(
|
||||
(async () => {
|
||||
try {
|
||||
const response = await this.api.credentials.set({ ref: key, value: text });
|
||||
if (response.result.ok) settled.push(key);
|
||||
else failed = true;
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true;
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
if (hasSettingsWrite) {
|
||||
try {
|
||||
await this.saveSettings(settingsPatch);
|
||||
} catch (_settingsWriteFailure) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
await Promise.all(credentialWrites);
|
||||
this.store.update((draft) => {
|
||||
draft.saving = false;
|
||||
draft.failed = failed;
|
||||
for (const key of settled) draft.drafts[key] = undefined;
|
||||
});
|
||||
await this.fetchSettings();
|
||||
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] = undefined;
|
||||
draft.failed = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch credentials by signing in to the Bailian console. The Host drives
|
||||
* the console browser login itself, always requesting a freshly issued api
|
||||
* key, then persists the key into the credential store and the workspace id
|
||||
* into the settings section — so both values belong to the account that just
|
||||
* signed in, and the plain key never rides the wire to this page.
|
||||
*
|
||||
* Deliberately does NOT adopt the bl CLI's stored login: that path can pair
|
||||
* one account's key with another account's workspace id, and nothing would
|
||||
* flag the mismatch until a knowledge-base call fails.
|
||||
*/
|
||||
async autofill(): Promise<void> {
|
||||
const phase = this.store.getSnapshot().autofill;
|
||||
if (phase === "running" || phase === "awaitingLogin") return;
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "running";
|
||||
draft.autofillLoginUrl = undefined;
|
||||
});
|
||||
await this.runConsoleLogin();
|
||||
await this.fetchSettings();
|
||||
await this.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the Host to open the console login page, then poll for the outcome.
|
||||
* The Host persists the credentials itself when the callback lands, always
|
||||
* requesting a freshly issued key — so the key and the workspace id both come
|
||||
* from the account signing in.
|
||||
*/
|
||||
private async runConsoleLogin(): Promise<void> {
|
||||
let started: { status?: string; loginUrl?: string };
|
||||
try {
|
||||
started = (await this.postAutofill("login")) as { status?: string; loginUrl?: string };
|
||||
} catch (_routeFailure) {
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "failed";
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (started.status !== "started" && started.status !== "already-running") {
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "failed";
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "awaitingLogin";
|
||||
draft.autofillLoginUrl = started.loginUrl;
|
||||
});
|
||||
await this.pollConsoleLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the Host until the console login resolves. Bounded so a login the
|
||||
* user abandons does not leave the button spinning forever; the Host keeps
|
||||
* its own (longer) timeout, so a late callback still persists and shows up
|
||||
* on the next page read.
|
||||
*/
|
||||
private async pollConsoleLogin(): Promise<void> {
|
||||
const deadline = Date.now() + 5 * 60 * 1000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
let phase: string | undefined;
|
||||
try {
|
||||
phase = ((await this.postAutofill("loginStatus")) as { phase?: string }).phase;
|
||||
} catch (_pollFailure) {
|
||||
continue;
|
||||
}
|
||||
if (phase === "done") {
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "done";
|
||||
draft.autofillLoginUrl = undefined;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (phase === "failed") {
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "failed";
|
||||
draft.autofillLoginUrl = undefined;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.store.update((draft) => {
|
||||
draft.autofill = "failed";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the stored default service from every writable layer — the
|
||||
* settings user layer AND the credential store, so the fallback chain does
|
||||
* not resurrect the value the user just cleared. Both removals are
|
||||
* idempotent; the credential unset is skipped when nothing is stored there.
|
||||
* @param key - which default service credential to clear.
|
||||
*/
|
||||
async clearDefaultAgent(
|
||||
key: "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" | "BAILIAN_DEFAULT_CHAT_AGENT_ID",
|
||||
): Promise<void> {
|
||||
const settingsField: BailianSettingsField =
|
||||
key === "BAILIAN_DEFAULT_RETRIEVE_AGENT_ID" ? "defaultRetrieveAgentId" : "defaultChatAgentId";
|
||||
const state = this.store.getSnapshot();
|
||||
if (state.clearing) return;
|
||||
this.store.update((draft) => {
|
||||
draft.clearing = true;
|
||||
});
|
||||
let failed = false;
|
||||
if (state.settings.status === "ready") {
|
||||
try {
|
||||
await this.saveSettings({ [settingsField]: null });
|
||||
} catch (_settingsWriteFailure) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
if (state.credentials[key].configured) {
|
||||
try {
|
||||
const response = await this.api.credentials.unset({ ref: key });
|
||||
if (!response.result.ok) failed = true;
|
||||
} catch (_credentialWriteFailure) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
this.store.update((draft) => {
|
||||
draft.clearing = false;
|
||||
draft.failed = failed;
|
||||
draft.drafts[key] = undefined;
|
||||
});
|
||||
await this.fetchSettings();
|
||||
await this.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read after the Host reports a change to a reference this page 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.
|
||||
* (Settings-document changes reach the page through the scope instead.)
|
||||
* @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 page's slot registration injects.
|
||||
* @returns the page'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: (key) => this.clearDefaultAgent(key),
|
||||
autofill: () => this.autofill(),
|
||||
refreshServices: () => this.refreshServices(),
|
||||
selectDefaultAgent: (scene, agentId) => this.selectDefaultAgent(scene, agentId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Post one autofill action to the Host bridge route.
|
||||
* @param action - `login` starts the console browser flow; `loginStatus`
|
||||
* reads that flow's progress.
|
||||
* @returns the route's JSON answer.
|
||||
*/
|
||||
private async postAutofill(action: "login" | "loginStatus"): Promise<unknown> {
|
||||
const resp = await fetch("/bailian-kb/autofill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a settings patch to the Host bridge route. `null`-valued keys are
|
||||
* removals (the field falls back to the entry config and then the
|
||||
* credential store); other values are merged into the user layer.
|
||||
* @param patch - the partial settings update.
|
||||
*/
|
||||
private async saveSettings(patch: Record<string, unknown>): Promise<void> {
|
||||
const resp = await fetch("/bailian-kb/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = (await resp.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(body.error ?? `HTTP ${resp.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the credentials domain about all four references and publish the
|
||||
* answer. A failed read keeps the last known state: the page stays usable
|
||||
* and a write still reaches the Host.
|
||||
*/
|
||||
private async read(): Promise<void> {
|
||||
let response: Awaited<ReturnType<IApiClient["credentials"]["describe"]>>;
|
||||
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 page guessing a refusal.
|
||||
draft.credentials[ref] = {
|
||||
configured: view[ref]?.configured ?? false,
|
||||
writable: view[ref]?.writable ?? true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Bailian knowledge-base plugin, browser half: one section page in the
|
||||
* Settings left nav. The workspace, default-retrieval-service and
|
||||
* default-chat-service ids ride the Host bridge route
|
||||
* (`/bailian-kb/settings`) the Host half registers, bypassing the settings
|
||||
* wire (which requires an apiproxy allowlist entry the composition does not
|
||||
* grant out-of-tree namespaces); the API key stays pure credentials-domain
|
||||
* and never echoes.
|
||||
*/
|
||||
|
||||
import type { ConnectionHandle } from "@deepseek-ai/dsh-client-connection/client";
|
||||
// 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";
|
||||
// The 'settings.section' SlotMap merge AND the ctx.settingsScope service,
|
||||
// both declared by the settings domain base (type-only: the service arrives
|
||||
// through cordis, never a value import).
|
||||
import type {} from "@deepseek-ai/dsh-client-ui-settings/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 section page'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 section page into the Settings left nav.
|
||||
* @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");
|
||||
|
||||
// Registration-time text: the nav label is a thunk the shell resolves per
|
||||
// render, so copy freshness rides the locale revision without re-registering.
|
||||
const t = ctx.locale.bind(NS);
|
||||
|
||||
// The echo transport: the Host bridge route (`/bailian-kb/settings`) lets
|
||||
// the page read and write the resolved section without riding the settings
|
||||
// wire (which requires an apiproxy allowlist entry).
|
||||
const card = new BailianCardController(api);
|
||||
// Values can change elsewhere (Models page, external file edits); the badges
|
||||
// must follow the Host, not the card's last write.
|
||||
ctx.effect(
|
||||
() =>
|
||||
ctx.remote.$on("credentials/updated", (ref) => {
|
||||
card.refresh(ref);
|
||||
}),
|
||||
"tool-bailian-kb: credential invalidations",
|
||||
);
|
||||
|
||||
ctx.slots.inject("settings.section", () =>
|
||||
ctx.slots.register(
|
||||
{
|
||||
name: "settings.section",
|
||||
id: "bailian-kb",
|
||||
order: 20,
|
||||
label: () => t("nav"),
|
||||
locale: NS,
|
||||
inject: () => card.inject(),
|
||||
},
|
||||
BailianCard,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Locale bundles for the Bailian knowledge-base settings page. The workspace,
|
||||
* default-retrieval-service and default-chat-service ids echo from the
|
||||
* settings section while it is available and fall back to write-only
|
||||
* credential controls otherwise; the API key copy is always written for a
|
||||
* write-only control: state is reported as configured/unconfigured, and a
|
||||
* stored key is never echoed back.
|
||||
*/
|
||||
|
||||
/** Locale keys this page renders. */
|
||||
export type BailianKbLocaleKey =
|
||||
| "nav"
|
||||
| "title"
|
||||
| "description"
|
||||
| "settingsUnavailable"
|
||||
| "fallbackConfigured"
|
||||
| "apiKey"
|
||||
| "apiKeyHint"
|
||||
| "apiKeySet"
|
||||
| "apiKeyUnset"
|
||||
| "apiKeyGet"
|
||||
| "workspaceId"
|
||||
| "workspaceIdHint"
|
||||
| "workspaceIdHintFallback"
|
||||
| "workspaceIdSet"
|
||||
| "workspaceIdUnset"
|
||||
| "workspaceIdGet"
|
||||
| "retrieveAgentId"
|
||||
| "retrieveAgentIdHint"
|
||||
| "chatAgentId"
|
||||
| "chatAgentIdHint"
|
||||
| "fromEnv"
|
||||
| "clear"
|
||||
| "clearing"
|
||||
| "save"
|
||||
| "saving"
|
||||
| "discard"
|
||||
| "unsaved"
|
||||
| "saveFailed"
|
||||
| "advancedConfig"
|
||||
| "autofill"
|
||||
| "autofilling"
|
||||
| "autofillHint"
|
||||
| "autofillDone"
|
||||
| "autofillAwaitingLogin"
|
||||
| "autofillOpenUrl"
|
||||
| "autofillFailed"
|
||||
| "autofillConfigured"
|
||||
| "cacheTitle"
|
||||
| "cacheHint"
|
||||
| "cacheLoading"
|
||||
| "cacheUnconfigured"
|
||||
| "cacheUnavailable"
|
||||
| "cacheFetchedAt"
|
||||
| "cacheNever"
|
||||
| "cacheStale"
|
||||
| "cacheSearchCount"
|
||||
| "cacheChatCount"
|
||||
| "cacheTruncated"
|
||||
| "cacheEmpty"
|
||||
| "cacheRefresh"
|
||||
| "cacheRefreshing"
|
||||
| "pickerNone"
|
||||
| "pickerClear";
|
||||
|
||||
/** English copy. */
|
||||
export const en: Record<BailianKbLocaleKey, string> = {
|
||||
nav: "Bailian KB",
|
||||
title: "Bailian knowledge base",
|
||||
description: "Account for the knowledge tools: API key, workspace, and default services.",
|
||||
settingsUnavailable:
|
||||
"The settings document is not reachable from this browser; values below are write-only and stored in the credential store.",
|
||||
fallbackConfigured: "Falling back to a configured credential-store value.",
|
||||
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.",
|
||||
apiKeyGet: "Get",
|
||||
workspaceId: "Workspace id",
|
||||
workspaceIdHint:
|
||||
"Bailian workspace id — the subdomain of your endpoints. Stored in the settings document; clear and save to fall back to the credential store.",
|
||||
workspaceIdHintFallback:
|
||||
"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.",
|
||||
workspaceIdGet: "Get",
|
||||
retrieveAgentId: "Default retrieval service",
|
||||
retrieveAgentIdHint:
|
||||
"The agent_id kb_search falls back to. Left unset, the injected catalog lists every deployed service instead.",
|
||||
chatAgentId: "Default Q&A service",
|
||||
chatAgentIdHint:
|
||||
"The agent_id kb_chat falls back to. Left unset, the injected catalog lists every deployed service instead.",
|
||||
fromEnv: "Set by the environment (read-only here)",
|
||||
clear: "Clear default",
|
||||
clearing: "Clearing…",
|
||||
save: "Save",
|
||||
saving: "Saving…",
|
||||
discard: "Discard",
|
||||
unsaved: "Unsaved",
|
||||
saveFailed: "The Host did not accept these values; they were left for you to correct.",
|
||||
advancedConfig: "Advanced configuration",
|
||||
autofill: "Fetch from console login",
|
||||
autofilling: "Starting…",
|
||||
autofillHint:
|
||||
"Sign in to the Bailian console to fill in that account’s API key and workspace id.",
|
||||
autofillDone: "Credentials adopted; the fields below reflect the new values.",
|
||||
autofillAwaitingLogin:
|
||||
"Waiting for the Bailian console login to finish in a browser on the host machine…",
|
||||
autofillOpenUrl: "Open the login page manually",
|
||||
autofillFailed:
|
||||
"Auto-fill failed — the credential may be locked by an environment variable, the Host refused the write, or the login was abandoned.",
|
||||
autofillConfigured:
|
||||
"Configured. Click button to fetch this account’s API key and workspace id again.",
|
||||
cacheTitle: "Retrieval service cache",
|
||||
cacheHint:
|
||||
"The service list injected into each conversation. Refreshes on its own; refresh here when you have just created a service and want it picked up now.",
|
||||
cacheLoading: "Reading…",
|
||||
cacheUnconfigured: "Set a workspace id first.",
|
||||
cacheUnavailable: "Not reachable from this browser.",
|
||||
cacheFetchedAt: "Last fetched",
|
||||
cacheNever: "never",
|
||||
cacheStale: "refresh due",
|
||||
cacheSearchCount: "Retrieval services",
|
||||
cacheChatCount: "Q&A services",
|
||||
cacheTruncated: "List truncated — the workspace holds more than were fetched.",
|
||||
cacheEmpty: "No deployed services cached. If you just created one, refresh.",
|
||||
cacheRefresh: "Refresh",
|
||||
cacheRefreshing: "Refreshing…",
|
||||
pickerNone: "Not set — the full list is injected instead",
|
||||
pickerClear: "Clear",
|
||||
};
|
||||
|
||||
/** Simplified Chinese copy. */
|
||||
export const zh: Record<BailianKbLocaleKey, string> = {
|
||||
nav: "百炼知识库",
|
||||
title: "百炼知识库",
|
||||
description: "知识库工具的账号信息:API 密钥、工作空间与默认服务。",
|
||||
settingsUnavailable: "当前浏览器无法访问设置文档;以下字段仅可写入凭据存储,不回显。",
|
||||
fallbackConfigured: "回退:凭据存储中已有值。",
|
||||
apiKey: "API 密钥",
|
||||
apiKeyHint: "DashScope API key。保存在凭据存储中且不会再次显示;留空表示保持当前值。",
|
||||
apiKeySet: "已配置密钥。",
|
||||
apiKeyUnset: "未配置密钥;配置前知识库工具不可用。",
|
||||
apiKeyGet: "去获取",
|
||||
workspaceId: "工作空间 ID",
|
||||
workspaceIdHint:
|
||||
"百炼工作空间 ID,即终端节点地址的子域名。存入设置文档;清空并保存则回退到凭据存储。",
|
||||
workspaceIdHintFallback: "百炼工作空间 ID,即终端节点地址的子域名。留空表示保持当前值。",
|
||||
workspaceIdSet: "已配置工作空间。",
|
||||
workspaceIdUnset: "未配置工作空间;配置前知识库工具不可用。",
|
||||
workspaceIdGet: "去获取",
|
||||
retrieveAgentId: "默认检索服务",
|
||||
retrieveAgentIdHint: "kb_search 缺省使用的 agent_id。不设置时,注入的清单会列出全部已部署服务。",
|
||||
chatAgentId: "默认对话服务",
|
||||
chatAgentIdHint: "kb_chat 缺省使用的 agent_id。不设置时,注入的清单会列出全部已部署服务。",
|
||||
fromEnv: "来自环境变量(此处只读)",
|
||||
clear: "清除默认",
|
||||
clearing: "清除中…",
|
||||
save: "保存",
|
||||
saving: "保存中…",
|
||||
discard: "放弃",
|
||||
unsaved: "未保存",
|
||||
saveFailed: "宿主未接受这些值,已保留供你修改。",
|
||||
advancedConfig: "高级配置",
|
||||
autofill: "自动获取",
|
||||
autofilling: "启动中…",
|
||||
autofillHint: "登录百炼控制台,自动填入该账号的 API 密钥与工作空间 ID。",
|
||||
autofillDone: "已回填凭据,下方字段已更新。",
|
||||
autofillAwaitingLogin: "等待在宿主机浏览器中完成百炼控制台登录…",
|
||||
autofillOpenUrl: "手动打开登录页",
|
||||
autofillFailed: "自动获取失败——凭据可能被环境变量锁定、宿主拒绝了写入,或登录未完成。",
|
||||
autofillConfigured: "已配置完成,点击按钮重新获取该账号的 API 密钥与工作空间 ID。",
|
||||
cacheTitle: "检索服务缓存",
|
||||
cacheHint: "注入到每次对话的服务清单。会自动刷新;刚建完服务想立即生效时在这里刷一下。",
|
||||
cacheLoading: "读取中…",
|
||||
cacheUnconfigured: "请先设置工作空间 ID。",
|
||||
cacheUnavailable: "当前浏览器无法访问。",
|
||||
cacheFetchedAt: "上次拉取",
|
||||
cacheNever: "尚未拉取",
|
||||
cacheStale: "待刷新",
|
||||
cacheSearchCount: "检索服务",
|
||||
cacheChatCount: "问答服务",
|
||||
cacheTruncated: "清单已截断 —— 工作空间里的服务多于已拉取的数量。",
|
||||
cacheEmpty: "缓存里没有已部署的服务。如果刚创建过,请刷新。",
|
||||
cacheRefresh: "刷新",
|
||||
cacheRefreshing: "刷新中…",
|
||||
pickerNone: "未设置 —— 会注入完整清单",
|
||||
pickerClear: "清空",
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { readBlCliConfig } from "../src/bl-cli.js";
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "bl-cli-test-"));
|
||||
|
||||
function fileWith(name: string, content: string): string {
|
||||
const path = join(dir, name);
|
||||
writeFileSync(path, content);
|
||||
return path;
|
||||
}
|
||||
|
||||
describe("readBlCliConfig", () => {
|
||||
it("reads api_key and workspace_id from the default profile (top level)", () => {
|
||||
const path = fileWith(
|
||||
"full.json",
|
||||
JSON.stringify({
|
||||
api_key: "sk-abc",
|
||||
workspace_id: "ws-1",
|
||||
output: "text",
|
||||
timeout: 600,
|
||||
}),
|
||||
);
|
||||
expect(readBlCliConfig(path)).toEqual({ apiKey: "sk-abc", workspaceId: "ws-1" });
|
||||
});
|
||||
|
||||
it("omits absent, blank, and non-string fields instead of returning empties", () => {
|
||||
const path = fileWith(
|
||||
"partial.json",
|
||||
JSON.stringify({
|
||||
api_key: " ",
|
||||
workspace_id: 42,
|
||||
}),
|
||||
);
|
||||
expect(readBlCliConfig(path)).toEqual({});
|
||||
const keyOnly = fileWith("key-only.json", JSON.stringify({ api_key: "sk-abc" }));
|
||||
expect(readBlCliConfig(keyOnly)).toEqual({ apiKey: "sk-abc" });
|
||||
});
|
||||
|
||||
it("reads a missing, malformed, or non-object file as empty", () => {
|
||||
expect(readBlCliConfig(join(dir, "nope.json"))).toEqual({});
|
||||
expect(readBlCliConfig(fileWith("broken.json", "{oops"))).toEqual({});
|
||||
expect(readBlCliConfig(fileWith("array.json", '["sk-abc"]'))).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
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/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
import { KbApiError, KbClient } from "../src/client.js";
|
||||
|
||||
function makeClient(fetchImpl: typeof fetch) {
|
||||
return new KbClient({
|
||||
resolveWorkspaceId: async () => "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<string, string>).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({
|
||||
resolveWorkspaceId: async () => "ws-1",
|
||||
endpointHost: "h",
|
||||
resolveApiKey,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
await client.postJson("/p", {});
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { Config } from "../src/index.js";
|
||||
|
||||
describe("Config", () => {
|
||||
it("applies defaults and accepts a pinned workspaceId", () => {
|
||||
const resolved = new Config({ workspaceId: "ws-1" } as never);
|
||||
expect(resolved.workspaceId).toBe("ws-1");
|
||||
expect(resolved.endpointHost).toBe("cn-beijing.maas.aliyuncs.com");
|
||||
expect(resolved.chatTimeoutMs).toBe(300_000);
|
||||
expect(resolved.defaultRetrieveAgentId).toBeUndefined();
|
||||
expect(resolved.defaultChatAgentId).toBeUndefined();
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("carries the bl-CLI seed ledger through validation", () => {
|
||||
const resolved = new Config({ seededFields: ["apiKey"] } as never);
|
||||
expect(resolved.seededFields).toEqual(["apiKey"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import {
|
||||
consoleLoginState,
|
||||
parseCallbackBody,
|
||||
pickCallbackCredentials,
|
||||
} from "../src/console-login.js";
|
||||
|
||||
describe("parseCallbackBody", () => {
|
||||
it("reads a plain JSON callback", () => {
|
||||
expect(parseCallbackBody('{"api_key":"sk-abc","workspace_id":"ws-1"}')).toMatchObject({
|
||||
api_key: "sk-abc",
|
||||
workspace_id: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("flattens a `data` envelope, the top level winning", () => {
|
||||
const parsed = parseCallbackBody(
|
||||
'{"data":{"api_key":"sk-inner","workspace_id":"ws-1"},"api_key":"sk-outer"}',
|
||||
);
|
||||
expect(parsed).toMatchObject({ api_key: "sk-outer", workspace_id: "ws-1" });
|
||||
});
|
||||
|
||||
it("reads a form-encoded callback", () => {
|
||||
expect(parseCallbackBody("api_key=sk-abc&workspace_id=ws-1")).toMatchObject({
|
||||
api_key: "sk-abc",
|
||||
workspace_id: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("tolerates a BOM, surrounding space, and an empty or broken body", () => {
|
||||
expect(parseCallbackBody('\uFEFF {"api_key":"sk-abc"} ')).toMatchObject({
|
||||
api_key: "sk-abc",
|
||||
});
|
||||
expect(parseCallbackBody("")).toEqual({});
|
||||
expect(parseCallbackBody(" ")).toEqual({});
|
||||
// A non-object JSON value carries no fields, and neither does an array.
|
||||
expect(parseCallbackBody('["sk-abc"]')).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickCallbackCredentials", () => {
|
||||
it("accepts both snake_case and camelCase field names", () => {
|
||||
expect(pickCallbackCredentials({}, { apiKey: "sk-abc", workspaceId: "ws-1" })).toEqual({
|
||||
apiKey: "sk-abc",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
expect(pickCallbackCredentials({}, { api_key: "sk-abc", workspace_id: "ws-1" })).toEqual({
|
||||
apiKey: "sk-abc",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("lets query parameters win over the body", () => {
|
||||
expect(pickCallbackCredentials({ api_key: "sk-query" }, { api_key: "sk-body" })).toEqual({
|
||||
apiKey: "sk-query",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits absent, blank, and non-string fields instead of returning empties", () => {
|
||||
expect(pickCallbackCredentials({}, {})).toEqual({});
|
||||
expect(pickCallbackCredentials({}, { api_key: " ", workspace_id: 42 })).toEqual({});
|
||||
expect(pickCallbackCredentials({}, { api_key: " sk-abc " })).toEqual({ apiKey: "sk-abc" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("consoleLoginState", () => {
|
||||
it("starts idle, carrying no secret", () => {
|
||||
expect(consoleLoginState()).toEqual({ phase: "idle" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
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.search).toBe("/api/v1/indices/knowledge/search");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
import type { KbClient } from "../src/client.js";
|
||||
import {
|
||||
CACHE_TTL_MS,
|
||||
EMPTY_CACHE_TTL_MS,
|
||||
ServiceCache,
|
||||
readServiceCache,
|
||||
serviceCachePath,
|
||||
writeServiceCache,
|
||||
type ServiceCacheDocument,
|
||||
} from "../src/service-cache.js";
|
||||
|
||||
const HOST = "cn-beijing.maas.aliyuncs.com";
|
||||
|
||||
function tempHome(): string {
|
||||
return mkdtempSync(join(tmpdir(), "bailian-kb-cache-"));
|
||||
}
|
||||
|
||||
function doc(overrides: Partial<ServiceCacheDocument> = {}): ServiceCacheDocument {
|
||||
return {
|
||||
version: 1,
|
||||
fetchedAt: 1_000,
|
||||
workspaceId: "llm-a",
|
||||
endpointHost: HOST,
|
||||
entries: [{ agent_id: "aid-1", agent_name: "svc", scene: "search", status: "deployed" }],
|
||||
total: 1,
|
||||
truncated: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** Write raw text to the cache path, bypassing the writer's validation. */
|
||||
function seedRaw(home: string, workspaceId: string, text: string): string {
|
||||
const path = serviceCachePath(workspaceId, home);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, text);
|
||||
return path;
|
||||
}
|
||||
|
||||
describe("serviceCachePath", () => {
|
||||
it("separates workspaces by filename", () => {
|
||||
// An api key only reaches its own workspace, so one shared file would blend accounts.
|
||||
expect(serviceCachePath("llm-a", "/home")).toBe("/home/cache/bailian-kb/services-llm-a.json");
|
||||
expect(serviceCachePath("llm-b", "/home")).not.toBe(serviceCachePath("llm-a", "/home"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("readServiceCache", () => {
|
||||
it("round-trips a document written by writeServiceCache", () => {
|
||||
const home = tempHome();
|
||||
const path = serviceCachePath("llm-a", home);
|
||||
writeServiceCache(path, doc());
|
||||
expect(readServiceCache(path, "llm-a", HOST)).toEqual(doc());
|
||||
});
|
||||
|
||||
it("reads every unusable file as a miss instead of throwing", () => {
|
||||
const home = tempHome();
|
||||
// Absent.
|
||||
expect(readServiceCache(serviceCachePath("nope", home), "nope", HOST)).toBeUndefined();
|
||||
// Malformed JSON.
|
||||
expect(readServiceCache(seedRaw(home, "a", "{oops"), "a", HOST)).toBeUndefined();
|
||||
// Non-object roots.
|
||||
expect(readServiceCache(seedRaw(home, "b", '["x"]'), "b", HOST)).toBeUndefined();
|
||||
// Newer or older schema: treated as a miss, never migrated.
|
||||
expect(
|
||||
readServiceCache(seedRaw(home, "c", JSON.stringify(doc({ version: 2 }))), "c", HOST),
|
||||
).toBeUndefined();
|
||||
// Required fields of the wrong type.
|
||||
expect(
|
||||
readServiceCache(
|
||||
seedRaw(home, "d", JSON.stringify(doc({ fetchedAt: "soon" as never }))),
|
||||
"d",
|
||||
HOST,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refuses a document belonging to another workspace or host", () => {
|
||||
const home = tempHome();
|
||||
const path = seedRaw(home, "llm-a", JSON.stringify(doc({ workspaceId: "llm-other" })));
|
||||
expect(readServiceCache(path, "llm-a", HOST)).toBeUndefined();
|
||||
const hostPath = seedRaw(
|
||||
home,
|
||||
"llm-b",
|
||||
JSON.stringify(doc({ workspaceId: "llm-b", endpointHost: "other.host" })),
|
||||
);
|
||||
expect(readServiceCache(hostPath, "llm-b", HOST)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeServiceCache", () => {
|
||||
it("publishes atomically and leaves no temp file behind", () => {
|
||||
const home = tempHome();
|
||||
const path = serviceCachePath("llm-a", home);
|
||||
writeServiceCache(path, doc());
|
||||
const names = readdirSync(dirname(path));
|
||||
expect(names).toEqual(["services-llm-a.json"]);
|
||||
expect(JSON.parse(readFileSync(path, "utf8"))).toEqual(doc());
|
||||
});
|
||||
|
||||
it("creates the cache directory owner-only", () => {
|
||||
const home = tempHome();
|
||||
const path = serviceCachePath("llm-a", home);
|
||||
writeServiceCache(path, doc());
|
||||
expect(statSync(dirname(path)).mode & 0o777).toBe(0o700);
|
||||
});
|
||||
});
|
||||
|
||||
/** A cache wired to a client returning one page per scene. */
|
||||
function cacheWith(home: string, onPost: () => Promise<unknown>, now: () => number = () => 5_000) {
|
||||
const warn = vi.fn();
|
||||
const postJson = vi.fn(onPost);
|
||||
const cache = new ServiceCache({
|
||||
client: { postJson } as unknown as KbClient,
|
||||
resolveWorkspaceId: async () => "llm-a",
|
||||
endpointHost: HOST,
|
||||
warn,
|
||||
home,
|
||||
now,
|
||||
});
|
||||
return { cache, warn, postJson };
|
||||
}
|
||||
|
||||
describe("ServiceCache", () => {
|
||||
const emptyPage = { code: "Success", data: { total_count: 0, rows: [] } };
|
||||
|
||||
it("treats a missing document as stale and a fresh one as current", () => {
|
||||
const home = tempHome();
|
||||
const { cache } = cacheWith(home, async () => emptyPage);
|
||||
expect(cache.isStale("llm-a")).toBe(true);
|
||||
|
||||
writeServiceCache(serviceCachePath("llm-a", home), doc({ fetchedAt: 5_000 }));
|
||||
cache.invalidate();
|
||||
expect(cache.isStale("llm-a")).toBe(false);
|
||||
});
|
||||
|
||||
it("goes stale once the TTL elapses", () => {
|
||||
const home = tempHome();
|
||||
writeServiceCache(serviceCachePath("llm-a", home), doc({ fetchedAt: 0 }));
|
||||
const { cache } = cacheWith(
|
||||
home,
|
||||
async () => emptyPage,
|
||||
() => CACHE_TTL_MS,
|
||||
);
|
||||
expect(cache.isStale("llm-a")).toBe(true);
|
||||
});
|
||||
|
||||
it("expires an EMPTY list on the much shorter negative TTL", () => {
|
||||
// The trap this closes: configure the plugin against a fresh workspace (0
|
||||
// services) → create a service → and then wait out the full TTL before the
|
||||
// catalog appears. An empty list is a setup-in-progress state, not a fact.
|
||||
const home = tempHome();
|
||||
writeServiceCache(
|
||||
serviceCachePath("llm-a", home),
|
||||
doc({ fetchedAt: 0, entries: [], total: 0 }),
|
||||
);
|
||||
const justAfterEmptyTtl = cacheWith(
|
||||
home,
|
||||
async () => emptyPage,
|
||||
() => EMPTY_CACHE_TTL_MS,
|
||||
);
|
||||
expect(justAfterEmptyTtl.cache.isStale("llm-a")).toBe(true);
|
||||
|
||||
// A non-empty list of the same age is still fresh, so the short window costs
|
||||
// nothing once services exist.
|
||||
writeServiceCache(serviceCachePath("llm-b", home), doc({ workspaceId: "llm-b", fetchedAt: 0 }));
|
||||
const nonEmpty = cacheWith(
|
||||
home,
|
||||
async () => emptyPage,
|
||||
() => EMPTY_CACHE_TTL_MS,
|
||||
);
|
||||
expect(nonEmpty.cache.isStale("llm-b")).toBe(false);
|
||||
expect(EMPTY_CACHE_TTL_MS).toBeLessThan(CACHE_TTL_MS);
|
||||
});
|
||||
|
||||
it('reports a panel snapshot that separates "empty workspace" from "stale list"', () => {
|
||||
const home = tempHome();
|
||||
const { cache } = cacheWith(
|
||||
home,
|
||||
async () => emptyPage,
|
||||
() => 5_000,
|
||||
);
|
||||
// Nothing cached at all: the panel must be able to say "never fetched"
|
||||
// rather than showing a zero that reads as "the workspace is empty".
|
||||
const empty = cache.status("llm-a");
|
||||
expect(empty).not.toHaveProperty("fetchedAt");
|
||||
expect(empty).toMatchObject({ searchCount: 0, chatCount: 0, stale: true });
|
||||
|
||||
writeServiceCache(
|
||||
serviceCachePath("llm-a", home),
|
||||
doc({
|
||||
fetchedAt: 5_000,
|
||||
entries: [
|
||||
{
|
||||
agent_id: "aid-1",
|
||||
agent_name: "a",
|
||||
scene: "search",
|
||||
status: "deployed",
|
||||
modify_time: "2026-08-01",
|
||||
},
|
||||
{
|
||||
agent_id: "aid-2",
|
||||
agent_name: "b",
|
||||
scene: "search",
|
||||
status: "deployed",
|
||||
modify_time: "2026-08-09",
|
||||
},
|
||||
{ agent_id: "aid-3", agent_name: "c", scene: "chat", status: "deployed" },
|
||||
],
|
||||
total: 900,
|
||||
truncated: true,
|
||||
}),
|
||||
);
|
||||
cache.invalidate();
|
||||
expect(cache.status("llm-a")).toMatchObject({
|
||||
fetchedAt: 5_000,
|
||||
searchCount: 2,
|
||||
chatCount: 1,
|
||||
total: 900,
|
||||
truncated: true,
|
||||
stale: false,
|
||||
});
|
||||
// The picker lists newest first so the likely-in-use service is on top.
|
||||
expect(cache.entriesFor("llm-a", "search").map((e) => e.agent_id)).toEqual(["aid-2", "aid-1"]);
|
||||
expect(cache.entriesFor("llm-a", "chat").map((e) => e.agent_id)).toEqual(["aid-3"]);
|
||||
});
|
||||
|
||||
it("re-reads from disk when the workspace changes", () => {
|
||||
const home = tempHome();
|
||||
writeServiceCache(serviceCachePath("llm-a", home), doc({ fetchedAt: 5_000 }));
|
||||
const { cache } = cacheWith(home, async () => emptyPage);
|
||||
expect(cache.peek("llm-a")?.workspaceId).toBe("llm-a");
|
||||
// Switching accounts must not keep serving the previous workspace's list.
|
||||
expect(cache.peek("llm-b")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shares one in-flight request across concurrent refreshes", async () => {
|
||||
const home = tempHome();
|
||||
// pre-step runs on every model request, so an unguarded refresh would pile up.
|
||||
const { cache, postJson } = cacheWith(home, async () => emptyPage);
|
||||
await Promise.all([cache.refresh(), cache.refresh(), cache.refresh()]);
|
||||
// Two calls total: one per scene, from a single shared refresh.
|
||||
expect(postJson).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("stores a fetched list and serves it synchronously afterwards", async () => {
|
||||
const home = tempHome();
|
||||
const { cache } = cacheWith(home, async () => ({
|
||||
code: "Success",
|
||||
data: {
|
||||
total_count: 1,
|
||||
rows: [{ agent_id: "aid-9", agent_name: "svc", agent_status: "deployed" }],
|
||||
},
|
||||
}));
|
||||
await cache.refresh();
|
||||
const stored = cache.peek("llm-a");
|
||||
expect(stored?.entries.map((e) => e.agent_id)).toEqual(["aid-9", "aid-9"]); // one per scene
|
||||
expect(stored?.fetchedAt).toBe(5_000);
|
||||
// And it survives as a file for the next process.
|
||||
expect(readServiceCache(serviceCachePath("llm-a", home), "llm-a", HOST)).toEqual(stored);
|
||||
});
|
||||
|
||||
it("never rejects on failure and keeps the previous document", async () => {
|
||||
const home = tempHome();
|
||||
writeServiceCache(serviceCachePath("llm-a", home), doc({ fetchedAt: 5_000 }));
|
||||
const { cache, warn } = cacheWith(home, async () => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
expect(cache.peek("llm-a")?.entries).toHaveLength(1);
|
||||
await expect(cache.refresh()).resolves.toBeUndefined();
|
||||
expect(warn).toHaveBeenCalled();
|
||||
// The stale-but-usable list is still there.
|
||||
expect(cache.peek("llm-a")?.entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not reject when the workspace is not configured yet", async () => {
|
||||
const home = tempHome();
|
||||
const warn = vi.fn();
|
||||
const cache = new ServiceCache({
|
||||
client: { postJson: vi.fn() } as unknown as KbClient,
|
||||
resolveWorkspaceId: async () => {
|
||||
throw new Error("workspace id is not configured");
|
||||
},
|
||||
endpointHost: HOST,
|
||||
warn,
|
||||
home,
|
||||
});
|
||||
await expect(cache.refresh()).resolves.toBeUndefined();
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining("refresh failed"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import {
|
||||
CATALOG_ENTRY_LIMIT,
|
||||
buildNoServiceNotice,
|
||||
buildRefreshedSceneList,
|
||||
buildServiceCatalog,
|
||||
} from "../src/service-catalog.js";
|
||||
import type { ServiceEntry } from "../src/services.js";
|
||||
|
||||
function entry(overrides: Partial<ServiceEntry> & { agent_id: string }): ServiceEntry {
|
||||
return {
|
||||
agent_name: `name-${overrides.agent_id}`,
|
||||
scene: "search",
|
||||
status: "deployed",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function catalog(
|
||||
entries: ServiceEntry[],
|
||||
extra: Partial<Parameters<typeof buildServiceCatalog>[0]> = {},
|
||||
) {
|
||||
return buildServiceCatalog({ entries, total: entries.length, truncated: false, ...extra });
|
||||
}
|
||||
|
||||
describe("buildServiceCatalog", () => {
|
||||
it("returns undefined when there is no service to render", () => {
|
||||
// Nothing to route to; the caller substitutes buildNoServiceNotice(), which
|
||||
// is the branch that tells the model what to do about it.
|
||||
expect(catalog([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lists every service when the count is within the limit", () => {
|
||||
const text = catalog([
|
||||
entry({ agent_id: "aid-1", agent_name: "RAG学习-检索" }),
|
||||
entry({ agent_id: "aid-2", agent_name: "产品文档" }),
|
||||
]);
|
||||
expect(text).toContain("aid-1 — RAG学习-检索");
|
||||
expect(text).toContain("aid-2 — 产品文档");
|
||||
// Nothing was cut, so nothing should claim otherwise.
|
||||
expect(text).not.toContain("showing");
|
||||
});
|
||||
|
||||
it("omits a scene with no services entirely", () => {
|
||||
const text = catalog([entry({ agent_id: "aid-1" })]);
|
||||
expect(text).toContain("kb_search");
|
||||
// "no chat services" is noise that invites handling a case that does not exist.
|
||||
expect(text).not.toContain("kb_chat");
|
||||
expect(text).not.toMatch(/no chat/i);
|
||||
});
|
||||
|
||||
it("renders both scenes without mixing their services", () => {
|
||||
const text =
|
||||
catalog([
|
||||
entry({ agent_id: "aid-s", scene: "search" }),
|
||||
entry({ agent_id: "aid-c", scene: "chat" }),
|
||||
]) ?? "";
|
||||
const searchAt = text.indexOf("kb_search");
|
||||
const chatAt = text.indexOf("kb_chat");
|
||||
expect(searchAt).toBeGreaterThanOrEqual(0);
|
||||
expect(chatAt).toBeGreaterThan(searchAt);
|
||||
// Each id belongs to its own section.
|
||||
expect(text.indexOf("aid-s")).toBeLessThan(chatAt);
|
||||
expect(text.indexOf("aid-c")).toBeGreaterThan(chatAt);
|
||||
});
|
||||
|
||||
it("shows only the configured default and says how many others exist", () => {
|
||||
const entries = Array.from({ length: 5 }, (_x, i) => entry({ agent_id: `aid-${i}` }));
|
||||
const text = catalog(entries, { defaultRetrieveAgentId: "aid-3" }) ?? "";
|
||||
expect(text).toContain("aid-3");
|
||||
expect(text).not.toContain("aid-0");
|
||||
expect(text).toContain("4 others exist");
|
||||
});
|
||||
|
||||
it("falls back to the full list when the configured default is not in the cache", () => {
|
||||
// A stale or mistyped default must not hide the services that do exist.
|
||||
const text =
|
||||
catalog([entry({ agent_id: "aid-1" })], { defaultRetrieveAgentId: "aid-gone" }) ?? "";
|
||||
expect(text).toContain("aid-1");
|
||||
expect(text).not.toContain("default service");
|
||||
});
|
||||
|
||||
it("caps the list at the limit, newest first, and states the shortfall", () => {
|
||||
const entries = Array.from({ length: 14 }, (_x, i) =>
|
||||
entry({
|
||||
agent_id: `aid-${i}`,
|
||||
// aid-13 newest, aid-0 oldest.
|
||||
modify_time: `2026-08-${String(i + 1).padStart(2, "0")}T00:00:00`,
|
||||
}),
|
||||
);
|
||||
const text = catalog(entries) ?? "";
|
||||
expect(text).toContain("aid-13");
|
||||
// The four oldest fall outside the window.
|
||||
expect(text).not.toContain("aid-0 ");
|
||||
expect(text).toContain(`showing ${CATALOG_ENTRY_LIMIT} most recently modified of 14`);
|
||||
// Silent truncation would make the model treat the list as complete.
|
||||
expect(text).toContain("--name <keyword>");
|
||||
});
|
||||
|
||||
it("marks a capped fetch as a lower bound rather than an exact total", () => {
|
||||
// The fetch itself stopped early, so even the count is unknown.
|
||||
const entries = Array.from({ length: 12 }, (_x, i) => entry({ agent_id: `aid-${i}` }));
|
||||
const text = catalog(entries, { truncated: true, total: 900 }) ?? "";
|
||||
expect(text).toContain("more than");
|
||||
});
|
||||
|
||||
it("states that agent_id is required and forbids guessing one", () => {
|
||||
const text = catalog([entry({ agent_id: "aid-1" })]) ?? "";
|
||||
expect(text).toContain("required");
|
||||
// Picking the closest-looking service returns unrelated evidence, which is
|
||||
// worse for the user than an honest "no such knowledge base".
|
||||
expect(text).toMatch(/say so plainly/i);
|
||||
});
|
||||
|
||||
it("labels an unnamed service instead of rendering a bare dash", () => {
|
||||
const text = catalog([entry({ agent_id: "aid-1", agent_name: "" })]) ?? "";
|
||||
expect(text).toContain("(unnamed)");
|
||||
});
|
||||
|
||||
it("renders a description when one arrives, truncated to its budget", () => {
|
||||
// Forward compatibility: the backend has not shipped this field yet.
|
||||
const long = "x".repeat(250);
|
||||
const text = catalog([entry({ agent_id: "aid-1", description: long })]) ?? "";
|
||||
expect(text).toContain("x".repeat(199));
|
||||
expect(text).not.toContain("x".repeat(201));
|
||||
expect(text).toContain("…");
|
||||
});
|
||||
|
||||
it("explains how to get bl only where it recommends running it", () => {
|
||||
// The plugin never shells out, so a fully configured deployment can have no
|
||||
// bl on PATH. Naming the command without the install line is a dead end.
|
||||
const complete = catalog([entry({ agent_id: "aid-1" })]) ?? "";
|
||||
expect(complete).not.toContain("bl knowledge service list");
|
||||
expect(complete).not.toContain("npm install -g bailian-cli");
|
||||
|
||||
const capped =
|
||||
catalog(Array.from({ length: 12 }, (_x, index) => entry({ agent_id: `aid-${index}` }))) ?? "";
|
||||
expect(capped).toContain("bl knowledge service list");
|
||||
expect(capped).toContain("npm install -g bailian-cli");
|
||||
|
||||
// Same rule on the collapsed-to-default branch, which also names the command.
|
||||
const collapsed =
|
||||
catalog([entry({ agent_id: "aid-1" }), entry({ agent_id: "aid-2" })], {
|
||||
defaultRetrieveAgentId: "aid-1",
|
||||
}) ?? "";
|
||||
expect(collapsed).toContain("bl knowledge service list");
|
||||
expect(collapsed).toContain("npm install -g bailian-cli");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNoServiceNotice", () => {
|
||||
it("names the way out instead of leaving the model to invent an agent_id", () => {
|
||||
const text = buildNoServiceNotice();
|
||||
// agent_id is required, so silence here means a guessed id or a silently
|
||||
// dropped retrieval — both look like a broken plugin to the user.
|
||||
expect(text).toMatch(/do not invent an id/i);
|
||||
expect(text).toContain("bl knowledge service create");
|
||||
expect(text).toContain("bl knowledge service deploy");
|
||||
// The management skill is a second-order path: a model that never loads it
|
||||
// must still learn how to get bl.
|
||||
expect(text).toContain("npm install -g bailian-cli");
|
||||
expect(text).toContain("https://bailian.console.aliyun.com/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRefreshedSceneList", () => {
|
||||
it("lists the scene's services for a rejected call", () => {
|
||||
const text = buildRefreshedSceneList("search", [
|
||||
entry({ agent_id: "aid-1", agent_name: "产品文档" }),
|
||||
]);
|
||||
expect(text).toContain("aid-1 — 产品文档");
|
||||
expect(text).toMatch(/re-read just now/i);
|
||||
expect(text).not.toContain("npm install -g bailian-cli");
|
||||
});
|
||||
|
||||
it("states the shortfall and how to get bl when the list is capped", () => {
|
||||
const entries = Array.from({ length: CATALOG_ENTRY_LIMIT + 3 }, (_x, index) =>
|
||||
entry({ agent_id: `aid-${index}` }),
|
||||
);
|
||||
const text = buildRefreshedSceneList("search", entries);
|
||||
expect(text).toContain("and 3 more");
|
||||
expect(text).toContain("npm install -g bailian-cli");
|
||||
});
|
||||
|
||||
it("tells a rejected call to stop retrying when the scene has no services", () => {
|
||||
// Returning nothing here used to let the bare "invalid agent_id" through,
|
||||
// which reads as an invitation to try a different id.
|
||||
const text = buildRefreshedSceneList("chat", []);
|
||||
expect(text).toMatch(/no deployed chat services/i);
|
||||
expect(text).toMatch(/do not retry with a different id/i);
|
||||
expect(text).toContain("bl knowledge service deploy");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { KbClient } from "../src/client.js";
|
||||
import { ServiceCache, serviceCachePath, writeServiceCache } from "../src/service-cache.js";
|
||||
import { installServiceContext } from "../src/service-context.js";
|
||||
|
||||
const HOST = "cn-beijing.maas.aliyuncs.com";
|
||||
const WS = "llm-a";
|
||||
const SOURCE_PLUGIN = "tool-bailian-kb/services";
|
||||
|
||||
type Injected = { id: string; source: { plugin?: string }; content: { text?: string }[] };
|
||||
type Decision = { kind: string; messages: Injected[] };
|
||||
|
||||
interface HarnessOptions {
|
||||
entries?: { agent_id: string; agent_name: string; scene: "search" | "chat"; status: string }[];
|
||||
cacheOverride?: ServiceCache;
|
||||
postJson?: () => Promise<unknown>;
|
||||
/** Skip seeding the cache file, i.e. the very first step of a fresh install. */
|
||||
withoutCacheFile?: boolean;
|
||||
}
|
||||
|
||||
/** Drives the installed `agent/pre-step` listener against a fake session. */
|
||||
function harness(options: HarnessOptions = {}) {
|
||||
const home = mkdtempSync(join(tmpdir(), "bailian-kb-ctx-"));
|
||||
const entries = options.entries ?? [
|
||||
{ agent_id: "aid-1", agent_name: "svc-one", scene: "search" as const, status: "deployed" },
|
||||
];
|
||||
if (options.withoutCacheFile !== true) {
|
||||
writeServiceCache(serviceCachePath(WS, home), {
|
||||
version: 1,
|
||||
fetchedAt: Date.now(),
|
||||
workspaceId: WS,
|
||||
endpointHost: HOST,
|
||||
entries,
|
||||
total: entries.length,
|
||||
truncated: false,
|
||||
});
|
||||
}
|
||||
const postJson = vi.fn(
|
||||
options.postJson ?? (async () => ({ code: "Success", data: { total_count: 0, rows: [] } })),
|
||||
);
|
||||
const cache =
|
||||
options.cacheOverride ??
|
||||
new ServiceCache({
|
||||
client: { postJson } as unknown as KbClient,
|
||||
resolveWorkspaceId: async () => WS,
|
||||
endpointHost: HOST,
|
||||
warn: () => {},
|
||||
home,
|
||||
});
|
||||
|
||||
let listener!: (payload: unknown, next: () => Promise<unknown>) => Promise<unknown>;
|
||||
const warn = vi.fn();
|
||||
const ctx = {
|
||||
on: (_event: string, handler: typeof listener) => {
|
||||
listener = handler;
|
||||
},
|
||||
};
|
||||
installServiceContext(ctx as never, {
|
||||
cache,
|
||||
resolveWorkspaceId: async () => WS,
|
||||
resolveDefaultRetrieveAgentId: async () => undefined,
|
||||
resolveDefaultChatAgentId: async () => undefined,
|
||||
warn,
|
||||
});
|
||||
|
||||
const events: { type: string; seq: number; data: unknown }[] = [];
|
||||
const agent = {
|
||||
session: {
|
||||
events,
|
||||
surface: { nodes: [] as number[] },
|
||||
header: { cwd: "/tmp" },
|
||||
},
|
||||
};
|
||||
|
||||
const run = async (proposed: Injected[], downstream: "enter" | "reject"): Promise<Decision> =>
|
||||
(await listener({ agent, signal: { aborted: false, throwIfAborted: () => {} } }, async () =>
|
||||
downstream === "reject" ? { kind: "reject" } : { kind: "enter", messages: proposed },
|
||||
)) as Decision;
|
||||
|
||||
return {
|
||||
cache,
|
||||
warn,
|
||||
postJson,
|
||||
/** One step whose downstream decision enters with `proposed`. */
|
||||
step: async (proposed: Injected[] = []) => await run(proposed, "enter"),
|
||||
/** One step whose downstream decision rejects. */
|
||||
stepRejecting: async () => await run([], "reject"),
|
||||
/** Record an entered message into the durable log and make it visible. */
|
||||
commit: (message: Injected) => {
|
||||
events.push({ type: "user/message", seq: events.length + 1, data: message });
|
||||
agent.session.surface.nodes = events.map((event) => event.seq);
|
||||
},
|
||||
/** Simulate compaction: the events remain, the surface shrinks. */
|
||||
setSurface: (seqs: number[]) => {
|
||||
agent.session.surface.nodes = seqs;
|
||||
},
|
||||
/** This module's own messages within a decision. */
|
||||
injected: (decision: Decision) =>
|
||||
decision.messages.filter((m) => m.source.plugin === SOURCE_PLUGIN),
|
||||
};
|
||||
}
|
||||
|
||||
describe("installServiceContext", () => {
|
||||
it("injects the catalog as a sourced message on the first step", async () => {
|
||||
const h = harness();
|
||||
const decision = await h.step();
|
||||
expect(decision.kind).toBe("enter");
|
||||
const own = h.injected(decision);
|
||||
expect(own).toHaveLength(1);
|
||||
expect(own[0]?.content[0]?.text).toContain("aid-1");
|
||||
});
|
||||
|
||||
it("does not re-inject on later steps of the same turn", async () => {
|
||||
// pre-step fires once per model request, so a turn with five tool calls
|
||||
// fires it six times. Re-injecting each time would insert six copies and
|
||||
// void the KV cache from the first insertion onward — this is a correctness
|
||||
// guard, not an optimization.
|
||||
const h = harness();
|
||||
const first = await h.step();
|
||||
const own = h.injected(first);
|
||||
expect(own).toHaveLength(1);
|
||||
h.commit(own[0]!);
|
||||
|
||||
for (let toolRoundTrip = 0; toolRoundTrip < 5; toolRoundTrip += 1) {
|
||||
expect(h.injected(await h.step())).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("re-injects once compaction drops the catalog from the visible surface", async () => {
|
||||
const h = harness();
|
||||
const first = await h.step();
|
||||
h.commit(h.injected(first)[0]!);
|
||||
expect(h.injected(await h.step())).toHaveLength(0);
|
||||
|
||||
// The event stays in the durable log but leaves the surface. Comparing only
|
||||
// "did we ever publish this" would suppress every future injection and the
|
||||
// model would finish the session with no service list at all.
|
||||
h.setSurface([]);
|
||||
expect(h.injected(await h.step())).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("replaces its own pending message instead of adding a second one", async () => {
|
||||
const h = harness();
|
||||
const pending = h.injected(await h.step())[0]!;
|
||||
const second = await h.step([pending]);
|
||||
expect(h.injected(second)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("injects the no-service notice when the cache holds no services", async () => {
|
||||
// A cached empty list is authoritative, not a missing fetch: staying silent
|
||||
// would leave the model to invent the required agent_id.
|
||||
const h = harness({ entries: [] });
|
||||
const own = h.injected(await h.step());
|
||||
expect(own).toHaveLength(1);
|
||||
expect(own[0]?.content[0]?.text).toMatch(/no\s+deployed knowledge service/i);
|
||||
});
|
||||
|
||||
it("injects nothing while no cache document exists yet", async () => {
|
||||
// Nothing has been fetched, so any claim about the workspace would be a
|
||||
// guess; the background refresh corrects this on a later step.
|
||||
const h = harness({ withoutCacheFile: true });
|
||||
expect(h.injected(await h.step())).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("never awaits the network refresh", async () => {
|
||||
// A slow list request must not delay the user's request.
|
||||
let settle: ((value: unknown) => void) | undefined;
|
||||
const h = harness({
|
||||
postJson: async () =>
|
||||
await new Promise((resolve) => {
|
||||
settle = resolve;
|
||||
}),
|
||||
});
|
||||
// Force staleness so this step schedules a refresh.
|
||||
h.cache.invalidate();
|
||||
const raced = await Promise.race([
|
||||
h.step().then(() => "stepped"),
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => resolve("timed out"), 200);
|
||||
}),
|
||||
]);
|
||||
expect(raced).toBe("stepped");
|
||||
settle?.({ data: { rows: [] } });
|
||||
});
|
||||
|
||||
it("degrades to no injection when anything throws, never failing the step", async () => {
|
||||
// A throwing pre-step listener fails the proposed step, i.e. stalls the
|
||||
// user's turn. A missing catalog is far cheaper than that.
|
||||
const exploding = {
|
||||
isStale: () => {
|
||||
throw new Error("corrupt cache");
|
||||
},
|
||||
peek: () => undefined,
|
||||
refresh: async () => {},
|
||||
invalidate: () => {},
|
||||
} as unknown as ServiceCache;
|
||||
const h = harness({ cacheOverride: exploding });
|
||||
const decision = await h.step();
|
||||
expect(decision.kind).toBe("enter");
|
||||
expect(h.injected(decision)).toHaveLength(0);
|
||||
expect(h.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes a rejected downstream decision straight through", async () => {
|
||||
const h = harness();
|
||||
expect((await h.stepRejecting()).kind).toBe("reject");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
import type { ServiceListResponse } from "../src/api-types.js";
|
||||
import type { KbClient } from "../src/client.js";
|
||||
import { listServices } from "../src/services.js";
|
||||
|
||||
/** A page of `count` rows, all deployed unless overridden. */
|
||||
function page(
|
||||
count: number,
|
||||
total: number,
|
||||
overrides: Record<string, unknown> = {},
|
||||
): ServiceListResponse {
|
||||
return {
|
||||
code: "Success",
|
||||
data: {
|
||||
total_count: total,
|
||||
rows: Array.from({ length: count }, (_row, index) => ({
|
||||
agent_id: `aid-${index}`,
|
||||
agent_name: `service-${index}`,
|
||||
agent_status: "deployed",
|
||||
modify_time: "2026-08-20T10:00:00",
|
||||
...overrides,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** A client whose postJson is driven by a queue of per-call responses or errors. */
|
||||
function clientReturning(...outcomes: (ServiceListResponse | Error)[]): {
|
||||
client: KbClient;
|
||||
postJson: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const postJson = vi.fn(async () => {
|
||||
const next = outcomes.shift();
|
||||
if (next === undefined) throw new Error("unexpected extra request");
|
||||
if (next instanceof Error) throw next;
|
||||
return next;
|
||||
});
|
||||
return { client: { postJson } as unknown as KbClient, postJson };
|
||||
}
|
||||
|
||||
describe("listServices", () => {
|
||||
it("queries both scenes for deployed services only and tags each entry with its scene", async () => {
|
||||
const { client, postJson } = clientReturning(page(1, 1), page(1, 1));
|
||||
const result = await listServices(client);
|
||||
|
||||
expect(postJson.mock.calls[0]?.[1]).toMatchObject({
|
||||
agent_scene: "search",
|
||||
// Verified server-side: this filter is honored and excludes drafts.
|
||||
agent_status: "deployed",
|
||||
page_number: 1,
|
||||
page_size: 100,
|
||||
});
|
||||
expect(postJson.mock.calls[1]?.[1]).toMatchObject({ agent_scene: "chat" });
|
||||
expect(result.entries.map((e) => e.scene)).toEqual(["search", "chat"]);
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.truncated).toBe(false);
|
||||
expect(result.failedScenes).toEqual([]);
|
||||
});
|
||||
|
||||
it("stops at a short page without asking for another", async () => {
|
||||
// 3 rows on a 100-row page is the last page; a second request would be waste.
|
||||
const { client, postJson } = clientReturning(page(3, 3), page(0, 0));
|
||||
const result = await listServices(client);
|
||||
expect(postJson).toHaveBeenCalledTimes(2); // one per scene, not one per page
|
||||
expect(result.entries).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("caps at two pages per scene and reports the shortfall as truncated", async () => {
|
||||
// A workspace claiming 500 rows: fetch 200, flag the rest as unfetched.
|
||||
const { client, postJson } = clientReturning(page(100, 500), page(100, 500), page(0, 0));
|
||||
const result = await listServices(client);
|
||||
// 2 pages for search + 1 short page for chat: the cap holds.
|
||||
expect(postJson).toHaveBeenCalledTimes(3);
|
||||
expect(result.entries).toHaveLength(200);
|
||||
expect(result.truncated).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps one scene when the other fails instead of losing the whole list", async () => {
|
||||
const { client } = clientReturning(page(2, 2), new Error("chat scene exploded"));
|
||||
const result = await listServices(client);
|
||||
expect(result.entries).toHaveLength(2);
|
||||
expect(result.entries.every((e) => e.scene === "search")).toBe(true);
|
||||
expect(result.failedScenes).toEqual(["chat"]);
|
||||
});
|
||||
|
||||
it("reports both scenes as failed without throwing", async () => {
|
||||
const { client } = clientReturning(new Error("down"), new Error("down"));
|
||||
const result = await listServices(client);
|
||||
expect(result.entries).toEqual([]);
|
||||
expect(result.failedScenes).toEqual(["search", "chat"]);
|
||||
});
|
||||
|
||||
it("drops rows without an agent_id and never reads pipeline_list", async () => {
|
||||
// pipeline_list is unreliable in production (missing names, sometimes empty),
|
||||
// so entries must not carry any knowledge-base label derived from it.
|
||||
const { client } = clientReturning(
|
||||
{
|
||||
code: "Success",
|
||||
data: {
|
||||
total_count: 2,
|
||||
rows: [
|
||||
{ agent_name: "no id", agent_status: "deployed" },
|
||||
{
|
||||
agent_id: "aid-1",
|
||||
agent_name: "ok",
|
||||
agent_status: "deployed",
|
||||
pipeline_list: [{ pipeline_id: "p1" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
page(0, 0),
|
||||
);
|
||||
const result = await listServices(client);
|
||||
expect(result.entries).toHaveLength(1);
|
||||
expect(result.entries[0]).toEqual({
|
||||
agent_id: "aid-1",
|
||||
agent_name: "ok",
|
||||
scene: "search",
|
||||
status: "deployed",
|
||||
});
|
||||
expect(JSON.stringify(result.entries)).not.toContain("p1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseSkillFile } from "../src/skill.js";
|
||||
|
||||
const SKILL_PATH = fileURLToPath(new URL("../skills/bailian-kb/SKILL.md", import.meta.url));
|
||||
|
||||
describe("parseSkillFile", () => {
|
||||
it("takes name and description from frontmatter and strips the block from the body", () => {
|
||||
const parsed = parseSkillFile(
|
||||
[
|
||||
"---",
|
||||
"name: demo-skill",
|
||||
"description: >-",
|
||||
" First line of the folded description,",
|
||||
" continued on the next source line.",
|
||||
"---",
|
||||
"",
|
||||
"# Heading",
|
||||
"",
|
||||
"Body text.",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(parsed?.name).toBe("demo-skill");
|
||||
// A folded scalar joins its lines with spaces; the value must arrive unfolded.
|
||||
expect(parsed?.description).toBe(
|
||||
"First line of the folded description, continued on the next source line.",
|
||||
);
|
||||
// The registry performs no parsing of its own, so the YAML must already be gone.
|
||||
expect(parsed?.content).toBe("\n# Heading\n\nBody text.\n");
|
||||
expect(parsed?.content).not.toContain("---");
|
||||
expect(parsed?.content).not.toContain("description:");
|
||||
});
|
||||
|
||||
it("carries optional whenToUse and metadata, omitting them when absent or blank", () => {
|
||||
const withExtras = parseSkillFile(
|
||||
[
|
||||
"---",
|
||||
"name: demo-skill",
|
||||
"description: Routing text.",
|
||||
"whenToUse: Extra guidance.",
|
||||
"metadata:",
|
||||
" owner: platform",
|
||||
"---",
|
||||
"Body.",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(withExtras?.whenToUse).toBe("Extra guidance.");
|
||||
expect(withExtras?.metadata).toEqual({ owner: "platform" });
|
||||
|
||||
const bare = parseSkillFile("---\nname: demo-skill\ndescription: Routing text.\n---\nBody.");
|
||||
expect(bare).not.toHaveProperty("whenToUse");
|
||||
expect(bare).not.toHaveProperty("metadata");
|
||||
});
|
||||
|
||||
it("rejects a file without usable frontmatter instead of throwing", () => {
|
||||
// No fence at all: a plain markdown file.
|
||||
expect(parseSkillFile("# Just markdown\n")).toBeUndefined();
|
||||
// Opening fence never closes.
|
||||
expect(parseSkillFile("---\nname: demo-skill\n")).toBeUndefined();
|
||||
// Unparsable YAML inside the fence.
|
||||
expect(parseSkillFile("---\nname: [unclosed\n---\nBody.")).toBeUndefined();
|
||||
// Scalar and sequence roots are not frontmatter records.
|
||||
expect(parseSkillFile("---\njust a string\n---\nBody.")).toBeUndefined();
|
||||
expect(parseSkillFile("---\n- one\n- two\n---\nBody.")).toBeUndefined();
|
||||
// name or description missing, blank, or the wrong type.
|
||||
expect(parseSkillFile("---\ndescription: Routing text.\n---\nBody.")).toBeUndefined();
|
||||
expect(parseSkillFile("---\nname: demo-skill\n---\nBody.")).toBeUndefined();
|
||||
expect(parseSkillFile('---\nname: demo-skill\ndescription: " "\n---\nBody.')).toBeUndefined();
|
||||
expect(parseSkillFile("---\nname: 42\ndescription: Routing text.\n---\nBody.")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the packaged bailian-kb SKILL.md", () => {
|
||||
const parsed = parseSkillFile(readFileSync(SKILL_PATH, "utf8"));
|
||||
|
||||
it("parses, and is the single source of the registered name and description", () => {
|
||||
expect(parsed?.name).toBe("bailian-kb");
|
||||
expect(parsed?.description).toContain("bl");
|
||||
// The frontmatter must state where credentials come from: that sentence used
|
||||
// to live in skill.ts and would otherwise be lost with the duplicate.
|
||||
expect(parsed?.description).toContain("DASHSCOPE_API_KEY");
|
||||
});
|
||||
|
||||
it("keeps its description within the catalog truncation budget", () => {
|
||||
// tool-skill publishes catalog entries through catalogDescription(), which
|
||||
// whitespace-normalizes and hard-truncates at 500 characters.
|
||||
const normalized = (parsed?.description ?? "").replaceAll(/\s+/g, " ").trim();
|
||||
expect(normalized.length).toBeLessThanOrEqual(500);
|
||||
});
|
||||
|
||||
it("registers a body with no frontmatter residue", () => {
|
||||
expect(parsed?.content.startsWith("---")).toBe(false);
|
||||
expect(parsed?.content).not.toContain("description: >-");
|
||||
// The real body still begins with the document heading.
|
||||
expect(parsed?.content.trimStart().startsWith("#")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { parseSseStream } from "../src/sse.js";
|
||||
|
||||
function streamOf(text: string): ReadableStream<Uint8Array> {
|
||||
return new Response(text).body as ReadableStream<Uint8Array>;
|
||||
}
|
||||
|
||||
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" }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import { describe, expect, it, vi } from "vite-plus/test";
|
||||
import { KbApiError, KbClient } from "../src/client.js";
|
||||
import { createKbTools } from "../src/tools.js";
|
||||
|
||||
const EXEC = {} as never;
|
||||
|
||||
function toolsWith(
|
||||
postJson: unknown,
|
||||
postSse?: unknown,
|
||||
resolveDefaultRetrieveAgentId?: () => Promise<string | undefined>,
|
||||
resolveDefaultChatAgentId?: () => Promise<string | undefined>,
|
||||
describeServicesAfterRefresh?: (scene: "search" | "chat") => Promise<string | undefined>,
|
||||
) {
|
||||
const client = { postJson, postSse, agentVersion: undefined } as unknown as KbClient;
|
||||
const list = createKbTools({
|
||||
client,
|
||||
...(resolveDefaultRetrieveAgentId ? { resolveDefaultRetrieveAgentId } : {}),
|
||||
...(resolveDefaultChatAgentId ? { resolveDefaultChatAgentId } : {}),
|
||||
...(describeServicesAfterRefresh ? { describeServicesAfterRefresh } : {}),
|
||||
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 kb_search and kb_chat", () => {
|
||||
const { list } = toolsWith(vi.fn());
|
||||
expect(list.map((t) => t.name).sort()).toEqual(["kb_chat", "kb_search"]);
|
||||
});
|
||||
|
||||
it("keeps both descriptions free of service ids so the schemas stay prefix-stable", () => {
|
||||
// The live service list rides an `agent/pre-step` context message precisely
|
||||
// because re-registering a tool to refresh its description would void the
|
||||
// prompt prefix cache. Any id leaking in here means that decision regressed.
|
||||
const { byName } = toolsWith(vi.fn());
|
||||
for (const tool of [byName.kb_search!, byName.kb_chat!]) {
|
||||
const text = `${tool.description} ${JSON.stringify(tool.parameters)}`;
|
||||
expect(text).not.toMatch(/aid-[0-9a-f]/);
|
||||
}
|
||||
});
|
||||
|
||||
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<string, unknown>;
|
||||
expect(body).not.toHaveProperty("top_k");
|
||||
expect(body.agent_id).toBe("aid-1");
|
||||
});
|
||||
|
||||
it("agent_id is required in the schema for both tools", () => {
|
||||
const { byName } = toolsWith(vi.fn());
|
||||
// defineTool compiles the spec into JSON Schema: requiredness lives in the top-level `required` array.
|
||||
const requiredList = (tool: { parameters: Record<string, unknown> }) =>
|
||||
(tool.parameters.required ?? []) as string[];
|
||||
expect(requiredList(byName.kb_search!)).toContain("agent_id");
|
||||
expect(requiredList(byName.kb_chat!)).toContain("agent_id");
|
||||
});
|
||||
|
||||
it("a missing agent_id is rejected by schema validation before execute (even with a default resolver)", async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse);
|
||||
const resolveDefaultRetrieveAgentId = vi.fn(async () => "aid-fixed");
|
||||
const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId);
|
||||
// defineTool validates args against the compiled schema before execute runs,
|
||||
// so with agent_id required the per-call default fallback is never consulted
|
||||
// through this entry point; it stays as defense-in-depth only.
|
||||
const err = await byName.kb_search!.execute({ query: "q" }, EXEC).catch((e: unknown) => e);
|
||||
expect((err as Error).message).toContain("agent_id");
|
||||
expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled();
|
||||
expect(postJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a missing agent_id without any default is also a schema rejection, not the runtime guidance error", async () => {
|
||||
const { byName } = toolsWith(vi.fn());
|
||||
const err = await byName.kb_search!.execute({ query: "q" }, EXEC).catch((e: unknown) => e);
|
||||
expect((err as Error).message).toContain("missing required property");
|
||||
});
|
||||
|
||||
it("a supplied agent_id bypasses the default resolver entirely", async () => {
|
||||
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse);
|
||||
const resolveDefaultRetrieveAgentId = vi.fn(async () => "aid-default");
|
||||
const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId);
|
||||
await byName.kb_search!.execute({ query: "q", agent_id: "aid-explicit" }, EXEC);
|
||||
expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled();
|
||||
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe("aid-explicit");
|
||||
});
|
||||
|
||||
it("a 4xx failure passes the original error through unchanged when no refresh hook is wired", async () => {
|
||||
const postJson = vi.fn(async (_path: string) => {
|
||||
throw new KbApiError("agent not found", 400);
|
||||
});
|
||||
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).toBe("agent not found");
|
||||
});
|
||||
|
||||
it("a 4xx failure appends the refreshed service list for the calling scene", async () => {
|
||||
// agent_id is the only caller-supplied identifier on these endpoints, so a
|
||||
// rejected request most often means the cached id is gone. The recovery the
|
||||
// model needs is the current list, and an error message carries it without
|
||||
// disturbing the request prefix.
|
||||
const postJson = vi.fn(async (_path: string) => {
|
||||
throw new KbApiError("agent not found", 400);
|
||||
});
|
||||
const describe = vi.fn(async (scene: "search" | "chat") => `services for ${scene}: aid-new`);
|
||||
const { byName } = toolsWith(postJson, undefined, undefined, undefined, describe);
|
||||
const err = await byName
|
||||
.kb_search!.execute({ query: "q", agent_id: "stale" }, EXEC)
|
||||
.catch((e: unknown) => e);
|
||||
expect(describe).toHaveBeenCalledWith("search");
|
||||
expect((err as Error).message).toContain("agent not found");
|
||||
expect((err as Error).message).toContain("services for search: aid-new");
|
||||
});
|
||||
|
||||
it("leaves a 5xx failure and a failing refresh alone", async () => {
|
||||
// A server-side fault is not an id problem, and a refresh that itself fails
|
||||
// must not replace the real error with its own.
|
||||
const serverError = vi.fn(async (_path: string) => {
|
||||
throw new KbApiError("upstream exploded", 502);
|
||||
});
|
||||
const describe = vi.fn(async () => "never used");
|
||||
const { byName } = toolsWith(serverError, undefined, undefined, undefined, describe);
|
||||
const err = await byName
|
||||
.kb_search!.execute({ query: "q", agent_id: "aid-1" }, EXEC)
|
||||
.catch((e: unknown) => e);
|
||||
expect(describe).not.toHaveBeenCalled();
|
||||
expect((err as Error).message).toBe("upstream exploded");
|
||||
|
||||
const badRequest = vi.fn(async (_path: string) => {
|
||||
throw new KbApiError("agent not found", 400);
|
||||
});
|
||||
const failing = vi.fn(async () => {
|
||||
throw new Error("refresh also down");
|
||||
});
|
||||
const { byName: byName2 } = toolsWith(badRequest, undefined, undefined, undefined, failing);
|
||||
const err2 = await byName2
|
||||
.kb_search!.execute({ query: "q", agent_id: "bad" }, EXEC)
|
||||
.catch((e: unknown) => e);
|
||||
expect((err2 as Error).message).toBe("agent not found");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("kb_chat reads chatTimeoutMs off deps per call (live-settings getter stays live)", async () => {
|
||||
const timeout = Object.assign(new Error("operation timed out"), { name: "TimeoutError" });
|
||||
const postSse = vi.fn(async () => {
|
||||
throw timeout;
|
||||
});
|
||||
const client = { postJson: vi.fn(), postSse, agentVersion: undefined } as unknown as KbClient;
|
||||
// Mirrors the host apply: a getter over the mutable settings source.
|
||||
let timeoutMs = 1000;
|
||||
const list = createKbTools({
|
||||
client,
|
||||
get chatTimeoutMs() {
|
||||
return timeoutMs;
|
||||
},
|
||||
});
|
||||
const chat = list.find((t) => t.name === "kb_chat")!;
|
||||
timeoutMs = 2222;
|
||||
const err = await chat
|
||||
.execute({ message: "q", agent_id: "aid-1" }, EXEC)
|
||||
.catch((e: unknown) => e);
|
||||
expect((err as Error).message).toContain("2222ms");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/web"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"types": ["node"],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/web"]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 <style data-plugin> tag at factory execution.
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename, dirname, resolve as resolvePath } from "node:path";
|
||||
import { defineConfig } from "tsdown";
|
||||
import { transform } from "lightningcss";
|
||||
|
||||
/** Plugin id stamped into the __ModuleLoader__.load handoff and style tags. */
|
||||
const PLUGIN_ID = "bailian-kb-dsh";
|
||||
|
||||
/** The module specifiers the shell shares into the frozen module table. */
|
||||
const PLATFORM_MODULES = [
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"@deepseek-ai/cordis",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-web-react",
|
||||
"@deepseek-ai/dsh-client-ui-primitives",
|
||||
"@deepseek-ai/dsh-client-ui-attachment",
|
||||
"@deepseek-ai/dsh-client-schema-form",
|
||||
];
|
||||
|
||||
/**
|
||||
* Documented host exemption (not a platform module): the snapshot-store
|
||||
* engine lives in runtime pending its rehoming; at runtime the lazy CJS table
|
||||
* answers the require natively.
|
||||
*/
|
||||
const RUNTIME_STORE_EXEMPTION = "@deepseek-ai/dsh-client-runtime/client";
|
||||
|
||||
/** Externals resolved from the loader module table. */
|
||||
const CLIENT_EXTERNALS: readonly string[] = [...PLATFORM_MODULES, RUNTIME_STORE_EXEMPTION];
|
||||
|
||||
/** Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline. */
|
||||
const CSS_VIRTUAL_PREFIX = "\0dsh-css:";
|
||||
const CSS_VIRTUAL_SUFFIX = ".mjs";
|
||||
|
||||
export default defineConfig({
|
||||
name: `${PLUGIN_ID}/client`,
|
||||
entry: { client: "src/web/index.ts" },
|
||||
// Browser bundle lands in its own dist/web subdir: the tsc node half owns
|
||||
// dist/ directly, and a shared outDir would clobber dist/client.js (the KbClient
|
||||
// module) with this artifact. The entryFileNames pin keeps it exactly
|
||||
// dist/web/client.js; the host serves it at /plugins/<id>/client.js via
|
||||
// exports["./client"]. clean must stay off — a default clean would wipe the
|
||||
// tsc-emitted node half.
|
||||
outDir: "dist/web",
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
dts: false,
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
external: [...CLIENT_EXTERNALS],
|
||||
// tsdown auto-externalizes package dependencies; anything NOT in the loader
|
||||
// module table must inline instead. A require() the table cannot answer is a
|
||||
// guaranteed runtime throw, so the rule is the table list itself.
|
||||
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"),
|
||||
"import.meta.env.MODE": JSON.stringify(process.env.NODE_ENV ?? "production"),
|
||||
"import.meta.env": JSON.stringify({ MODE: process.env.NODE_ENV ?? "production" }),
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
// Bundle purity gate (build-time mirror of the module-edge rules):
|
||||
// platform seed entries stay external; every other @deepseek-ai value
|
||||
// import is a build error — a cross-plugin value import either inlines a
|
||||
// duplicate runtime instance or requires a specifier the frozen module
|
||||
// table cannot answer. Cross-plugin collaboration goes through cordis
|
||||
// services instead (type-only imports are erased and never reach this gate).
|
||||
name: "dsh-client-bundle-purity",
|
||||
resolveId(source: string) {
|
||||
if (!source.startsWith("@deepseek-ai/")) return null;
|
||||
if (CLIENT_EXTERNALS.includes(source)) return null; // platform module: external wins
|
||||
throw new Error(
|
||||
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS) — ` +
|
||||
"cross-plugin value imports are forbidden; collaborate through cordis services " +
|
||||
"(type-only imports are erased and never reach this gate)",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dsh-css-modules-inline",
|
||||
resolveId(source: string, importer: string | undefined) {
|
||||
if (!source.endsWith(".module.css")) return null;
|
||||
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source;
|
||||
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX;
|
||||
},
|
||||
async load(virtualId: string) {
|
||||
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null;
|
||||
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
|
||||
// The virtual id otherwise hides the physical stylesheet from the watch graph.
|
||||
this.addWatchFile(fileId);
|
||||
const source = await readFile(fileId);
|
||||
const { code, exports: cssExports } = transform({
|
||||
filename: fileId,
|
||||
code: source,
|
||||
cssModules: { pattern: "[hash]_[local]" },
|
||||
minify: true,
|
||||
});
|
||||
const classMap: Record<string, string> = {};
|
||||
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name;
|
||||
// One <style data-plugin> per module file; idempotent under re-evaluation.
|
||||
return [
|
||||
`const css = ${JSON.stringify(code.toString())};`,
|
||||
`const tagId = ${JSON.stringify(`${PLUGIN_ID}/${basename(fileId)}`)};`,
|
||||
"if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {",
|
||||
" const tag = document.createElement('style');",
|
||||
` tag.dataset.plugin = ${JSON.stringify(PLUGIN_ID)};`,
|
||||
" tag.dataset.pluginCss = tagId;",
|
||||
" tag.textContent = css;",
|
||||
" document.head.appendChild(tag);",
|
||||
"}",
|
||||
`export default ${JSON.stringify(classMap)};`,
|
||||
].join("\n");
|
||||
},
|
||||
},
|
||||
],
|
||||
outputOptions: {
|
||||
entryFileNames: "client.js",
|
||||
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(PLUGIN_ID)}, factory: (require) => {`,
|
||||
footer: "return module.exports; } });",
|
||||
intro: "var module = { exports: {} }; var exports = module.exports;",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
// 本包不用 `vp pack`:产物是 tsc 出的 node 半(dist/)+ tsdown 出的浏览器 bundle
|
||||
// (dist/web/client.js,带 __ModuleLoader__ banner 与 lightningcss CSS Modules 内联),
|
||||
// 由包内 `build` script 负责。这里只接管 lint / fmt / test。
|
||||
export default defineConfig({
|
||||
lint: {
|
||||
options: {
|
||||
typeAware: true,
|
||||
typeCheck: true,
|
||||
},
|
||||
},
|
||||
fmt: {},
|
||||
});
|
||||
@@ -115,6 +115,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
|
||||
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
|
||||
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
|
||||
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
|
||||
| Model selection | "Recommend a model for image understanding and customer support." |
|
||||
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
|
||||
@@ -166,6 +167,9 @@ bl config list
|
||||
|
||||
# Switch config profile
|
||||
bl config use --name token-plan
|
||||
|
||||
# Switch the CLI interface to Chinese
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
Config file location: `~/.bailian/config.json`
|
||||
|
||||
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
@@ -165,6 +166,9 @@ bl config list
|
||||
|
||||
# 切换配置档
|
||||
bl config use --name token-plan
|
||||
|
||||
# 将 CLI 界面切换为中文
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
配置文件位置:`~/.bailian/config.json`
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# 迭代一设计 · doc 组命令
|
||||
|
||||
> 命令:`doc upload` / `doc list` / `doc status` / `doc delete` / `doc tag` / `doc import-oss`
|
||||
> 公共约定见 [README.md](README.md)。
|
||||
|
||||
## doc upload — 上传本地文件入库(编排命令)
|
||||
|
||||
**说明**:本迭代最复杂命令。把"本地文件 → 数据中心 →(可选)导入知识库"封装为一条命令,替代构建期最高频的控制台操作(S2.2 痛点:高)。对标竞品 add-file。
|
||||
|
||||
**编排四步**:
|
||||
|
||||
| 步 | API | 输入 | 输出 |
|
||||
| ---------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| 1 申请租约 | `POST /api/v1/connector/dash/applyFileUploadLease` | `category`(类目ID) + `fileName` + `sizeBytes`(字符串!) + `contentMd5`(Base64) | `leaseId` + `param.url/method/headers` |
|
||||
| 2 OSS 上传 | `PUT {param.url}` | 文件二进制 + `param.headers`(含 `x-bailian-extra`、`Content-Type`) | HTTP 200 |
|
||||
| 3 注册文件 | `POST /api/v1/connector/dash/addFile` | `leaseId` + `category` + `parser: "AUTO_SELECT"` + `tags?` | `fileId` |
|
||||
| 4 导入(可选,传 `--index-id` 时) | `POST /api/v1/indices/rag/index/job/create` | `indexId` + `dataSource: { sourceType: "DATA_CENTER_FILE", fileIds }` | `ingestionId` |
|
||||
|
||||
坑位(实现注释必须标注):
|
||||
|
||||
- `sizeBytes` 必须字符串;`contentMd5` = `crypto.createHash("md5").update(buf).digest("base64")`
|
||||
- 租约/注册的类目参数名是 `category`,不是 `categoryId`
|
||||
- 第 4 步 body 是嵌套 `dataSource: { sourceType, fileIds }`(实测;公开文档的平铺 `documentIds` 会报 `Index.InvalidParameter`)
|
||||
- **第 4 步必须显式传 `sourceType`,不传会导入整个数据中心(API 文档明示的默认行为)**
|
||||
- 步骤 2 走 OSS 域名不走 DashScope 网关,用原生 fetch 而非 ctx.client(无 Bearer 头);失败归类 NETWORK
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 类型 | 必填 | 说明 |
|
||||
| -------------------------------------------------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--file <path>` | array | 是 | 本地文件路径,可重复;扩展名与大小按产品支持范围预校验(见下方格式白名单) |
|
||||
| `--index-id <id>` | string | 否 | 注册后立即导入该知识库(触发第 4 步,多文件合并为一个 job) |
|
||||
| `--category-id <id>` | string | 否 | 目标类目;缺省自动解析默认类目(listCategory 取 `isDefault: true`),解析失败报 GENERAL + hint 显式传 `--category-id` |
|
||||
| `--tag <text>` | array | 否 | addFile tags,可重复 |
|
||||
| `--wait` / `--poll-interval <s>` / `--timeout <s>` | — | 否 | 与 `--index-id` 联用,轮询 job status 至终态 |
|
||||
|
||||
**validate**:`--wait` 无 `--index-id` → USAGE;文件不存在/不可读 → GENERAL + errno hint(沿用错误边界规范)。
|
||||
|
||||
**格式白名单与大小预校验**(依据 data/documents.md「支持的格式」,读文件前拦截,避免白传 OSS):
|
||||
|
||||
| 类型 | 扩展名 | 硬限(超限 USAGE) |
|
||||
| ------ | -------------------------- | ----------------------------------------------------- |
|
||||
| 文档 | .doc .docx .ppt .pptx .pdf | 150 MB |
|
||||
| 表格 | .xls .xlsx | 10 MB(产品为“建议值”,超限降级为 stderr 警告不拦截) |
|
||||
| 图片 | .png .jpg .jpeg .bmp .gif | 20 MB(尺寸约束不做客户端校验,留服务端) |
|
||||
| 纯文本 | .md .txt .html | 10 MB(同表格,警告不拦截) |
|
||||
|
||||
- 扩展名不在白名单 → USAGE,错误信息列出支持格式;白名单常量独立导出便于后续随产品更新
|
||||
- 开放问题:create-kb.md 提及 .csv 但 documents.md 格式表未列——文档口径不一致,实现前向产品确认;确认前 .csv 暂入白名单(服务端拒绝会透传)
|
||||
|
||||
**输出**:
|
||||
|
||||
- text:每文件一行 `<fileName> <fileId> registered`;有导入时追加 `job: <ingestionId>`;--wait 结束追加终态
|
||||
- json:`{ files: [{path, fileId}], index_id?, ingestion_id?, final_status? }`(编排命令无单一响应可透传,输出自定义稳定结构)
|
||||
- quiet:仅 fileId 每行一个
|
||||
|
||||
**实现方案**:
|
||||
|
||||
- 文件 `doc-upload.ts`;多文件串行执行 1-3 步(首版不并发,避免 OSS 限流复杂化),全部注册成功后合并执行第 4 步
|
||||
- 部分失败语义:任一文件步骤 1-3 失败即中止并报错,已成功的 fileId 列入错误 hint(幂等重传代价低)
|
||||
- 默认类目解析结果进程内缓存(多文件只查一次)
|
||||
- dry-run:不读文件内容(size/md5 以占位符表示),输出四步编排计划 `{ steps: [{step, endpoint, request}] }`
|
||||
|
||||
**测试方案**:
|
||||
|
||||
- help / 缺 `--file` exitCode 2 / `--wait` 无 `--index-id` exitCode 2
|
||||
- 文件不存在 → 非零退出 + ENOENT hint;`.zip` 扩展名 → USAGE 列出支持格式
|
||||
- dry-run:断言 steps 长度(带/不带 --index-id 为 4/3)、lease 请求 `sizeBytes` 为字符串类型、job 请求含 `sourceType: "DATA_CENTER_FILE"`
|
||||
- live:上传 1KB 临时 md 文件 → 断言 fileId 前缀 `file_` → afterAll doc delete + 数据中心 deleteFile 清理
|
||||
|
||||
## doc list — 查询知识库文档列表
|
||||
|
||||
**说明**:列出库内文档及解析/索引状态,含 FAILED 发现(S2.3 / S5.2)。
|
||||
|
||||
**API**:`GET /api/v1/indices/rag/index/files`,query string:`index_id` + `page_num`(注意本接口是 page_num)+ `page_size`(默认 10,最大 100)。
|
||||
|
||||
**Flags**:`--index-id` 必填;`--page-number` / `--page-size`。
|
||||
|
||||
**输出**:
|
||||
|
||||
- text:每行 `doc_id status doc_name doc_type size`;status=FAILED 行红色高亮(TTY);尾行 `total: N`
|
||||
- json 透传;quiet 仅 doc_id
|
||||
|
||||
**实现/测试**:单 API 直映射(`doc-list.ts`);dry-run 断言 query 参数名为 `page_num`;live 断言 rows 结构与 doc_id 前缀。
|
||||
|
||||
## doc status — 查询导入任务状态
|
||||
|
||||
**说明**:查导入任务进度,`--wait` 阻塞至终态供脚本串行(S2.3 痛点:高,L3 验收:FAILED 时非零 exit code)。
|
||||
|
||||
**API**:`GET /api/v1/indices/rag/index_job/status`,query string:`index_id` + `job_id`(**双必填,仅传其一服务端返回 SystemError,客户端前置双校验拦截**)+ 分页参数。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| -------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | 是 | 知识库 ID |
|
||||
| `--job-id <id>` | 是 | 导入任务 ID(kb create / doc upload 返回的 ingestionId;也见 doc list 的 ingestion_id) |
|
||||
| `--page-number` / `--page-size` | 否 | 任务含大量文档时分页 |
|
||||
| `--wait` / `--poll-interval <s>`(默认 5) / `--timeout <s>`(默认 600) | 否 | 轮询至终态 |
|
||||
|
||||
**行为**:
|
||||
|
||||
- 终态 FINISH → exit 0;FAILED → `BailianError(GENERAL)` 透传服务端 message(含文档级失败明细摘要),exit 1
|
||||
- `--wait` 超时 → TIMEOUT(5)
|
||||
- 已知行为:库无进行中任务时接口可能返回 SystemError——hint 引导 "check ingestion_id via doc list"
|
||||
|
||||
**输出**:text 顶部任务总状态 + 文档级状态列表(FAILED 高亮);json 透传。
|
||||
|
||||
**测试方案**:help / 缺任一必填(两条用例)/ dry-run 断言 query 含两个 id / live:配合 upload 用例拿真实 job 轮询到 FINISH;`--wait --timeout 1` 对慢任务断言 exitCode 5(若不稳定则仅静态覆盖超时路径,live 标记 skip 原因)。
|
||||
|
||||
## doc delete — 删除文档【危险操作】
|
||||
|
||||
**说明**:从知识库删除文档及其全部切片(S5.1 内容更新循环)。
|
||||
|
||||
**API**:`POST /api/v1/indices/rag/index/delete_file`,body `{ index_id, doc_ids }`(snake_case)。响应 `data.deleted[]` 为实际删除列表。
|
||||
|
||||
**Flags**:`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`。
|
||||
|
||||
**实现方案**:`doc-delete.ts`;命令在 `risk` 对象中同时声明 `level: "high"` 和双语 `message`,由 runtime 在 `run()` 前统一确认;输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
|
||||
|
||||
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 无 `--yes` 返回 exitCode 7 + `requires_confirmation` / live 配合 upload 清理链。
|
||||
|
||||
## doc tag — 批量更新文档标签
|
||||
|
||||
**说明**:批量打标,支撑标签过滤检索(S2.4)。
|
||||
|
||||
**API**:`POST /api/v1/connector/dash/batchUpdateFileTag`。`fileInfos`(1-20 项,每项 `fileId` + `tags`,单标签 ≤32 字符、单文件 ≤100 个、总长 ≤700)+ `updateMode`(OVERWRITE/APPEND)。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| --------------- | ---- | ------------------------------------------------------------------------- |
|
||||
| `--doc-id <id>` | 是 | 可重复,1-20 个(客户端预校验),映射 fileInfos[].fileId |
|
||||
| `--tag <text>` | 是 | 可重复,应用到所有 `--doc-id`(首版同一组标签批量打;异构标签用多次调用) |
|
||||
| `--mode <m>` | 否 | choices: `overwrite`/`append`,默认 `append`(追加比覆盖安全,作为缺省) |
|
||||
|
||||
**实现/测试**:`doc-tag.ts` 单 API 直映射;客户端预校验标签长度约束(USAGE 前置拦截);dry-run 断言 `updateMode: "APPEND"` 大写映射与 fileInfos 结构;live 打标后 listFile/describeFile 验证回读。
|
||||
|
||||
## doc import-oss — 从授权 OSS 批量导入
|
||||
|
||||
**说明**:从已 SLR 授权的 OSS Bucket 批量导入数据中心(大客户批量场景)。
|
||||
|
||||
**API**:`POST /api/v1/connector/dash/addFilesFromAuthorizedOss`。必填 `categoryId/categoryType/ossBucket/ossRegionId/fileDetails`(1-10 项,每项 `fileName+ossKey`)。返回 `data.fileIds`。
|
||||
|
||||
**Flags**:
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
| -------------------- | ---- | -------------------------------------------- |
|
||||
| `--bucket <name>` | 是 | 映射 ossBucket |
|
||||
| `--region <id>` | 是 | 映射 ossRegionId(如 cn-beijing) |
|
||||
| `--oss-key <key>` | 是 | 可重复,1-10 个;fileName 取 key 的 basename |
|
||||
| `--category-id <id>` | 否 | 缺省走默认类目解析(复用 upload 的解析函数) |
|
||||
| `--tag <text>` | 否 | 可重复,≤10 |
|
||||
| `--overwrite` | 否 | switch,映射 overWriteFileByOssKey |
|
||||
|
||||
固定值:`categoryType: "UNSTRUCTURED"`;`parser` 不暴露(默认 AUTO_SELECT,审慎原则——DASH_QWEN_VL_PARSER 等需配 parserConfig,使用方式未验证)。
|
||||
|
||||
**错误边界**:SLR 未授权的服务端权限错误原样透传,hint 附 RAM 控制台确认 `AliyunServiceRoleForBailian` 的指引(该指引来自 API 文档 Note,属可权威解释范围)。
|
||||
|
||||
**实现/测试**:`doc-import-oss.ts` 单 API 直映射;dry-run 断言 fileDetails 结构与 fileName 派生逻辑;live 依赖 OSS 授权环境,gating 追加 `BAILIAN_E2E_OSS_BUCKET` 环境变量,无则 skip。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.15.1",
|
||||
"version": "1.23.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
|
||||
// succeeded, so a backup deletion failure must not fail the pre-download
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed swap as failed */
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -214,7 +220,11 @@ async function main() {
|
||||
}
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* cleanup must not mask the original error */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,16 +29,56 @@ import {
|
||||
memoryUpdate,
|
||||
memoryDelete,
|
||||
memoryProfileCreate,
|
||||
memoryProfileList,
|
||||
memoryProfileShow,
|
||||
memoryProfileUpdate,
|
||||
memoryProfileDelete,
|
||||
memoryProfileGet,
|
||||
knowledgeRetrieve,
|
||||
knowledgeSearch,
|
||||
knowledgeChat,
|
||||
knowledgeKbList,
|
||||
knowledgeKbInfo,
|
||||
knowledgeDocList,
|
||||
knowledgeDocStatus,
|
||||
knowledgeDocUpload,
|
||||
knowledgeKbCreate,
|
||||
knowledgeKbUpdate,
|
||||
knowledgeKbDelete,
|
||||
knowledgeDocDelete,
|
||||
knowledgeDocTag,
|
||||
knowledgeServiceList,
|
||||
knowledgeServiceGet,
|
||||
knowledgeServiceCreate,
|
||||
knowledgeServiceUpdate,
|
||||
knowledgeServiceDeploy,
|
||||
knowledgeServiceDelete,
|
||||
knowledgeServiceCopy,
|
||||
knowledgeChunkAdd,
|
||||
knowledgeChunkList,
|
||||
knowledgeChunkUpdate,
|
||||
knowledgeChunkDelete,
|
||||
knowledgeKbStats,
|
||||
knowledgeCategoryList,
|
||||
knowledgeCategoryAdd,
|
||||
knowledgeCategoryDelete,
|
||||
knowledgeFileList,
|
||||
knowledgeFileGet,
|
||||
knowledgeFileDelete,
|
||||
knowledgeCollectionCreate,
|
||||
knowledgeCollectionGet,
|
||||
knowledgeDocImportOss,
|
||||
mcpCall,
|
||||
mcpList,
|
||||
mcpTools,
|
||||
searchWeb,
|
||||
speechSynthesize,
|
||||
speechRecognize,
|
||||
speechVocabularyCreate,
|
||||
speechVocabularyList,
|
||||
speechVocabularyGet,
|
||||
speechVocabularyUpdate,
|
||||
speechVocabularyDelete,
|
||||
fileUpload,
|
||||
consoleCall,
|
||||
usageFree,
|
||||
@@ -54,6 +94,7 @@ import {
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaUpdate,
|
||||
quotaDelete,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
@@ -67,6 +108,7 @@ import {
|
||||
finetuneTextCreate,
|
||||
finetuneAudioCreate,
|
||||
finetuneImageCreate,
|
||||
finetuneVideoCreate,
|
||||
finetuneList,
|
||||
finetuneGet,
|
||||
finetuneCancel,
|
||||
@@ -76,6 +118,7 @@ import {
|
||||
finetuneExport,
|
||||
finetuneWatch,
|
||||
finetuneCapability,
|
||||
finetunePrice,
|
||||
deployTextCreate,
|
||||
deployAudioCreate,
|
||||
deployImageCreate,
|
||||
@@ -85,6 +128,8 @@ import {
|
||||
deployScale,
|
||||
deployUpdate,
|
||||
deployDelete,
|
||||
deployPause,
|
||||
deployResume,
|
||||
tokenPlanListSeats,
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
@@ -104,6 +149,18 @@ import {
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
managedAgentPlayground,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectWorkbench,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
managedAgentStateRm,
|
||||
@@ -116,6 +173,48 @@ import {
|
||||
managedAgentSessionSend,
|
||||
managedAgentSessionEvents,
|
||||
managedAgentSkillList,
|
||||
managedAgentAgentCreate,
|
||||
managedAgentEnvironmentCreate,
|
||||
managedAgentSkillCreate,
|
||||
managedAgentVaultCreate,
|
||||
managedAgentVaultCredentialCreate,
|
||||
managedAgentDeploymentCreate,
|
||||
managedAgentAgentList,
|
||||
managedAgentAgentGet,
|
||||
managedAgentAgentSearch,
|
||||
managedAgentAgentVersions,
|
||||
managedAgentEnvironmentList,
|
||||
managedAgentEnvironmentGet,
|
||||
managedAgentEnvironmentSearch,
|
||||
managedAgentSkillGet,
|
||||
managedAgentSkillSearch,
|
||||
managedAgentSkillVersions,
|
||||
managedAgentSkillDownload,
|
||||
managedAgentVaultList,
|
||||
managedAgentVaultGet,
|
||||
managedAgentVaultSearch,
|
||||
managedAgentDeploymentList,
|
||||
managedAgentDeploymentGet,
|
||||
managedAgentDeploymentSearch,
|
||||
managedAgentDeploymentRunsList,
|
||||
managedAgentDeploymentRunsGet,
|
||||
managedAgentDeploymentRun,
|
||||
managedAgentDeploymentPause,
|
||||
managedAgentDeploymentUnpause,
|
||||
managedAgentSessionSearch,
|
||||
managedAgentSessionUpdate,
|
||||
managedAgentSessionArchive,
|
||||
managedAgentSessionEventList,
|
||||
managedAgentSessionEventSend,
|
||||
managedAgentSessionEventStream,
|
||||
managedAgentSessionDebug,
|
||||
managedAgentSessionExport,
|
||||
managedAgentFileUpload,
|
||||
managedAgentFileList,
|
||||
managedAgentFileGet,
|
||||
managedAgentFileSearch,
|
||||
managedAgentFileDownload,
|
||||
managedAgentFileDelete,
|
||||
} from "bailian-cli-commands";
|
||||
|
||||
// Full bailian-cli product: every command, exposed under the `bl` binary.
|
||||
@@ -153,16 +252,58 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"memory update": memoryUpdate,
|
||||
"memory delete": memoryDelete,
|
||||
"memory profile create": memoryProfileCreate,
|
||||
"memory profile list": memoryProfileList,
|
||||
"memory profile show": memoryProfileShow,
|
||||
"memory profile update": memoryProfileUpdate,
|
||||
"memory profile delete": memoryProfileDelete,
|
||||
"memory profile get": memoryProfileGet,
|
||||
"knowledge retrieve": knowledgeRetrieve,
|
||||
"knowledge search": knowledgeSearch,
|
||||
"knowledge chat": knowledgeChat,
|
||||
"knowledge list": knowledgeKbList,
|
||||
"knowledge info": knowledgeKbInfo,
|
||||
"knowledge create": knowledgeKbCreate,
|
||||
"knowledge update": knowledgeKbUpdate,
|
||||
"knowledge delete": knowledgeKbDelete,
|
||||
"knowledge doc list": knowledgeDocList,
|
||||
"knowledge doc status": knowledgeDocStatus,
|
||||
"knowledge doc upload": knowledgeDocUpload,
|
||||
"knowledge doc delete": knowledgeDocDelete,
|
||||
"knowledge doc tag": knowledgeDocTag,
|
||||
"knowledge service list": knowledgeServiceList,
|
||||
"knowledge service get": knowledgeServiceGet,
|
||||
"knowledge service create": knowledgeServiceCreate,
|
||||
"knowledge service update": knowledgeServiceUpdate,
|
||||
"knowledge service deploy": knowledgeServiceDeploy,
|
||||
"knowledge service delete": knowledgeServiceDelete,
|
||||
"knowledge service copy": knowledgeServiceCopy,
|
||||
"knowledge chunk add": knowledgeChunkAdd,
|
||||
"knowledge chunk list": knowledgeChunkList,
|
||||
"knowledge chunk update": knowledgeChunkUpdate,
|
||||
"knowledge chunk delete": knowledgeChunkDelete,
|
||||
"knowledge stats": knowledgeKbStats,
|
||||
"knowledge doc import-oss": knowledgeDocImportOss,
|
||||
// Data-center commands live under knowledge (no separate connector namespace);
|
||||
// the user-facing term for connector is "collection".
|
||||
"knowledge collection create": knowledgeCollectionCreate,
|
||||
"knowledge collection get": knowledgeCollectionGet,
|
||||
"knowledge category list": knowledgeCategoryList,
|
||||
"knowledge category add": knowledgeCategoryAdd,
|
||||
"knowledge category delete": knowledgeCategoryDelete,
|
||||
"knowledge file list": knowledgeFileList,
|
||||
"knowledge file get": knowledgeFileGet,
|
||||
"knowledge file delete": knowledgeFileDelete,
|
||||
"mcp call": mcpCall,
|
||||
"mcp list": mcpList,
|
||||
"mcp tools": mcpTools,
|
||||
"search web": searchWeb,
|
||||
"speech synthesize": speechSynthesize,
|
||||
"speech recognize": speechRecognize,
|
||||
"speech vocabulary create": speechVocabularyCreate,
|
||||
"speech vocabulary list": speechVocabularyList,
|
||||
"speech vocabulary get": speechVocabularyGet,
|
||||
"speech vocabulary update": speechVocabularyUpdate,
|
||||
"speech vocabulary delete": speechVocabularyDelete,
|
||||
"file upload": fileUpload,
|
||||
"console call": consoleCall,
|
||||
"usage free": usageFree,
|
||||
@@ -178,6 +319,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"workspace list": workspaceList,
|
||||
"quota list": quotaList,
|
||||
"quota update": quotaUpdate,
|
||||
"quota delete": quotaDelete,
|
||||
"quota history": quotaHistory,
|
||||
"quota check": quotaCheck,
|
||||
"permission list": permissionList,
|
||||
@@ -191,6 +333,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"finetune text create": finetuneTextCreate,
|
||||
"finetune audio create": finetuneAudioCreate,
|
||||
"finetune image create": finetuneImageCreate,
|
||||
"finetune video create": finetuneVideoCreate,
|
||||
"finetune list": finetuneList,
|
||||
"finetune get": finetuneGet,
|
||||
"finetune cancel": finetuneCancel,
|
||||
@@ -200,6 +343,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"finetune export": finetuneExport,
|
||||
"finetune watch": finetuneWatch,
|
||||
"finetune capability": finetuneCapability,
|
||||
"finetune price": finetunePrice,
|
||||
"deploy text create": deployTextCreate,
|
||||
"deploy audio create": deployAudioCreate,
|
||||
"deploy image create": deployImageCreate,
|
||||
@@ -209,6 +353,8 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"deploy scale": deployScale,
|
||||
"deploy update": deployUpdate,
|
||||
"deploy delete": deployDelete,
|
||||
"deploy pause": deployPause,
|
||||
"deploy resume": deployResume,
|
||||
"token-plan list-seats": tokenPlanListSeats,
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
@@ -228,6 +374,18 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent plan": managedAgentPlan,
|
||||
"managed-agent apply": managedAgentApply,
|
||||
"managed-agent destroy": managedAgentDestroy,
|
||||
"managed-agent playground": managedAgentPlayground,
|
||||
"managed-agent project init": managedAgentProjectInit,
|
||||
"managed-agent project validate": managedAgentProjectValidate,
|
||||
"managed-agent project build": managedAgentProjectBuild,
|
||||
"managed-agent project publish": managedAgentProjectPublish,
|
||||
"managed-agent project workbench": managedAgentProjectWorkbench,
|
||||
"managed-agent project version enable": managedAgentProjectVersionEnable,
|
||||
"managed-agent project version disable": managedAgentProjectVersionDisable,
|
||||
"managed-agent project version status": managedAgentProjectVersionStatus,
|
||||
"managed-agent project version list": managedAgentProjectVersionList,
|
||||
"managed-agent project version preview": managedAgentProjectVersionPreview,
|
||||
"managed-agent project version restore": managedAgentProjectVersionRestore,
|
||||
"managed-agent state list": managedAgentStateList,
|
||||
"managed-agent state show": managedAgentStateShow,
|
||||
"managed-agent state rm": managedAgentStateRm,
|
||||
@@ -240,6 +398,49 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent session send": managedAgentSessionSend,
|
||||
"managed-agent session events": managedAgentSessionEvents,
|
||||
"managed-agent skill-list": managedAgentSkillList,
|
||||
"managed-agent agent create": managedAgentAgentCreate,
|
||||
"managed-agent agent list": managedAgentAgentList,
|
||||
"managed-agent agent get": managedAgentAgentGet,
|
||||
"managed-agent agent search": managedAgentAgentSearch,
|
||||
"managed-agent agent versions": managedAgentAgentVersions,
|
||||
"managed-agent environment create": managedAgentEnvironmentCreate,
|
||||
"managed-agent environment list": managedAgentEnvironmentList,
|
||||
"managed-agent environment get": managedAgentEnvironmentGet,
|
||||
"managed-agent environment search": managedAgentEnvironmentSearch,
|
||||
"managed-agent skill create": managedAgentSkillCreate,
|
||||
"managed-agent skill list": managedAgentSkillList,
|
||||
"managed-agent skill get": managedAgentSkillGet,
|
||||
"managed-agent skill search": managedAgentSkillSearch,
|
||||
"managed-agent skill versions": managedAgentSkillVersions,
|
||||
"managed-agent skill download": managedAgentSkillDownload,
|
||||
"managed-agent vault create": managedAgentVaultCreate,
|
||||
"managed-agent vault credential create": managedAgentVaultCredentialCreate,
|
||||
"managed-agent vault list": managedAgentVaultList,
|
||||
"managed-agent vault get": managedAgentVaultGet,
|
||||
"managed-agent vault search": managedAgentVaultSearch,
|
||||
"managed-agent deployment create": managedAgentDeploymentCreate,
|
||||
"managed-agent deployment list": managedAgentDeploymentList,
|
||||
"managed-agent deployment get": managedAgentDeploymentGet,
|
||||
"managed-agent deployment search": managedAgentDeploymentSearch,
|
||||
"managed-agent deployment runs list": managedAgentDeploymentRunsList,
|
||||
"managed-agent deployment runs get": managedAgentDeploymentRunsGet,
|
||||
"managed-agent deployment run": managedAgentDeploymentRun,
|
||||
"managed-agent deployment pause": managedAgentDeploymentPause,
|
||||
"managed-agent deployment unpause": managedAgentDeploymentUnpause,
|
||||
"managed-agent session search": managedAgentSessionSearch,
|
||||
"managed-agent session update": managedAgentSessionUpdate,
|
||||
"managed-agent session archive": managedAgentSessionArchive,
|
||||
"managed-agent session event list": managedAgentSessionEventList,
|
||||
"managed-agent session event send": managedAgentSessionEventSend,
|
||||
"managed-agent session event stream": managedAgentSessionEventStream,
|
||||
"managed-agent session debug": managedAgentSessionDebug,
|
||||
"managed-agent session export": managedAgentSessionExport,
|
||||
"managed-agent file upload": managedAgentFileUpload,
|
||||
"managed-agent file list": managedAgentFileList,
|
||||
"managed-agent file get": managedAgentFileGet,
|
||||
"managed-agent file search": managedAgentFileSearch,
|
||||
"managed-agent file download": managedAgentFileDownload,
|
||||
"managed-agent file delete": managedAgentFileDelete,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,11 @@ import { commandPackPolicy } from "./command-pack-policy.ts";
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
const quickStartTasks = [
|
||||
"Help me generate a set of Amazon e-commerce main images for baseball caps (white background + lifestyle shots + model wear shots)",
|
||||
"Help me generate a 3-minute humorous crosstalk audio clip",
|
||||
"Help me generate a Little Red Riding Hood picture-book PDF (with illustrations)",
|
||||
"Help me analyze this video and write a Xiaohongshu-style post",
|
||||
"帮我创建一个能够生成短片分镜和视频的 Managed Agent。\n Help me create a Managed Agent that can generate short-film storyboards and videos.",
|
||||
"生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。\n Generate an image of a cat in a spacesuit standing on Mars, then turn it into a video.",
|
||||
"查看最近的模型用量、免费额度和限流情况。\n Check my recent model usage, free quota, and rate limits.",
|
||||
"推荐一个适合图片理解和智能客服的模型。\n Recommend a model suitable for image understanding and intelligent customer service.",
|
||||
"介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。\n Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.",
|
||||
] as const;
|
||||
|
||||
void createCli(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -9,7 +9,19 @@ const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures",
|
||||
let configDir: string;
|
||||
|
||||
function env(): NodeJS.ProcessEnv {
|
||||
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
|
||||
return {
|
||||
BAILIAN_CONFIG_DIR: configDir,
|
||||
DO_NOT_TRACK: "1",
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
};
|
||||
}
|
||||
|
||||
function parseStderrJsonDiagnostics(stderr: string): unknown[] {
|
||||
return stderr
|
||||
.trim()
|
||||
.split(/\n\s*\n/)
|
||||
.map((diagnostic) => JSON.parse(diagnostic) as unknown);
|
||||
}
|
||||
|
||||
describe("e2e: Command Pack", () => {
|
||||
@@ -53,6 +65,7 @@ describe("e2e: Command Pack", () => {
|
||||
expect(linkedJson.linked.commands).toEqual([
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
@@ -71,6 +84,68 @@ describe("e2e: Command Pack", () => {
|
||||
expect(executed.exitCode, executed.stderr).toBe(0);
|
||||
expect(executed.stdout).toContain("command-pack:hello");
|
||||
|
||||
await writeFile(
|
||||
join(configDir, "config.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
base_url: "https://default.example.com",
|
||||
active_config: "company-plan",
|
||||
"company-plan": {
|
||||
api_key: "sk-plan",
|
||||
base_url: "https://plan.example.com",
|
||||
api_key_capabilities: [],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
const fallbackFailure = await runCli(["agent", "credential", "--output", "json"], env());
|
||||
expect(fallbackFailure.exitCode).toBe(3);
|
||||
expect(fallbackFailure.stderr).toMatch(/^\{\n {2}"warning": \{/);
|
||||
expect(fallbackFailure.stderr).toContain("\n\n{\n");
|
||||
expect(parseStderrJsonDiagnostics(fallbackFailure.stderr)).toEqual([
|
||||
{
|
||||
warning: {
|
||||
code: "PROFILE_API_KEY_FALLBACK",
|
||||
message:
|
||||
'Profile "company-plan" does not support command "agent credential"; API Key settings will be read from Profile "default" for this run.',
|
||||
profile: "company-plan",
|
||||
command_path: ["agent", "credential"],
|
||||
fallback_profile: "default",
|
||||
credential_fields: ["api_key", "base_url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
error: expect.objectContaining({
|
||||
code: 3,
|
||||
message: "No API key found.",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
await writeFile(
|
||||
join(configDir, "config.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
api_key: "sk-default",
|
||||
base_url: "https://default.example.com",
|
||||
active_config: "company-plan",
|
||||
"company-plan": {
|
||||
api_key: "sk-plan",
|
||||
base_url: "https://plan.example.com",
|
||||
api_key_capabilities: [],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
const fallbackCredential = await runCli(["agent", "credential"], env());
|
||||
expect(fallbackCredential.exitCode, fallbackCredential.stderr).toBe(0);
|
||||
expect(fallbackCredential.stderr).toContain('command "agent credential"');
|
||||
expect(fallbackCredential.stdout).toContain("credential-base-url:https://default.example.com");
|
||||
|
||||
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
|
||||
expect(credential.exitCode, credential.stderr).toBe(0);
|
||||
expect(credential.stdout).toContain("credential-source:flag");
|
||||
@@ -96,6 +171,34 @@ describe("e2e: Command Pack", () => {
|
||||
expect(failed.stderr).toContain("Use agent fail only in tests.");
|
||||
});
|
||||
|
||||
test("high-risk 命令由 runtime 统一确认并支持安全 dry-run", async () => {
|
||||
const dangerousHelp = await runCli(["agent", "dangerous", "--help"], env());
|
||||
expect(dangerousHelp.exitCode, dangerousHelp.stderr).toBe(0);
|
||||
expect(dangerousHelp.stderr).toContain("--yes");
|
||||
|
||||
const unconfirmed = await runCli(["agent", "dangerous", "--output", "json"], env());
|
||||
expect(unconfirmed.exitCode).toBe(7);
|
||||
expect(JSON.parse(unconfirmed.stderr)).toMatchObject({
|
||||
error: { code: 7, type: "requires_confirmation" },
|
||||
});
|
||||
|
||||
const confirmed = await runCli(["agent", "dangerous", "--yes", "--output", "json"], env());
|
||||
expect(confirmed.exitCode, confirmed.stderr).toBe(0);
|
||||
expect(parseStdoutJson(confirmed.stdout)).toEqual({
|
||||
executed: true,
|
||||
dry_run: false,
|
||||
command_flags: [],
|
||||
});
|
||||
|
||||
const preview = await runCli(["agent", "dangerous", "--dry-run", "--output", "json"], env());
|
||||
expect(preview.exitCode, preview.stderr).toBe(0);
|
||||
expect(parseStdoutJson(preview.stdout)).toEqual({
|
||||
executed: false,
|
||||
dry_run: true,
|
||||
command_flags: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("plugin list 输出加载状态", async () => {
|
||||
const result = await runCli(["plugin", "list", "--output", "json"], env());
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
@@ -109,6 +212,7 @@ describe("e2e: Command Pack", () => {
|
||||
commands: [
|
||||
"agent credential",
|
||||
"agent credential-denied",
|
||||
"agent dangerous",
|
||||
"agent fail",
|
||||
"agent output",
|
||||
"agent ping",
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runCli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "bl");
|
||||
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
|
||||
|
||||
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
|
||||
|
||||
function runCliSmoke(args: string[]) {
|
||||
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
|
||||
}
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Global Flags:/);
|
||||
expect(stderr).toMatch(/--console-region <region>/);
|
||||
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: bl ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+28
-2
@@ -1,5 +1,8 @@
|
||||
const ping = {
|
||||
description: "Ping the Command Pack fixture",
|
||||
description: {
|
||||
"en-US": "Ping the Command Pack fixture",
|
||||
"zh-CN": "调用 Command Pack 测试命令",
|
||||
},
|
||||
auth: "none",
|
||||
flags: {
|
||||
message: {
|
||||
@@ -16,12 +19,34 @@ const ping = {
|
||||
},
|
||||
};
|
||||
|
||||
const dangerous = {
|
||||
description: "Exercise runtime confirmation for a high-risk Command Pack command",
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This fixture represents a high-risk operation.",
|
||||
"zh-CN": "该测试命令代表高风险操作。",
|
||||
},
|
||||
},
|
||||
async run(ctx) {
|
||||
const dryRun = ctx.settings.dryRun;
|
||||
ctx.output.result({
|
||||
executed: !dryRun,
|
||||
dry_run: dryRun,
|
||||
command_flags: Object.keys(ctx.flags),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const credential = {
|
||||
description: "Read an API key through the Command Pack host adapter",
|
||||
auth: "apiKey",
|
||||
async run(ctx) {
|
||||
const apiKey = ctx.credentials.apiKey();
|
||||
process.stdout.write(`credential-source:${apiKey.source}\n`);
|
||||
process.stdout.write(
|
||||
`credential-source:${apiKey.source} credential-base-url:${apiKey.baseUrl}\n`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,6 +77,7 @@ const fail = {
|
||||
export default {
|
||||
"agent credential": credential,
|
||||
"agent credential-denied": credentialDenied,
|
||||
"agent dangerous": dangerous,
|
||||
"agent fail": fail,
|
||||
"agent output": output,
|
||||
"agent ping": ping,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const skillsRoot = join(repositoryRoot, "skills");
|
||||
const scopedCreateCommands = new Set([
|
||||
"bl managed-agent agent create",
|
||||
"bl managed-agent deployment create",
|
||||
"bl managed-agent environment create",
|
||||
"bl managed-agent skill create",
|
||||
"bl managed-agent vault create",
|
||||
"bl managed-agent vault credential create",
|
||||
]);
|
||||
|
||||
test("generated references distinguish runtime high-risk confirmation from scoped create execution", () => {
|
||||
let highRiskCommandCount = 0;
|
||||
const seenScopedCreateCommands = new Set<string>();
|
||||
|
||||
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!skillDirectory.isDirectory()) continue;
|
||||
const referenceDirectory = join(skillsRoot, skillDirectory.name, "reference");
|
||||
|
||||
let referenceFiles: string[];
|
||||
try {
|
||||
referenceFiles = readdirSync(referenceDirectory).filter(
|
||||
(fileName) => fileName.endsWith(".md") && fileName !== "index.md",
|
||||
);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const referenceFile of referenceFiles) {
|
||||
const markdown = readFileSync(join(referenceDirectory, referenceFile), "utf8");
|
||||
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
|
||||
|
||||
for (const commandSection of commandSections) {
|
||||
const commandName = commandSection.match(/^### `([^`]+)`/m)?.[1];
|
||||
const hasConfirmationFlag = commandSection.includes("`--yes`");
|
||||
const hasHighRiskMetadata = /\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/.test(commandSection);
|
||||
|
||||
if (!hasHighRiskMetadata) {
|
||||
if (!hasConfirmationFlag) continue;
|
||||
expect(commandName).toBeDefined();
|
||||
expect(scopedCreateCommands.has(commandName ?? "")).toBe(true);
|
||||
expect(commandSection).toMatch(/Without --yes, .*preview/i);
|
||||
seenScopedCreateCommands.add(commandName ?? "");
|
||||
continue;
|
||||
}
|
||||
|
||||
highRiskCommandCount += 1;
|
||||
expect(hasConfirmationFlag).toBe(true);
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
|
||||
expect(commandSection).toMatch(/type=.*requires_confirmation/);
|
||||
const agentSafetyLine = commandSection
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("> **Agent safety:**"));
|
||||
expect(agentSafetyLine).toBeDefined();
|
||||
expect(agentSafetyLine).toMatch(/never add `--yes` automatically/i);
|
||||
expect(agentSafetyLine).toMatch(/explicit user confirmation/i);
|
||||
expect(agentSafetyLine).not.toContain("`--dry-run`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(highRiskCommandCount).toBeGreaterThan(0);
|
||||
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
|
||||
});
|
||||
@@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) {
|
||||
"video",
|
||||
"generate",
|
||||
"--model",
|
||||
"happyhorse-1.1-t2v",
|
||||
"wan3.0-video",
|
||||
"--prompt",
|
||||
"压测前置短视频:海浪与静态远景,无明显人物。",
|
||||
"--duration",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user