mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
571 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 | |||
| 5ed15d3a16 | |||
| 640dd02bc5 | |||
| 6eeb8fe0cb | |||
| d0610a61dc | |||
| 57c2d98308 | |||
| 78e6993475 | |||
| 79a0d2db9a | |||
| 8e6af6c669 | |||
| 589e3a4f14 | |||
| 50b59f8640 | |||
| 73233658bf | |||
| 188090af39 | |||
| a9ac7c0bea | |||
| acccca9e2f | |||
| a6ee267071 | |||
| b4129c6cbd | |||
| f2a3775516 | |||
| 5800222ea4 | |||
| 95708ccf32 | |||
| cdc1f10882 | |||
| bbe96d35a5 | |||
| fba98bf65e | |||
| 3aedb7b7fb | |||
| f7d32504ab | |||
| cdf94a8c89 | |||
| 7461189007 | |||
| 4ccda5f929 | |||
| 4086da572f | |||
| 8b7956d547 | |||
| 7e21573793 | |||
| ce4d66b736 | |||
| f6cf2b999a | |||
| 196a0aa506 | |||
| a9b0a752a8 | |||
| 2b7a0c742a | |||
| e5818e103c | |||
| 2fe50f59a4 | |||
| 7797940626 | |||
| 5f1c97940d | |||
| 94ccab0898 | |||
| 418dcffc53 | |||
| b895f88abb | |||
| 7eedc05b99 | |||
| f3c7b6fb10 | |||
| eb196cb4a6 | |||
| f1b6cacd7f | |||
| 9ddb8dab53 | |||
| 9133b6bdd1 | |||
| 23f1ab7fd4 | |||
| 98ba3279fa | |||
| 3b7c4cfabc | |||
| d5d9fcb50f | |||
| d5c4bd3572 | |||
| f67ca55ec6 | |||
| 6b2f49de71 | |||
| eb4f9af3e7 | |||
| 3ea2931152 | |||
| b402f3eacd | |||
| b7a4efe619 | |||
| bedd59df27 | |||
| 39a488181e | |||
| 4ec0f6828b | |||
| 4dcec7d075 | |||
| 29ce8990b9 | |||
| a770cbe787 | |||
| 30f7525d50 | |||
| daefc094ec | |||
| aa38d5c670 | |||
| cc51164c2f | |||
| 6b685964f3 | |||
| ae0c2c1213 | |||
| 01a62eb85b | |||
| 798ce596f6 | |||
| bd91e9d1c2 | |||
| e244771ee9 | |||
| 94f9dbbe9e | |||
| 8a0dd70206 | |||
| 9379da7a4c | |||
| ddcd564e61 | |||
| 0e4dd4b824 | |||
| 241de61866 | |||
| 61d9a74166 | |||
| 69eb759490 | |||
| 313966d7a9 | |||
| d74d4efcd0 | |||
| e7422bd2e5 | |||
| 2d5c49b02e | |||
| 9bd8b60c22 | |||
| 0369bd36b0 | |||
| 92a978af3c | |||
| ea7b0f016f | |||
| 9749a11d76 | |||
| 2965080cb7 | |||
| 81959145d7 | |||
| 4343fc87af | |||
| 1c76749ee5 | |||
| 1b568e8d37 | |||
| 5d1b7aac3a | |||
| e292b20d4b | |||
| 12e7a22195 | |||
| 219d8be80a | |||
| ab766d44d3 | |||
| 1d9852805f | |||
| 99a3dbae2d | |||
| 4d84af614b | |||
| 2389681ad6 | |||
| 9ae5dc924d | |||
| 946b7029c6 | |||
| d6cb075629 | |||
| b9ecd5c43b | |||
| 978f332fea | |||
| 4502424200 | |||
| 03839766bc | |||
| 5007b9b574 | |||
| 1f8b9ace7e | |||
| 8286a74fb6 | |||
| 9eb2acbb65 | |||
| 1d35326c86 | |||
| eb6c2b8e2a | |||
| ebd6226a9f | |||
| e25d3b0b8e | |||
| 0e33c70e65 | |||
| 3c64461cca | |||
| 24092b423c | |||
| f30fff9065 | |||
| a7245c0f62 | |||
| 752a79e442 | |||
| 80bdcb83f6 | |||
| d9e8601a50 | |||
| e3bb5a7fa0 | |||
| 54da9aa29a | |||
| ef463e8d5d | |||
| 6338df36be | |||
| cb6740965f | |||
| 8ee2c378f5 | |||
| 9fc6434a26 | |||
| 2dffee5b7a | |||
| 01ec13aad8 | |||
| b68ff45fb9 | |||
| 4990b27436 | |||
| 262681484b | |||
| 8488b251f7 | |||
| 43abf0aca5 | |||
| b1908fa879 | |||
| d64ba09bef | |||
| 8cdd54cf7a | |||
| 121fa1317f | |||
| 564e21d9f1 | |||
| 081d09863b | |||
| 17b13de162 | |||
| ca98d8a25d | |||
| 1e1f5306b3 | |||
| 13158856e8 | |||
| cf2592c07d | |||
| 3766b6d7ca | |||
| 1962758b0c | |||
| 026e250cd3 | |||
| 6d61afc1d5 | |||
| 7a870ec417 | |||
| 1c38c381e5 | |||
| 658763af2c | |||
| da2ddb7a55 | |||
| be3033baf9 | |||
| 8ad3e7b947 | |||
| 525412f566 | |||
| 75b056ba64 | |||
| f5a36b1787 | |||
| 9fb388b75d | |||
| 45d468838f | |||
| ed81178ad7 | |||
| 7e23ba00fb | |||
| 72955d66a7 | |||
| 389c932390 | |||
| 6870dc50a6 | |||
| 54b95ed122 | |||
| 5e2833569a | |||
| 434aac5b08 | |||
| e46053b93e | |||
| 30fe8182f4 | |||
| 65c0fe9604 | |||
| 2c53b0692b | |||
| adc89f635d | |||
| afa43a42b9 | |||
| bbf45a5961 | |||
| 3988e701e1 | |||
| 96744e3328 | |||
| 3b7993e854 | |||
| be6ddb6126 | |||
| 25ac5c9c84 | |||
| 8ef91fe395 | |||
| 5d9e22de8f | |||
| 20e3555b84 | |||
| 81fa5b567c | |||
| df987ad536 | |||
| 4c4e7afb83 | |||
| 17c52fb86f | |||
| fb0c4b81be | |||
| 634d7045c6 | |||
| 952f2277a4 | |||
| 871c667e97 | |||
| eadd92327f | |||
| 4c494207d6 | |||
| af3286dd00 | |||
| 6465c4a78a | |||
| 467756b319 | |||
| 5a58f56b06 | |||
| 7ad14a79b9 | |||
| 8211268bd8 | |||
| 7319f6d1ce | |||
| 2dce9fe093 | |||
| 9819eb6ddc | |||
| 58252911a8 | |||
| a03ee0c72c | |||
| 05860b3bdd | |||
| 63ee5aaec3 | |||
| 93c9149e45 | |||
| 6f9e006fef | |||
| e22058b0f7 | |||
| 0221e35803 | |||
| 7250de9228 | |||
| 36ebd63716 | |||
| dac254af86 | |||
| 8a0fb870f1 | |||
| 51ed69596e | |||
| 67b7fa30a7 | |||
| bd17c27023 | |||
| 87c37994f2 | |||
| ff469ce717 | |||
| 5f0966ec8d | |||
| c4f5bb09c6 | |||
| 9a13700390 | |||
| 32c497db63 | |||
| ebbd173b79 | |||
| 6bdc16597b | |||
| e736bab9c1 | |||
| 8dd786287f | |||
| 247bb82154 | |||
| 1e6165d7ff | |||
| 1bf4fec9e6 | |||
| d30fb2ae68 | |||
| 1d589c5178 | |||
| 9cad1994e7 | |||
| fac2b2d18b | |||
| 3e249279bc | |||
| a1a448c5d2 | |||
| 7b949d3d3c | |||
| f9012a6330 | |||
| 92ee845bdd | |||
| 4751145283 | |||
| 168e2b5ccb | |||
| 9fbd2e4ec6 | |||
| 4bd84e934c | |||
| 08bdc3be97 | |||
| 66a797203c | |||
| 64335a6201 | |||
| 90a44d7140 | |||
| 26a69a7c99 | |||
| e1caee99f2 | |||
| 1da3367de8 | |||
| 9e59b01326 | |||
| 7cbd61dd5c | |||
| 1c9dac24e9 | |||
| d6bd38a46a | |||
| 678f60be75 | |||
| d11b55b956 | |||
| e4e3f069e1 | |||
| 440cbfe6ae | |||
| 1adfe797bd | |||
| d04012b0a4 | |||
| 81539005cc | |||
| 4c566fd60e | |||
| 9ab5de8c2e | |||
| 853ce3caae | |||
| 3b779a708d | |||
| 6329427b4d | |||
| b4a2a1c42d | |||
| 4ca3e2de80 | |||
| 1f91fa42fa | |||
| 39f12e1a78 | |||
| a853319dd0 | |||
| bc162f4678 | |||
| 12ecac4d96 | |||
| 52f6e267e6 | |||
| c9e5913034 | |||
| ba062c1a71 | |||
| d08edf0cd8 | |||
| a03ba673be | |||
| 8196f67300 | |||
| 4b504a1a52 | |||
| 00b4d095ed | |||
| 92719a1a21 | |||
| 310e6ead33 | |||
| ece0c8dd1c | |||
| 39f92567d3 | |||
| 8b91b9f35f | |||
| 1fbafa8b1d | |||
| 052960e269 | |||
| a8f45e93af | |||
| 84805f287c | |||
| 196b2a1f51 | |||
| de9f1a3889 | |||
| 75a45e1a2a | |||
| 64ff057fe5 | |||
| 88fcda62aa | |||
| 68336d41a2 | |||
| e246cb96d4 | |||
| 569057f4d0 | |||
| f7dbafc6d2 | |||
| f45d0e3a71 | |||
| 6d588a57b4 | |||
| 52aab33a3b | |||
| cc23b4be37 | |||
| 91cd68b46e | |||
| 054d4deb26 | |||
| cbf871c8d6 | |||
| 20b67a6628 | |||
| 0956695f28 | |||
| 96fcb1892c | |||
| 84383f1c83 | |||
| fc8351f136 | |||
| de78a281c6 | |||
| 86aec0b723 | |||
| 06f932c1bd | |||
| 1c5d1a8fcd | |||
| 6388fd11e2 | |||
| eca5403e71 | |||
| 153e176c13 | |||
| 16893b3f00 | |||
| 78977dbe4e | |||
| 7dd5431a22 | |||
| 63edde3588 | |||
| e932495560 | |||
| 2ae76e2013 | |||
| f3b85914ab | |||
| 8b99617716 | |||
| b2ca78512f | |||
| 7004e58866 | |||
| 97e47f7054 | |||
| e0f3d450ae | |||
| d37f4c07eb | |||
| ac4dbb9e88 | |||
| 155c9dc883 | |||
| 0e857775fe | |||
| 1a63fcdb5c | |||
| 58ab622e11 | |||
| e1532bf35c | |||
| 6e095a6ce5 | |||
| 94adb919b1 | |||
| a0b4666940 | |||
| e8666b33ae | |||
| d646a4e780 | |||
| 91a7106bf4 | |||
| 03405d5cc1 | |||
| 1870500f97 | |||
| 3fb0c7211c | |||
| 7b77bf6a6d | |||
| 6e5ecf8923 | |||
| 301986b669 | |||
| 0d420b6e5a | |||
| 9602209113 | |||
| 19b7aacb2e | |||
| 049eecd991 | |||
| 2907ad2625 | |||
| 0f23527bfc | |||
| 8e3f8586b0 | |||
| efb5243d0d | |||
| f66889c939 | |||
| 03f0e7c5c4 | |||
| 0a301ee641 | |||
| 66402d9868 | |||
| 4525d5df6c | |||
| 749549aa28 | |||
| 7ce018cc53 | |||
| 13ade9181f | |||
| 068ec0bfd3 | |||
| d8aa89dc6c | |||
| bd4b0ad9a5 | |||
| 2debfdba6b | |||
| 8fd072bcd1 | |||
| 49095c3a8a | |||
| c18844120a | |||
| 4dfe3ac907 | |||
| b0c48bab6b | |||
| d20e037dec | |||
| 476dd3b841 | |||
| c35f2856e5 | |||
| b3b1a08baf | |||
| 468b4d710e | |||
| deab3b3841 | |||
| d31b7f83ca | |||
| 7b08b8863e | |||
| d118875772 | |||
| d2312847eb | |||
| b5f2b8b691 | |||
| acfbc58516 | |||
| e4849224c4 | |||
| 3aa08e5d79 | |||
| 4914c5258b | |||
| a20ab54406 | |||
| e6a8bf09e7 | |||
| c6426e9e94 | |||
| 03541b4fd1 | |||
| 6dd206eda9 | |||
| e2efcfda77 | |||
| a078670445 | |||
| 8fc2fc54fb | |||
| 9bf6c6d9af | |||
| 892ae300ae | |||
| 6c4f31ddb2 | |||
| 9ff8c53d53 | |||
| 7c9ad7d6ce | |||
| ae88f7a4ad | |||
| bd431d769f | |||
| df89ededc2 | |||
| 2ec2f34763 | |||
| 1f56feab24 | |||
| 2f9558c161 | |||
| 95eb07d04a | |||
| f9bf36c242 | |||
| cbd3c1232c | |||
| d2aa8cac17 | |||
| ead1bc0f5f | |||
| 4745d70587 | |||
| 91e6c6f553 | |||
| eaa6b07c7d | |||
| ca69316446 | |||
| 7a0a083b2e |
@@ -39,7 +39,7 @@ body:
|
||||
attributes:
|
||||
label: Node version
|
||||
description: "Output of node --version"
|
||||
placeholder: "v22.12.0"
|
||||
placeholder: "v18.17.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -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: "dist-tag (channel mode only, e.g. mcp/plugin/advisor)"
|
||||
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,12 +29,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
if: inputs.mode == 'stable'
|
||||
name: publish stable (${{ inputs.package }}) to npm + tag
|
||||
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
|
||||
permissions:
|
||||
contents: write # push lightweight tag to origin
|
||||
contents: write # push tag + create GitHub Release with binary assets
|
||||
id-token: write # OIDC for npm Trusted Publishing + provenance
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -55,19 +56,112 @@ jobs:
|
||||
| sudo tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- name: Ensure zip (per-platform binary archives)
|
||||
run: sudo apt-get update && sudo apt-get install -y zip
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# Binary compile uses `bun build --compile` CLI (not Bun.build API).
|
||||
# Keep this pin in sync with any local smoke tests of binary-compile.mjs.
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.2.19"
|
||||
|
||||
- name: publish-stable
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# OSS release channel runs fully in CI: upload + reconcile + manifest.json.
|
||||
# All values come from repo Settings → Secrets — no OSS defaults live in
|
||||
# code. Leave AK/SK unset to skip the OSS channel; once enabled,
|
||||
# bucket/region/prefix are required.
|
||||
BAILIAN_OSS_AK: ${{ secrets.BAILIAN_OSS_AK }}
|
||||
BAILIAN_OSS_SK: ${{ secrets.BAILIAN_OSS_SK }}
|
||||
BAILIAN_OSS_BUCKET: ${{ secrets.BAILIAN_OSS_BUCKET }}
|
||||
BAILIAN_OSS_REGION: ${{ secrets.BAILIAN_OSS_REGION }}
|
||||
BAILIAN_OSS_ENDPOINT: ${{ secrets.BAILIAN_OSS_ENDPOINT }}
|
||||
BAILIAN_RELEASE_PREFIX: ${{ secrets.BAILIAN_RELEASE_PREFIX }}
|
||||
BAILIAN_STATIC_PREFIX: ${{ secrets.BAILIAN_STATIC_PREFIX }}
|
||||
run: node tools/release/publish-stable.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }}
|
||||
|
||||
publish-channel:
|
||||
if: inputs.mode == 'channel'
|
||||
name: publish channel (${{ inputs.package }}) to npm
|
||||
if: inputs.mode == 'channel' && inputs.package != 'bailian-kb-dsh'
|
||||
name: publish channel (${{ inputs.package }}) to npm + binary
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # no tag, no Release; just publish
|
||||
contents: write # create prerelease GitHub Release with binary assets
|
||||
id-token: write # OIDC for npm Trusted Publishing + provenance
|
||||
steps:
|
||||
- name: Require channel input
|
||||
if: ${{ inputs.channel == '' }}
|
||||
run: |
|
||||
echo "::error::mode=channel requires the workflow input \"channel\" (npm dist-tag, e.g. mcp / plugin / sync-release). 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
|
||||
|
||||
- name: Ensure zip (per-platform binary archives)
|
||||
run: sudo apt-get update && sudo apt-get install -y zip
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# Binary compile uses `bun build --compile` CLI (not Bun.build API).
|
||||
# Keep this pin in sync with any local smoke tests of binary-compile.mjs.
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.2.19"
|
||||
|
||||
- name: publish-channel
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# OSS release channel — same Settings-injected values as stable.
|
||||
BAILIAN_OSS_AK: ${{ secrets.BAILIAN_OSS_AK }}
|
||||
BAILIAN_OSS_SK: ${{ secrets.BAILIAN_OSS_SK }}
|
||||
BAILIAN_OSS_BUCKET: ${{ secrets.BAILIAN_OSS_BUCKET }}
|
||||
BAILIAN_OSS_REGION: ${{ secrets.BAILIAN_OSS_REGION }}
|
||||
BAILIAN_OSS_ENDPOINT: ${{ secrets.BAILIAN_OSS_ENDPOINT }}
|
||||
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
|
||||
@@ -89,5 +183,5 @@ jobs:
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: publish-channel
|
||||
run: node tools/release/publish-channel.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }} --channel "${{ inputs.channel }}"
|
||||
- name: publish-kb-dsh
|
||||
run: node tools/release/publish-kb-dsh.mjs ${{ inputs.mode == 'channel' && format('--channel "{0}"', inputs.channel) || '' }}
|
||||
|
||||
@@ -10,9 +10,11 @@ lerna-debug.log*
|
||||
# Dependencies & build output
|
||||
node_modules
|
||||
dist
|
||||
dist-bin
|
||||
dist-ssr
|
||||
tools/generated
|
||||
.node-version
|
||||
*.tsbuildinfo
|
||||
|
||||
*.local
|
||||
|
||||
@@ -36,7 +38,9 @@ tools/generated
|
||||
.claude/settings.local.json
|
||||
.claude/scheduled_tasks.lock
|
||||
.cursor/
|
||||
.qoder/
|
||||
.qwen/
|
||||
.qoder
|
||||
.playwright-mcp/
|
||||
.pnpm-store/
|
||||
|
||||
@@ -46,3 +50,6 @@ packages/cli/scene/**/outputs/
|
||||
|
||||
# Environment variables (sensitive data)
|
||||
.env
|
||||
|
||||
# Local scratch / plan drafts (never commit)
|
||||
.scratch/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
24
|
||||
+12
-2
@@ -1,10 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Regenerate skill reference + SKILL metadata (needs bailian-cli-core dist).
|
||||
# Regenerate skill reference + SKILL metadata from source (no package build).
|
||||
pnpm run sync:skill-assets
|
||||
|
||||
# Stage generator output so it is included in this commit.
|
||||
git add skills/bailian-cli/reference skills/bailian-cli/SKILL.md
|
||||
git add \
|
||||
skills/bailian-protocol/SKILL.md \
|
||||
skills/bailian-cli/SKILL.md \
|
||||
skills/bailian-cli/reference \
|
||||
skills/bailian-gen/SKILL.md \
|
||||
skills/bailian-gen/reference \
|
||||
skills/bailian-finetune/SKILL.md \
|
||||
skills/bailian-finetune/reference \
|
||||
skills/bailian-managed-agent/SKILL.md \
|
||||
skills/bailian-managed-agent/reference \
|
||||
skills/bailian-web-search/SKILL.md
|
||||
|
||||
vp staged
|
||||
|
||||
@@ -1,82 +1,101 @@
|
||||
# bailian-cli — AI 维护指南
|
||||
|
||||
本文件是 AI agent 维护本仓库时的契约。每次进入项目首先读这里,从下方"业务场景索引"挑一条,跳到对应的详细文档,按它的清单完成改动。
|
||||
本文件是 AI agent 维护本仓库时的契约。每次进入项目先读这里,从"业务场景索引"挑一条,再进入对应 `docs/agents/*.md` 清单。
|
||||
|
||||
## 项目地图
|
||||
|
||||
monorepo 双包结构:
|
||||
monorepo 现在按"纯逻辑 → 运行时框架 → 命令库 → 产品入口"分层:
|
||||
|
||||
- `packages/cli` — `bailian-cli` 包,CLI 命令、UI、入口
|
||||
- `packages/core` — `bailian-cli-core` 包,鉴权 / HTTP / 类型,纯逻辑层
|
||||
- `packages/core` — `bailian-cli-core`,纯逻辑层:鉴权、配置、HTTP client、错误、类型、文件工具
|
||||
- `packages/runtime` — `bailian-cli-runtime`,通用 CLI 运行时:`createCli`、参数解析、registry/help、middleware、error handler、输出、pipeline、Command Pack host
|
||||
- `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)
|
||||
|
||||
### `packages/cli` 目录要点
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
packages/cli/
|
||||
├── src/
|
||||
│ ├── main.ts # 入口、鉴权分支、调用 registry
|
||||
│ ├── registry.ts # 命令树解析、动态 help(读 catalog)
|
||||
│ ├── commands/
|
||||
│ │ ├── catalog.ts # 命令总表(登记处,构建脚本也读它)
|
||||
│ │ ├── index.ts # re-export commands
|
||||
│ │ └── <group>/...ts # 各命令 defineCommand 实现
|
||||
│ ├── output/ # CLI 输出、prompt、progress
|
||||
│ └── urls.ts # 控制台/文档 URL(仅 cli)
|
||||
└── tests/e2e/
|
||||
packages/cli/src/main.ts # bl 入口,注入 binName/version/clientName/npmPackage
|
||||
packages/cli/src/commands.ts # bl 产品命令 map,tools/generate-reference.ts 也读它
|
||||
packages/cli/src/command-pack-policy.ts # bl 的 Command Pack policy
|
||||
packages/kscli/src/main.ts # kscli 入口和命令 map
|
||||
|
||||
packages/commands/src/index.ts # re-export 单个命令实现
|
||||
packages/commands/src/commands/ # defineCommand({ auth, flags, usageArgs, exampleArgs, run })
|
||||
|
||||
packages/runtime/src/create-cli.ts # createCli(commands, options)
|
||||
packages/runtime/src/registry.ts # 命令树解析 + 动态 help
|
||||
packages/runtime/src/middleware.ts # auth / telemetry / update / run command
|
||||
packages/runtime/src/command-packs/ # 通用 Command Pack 加载、校验、隔离安装目录和管理命令
|
||||
packages/runtime/src/urls.ts # 用户面控制台 URL
|
||||
|
||||
packages/core/src/types/command.ts # Command / flags / auth 类型
|
||||
packages/core/src/config/ # ConfigFile / Settings / source 解析
|
||||
packages/core/src/auth/ # apiKey / console credential 解析与落盘
|
||||
packages/core/src/client/ # HTTP client / endpoints / console gateway
|
||||
```
|
||||
|
||||
Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts` 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);与 `tools/sync-skill-metadata.ts` 一起在 **pre-commit**(`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行。
|
||||
|
||||
非代码资产:
|
||||
|
||||
- `tools/release/` — 发版自动化(CI 驱动,见 `.github/workflows/publish.yml`)
|
||||
- `tools/generate-reference.ts` — 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`
|
||||
- `tools/sync-skill-metadata.ts` — 从 `packages/cli/package.json` 同步 `skills/bailian-cli/SKILL.md` 的 `metadata.version`(与 `generate:reference` 一并由根目录 `pnpm run sync:skill-assets` 及 pre-commit 执行)
|
||||
- `README.md` / `README.zh.md` — npm 和 GitHub 主页
|
||||
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)。
|
||||
|
||||
约定:
|
||||
|
||||
- core 是纯库,不依赖 cli(详见下方通用约定)
|
||||
- 文件路径与命令路径一一对应:`commands/text/chat.ts` ↔ `bl text chat`
|
||||
- 单级命令:`commands/<name>.ts`(如 `update.ts`);两级:`commands/<group>/<action>.ts`
|
||||
- 命令登记在 **`catalog.ts`**;`bl --help` 与 `tools/generate-reference.ts` 生成的命令手册同源,见 [command-add-remove.md](docs/agents/command-add-remove.md)
|
||||
- 命令实现文件路径仍按能力放置:`packages/commands/src/commands/text/chat.ts`
|
||||
- 产品命令路径由入口 map 决定:同一个实现可暴露为 `bl knowledge retrieve` 或 `kscli retrieve`
|
||||
- `defineCommand` 只写命令元数据与逻辑: `auth`、`flags`、`usageArgs`、`exampleArgs`、`validate`、`run`
|
||||
- `usageArgs` / `exampleArgs` 不写 `bl` 或 `kscli` 前缀;runtime / reference 生成器按产品路径补前缀
|
||||
- 不再使用 `catalog.ts` 作为登记处;新增/重命名命令必须同时看命令库导出和产品入口 map
|
||||
|
||||
非代码资产:
|
||||
|
||||
- `tools/release/` — 发版自动化(CI 驱动,见 `.github/workflows/publish.yml`)
|
||||
- `tools/generate-reference.ts` — 从 `packages/cli/src/commands.ts` 按归属表生成各 `skills/<skill>/reference/`
|
||||
- `tools/sync-skill-metadata.ts` — 同步各 `skills/*/SKILL.md` 的 `metadata.version`(含 `bailian-protocol`)
|
||||
- `README.md` / `README.zh.md` — npm 和 GitHub 主页
|
||||
|
||||
## 业务场景索引
|
||||
|
||||
按当前任务从下表挑一条进入对应文档:
|
||||
|
||||
| 场景 | 何时进入 | 详见 |
|
||||
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| 命令增删改 | 增加 / 删除 / 重命名 `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) |
|
||||
| 错误文案变更 | 改 `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) |
|
||||
| 鉴权扩展 | 加 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) |
|
||||
| 发布 | channel / stable 发布到 npm(CI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
|
||||
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.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`,把清单沉淀下来。
|
||||
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增 `docs/agents/<scenario>.md`,把清单沉淀下来。
|
||||
|
||||
## 通用约定
|
||||
|
||||
下面两条与场景无关,任何改动都适用。每次完成改动后自查。
|
||||
### 1. 发布包版本号同步
|
||||
|
||||
### 1. cli 和 core 版本号同步
|
||||
源码包的 `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/cli/package.json` 和 `packages/core/package.json` 的 `version` 字段必须始终相等。一动两动。
|
||||
**例外**: `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 是纯库,cli 是 core 的 UI 层
|
||||
### 2. 分层边界
|
||||
|
||||
core 不应该知道 cli 的存在。具体表现:
|
||||
|
||||
- core 不写 stderr,不调 `process.exit`(用 `console.*` 或 `throw`)
|
||||
- core 抛的 `BailianError`,hint 字符串不出现 `bl xxx` 命令名
|
||||
- core 不写死域名 / region / 追踪参数(URL 集中在 `packages/cli/src/urls.ts`)
|
||||
- core 接收 cli 通过 `Config` 注入的 metadata(`clientName` / `clientVersion`)
|
||||
- `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 不翻译服务端错误
|
||||
|
||||
@@ -86,30 +105,37 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的
|
||||
| ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
|
||||
| 命令解析、缺 flag、参数校验 | **内部** | `BailianError(USAGE)` |
|
||||
| 文件 I/O(ENOENT/EACCES/...) | **内部** | `BailianError(GENERAL)` + errno-specific hint |
|
||||
| 本地 credentials 缺失(resolver/ensure-key/AK-SK 等) | **内部** | `BailianError(AUTH)` |
|
||||
| 本地 credentials 缺失(resolver / auth stage 等) | **内部** | `BailianError(AUTH)` |
|
||||
| `fetch` 自身失败(DNS/TCP/TLS/proxy) | **内部** | `BailianError(NETWORK)` + 读 `err.cause.code` 给 errno-hint |
|
||||
| polling 客户端超时 | **内部** | `BailianError(TIMEOUT)` |
|
||||
| HTTP 4xx/5xx、HTTP 200 + 业务错码、async task FAILED | **服务** | `BailianError(GENERAL)`,**message 原样透传**,不分类、不替换 |
|
||||
|
||||
不要扮演服务端错误的翻译官——我们没有最新的错误码体系认知,二次包装只会撒谎(详见 `docs/agents/error-hint-change.md` 中的反面 case)。
|
||||
不要扮演服务端错误的翻译官——我们没有最新的错误码体系认知,二次包装只会撒谎。
|
||||
|
||||
### 4. Console Gateway 命令必须声明 console 全局 flags
|
||||
### 4. Console Gateway 命令必须声明鉴权域
|
||||
|
||||
如果新命令使用了 `callConsoleGateway`,必须在 `options` 中添加以下三个全局 flag 的说明,以便 `--help` 中展示:
|
||||
如果命令调用 Console Gateway,`defineCommand` 必须设置 `auth: "console"`。runtime 会基于 `CONSOLE_AUTH_FLAGS` 自动在 help 中展示 `--console-region`、`--console-site`、`--console-switch-agent`、`--workspace-id`,并由 `authStage` 解析/注入 console credential。命令不要重复声明这些凭证域 flag,也不要手动从 env/config 解析 token。
|
||||
|
||||
```ts
|
||||
{ flag: "--console-region <region>", description: "Console region" },
|
||||
{ flag: "--console-site <site>", description: "Console site: domestic, international" },
|
||||
{ flag: "--console-switch-agent <uid>", description: "Switch agent UID", type: "number" },
|
||||
```
|
||||
### 5. 禁止单字母变量命名
|
||||
|
||||
这些 flag 已在 `GLOBAL_OPTIONS`(`packages/core/src/types/command.ts`)中注册,由 `loadConfig` 写入 `config.consoleRegion` / `config.consoleSite` / `config.consoleSwitchAgent`,`callConsoleGateway` 自动读取——命令无需手动提取或传递。
|
||||
所有变量、参数、回调形参必须使用有语义的命名,不允许单字母(如 `i`、`m`、`p`、`t`、`e`、`s`)。具体表现:
|
||||
|
||||
- 回调参数: `.map((m) => ...)` → `.map((model) => ...)`, `.find((t) => ...)` → `.find((template) => ...)`
|
||||
- catch 变量: `catch (e)` → `catch (error)`
|
||||
- for-of 循环: `for (const i of items)` → `for (const item of items)`
|
||||
- 临时变量: `const s = ...` → `const strategy = ...`
|
||||
|
||||
例外: 仅当作用域极小(≤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
|
||||
vp check # format + lint + type check
|
||||
vp test # unit + e2e (e2e 需 API key)
|
||||
vp test # unit + e2e (真实集成需 API key / console token)
|
||||
```
|
||||
|
||||
## 这份指南本身怎么演化
|
||||
|
||||
+450
-2
@@ -1,11 +1,459 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `bailian-cli` and `bailian-cli-core` are documented here.
|
||||
All notable changes to the `bailian-cli` packages are documented here.
|
||||
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The `bailian-cli`, `bailian-cli-core`, `bailian-cli-runtime`, and `bailian-cli-commands` packages share a single version number — they are always released together.
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The `bailian-cli`, `bailian-cli-core`, `bailian-cli-runtime`, `bailian-cli-commands`, and `knowledge-studio-cli` packages share a single version number.
|
||||
|
||||
[中文版](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
|
||||
|
||||
- **Model permission management** — `bl permission list` shows per-model inference / fine-tune / deploy grants; `bl permission grant` and `bl permission revoke` manage them, with `--all` to one-key grant inference for every model in the workspace (including future ones).
|
||||
|
||||
### Changed
|
||||
|
||||
- **`bl quota request` renamed to `bl quota update`** — set per-model QPM/TPM via `--rpm`/`--tpm` and clear custom limits with the new `--delete`; omitted fields keep their current values, and the old `quota request` path keeps working as an alias.
|
||||
- **`bl quota list` reworked** — now reads the model-limits API and shows per-model and workspace-level request/usage limits plus async queue/concurrency limits in a single table.
|
||||
- **`bl model list` no longer requires Console login** — the model catalog and `--enrich` parameter-schema endpoints are public.
|
||||
- **`bl skill init` output simplified** — per-skill status is now `success`/`failed` (previously `installed`) with an aggregate `success`/`partial`/`failed` result; the `publishedAt` and `agents` fields were removed.
|
||||
|
||||
## [1.15.0] - 2026-08-14
|
||||
|
||||
### Added
|
||||
|
||||
- **Responses API for `bl text chat`** — Use `--api responses` to call the DashScope Responses API with streaming, tool definitions, and structured JSON output; Chat Completions remains the default.
|
||||
- **Subscription plan usage views** — `bl usage token-plan` displays 5-hour and weekly quota usage, while `bl usage coding-plan` displays 5-hour, weekly, and monthly usage; both support text and JSON output.
|
||||
- **Authentication requirements in command help** — Help output now states whether a command requires an API Key, Console login, or Alibaba Cloud OpenAPI credentials.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Broader speech-recognition model support** — `bl speech recognize` now routes asynchronous file-transcription and synchronous Flash ASR models to the appropriate DashScope APIs, with clear guidance for unsupported realtime models.
|
||||
- **MCP transport compatibility** — MCP commands now fall back from Streamable HTTP to classic SSE for compatible Bailian and custom endpoints.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Binary updates now refresh installed Agent Skills after a successful CLI upgrade.
|
||||
- Fixed unavailable Token Plan quota values and missing reset times.
|
||||
- Fixed Qwen3 file-transcription result handling so waiting mode and `--out` work correctly.
|
||||
- Fixed MCP SSE chunk parsing, header timeouts, abort cleanup, and fallback status matching.
|
||||
- Network failures in JSON output now preserve the errno value in `cause.code`.
|
||||
|
||||
## [1.14.3] - 2026-08-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Free-tier quota compatibility** — `bl usage free` and `bl usage freetier` now use the current Bailian Commerce console APIs for quota queries, activation, and deactivation, with consistent asynchronous-task polling.
|
||||
|
||||
## [1.14.2] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl skill init`** — Install all first-party `bailian-*` skills into detected local AI Agents in one step.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Skill command interface** — Skill management commands now default to JSON output for Agent workflows; `bl skill add` and `bl skill update` use explicit `--all` and `--name` selectors.
|
||||
|
||||
## [1.14.1] - 2026-08-05
|
||||
|
||||
### Added
|
||||
|
||||
- **Focused Bailian Skills** — `npx skills add modelstudioai/cli --all -g` now installs dedicated skills for media generation, fine-tuning, Managed Agent, and shared execution rules, improving task routing while reducing irrelevant context.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Default image model upgraded to Qwen-Image 3.0** — image generation, image editing, pipelines, the config UI, and related documentation now default to `qwen-image-3.0` for API Key users.
|
||||
- **Broader coding-agent compatibility** — Skill installation and updates now detect more coding agents, preserve existing installation links, and automatically backfill skills into newly detected agents.
|
||||
|
||||
## [1.14.0] - 2026-08-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Standalone installation without Node.js** — binary packages are available for macOS on Apple Silicon and Intel, Linux x64, and Windows x64; npm installation remains supported.
|
||||
- **Exact-version updates** — binary and npm installations can use `bl update --to <version>` to update or switch to a specified version.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Binary self-updates** — binary installations now check and download updates through a dedicated release channel. `bl update` no longer replaces the running executable, and the next invocation automatically uses the new version.
|
||||
|
||||
## [1.13.1] - 2026-08-03
|
||||
|
||||
### Changed
|
||||
|
||||
- **Default text model upgraded to Qwen3.8-Max** — `bl text chat`, pipelines, API key validation, the config UI, and Managed Agent init templates now default to `qwen3.8-max`; Token Plan also moves from the preview model to the stable release.
|
||||
|
||||
## [1.13.0] - 2026-07-30
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl config ui` Skills / MCP / Agents / Assets inventory** — browse installed skills, MCP servers, coding agents, and generated assets in the local Web UI with click-to-open detail drawers:
|
||||
- Skills: render `SKILL.md` as Markdown (GFM tables supported), show local vs remote origin badges, and install a skill by uploading a `.zip` archive into any supported agent's skills root.
|
||||
- MCP: view and edit JSON configuration with secret masking and mask-preserving writes; create, update, and delete MCP entries across Claude Code, Qwen Code, OpenCode, Cursor, Windsurf, Gemini, Qoder Work, OpenClaw, and Claude Desktop.
|
||||
- Agents: quick-launch coding agents directly from the UI (gated on the CLI binary being on PATH).
|
||||
- Assets: categorized, time-sorted browser with preview, open-locally, and delete.
|
||||
- **Model catalog suggestion chips** — per-category model names surfaced as click-to-fill chips under each `default_*_model` field in the config UI.
|
||||
- **Profiles tile grid** — profiles displayed as a tile grid with an add-tile and a design-consistent new-profile modal.
|
||||
|
||||
### Changed
|
||||
|
||||
- Config UI layout: collapsible grouped sidebar with icons and persistent state, responsive breakpoint, wider main area, sticky view headers, and right-side drawers for editing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Symlinked skill directories are now correctly identified as an installed source.
|
||||
- Config file detection now supports environment-variable-based paths and legacy configuration schemes.
|
||||
|
||||
## [1.12.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl config agent --key` / `--region`** — run commands generated by the Model Studio web console as-is: `--key` accepts the console's encoded API key and decodes it locally (use instead of `--api-key`), and `--region` derives the Token Plan endpoint from a region name (use instead of `--base-url`).
|
||||
- **`bl config agent --context-window`** — set the context window written to the OpenClaw configuration (default 256000).
|
||||
- **`bl config agent --wire-api`** — choose the wire protocol written to the Codex configuration; `chat` is kept for legacy Codex 0.80.0 and earlier (a warning is shown).
|
||||
|
||||
### Changed
|
||||
|
||||
- `bl config agent` for Codex now writes `wire_api = "responses"` by default, matching current Codex releases that no longer accept `chat`.
|
||||
- `bl config agent` for Qwen Code now writes the `DASHSCOPE_API_KEY` environment variable instead of `BAILIAN_CLI_API_KEY`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `bl config agent` configurations now match each agent's official format: Claude Code honors `CLAUDE_CONFIG_DIR` and removes a stale `ANTHROPIC_API_KEY`; Qwen Code uses the v3 settings schema and writes credentials so a system-level `OPENAI_API_KEY` no longer takes precedence; OpenCode accepts JSONC config files (comments and trailing commas); OpenClaw registers the primary model in the model allowlist with complete cost metadata; Hermes uses the official flat `model.*` layout; Codex writes the official `env_key` with an `auth.json` fallback.
|
||||
- `bl config agent` now preserves existing user configuration when writing: it merges instead of overwriting, avoids duplicate provider entries, and keeps custom display names.
|
||||
|
||||
## [1.11.2] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- MCP tools and WebSearch now provide activation guidance and direct marketplace links when Bailian reports that the corresponding service is not activated. WebSearch also guides users with legacy SSE connections to reactivate the service using Streamable HTTP.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed text chat and API Key validation compatibility failures caused by sending unsupported `enable_thinking` values. Text chat now sends the parameter only when thinking is explicitly enabled, while validation uses a compatible model without sending it.
|
||||
|
||||
## [1.11.1] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- `bl image edit` now supports `--function` for specifying edit operations with Wanx image-edit models such as `wanx2.1-imageedit`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed image generation and editing failures and incorrect size parameters for some image models, improving compatibility with Qwen-Image, Wan/Wanx, Z-Image, and dated `wanx-v1` variants.
|
||||
|
||||
## [1.11.0] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl managed-agent`** — declaratively manage Managed Agent infrastructure through a unified CLI. The Bailian provider connects to AgentStudio, with Claude, Qoder, and Ark providers also supported:
|
||||
- `init` / `validate` / `plan` / `apply` / `destroy` — initialize and validate `agents.yaml`, preview and apply resource changes, and destroy managed resources.
|
||||
- `state list` / `state show` / `state rm` / `state import` — inspect and manage local resource state, including adopting an existing remote resource or removing it from local state without destroying it remotely.
|
||||
- `session create` / `session list` / `session get` / `session delete` / `session run` / `session send` / `session events` — manage the full session lifecycle with streaming responses and structured `--output json` output.
|
||||
- `skill-list` — browse custom and official skills; use `--source all` to return both catalogs in one call.
|
||||
|
||||
### Changed
|
||||
|
||||
- Model Base URLs are now normalized to the URL origin; paths, query parameters, and fragments supplied in the Base URL are no longer included when constructing API request paths.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The installation guide no longer recommends the removed `--non-interactive` flag and now documents explicit required arguments, `--output json`, and `NO_COLOR=1` for non-interactive environments.
|
||||
|
||||
## [1.10.1] - 2026-07-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Token Plan defaults now use the current text, image, and dedicated text-to-video, image-to-video, and reference-to-video models.
|
||||
- The Bailian CLI Skill now distinguishes Bailian-specific tasks from ordinary host-agent work more accurately and avoids repeated consent prompts within an approved workflow.
|
||||
- Published CLI packages now support Node.js 18.17 and later, lowering the previous minimum requirement from Node.js 22.12.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Token Plan now handles local images correctly for image editing, image-to-video, reference-to-video, and vision understanding without requiring a separately hosted URL.
|
||||
|
||||
## [1.10.0] - 2026-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl config agent`** — configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope in one command.
|
||||
|
||||
### Changed
|
||||
|
||||
- The Bailian CLI Skill now routes only matching Bailian and multimodal tasks to `bl`, and asks for consent before provider-neutral remote or billable calls.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Full `bl auth logout` now clears the model Base URL so later logins cannot inherit a stale custom or Token Plan endpoint.
|
||||
|
||||
## [1.9.0] - 2026-07-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Token Plan support** — log in and call supported models directly without manually configuring the endpoint.
|
||||
- **Named Config Profiles** — create, switch, and manage isolated configurations; logging in to a named Profile activates it automatically.
|
||||
- **Console Access Token automation** — generate and automatically refresh Console Access Tokens.
|
||||
- **`bl workspace init`** — initialize a Bailian workspace and activate the required services in one workflow.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved configuration safety and consistency, including secret masking and preservation of custom configuration fields.
|
||||
|
||||
## [1.8.3] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `bl text chat --messages-file -` failing on Windows by treating standard input as a `/dev/stdin` file path; piped JSON messages are now read from standard input correctly. (#103)
|
||||
|
||||
## [1.8.2] - 2026-07-15
|
||||
|
||||
### Changed
|
||||
|
||||
- `bl model list` now defaults to JSON output; pass `--output text` for the table view.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `bl model list --enrich` now returns each model's input parameter schema (predictConfig); it was previously always empty because the console gateway response envelope was not unwrapped.
|
||||
|
||||
## [1.8.1] - 2026-07-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Expanded the Command Pack allowlist to accept an additional internal command extension.
|
||||
|
||||
## [1.8.0] - 2026-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- **`bl model list`** — browse the Bailian model marketplace: list model families or show full details for a single family (`--model`), with filters for provider, capability, feature, and context-window, pagination (`--page` / `--page-size`), pricing, and `--enrich` for richer metadata.
|
||||
- **`bl usage summary`** — a unified usage view combining free-tier quota and a recent usage overview; `--days` sets the overview window (default 7).
|
||||
- **Command Pack host support** — added support for allowlisted internal command extensions.
|
||||
- **Audio & image fine-tuning** — `bl finetune audio create` (CosyVoice TTS) and `bl finetune image create` (Wan image generation) join the existing text flow. `bl finetune image create` supports `--generation-type t2i|i2i` to select text-to-image or image-to-image training.
|
||||
- **Audio & image deployment** — `bl deploy audio create` and `bl deploy image create` deploy fine-tuned TTS and image models as endpoints.
|
||||
- **Multimodal dataset validation** — `bl dataset upload` and `bl dataset validate` now accept `.zip` archives with `tts` and `image` schemas, validate referenced media files, and allow image archives up to 1 GB.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Fine-tune and deploy commands are now split by modality (BREAKING)**: `bl finetune create` → `bl finetune text create`, and `bl deploy create` → `bl deploy text create`. Update any scripts that use the old paths.
|
||||
- **Deployment option renamed (BREAKING)**: `--template-id` → `--deploy-spec` on deployment creation commands.
|
||||
- **Fine-tune status exit behavior changed (BREAKING)**: `bl finetune watch` no longer reserves exit code 3 for running jobs. Running and succeeded jobs return 0; failed and canceled jobs use normal CLI errors.
|
||||
- `bl deploy audio create` now defaults to `--plan mu` (model-unit billing, per the CosyVoice deployment contract); text and image continue to default to `lora`.
|
||||
- `bl finetune audio create` now validates CosyVoice training data: audio files must be `.wav`, each `wav_fn` must start with `train/`, and exactly one training file is accepted.
|
||||
- `bl quota list` and `bl quota check` now report real RPM/TPM usage against limits, adding `RPM Left` / `TPM Left` columns with remaining-quota progress bars sourced from monitoring data.
|
||||
- `bl usage free` output now shares its rendering with `bl usage summary` for consistent free-tier tables.
|
||||
- `bl advisor recommend` no longer depends on a dedicated intent-detection model to analyze your request.
|
||||
|
||||
### Removed
|
||||
|
||||
- **Removed the `tongyi-intent-detect-v3` integration (BREAKING)** used by `bl advisor recommend`, along with the `intent_detect_base_url` config field and the `DASHSCOPE_INTENT_DETECT_BASE_URL` environment variable.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Skill command-reference generation now reads product command maps directly from source and produces stable formatting during release checks.
|
||||
|
||||
## [1.7.0] - 2026-07-09
|
||||
|
||||
### Added
|
||||
|
||||
- `bl auth login --open-api` now stores Alibaba Cloud OpenAPI AK/SK credentials for Token Plan commands; `bl auth status` reports API key, console, and OpenAPI credential state separately, and `bl auth logout --open-api` clears only OpenAPI credentials.
|
||||
- `kscli` help and examples now render as Knowledge Studio paths such as `kscli search`, `kscli chat`, and `kscli retrieve`, matching the standalone CLI.
|
||||
|
||||
### Changed
|
||||
|
||||
- Token Plan commands now use the shared OpenAPI AK/SK credential flow, including persisted credentials and `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` environment variables.
|
||||
- Auth flags are now scoped to the commands that can use them. Passing model, console, or OpenAPI credential flags to the wrong command now reports an unknown flag instead of being accepted and ignored.
|
||||
- Help and command reference output now show only the flags that apply to each command's auth mode, making model, console, and OpenAPI credentials easier to distinguish.
|
||||
- Missing required flags now return usage errors with exit code 2 instead of opening interactive prompts or printing help with exit code 0.
|
||||
- Image, video, and speech task commands now use `--async` consistently for returning task IDs without waiting; `--concurrent` is shown only on commands that support parallel requests.
|
||||
- Default command output is text unless `--output json`, `DASHSCOPE_OUTPUT=json`, or config explicitly requests JSON.
|
||||
- Update checks are throttled to once per day and can surface in non-TTY/agent runs.
|
||||
- Proxy setup now reads uppercase `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` only; lowercase proxy environment variables are ignored.
|
||||
- `bl auth login` no longer prints the onboarding quick start block after a successful login.
|
||||
|
||||
### Removed
|
||||
|
||||
- Deprecated AK/SK authentication for `bl knowledge retrieve`; use DashScope API key auth for knowledge commands.
|
||||
- Removed `--no-color`, `--non-interactive`, and `--no-wait`. Use `NO_COLOR=1` for plain output and `--async` for task submission without waiting.
|
||||
- Removed `--yes` and interactive confirmation prompts from delete/logout commands; use `--dry-run` to preview before running destructive operations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Credential-gated `--dry-run` paths now skip auth preflight so commands such as Token Plan can print request details without configured credentials.
|
||||
- `--verbose` model requests again print request method, URL, auth source, and response status details.
|
||||
|
||||
## [1.6.1] - 2026-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- `bl vision describe` examples and skill reference now use `qwen3-vl-plus` instead of the legacy `qwen-vl-plus` model id, matching the command's default model.
|
||||
|
||||
## [1.6.0] - 2026-07-02
|
||||
|
||||
### Added
|
||||
|
||||
- `bl knowledge search` — semantic search across knowledge bases using the new workspace-based RAG API. Supports `--query`, `--agent-id`, `--workspace-id`, `--image` (multimodal retrieval, repeatable), and `--query-history` (JSON conversation context for multi-turn query rewriting).
|
||||
- `bl knowledge chat` — knowledge-base Q&A with SSE streaming. Supports `--message` (repeatable, with `role:content` prefix for multi-turn history), `--agent-id`, `--workspace-id`, and `--image` (multimodal). Displays real-time progress with step-change labels (retrieval, planning, generation) in interactive mode.
|
||||
- `bailian-cli-core` gains new types and endpoints for the workspace-based knowledge API: `KnowledgeSearchRequest` / `KnowledgeSearchResponse`, `KnowledgeChatRequest` / `KnowledgeChatStreamChunk` / `KnowledgeChatMessage` / `KnowledgeChatContentPart`, and `knowledgeSearchEndpoint` / `knowledgeChatEndpoint`.
|
||||
- `kscli` now ships `search` and `chat` commands alongside the existing `retrieve`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `bl knowledge retrieve` is now marked as deprecated in its description; use `bl knowledge search` instead.
|
||||
- `kscli` README (EN + ZH) updated to feature `search` and `chat` as the primary commands, with `retrieve` marked deprecated.
|
||||
|
||||
## [1.5.0] - 2026-07-01
|
||||
|
||||
### Added
|
||||
|
||||
+450
-2
@@ -1,11 +1,459 @@
|
||||
# 更新日志
|
||||
|
||||
`bailian-cli` 和 `bailian-cli-core` 的所有重要变更都记录在此。
|
||||
`bailian-cli` 系列包的所有重要变更都记录在此。
|
||||
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。`bailian-cli`、`bailian-cli-core`、`bailian-cli-runtime`、`bailian-cli-commands` 共享一个版本号,总是一起发布。
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。`bailian-cli`、`bailian-cli-core`、`bailian-cli-runtime`、`bailian-cli-commands`、`knowledge-studio-cli` 共享一个版本号。
|
||||
|
||||
[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
|
||||
|
||||
### 新增
|
||||
|
||||
- **模型权限管理** —— `bl permission list` 查看各模型的推理 / 微调 / 部署授权;`bl permission grant` 与 `bl permission revoke` 负责授予和回收,支持 `--all` 一键为工作区全部模型(含后续新增模型)开启推理授权。
|
||||
|
||||
### 变更
|
||||
|
||||
- **`bl quota request` 更名为 `bl quota update`** —— 通过 `--rpm`/`--tpm` 设置单模型 QPM/TPM,新增 `--delete` 一键清除自定义限制;未指定的字段保持当前值,旧命令 `quota request` 仍作为别名可用。
|
||||
- **`bl quota list` 重构** —— 改从模型限制接口读取数据,单表展示模型级与工作区级的请求/用量限制及异步队列/并发限制。
|
||||
- **`bl model list` 不再需要控制台登录** —— 模型目录与 `--enrich` 参数结构端点均为公开接口。
|
||||
- **`bl skill init` 输出精简** —— 单技能状态改为 `success`/`failed`(原为 `installed`),新增 `success`/`partial`/`failed` 汇总结果;移除 `publishedAt` 与 `agents` 字段。
|
||||
|
||||
## [1.15.0] - 2026-08-14
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl text chat` 支持 Responses API** —— 可通过 `--api responses` 调用 DashScope Responses API,支持流式输出、工具定义和结构化 JSON 输出;默认仍使用 Chat Completions。
|
||||
- **订阅套餐用量视图** —— `bl usage token-plan` 支持查看 5 小时和每周额度,`bl usage coding-plan` 支持查看 5 小时、每周和每月额度;两者均提供文本与 JSON 输出。
|
||||
- **命令帮助展示鉴权要求** —— Help 输出现在会明确标注命令需要 API Key、控制台登录还是阿里云 OpenAPI 凭证。
|
||||
|
||||
### 变更
|
||||
|
||||
- **扩展语音识别模型支持** —— `bl speech recognize` 现在会将异步文件转写和同步 Flash ASR 模型路由至对应的 DashScope API,并为暂不支持的实时模型提供明确提示。
|
||||
- **增强 MCP 传输兼容性** —— MCP 命令现在可为兼容的百炼及自定义端点从 Streamable HTTP 自动回退至经典 SSE。
|
||||
|
||||
### 修复
|
||||
|
||||
- 二进制方式升级 CLI 成功后,现在会同步刷新已安装的 Agent Skills。
|
||||
- 修复 Token Plan 额度不可用或缺少重置时间时的展示问题。
|
||||
- 修复 Qwen3 文件转写结果处理,使等待模式和 `--out` 能够正常工作。
|
||||
- 修复 MCP SSE 分块解析、响应头超时、中止清理和回退状态匹配问题。
|
||||
- JSON 输出中的网络错误现在会在 `cause.code` 中保留 errno。
|
||||
|
||||
## [1.14.3] - 2026-08-12
|
||||
|
||||
### 修复
|
||||
|
||||
- **免费额度兼容性** —— `bl usage free` 和 `bl usage freetier` 现在使用最新的 Bailian Commerce 控制台 API 查询、开通和关闭免费额度,并统一处理异步任务轮询。
|
||||
|
||||
## [1.14.2] - 2026-08-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl skill init`** —— 一次性将全部官方 `bailian-*` Skill 安装到本机检测到的 AI Agent。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Skill 命令接口** —— Skill 管理命令现在默认输出适合 Agent 工作流的 JSON;`bl skill add` 和 `bl skill update` 使用明确的 `--all` 与 `--name` 选择参数。
|
||||
|
||||
## [1.14.1] - 2026-08-05
|
||||
|
||||
### 新增
|
||||
|
||||
- **百炼 Skill 按领域拆分** —— 通过 `npx skills add modelstudioai/cli --all -g` 可统一安装图片与视频生成、模型微调、Managed Agent 和共享执行协议等专用 Skill,提升任务路由准确性并减少无关上下文。
|
||||
|
||||
### 变更
|
||||
|
||||
- **默认图片模型升级至 Qwen-Image 3.0** —— 普通 API Key 用户的图片生成、图片编辑、Pipeline、配置 UI 和相关文档现在默认使用 `qwen-image-3.0`。
|
||||
- **扩展 Coding Agent 兼容范围** —— Skill 安装与更新现在能够识别更多 Coding Agent,保留已有安装链接,并自动将 Skill 补充到新识别的 Agent。
|
||||
|
||||
## [1.14.0] - 2026-08-04
|
||||
|
||||
### 新增
|
||||
|
||||
- **免 Node.js 的二进制安装** — 支持 macOS Apple Silicon / Intel、Linux x64 和 Windows x64;npm 安装方式继续保留。
|
||||
- **指定版本更新** — 二进制和 npm 安装均可通过 `bl update --to <version>` 更新或切换到指定版本。
|
||||
|
||||
### 变更
|
||||
|
||||
- **二进制自更新** — 二进制安装现在通过独立的发布通道检查和下载更新;执行 `bl update` 时不会覆盖正在运行的程序,下次运行自动使用新版本。
|
||||
|
||||
## [1.13.1] - 2026-08-03
|
||||
|
||||
### 变更
|
||||
|
||||
- **默认文本模型升级至 Qwen3.8-Max** — `bl text chat`、Pipeline、API Key 登录校验、配置 UI 和 Managed Agent 初始化模板现在默认使用 `qwen3.8-max`;Token Plan 也由预览版切换至正式版。
|
||||
|
||||
## [1.13.0] - 2026-07-30
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl config ui` 技能 / MCP / 代理 / 资产清单** — 在本地 Web UI 中浏览已安装的技能、MCP 服务器、编码代理和生成的资产,点击打开右侧详情抽屉:
|
||||
- 技能:将 `SKILL.md` 渲染为 Markdown(支持 GFM 表格),展示本地/远程来源徽章,支持上传 `.zip` 压缩包将技能安装到任意受支持代理的技能目录。
|
||||
- MCP:查看和编辑 JSON 配置,支持密钥掩码与掩码保真写回;支持在 Claude Code、Qwen Code、OpenCode、Cursor、Windsurf、Gemini、Qoder Work、OpenClaw 和 Claude Desktop 中创建、更新、删除 MCP 条目。
|
||||
- 代理:从 UI 一键启动编码代理(需对应 CLI 二进制在 PATH 中)。
|
||||
- 资产:按类别分组、按时间排序的浏览器,支持预览、本地打开和删除。
|
||||
- **模型目录建议芯片** — 在配置 UI 的每个 `default_*_model` 字段下方展示按类别分组的模型名称,点击即可填入。
|
||||
- **Profile 磁贴网格** — 配置文件以磁贴网格展示,新增添加磁贴和设计一致的新建 Profile 弹窗。
|
||||
|
||||
### 变更
|
||||
|
||||
- 配置 UI 布局:可折叠分组侧边栏(带图标和持久化状态)、响应式断点、更宽的主区域、吸顶视图标题、右侧抽屉式编辑。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复软链接技能目录未被正确识别为已安装来源的问题。
|
||||
- 配置文件检测现支持基于环境变量的路径和旧版配置方案。
|
||||
|
||||
## [1.12.0] - 2026-07-28
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl config agent --key` / `--region`** —— 百炼控制台生成的命令可直接运行:`--key` 接收控制台编码后的 API Key 并在本地解码(与 `--api-key` 二选一);`--region` 根据地域名自动派生 Token Plan 接入地址(与 `--base-url` 二选一)。
|
||||
- **`bl config agent --context-window`** —— 设置写入 OpenClaw 配置的上下文窗口大小(默认 256000)。
|
||||
- **`bl config agent --wire-api`** —— 选择写入 Codex 配置的通信协议;`chat` 仅保留给 Codex 0.80.0 及更早版本(会显示警告)。
|
||||
|
||||
### 变更
|
||||
|
||||
- `bl config agent` 配置 Codex 时默认写入 `wire_api = "responses"`,以适配已不再支持 `chat` 的新版 Codex。
|
||||
- `bl config agent` 配置 Qwen Code 时改用 `DASHSCOPE_API_KEY` 环境变量,不再使用 `BAILIAN_CLI_API_KEY`。
|
||||
|
||||
### 修复
|
||||
|
||||
- `bl config agent` 写入的配置现已与各 Agent 官方格式对齐:Claude Code 尊重 `CLAUDE_CONFIG_DIR` 并清理残留的 `ANTHROPIC_API_KEY`;Qwen Code 采用 v3 配置 schema 并正确写入凭证,避免被系统级 `OPENAI_API_KEY` 干扰;OpenCode 支持带注释和尾部逗号的 JSONC 配置文件;OpenClaw 会将主模型注册进模型白名单并补齐计费元数据;Hermes 改用官方扁平 `model.*` 结构;Codex 写入官方 `env_key` 并支持 `auth.json` 兜底。
|
||||
- `bl config agent` 写入配置时现会保留用户已有配置:合并而非覆盖,避免重复添加 provider 条目,并保留用户自定义的显示名。
|
||||
|
||||
## [1.11.2] - 2026-07-28
|
||||
|
||||
### 变更
|
||||
|
||||
- MCP 工具或 WebSearch 因对应服务未开通而不可用时,CLI 现在会提供开通指引和市场直达链接;对于使用旧版 SSE 连接的 WebSearch,还会提示重新开通以切换至 Streamable HTTP。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复文本对话与 API Key 登录校验因传递不受支持的 `enable_thinking` 参数值而产生的兼容性错误。文本对话仅在用户明确开启思考模式时传递该参数,登录校验则改用兼容模型且不再传递该参数。
|
||||
|
||||
## [1.11.1] - 2026-07-28
|
||||
|
||||
### 新增
|
||||
|
||||
- `bl image edit` 新增 `--function` 参数,支持为万相图片编辑模型(如 `wanx2.1-imageedit`)指定编辑功能。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复部分图片模型在图片生成与编辑时的调用失败和尺寸参数错误,并完善 Qwen-Image、Wan/Wanx、Z-Image 系列及 `wanx-v1` 日期版本的兼容性。
|
||||
|
||||
## [1.11.0] - 2026-07-28
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl managed-agent`** —— 通过统一 CLI 声明式管理 Managed Agent 基础设施;百炼 Provider 对接 AgentStudio,并支持 Claude、Qoder 和 Ark:
|
||||
- `init` / `validate` / `plan` / `apply` / `destroy` —— 基于 `agents.yaml` 初始化、校验、预览和执行资源变更,以及销毁已托管资源。
|
||||
- `state list` / `state show` / `state rm` / `state import` —— 查看和管理本地资源状态,包括纳管已有远端资源或仅解除本地跟踪。
|
||||
- `session create` / `session list` / `session get` / `session delete` / `session run` / `session send` / `session events` —— 完整的会话生命周期操作,支持流式响应和结构化的 `--output json` 输出。
|
||||
- `skill-list` —— 浏览自定义与官方 Skill;使用 `--source all` 可一次返回两个来源。
|
||||
|
||||
### 变更
|
||||
|
||||
- 模型 Base URL 现在统一仅保留 URL Origin;传入的路径、查询参数和 Fragment 不再参与后续 API 请求路径拼接。
|
||||
|
||||
### 修复
|
||||
|
||||
- 安装指南不再推荐已移除的 `--non-interactive`,改为说明显式传入必填参数,并使用 `--output json` 或 `NO_COLOR=1` 适配非交互环境。
|
||||
|
||||
## [1.10.1] - 2026-07-22
|
||||
|
||||
### 变更
|
||||
|
||||
- Token Plan 默认模型已更新为当前文本、图片,以及文生视频、图生视频和参考生视频的专用模型。
|
||||
- 百炼 CLI Skill 现在能更准确地区分百炼专属任务与普通宿主 Agent 任务,并避免在已授权的工作流中重复征求同意。
|
||||
- 已发布的 CLI 包现在支持 Node.js 18.17 及以上版本,最低版本要求由 Node.js 22.12 下调至 18.17。
|
||||
|
||||
### 修复
|
||||
|
||||
- Token Plan 现在能在图片编辑、图生视频、参考生视频和视觉理解中正确处理本地图片,无需另行托管为 URL。
|
||||
|
||||
## [1.10.0] - 2026-07-19
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl config agent`** —— 一键配置 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 和 Codex 接入百炼模型服务。
|
||||
|
||||
### 变更
|
||||
|
||||
- 百炼 CLI Skill 现在只将匹配的百炼任务与多模态任务路由到 `bl`,并会在调用与平台无关的远程或计费能力前征求同意。
|
||||
|
||||
### 修复
|
||||
|
||||
- 完整执行 `bl auth logout` 时会同时清除模型 Base URL,避免后续登录继承失效的自定义或 Token Plan 接入地址。
|
||||
|
||||
## [1.9.0] - 2026-07-17
|
||||
|
||||
### 新增
|
||||
|
||||
- **支持 Token Plan** —— 登录后即可直接调用支持的模型,无需手动配置接入地址。
|
||||
- **命名 Config Profile** —— 支持创建、切换和管理相互隔离的配置,登录后会自动激活当前 Profile。
|
||||
- **Console Access Token 自动化** —— 支持生成并自动刷新 Console Access Token。
|
||||
- **`bl workspace init`** —— 一站式完成百炼工作空间初始化和所需服务开通。
|
||||
|
||||
### 修复
|
||||
|
||||
- 提升配置安全性与一致性,包括密钥脱敏和自定义配置字段保留。
|
||||
|
||||
## [1.8.3] - 2026-07-16
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 Windows 上 `bl text chat --messages-file -` 将标准输入当作 `/dev/stdin` 文件路径读取的问题;通过管道传入的 JSON 消息现在可以从标准输入正常读取。(#103)
|
||||
|
||||
## [1.8.2] - 2026-07-15
|
||||
|
||||
### 变更
|
||||
|
||||
- `bl model list` 现在默认以 JSON 输出;需要表格视图请传 `--output text`。
|
||||
|
||||
### 修复
|
||||
|
||||
- `bl model list --enrich` 现在能正确返回每个模型的输入参数 schema(predictConfig);此前因未解包控制台网关响应信封而始终为空。
|
||||
|
||||
## [1.8.1] - 2026-07-14
|
||||
|
||||
### 变更
|
||||
|
||||
- 扩展 Command Pack 白名单,允许加载额外的内部命令扩展。
|
||||
|
||||
## [1.8.0] - 2026-07-13
|
||||
|
||||
### 新增
|
||||
|
||||
- **`bl model list`** —— 浏览百炼模型市场:列出模型家族,或用 `--model` 查看单个家族的完整详情;支持按 provider、能力、特性、上下文窗口过滤,分页(`--page` / `--page-size`)、价格展示,以及 `--enrich` 获取更丰富的元数据。
|
||||
- **`bl usage summary`** —— 统一用量视图,一屏合并免费额度与近期用量概览;`--days` 设置概览时间窗口(默认 7 天)。
|
||||
- **Command Pack 宿主支持** —— 新增面向白名单内部命令扩展包的加载能力。
|
||||
- **音频与图像精调** —— 在原有文本流程之外新增 `bl finetune audio create`(CosyVoice 语音合成)与 `bl finetune image create`(万相图像生成)。`bl finetune image create` 支持 `--generation-type t2i|i2i` 显式选择文生图或图生图训练。
|
||||
- **音频与图像部署** —— `bl deploy audio create` 与 `bl deploy image create` 可将精调后的语音合成与图像模型部署为推理接入点。
|
||||
- **多模态数据集校验** —— `bl dataset upload` 与 `bl dataset validate` 现在支持使用 `tts`、`image` schema 的 `.zip` 压缩包,可校验包内引用的媒体文件,图像数据压缩包上限提升至 1 GB。
|
||||
|
||||
### 变更
|
||||
|
||||
- **精调与部署命令按模态拆分(BREAKING)**:`bl finetune create` → `bl finetune text create`,`bl deploy create` → `bl deploy text create`。请更新使用旧路径的脚本。
|
||||
- **部署参数重命名(BREAKING)**:部署创建命令的 `--template-id` 更名为 `--deploy-spec`。
|
||||
- **精调状态退出行为变更(BREAKING)**:`bl finetune watch` 不再使用退出码 3 表示任务运行中;运行中与成功均返回 0,失败与取消使用 CLI 的常规错误流程。
|
||||
- `bl deploy audio create` 默认使用 `--plan mu`(按模型单元计费,符合 CosyVoice 部署契约);文本与图像仍默认 `lora`。
|
||||
- `bl finetune audio create` 现在会校验 CosyVoice 训练数据:音频必须为 `.wav`,每条 `wav_fn` 必须以 `train/` 开头,且只接受一个训练文件。
|
||||
- `bl quota list` 与 `bl quota check` 现在会基于监控数据展示真实的 RPM/TPM 用量与限额,新增 `RPM Left` / `TPM Left` 列及剩余额度进度条。
|
||||
- `bl usage free` 的输出现在与 `bl usage summary` 共用渲染逻辑,免费额度表格更一致。
|
||||
- `bl advisor recommend` 不再依赖独立的意图识别模型来分析你的需求。
|
||||
|
||||
### 已移除
|
||||
|
||||
- **移除 `bl advisor recommend` 使用的 `tongyi-intent-detect-v3` 集成(BREAKING)**,同时移除 `intent_detect_base_url` 配置字段与 `DASHSCOPE_INTENT_DETECT_BASE_URL` 环境变量。
|
||||
|
||||
### 修复
|
||||
|
||||
- Skill 命令参考文档生成现在直接读取产品命令源码,并在发布检查中保持稳定格式。
|
||||
|
||||
## [1.7.0] - 2026-07-09
|
||||
|
||||
### 新增
|
||||
|
||||
- `bl auth login --open-api` 现在可以保存阿里云 OpenAPI AK/SK 凭据,供 Token Plan 命令使用;`bl auth status` 会分别展示 API Key、控制台和 OpenAPI 凭据状态,`bl auth logout --open-api` 可只清除 OpenAPI 凭据。
|
||||
- `kscli` 的 help 与示例现在展示为 `kscli search`、`kscli chat`、`kscli retrieve` 等 Knowledge Studio 独立入口路径。
|
||||
|
||||
### 变更
|
||||
|
||||
- Token Plan 命令统一使用 OpenAPI AK/SK 凭据流程,支持登录持久化凭据和 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` 环境变量。
|
||||
- 鉴权 flag 现在只对可使用它们的命令生效。把模型、控制台或 OpenAPI 凭据 flag 传给错误的命令时,现在会报 unknown flag,而不是接受后忽略。
|
||||
- help 与命令参考现在只展示当前命令鉴权域适用的 flag,更容易区分模型、控制台和 OpenAPI 凭据。
|
||||
- 缺少必填 flag 时现在返回用法错误并以退出码 2 退出,不再进入交互式补全或打印 help 后以退出码 0 退出。
|
||||
- 图片、视频、语音任务类命令现在统一用 `--async` 表示提交任务后不等待;`--concurrent` 只在支持并发请求的命令上展示。
|
||||
- 命令默认输出为文本;仅在显式设置 `--output json`、`DASHSCOPE_OUTPUT=json` 或配置文件要求 JSON 时输出 JSON。
|
||||
- 更新检查节流调整为每天一次,并可在非 TTY / agent 场景展示更新提示。
|
||||
- 代理配置现在只读取大写 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`,忽略小写代理环境变量。
|
||||
- `bl auth login` 成功后不再额外打印 onboarding quick start 内容。
|
||||
|
||||
### 已移除
|
||||
|
||||
- 移除 `bl knowledge retrieve` 已废弃的 AK/SK 鉴权;知识库命令请使用 DashScope API Key。
|
||||
- 移除 `--no-color`、`--non-interactive` 和 `--no-wait`。纯文本输出使用 `NO_COLOR=1`,提交任务后不等待使用 `--async`。
|
||||
- 移除删除 / 登出类命令的 `--yes` 与交互式确认提示;执行破坏性操作前请用 `--dry-run` 预览。
|
||||
|
||||
### 修复
|
||||
|
||||
- 需要凭据的 `--dry-run` 路径现在会跳过鉴权前置检查,例如 Token Plan 可在未配置凭据时先打印请求信息。
|
||||
- `--verbose` 的模型请求日志恢复输出请求方法、URL、鉴权来源与响应状态等信息。
|
||||
|
||||
## [1.6.1] - 2026-07-03
|
||||
|
||||
### 变更
|
||||
|
||||
- `bl vision describe` 的示例与 skill 参考文档中的模型 id 由旧版 `qwen-vl-plus` 更新为 `qwen3-vl-plus`,与命令默认模型保持一致。
|
||||
|
||||
## [1.6.0] - 2026-07-02
|
||||
|
||||
### 新增
|
||||
|
||||
- `bl knowledge search` — 基于新版 workspace RAG API 的知识库语义检索。支持 `--query`、`--agent-id`、`--workspace-id`、`--image`(多模态检索,可重复)和 `--query-history`(多轮对话上下文 JSON,用于查询重写)。
|
||||
- `bl knowledge chat` — 知识库 SSE 流式问答。支持 `--message`(可重复,支持 `角色:内容` 前缀传入多轮历史)、`--agent-id`、`--workspace-id` 和 `--image`(多模态)。交互模式下实时展示检索、规划、生成等步骤进度。
|
||||
- `bailian-cli-core` 新增 workspace 级知识 API 类型与端点:`KnowledgeSearchRequest` / `KnowledgeSearchResponse`、`KnowledgeChatRequest` / `KnowledgeChatStreamChunk` / `KnowledgeChatMessage` / `KnowledgeChatContentPart`,以及 `knowledgeSearchEndpoint` / `knowledgeChatEndpoint`。
|
||||
- `kscli` 现已包含 `search` 和 `chat` 命令。
|
||||
|
||||
### 变更
|
||||
|
||||
- `bl knowledge retrieve` 描述中已标记为废弃,请改用 `bl knowledge search`。
|
||||
- `kscli` README(中英文)更新,以 `search` 和 `chat` 为主推命令,`retrieve` 标记为废弃。
|
||||
|
||||
## [1.5.0] - 2026-07-01
|
||||
|
||||
### 新增
|
||||
|
||||
+13
-12
@@ -6,7 +6,8 @@ Developer guide for `bailian-cli` — the official CLI for Aliyun Model Studio (
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js ≥ 22.12
|
||||
- Node.js ≥ 22.12 (required for developing this repo)
|
||||
- End users installing the published CLI only need Node.js >= 18.17
|
||||
- pnpm 10.33.2 (`npm i -g pnpm@10.33.2`)
|
||||
- A DashScope API key for running e2e tests
|
||||
|
||||
@@ -15,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
|
||||
```
|
||||
|
||||
@@ -33,13 +40,7 @@ pnpm install
|
||||
|
||||
### Running the CLI from source
|
||||
|
||||
Open two terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1 — watch-build core
|
||||
pnpm dev
|
||||
|
||||
# Terminal 2 — run any bl command
|
||||
pnpm bl auth login --api-key sk-xxxxx
|
||||
pnpm bl text chat --message "hello"
|
||||
pnpm bl video generate --prompt "a cat walking"
|
||||
|
||||
+14
-13
@@ -6,8 +6,9 @@
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js ≥ 22.12
|
||||
- pnpm 10.33.2(`npm i -g pnpm@10.33.2`)
|
||||
- 参与本仓库开发:Node.js ≥ 22.12
|
||||
- 仅安装/使用已发布的 CLI:Node.js >= 18.17
|
||||
- pnpm 10.33.2(`npm i -g pnpm@10.33.2`)
|
||||
- 跑 e2e 需要一个百炼 API Key
|
||||
|
||||
## 仓库结构
|
||||
@@ -15,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
|
||||
```
|
||||
|
||||
@@ -33,13 +40,7 @@ pnpm install
|
||||
|
||||
### 从源码运行 CLI
|
||||
|
||||
开两个终端:
|
||||
|
||||
```bash
|
||||
# 终端 1 —— core watch 重建
|
||||
pnpm dev
|
||||
|
||||
# 终端 2 —— 跑任意 bl 命令
|
||||
pnpm bl auth login --api-key sk-xxxxx
|
||||
pnpm bl text chat --message "你好"
|
||||
pnpm bl video generate --prompt "一只走路的猫"
|
||||
|
||||
+100
-75
@@ -1,59 +1,76 @@
|
||||
# 阿里云百炼CLI 安装说明(供 AI Agent 阅读)
|
||||
|
||||
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(npm 包名 `bailian-cli`,命令 `bl` / `bailian`)。用户通常通过 **npm 全局安装** 使用,**无需**访问本仓库源码。不要臆造版本号或路径;以用户环境为准。
|
||||
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**在已有 Node.js(**≥ 18.17.0**)的环境用 npm 安装;若没有可用 Node / npm,再走二进制一键安装。不要臆造版本号或路径;以用户环境为准。
|
||||
|
||||
## 1. 安装前检查
|
||||
## 1. 推荐:npm 安装(要求 **≥ 18.17.0**)
|
||||
|
||||
**原则**:先执行检查;若 **Node / 包管理器 / 网络** 任一不满足下文要求,Agent 应 **在本机终端内主动完成安装或配置**(例如升级 Node、切换镜像、配置 `npm` 代理等),再继续 `bailian-cli` 安装。仅在无权限或环境无法自动化时,再向用户说明原因并请其配合。
|
||||
|
||||
1. **Node.js**
|
||||
- 要求 **≥ 22.12.0**,执行 `node -v` 确认。
|
||||
- 未安装或版本不足:由 Agent 自行安装或升级 Node。
|
||||
|
||||
2. **npm**(**唯一**允许的包管理器)
|
||||
- 执行 `npm -v` 确认可用。
|
||||
- 未安装或不可用:由 Agent **安装或修复 npm**(不要改用 pnpm/yarn 装 `bailian-cli`),再执行 `npm install -g bailian-cli`。
|
||||
- 即使本机已安装 `pnpm`、`yarn` 等,**也仅用于其它项目**;装 `bailian-cli` 时仍必须用 `npm`。
|
||||
|
||||
3. **网络**
|
||||
- 需能访问 npm registry(默认 **registry.npmjs.org**)。
|
||||
- 不可达:由 Agent 自行配置镜像或代理后重试。
|
||||
|
||||
---
|
||||
|
||||
## 2. 安装 CLI 与 Skills
|
||||
|
||||
**仅允许以下命令**(不要用 `pnpm add -g`、`yarn global add` 等)。按顺序执行,上一步通过后再进行下一步。
|
||||
|
||||
**2.1 安装 CLI**
|
||||
1. `node -v` 确认版本 ≥ 18.17.0。
|
||||
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn)。
|
||||
3. 执行:
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
安装成功后,应能在 PATH 中找到:
|
||||
4. 校验:`bl --version`。
|
||||
5. 按下方「安装 skills」安装。
|
||||
|
||||
- `bl`(短别名)
|
||||
- `bailian`(全名)
|
||||
### 安装 skills
|
||||
|
||||
**校验**(Agent 应执行并检查退出码与输出):
|
||||
**Supported(优先):** 用 CLI 内置命令一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`):
|
||||
|
||||
```bash
|
||||
bl skill init
|
||||
```
|
||||
|
||||
**降级:** 若 `bl skill init` 失败,先确认本机同时满足两个条件:`node -v` / `npm -v` 成功,**且** `git --version` 成功。两个条件都成立时,再回退到:
|
||||
|
||||
```bash
|
||||
npx skills@1.5.18 add modelstudioai/cli --all -g
|
||||
```
|
||||
|
||||
> 必须使用 `skills@1.5.18`:这是最后兼容 Node ≥18 的版本(与 Bailian CLI 的 Node ≥18.17.0 一致);
|
||||
|
||||
安装成功后,按第 5 节向用户说明语言切换与能力 tip(若也装了 skills,一并简要说明)。
|
||||
|
||||
## 2. 备选:二进制安装(无需 Node)
|
||||
|
||||
当环境没有 Node / npm,或 Node 版本过低无法走 npm 时,使用二进制安装脚本。脚本安装 CLI 成功后会自动执行 `bl skill init`。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
可选:正式安装读 CDN 上的 `manifest.json`。预发 / channel 验证一律读 `sync-release.json`(bailian-cli 的 channel 发版都会覆盖它):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash -s -- --channel sync-release
|
||||
```
|
||||
|
||||
也可用 `--version` / `--cdn`(或环境变量 `BAILIAN_CLI_CDN`)覆盖资源根。安装脚本本身不在本仓库维护。
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
# 正式(manifest.json)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
|
||||
# channel / 预发验证(sync-release.json)
|
||||
$env:BAILIAN_CHANNEL = 'sync-release'; irm 'https://bailian.aliyun.com/cli/install.ps1' | iex
|
||||
```
|
||||
|
||||
带其它参时先落盘再执行(`irm | iex` 不便传参),或使用仓外静态资源文档中的预发入口。
|
||||
|
||||
二进制安装布局为 `versions/<ver>/` + `current` 指针;`bl update` 只切换指针并清理旧版本(保留当前与上一版)。更新进程退出后,下次执行 `bl` 即使用新版本(无需「重启应用」)。
|
||||
|
||||
校验:
|
||||
|
||||
```bash
|
||||
bl --version
|
||||
which bl # Windows 可用 where bl
|
||||
which bl # Windows: where.exe bl
|
||||
```
|
||||
|
||||
若 `command not found`:检查全局 bin 是否在 PATH(`npm config get prefix`,其下 `bin` 目录应加入 PATH)。
|
||||
|
||||
**2.2 安装 Skills**
|
||||
|
||||
CLI 校验通过后,在本机终端执行:
|
||||
|
||||
```bash
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
```
|
||||
|
||||
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
|
||||
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。
|
||||
|
||||
---
|
||||
|
||||
@@ -61,57 +78,65 @@ npx skills add modelstudioai/cli --all -g
|
||||
|
||||
### 推荐:浏览器登录(控制台会话)
|
||||
|
||||
适用于本机交互式安装,无需用户手动复制 API Key:
|
||||
|
||||
1. 执行 `bl auth status --output json`,判断是否已配置。
|
||||
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`;命令会拉起浏览器完成阿里云控制台登录授权。
|
||||
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`。
|
||||
3. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整凭据。
|
||||
|
||||
> 此方式同时打通 `app list`、`usage free` 等控制台能力,并自动配置 API Key 调用所需的鉴权信息。
|
||||
### 备选:API Key / Token Plan
|
||||
|
||||
### 备选:由 Agent 引导用户输入 API Key 后登录
|
||||
|
||||
适用于无法拉起浏览器的对话式安装(远程 SSH、CI 调试、纯终端环境等):
|
||||
|
||||
- 获取入口:[百炼控制台 API Key](https://bailian.console.aliyun.com/cn-beijing/?tab=app#/api-key)
|
||||
|
||||
1. 执行 `bl auth status --output json`,判断是否已配置。
|
||||
2. 若未配置或后续 API 校验失败,**请用户粘贴 API Key**(可说明从上述控制台复制;勿要求用户发到公开渠道)。
|
||||
3. 用户提供了 Key 之后,在**用户本机终端**执行(Agent 用终端工具跑,勿把 Key 写进回复正文):`bl auth login --api-key <用户提供的_Key>`
|
||||
4. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整 Key。
|
||||
|
||||
### 其他方式
|
||||
|
||||
- **环境变量**(不落盘到配置文件):在 shell 中配置 API Key 环境变量;变量名见 `bl auth status --help`,勿在对话中向用户解释底层命名。
|
||||
- **写入配置文件**(持久化,与 `auth login` 落盘相同):`bl config set --key api_key --value <key>`(`--key api-key` 亦可)。**不会**像 `bl auth login --api-key` 那样先校验 Key 是否可用;Agent 引导安装时仍**优先**用 `auth login`。
|
||||
- **命令行临时传入**:需要 API Key 的 `bl` 子命令可在**当次**执行附加全局 `--api-key <key>`,仅本次生效、不落盘(例:`bl text chat --api-key sk-xxx --message "你好"`)。与上文持久化方式不是同一用途。
|
||||
- 普通 Key:`bl auth login --api-key <Key>`
|
||||
- Token Plan:`bl auth login --config token-plan --api-key <Key>`
|
||||
|
||||
### Agent 安全约束
|
||||
|
||||
- **禁止**把真实 API Key 写入仓库、日志、Skill、聊天记录的可公开部分。
|
||||
- CI / 非交互环境:使用 `bl ... --non-interactive`;通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
|
||||
- CI / 非交互环境:显式传入必填参数并使用 `--output json` 获取机器可读结果;如需纯文本输出,设置 `NO_COLOR=1`。通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
|
||||
|
||||
---
|
||||
|
||||
## 4. 最小功能验证
|
||||
|
||||
在鉴权配置完成后执行:
|
||||
## 4. 配置验证
|
||||
|
||||
```bash
|
||||
bl auth status --output json
|
||||
bl text chat --message "ping" --non-interactive --output json
|
||||
```
|
||||
|
||||
若失败:根据 stderr / JSON 中的 `hint` 或 `message` 排查(网络、Key 无效、`base_url` 等)。DashScope 端点:使用 `--base-url` / `bl config set --key base_url` / `DASHSCOPE_BASE_URL`,默认中国大陆 `https://dashscope.aliyuncs.com`。
|
||||
## 5. 安装成功后向用户说明(必做)
|
||||
|
||||
---
|
||||
安装与校验完成后,**主动**用中文向用户说明下列两项;不要等用户追问。可执行 `bl`(已登录时)对照终端 tip,但以下列表为准,勿臆造。
|
||||
|
||||
## 5. 常见问题(Agent 排障清单)
|
||||
### 5.1 界面语言
|
||||
|
||||
| 现象 | 可能原因 | 建议动作 |
|
||||
| ----------------------- | -------------------- | --------------------------------------------------------------- |
|
||||
| `bl: command not found` | 全局 bin 不在 PATH | 检查 `npm prefix -g` 与 PATH |
|
||||
| 安装报错 engines | Node 版本过低 | 升级到 ≥ 22.12 |
|
||||
| 401 / 鉴权失败 | 未 login 或 Key 无效 | 引导用户更新 Key 并 `bl auth login --api-key` |
|
||||
| 企业网络无法访问 npm | 代理 / 镜像 | 配置 registry 或代理后再装 |
|
||||
| 本机只有 pnpm、没有 npm | Agent 误用 pnpm 安装 | 先装/修好 **npm**,再用 `npm install -g bailian-cli`;勿用 pnpm |
|
||||
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`;否则重试或告知用户 |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
**The official command-line interface for Aliyun Model Studio (DashScope) AI Platform**
|
||||
|
||||
[](https://www.npmjs.com/package/bailian-cli)
|
||||
[](https://nodejs.org)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](LICENSE)
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
|
||||
---
|
||||
|
||||
_Chat with Qwen, generate images & videos, understand images, call agents,_
|
||||
_manage memory, search the web — all from your terminal._
|
||||
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
|
||||
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
|
||||
_every AI capability, one command away._
|
||||
|
||||
_Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
@@ -22,27 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
|
||||
|
||||
## Features
|
||||
|
||||
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
|
||||
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
|
||||
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
|
||||
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
|
||||
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
|
||||
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
|
||||
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
|
||||
|
||||
- **Text chat** — Qwen3.7-max: major gains in agentic coding, frontend coding, and vibe coding
|
||||
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
|
||||
- **Image generation & editing** — Qwen-Image 2.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
|
||||
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
|
||||
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 5–20s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
|
||||
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
|
||||
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
|
||||
|
||||
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
|
||||
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
|
||||
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
|
||||
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
|
||||
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
|
||||
- **Fine-tuning & deployment** — Upload datasets, create SFT/LoRA/DPO/CPT jobs (`finetune create`), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy create`)
|
||||
- **Console capabilities** — Browse Bailian apps (`app list`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
|
||||
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
|
||||
|
||||
## Showcase: One-Sentence Cinematic Video
|
||||
## Showcase 1: A Cinematic Short Film from One Sentence
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -55,126 +45,115 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
|
||||
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
|
||||
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
|
||||
>
|
||||
> _(Original: "帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2分钟左右的视频,尺寸是16:9")_
|
||||
|
||||
### How it works
|
||||
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
|
||||
|
||||
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
|
||||
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
|
||||
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
|
||||
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
|
||||
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
|
||||
|
||||
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
|
||||
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
|
||||
|
||||
### The single prompt
|
||||
|
||||
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
**Agent install (recommended)**
|
||||
|
||||
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
|
||||
|
||||
```text
|
||||
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
|
||||
```
|
||||
|
||||
> Requires Node.js >= 22.12.
|
||||
**Install with NPM**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> Requires Node.js >= 18.17.
|
||||
|
||||
**Install on macOS/Linux**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
**Install on Windows**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> No Node.js required. The installer automatically installs Bailian Skills.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate, recommended
|
||||
bl auth login --console
|
||||
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
|
||||
|
||||
# Or authenticate with an API key
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# Chat with Qwen
|
||||
bl text chat --message "What is DashScope?"
|
||||
|
||||
# Multimodal chat (text + image + audio + video)
|
||||
bl omni --message "Describe this image" --image ./photo.jpg
|
||||
|
||||
# Generate an image
|
||||
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
|
||||
|
||||
# Generate a video from local image
|
||||
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
|
||||
|
||||
# Model recommendation — find the best model for your use case
|
||||
bl advisor recommend --message "I need a visual-understanding chatbot"
|
||||
|
||||
# Compare specific models
|
||||
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
|
||||
|
||||
# Browser login (required for console capability commands)
|
||||
bl auth login --console
|
||||
|
||||
# Fine-tune & deploy — a one-shot train-to-serve workflow
|
||||
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
|
||||
bl finetune create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
|
||||
bl finetune watch --job-id ft-xxx --output json # Non-blocking status probe (exit 0/1/3 = done/failed/running)
|
||||
bl finetune capability --model qwen3-8b # Which training types a model supports
|
||||
bl deploy create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
|
||||
|
||||
# Browse apps / free-tier quota / usage statistics / workspaces
|
||||
bl app list
|
||||
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
|
||||
bl workspace list # List all workspaces
|
||||
|
||||
# Rate limit management (list / check / request / history)
|
||||
bl quota list # View RPM/TPM limits (add --model to filter)
|
||||
bl quota check # Current usage vs rate limits (add --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
|
||||
bl quota history # View quota-change history
|
||||
|
||||
# Token Plan team management (requires AK/SK, see auth below)
|
||||
bl token-plan list-seats # View subscription seat details
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| Scenario | What to say to your Agent |
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| 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." |
|
||||
|
||||
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## Authentication
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# Option 2: Login command (persisted to ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
```
|
||||
|
||||
# Option 3: Per-command flag
|
||||
bl text chat --api-key sk-xxxxx --message "Hello"
|
||||
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
```
|
||||
|
||||
### Console Login (OAuth)
|
||||
|
||||
Required for console capability commands (`app list`, `usage free`, `usage stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
|
||||
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### Alibaba Cloud AK/SK (Knowledge Base & Token Plan)
|
||||
### Alibaba Cloud OpenAPI AK/SK
|
||||
|
||||
Required for `knowledge retrieve` and the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
|
||||
|
||||
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
|
||||
|
||||
```bash
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -183,17 +162,34 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# View current config
|
||||
bl config show
|
||||
|
||||
# Set defaults
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# List all config profiles
|
||||
bl config list
|
||||
|
||||
# Self-update to latest version
|
||||
bl update
|
||||
# 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`
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.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, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
|
||||
|
||||
## Links
|
||||
|
||||
| Resource | URL |
|
||||
@@ -203,12 +199,5 @@ Config file location: `~/.bailian/config.json`
|
||||
| Qwen Model List | https://help.aliyun.com/zh/model-studio/getting-started/models |
|
||||
| Aliyun Model Studio Console | https://bailian.console.aliyun.com/?source_channel=cli_github |
|
||||
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## Changelog
|
||||
|
||||
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.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, repo layout, and the workflow for adding or changing commands.
|
||||
|
||||
+103
-113
@@ -5,7 +5,7 @@
|
||||
**阿里云百炼 (DashScope) AI 平台命令行工具**
|
||||
|
||||
[](https://www.npmjs.com/package/bailian-cli)
|
||||
[](https://nodejs.org)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](LICENSE)
|
||||
|
||||
@@ -22,27 +22,16 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
## 功能特性
|
||||
|
||||
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
|
||||
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
|
||||
- **素材理解** — 图像、文档、音频、长视频的解析与问答
|
||||
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具
|
||||
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
|
||||
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
|
||||
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
|
||||
|
||||
- **文本对话** — Qwen3.7-max:Agentic coding、前端编程、Vibe coding 等能力显著增强
|
||||
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
|
||||
- **图像生成与编辑** — Qwen-Image 2.0:专业文字渲染、真实质感、强语义遵循、多图合成
|
||||
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
|
||||
- **语音合成与识别** — CosyVoice 实时流式合成,5-20s 样本即可克隆;FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
|
||||
- **图像与视频理解** — Qwen-VL:长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
|
||||
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站(aliyun.com)账号,暂不支持国际站 / 全球站账号。
|
||||
|
||||
> **注意:** 以下功能目前仅对中国站(aliyun.com)账号开放,国际站 / 全球站账号暂不支持。
|
||||
|
||||
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
|
||||
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
|
||||
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
|
||||
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
|
||||
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
|
||||
- **微调与部署** — 上传数据集、创建 SFT/LoRA/DPO/CPT 调优任务(`finetune create`)、非阻塞探测任务状态(`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy create`)
|
||||
- **控制台能力** — 浏览百炼应用(`app list`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`)
|
||||
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
|
||||
|
||||
## 示例:一句话生成一部电影短片
|
||||
## 示例 1:一句话生成一部电影短片
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
|
||||
@@ -52,127 +41,118 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_
|
||||
|
||||
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
|
||||
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
|
||||
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
|
||||
> _“帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9。”_
|
||||
|
||||
### 工作流程
|
||||
## 示例 2:一句话构建短片导演 Managed Agent
|
||||
|
||||
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
|
||||
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
|
||||
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**。
|
||||
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
|
||||
<p align="center">
|
||||
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
|
||||
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
|
||||
<p align="center"><i>👆 点击封面播放完整演示</i></p>
|
||||
|
||||
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
|
||||
|
||||
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
|
||||
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
|
||||
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
|
||||
|
||||
### 唯一的提示词
|
||||
|
||||
> _“帮我构建一个 managedagent 应用,能够实现短片拍摄,导演专家生成视频,然后也能进行设计对应的分镜图。”_
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
npx skills add modelstudioai/cli --all -g
|
||||
**Agent 安装(推荐)**
|
||||
|
||||
把下面这句话发给你的 Agent,它会自行判断环境并完成安装与校验:
|
||||
|
||||
```text
|
||||
请阅读:https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
|
||||
```
|
||||
|
||||
> 需要预先安装 Node.js >= 22.12。
|
||||
**NPM 安装**
|
||||
|
||||
```bash
|
||||
npm install -g bailian-cli
|
||||
bl skill init
|
||||
```
|
||||
|
||||
> 需要预先安装 Node.js >= 18.17。
|
||||
|
||||
**macOS/Linux 安装**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
**Windows 安装**
|
||||
|
||||
```powershell
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
> 无需预先安装 Node.js,安装脚本会自动安装 Bailian Skills。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 认证(推荐浏览器登录)
|
||||
bl auth login --console
|
||||
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
|
||||
|
||||
# 或使用 API key 认证
|
||||
bl auth login --api-key sk-xxxxx
|
||||
|
||||
# 和通义千问对话
|
||||
bl text chat --message "你好,介绍一下阿里云百炼平台"
|
||||
|
||||
# 多模态对话(文本 + 图片 + 音频 + 视频)
|
||||
bl omni --message "描述这张图片" --image ./photo.jpg
|
||||
|
||||
# 生成图片
|
||||
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
|
||||
|
||||
# 图生视频(本地文件自动上传)
|
||||
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
|
||||
|
||||
# 模型推荐 — 根据场景推荐最适合的模型
|
||||
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
|
||||
|
||||
# 对比特定模型
|
||||
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
|
||||
|
||||
# 浏览器登录(控制台能力相关命令需要)
|
||||
bl auth login --console
|
||||
|
||||
# 微调与部署 — 从训练到服务的一站式流程
|
||||
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
|
||||
bl finetune create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
|
||||
bl finetune watch --job-id ft-xxx --output json # 非阻塞状态探测(退出码 0/1/3 = 成功/失败/进行中)
|
||||
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
|
||||
bl deploy create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
|
||||
|
||||
# 浏览应用 / 免费额度 / 用量统计 / 业务空间
|
||||
bl app list
|
||||
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort)
|
||||
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
|
||||
bl workspace list # 列出所有业务空间
|
||||
|
||||
# 限流管理与提额(list / check / request / history)
|
||||
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
|
||||
bl quota check # 当前用量 vs 限流阈值(加 --model/--period)
|
||||
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
|
||||
bl quota history # 查看提额历史记录
|
||||
|
||||
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
|
||||
bl token-plan list-seats # 查看订阅席位明细
|
||||
bl token-plan add-member --account-name dev --org-id org_xxx
|
||||
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
|
||||
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
|
||||
```
|
||||
| 场景 | 可以这样对 Agent 说 |
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
|
||||
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
|
||||
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
|
||||
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
|
||||
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
|
||||
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
|
||||
|
||||
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
|
||||
|
||||
## 认证方式
|
||||
|
||||
### DashScope API Key
|
||||
### API Key
|
||||
|
||||
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
|
||||
|
||||
```bash
|
||||
# 方式一:环境变量
|
||||
export DASHSCOPE_API_KEY=sk-xxxxx
|
||||
|
||||
# 方式二:登录命令(持久化到 ~/.bailian/config.json)
|
||||
bl auth login --api-key sk-xxxxx
|
||||
```
|
||||
|
||||
# 方式三:命令行参数
|
||||
bl text chat --api-key sk-xxxxx --message "你好"
|
||||
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
|
||||
|
||||
```bash
|
||||
bl auth login --config token-plan --api-key sk-sp-xxxxx
|
||||
```
|
||||
|
||||
### 控制台登录(OAuth)
|
||||
|
||||
控制台能力命令(`app list`、`usage free`、`usage stats`、`workspace list`、`quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
|
||||
|
||||
```bash
|
||||
bl auth login --console
|
||||
```
|
||||
|
||||
### 阿里云 AK/SK(知识库检索与 Token Plan)
|
||||
### 阿里云 OpenAPI AK/SK
|
||||
|
||||
`knowledge retrieve` 与 `token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
|
||||
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
|
||||
|
||||
```bash
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
|
||||
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
|
||||
export BAILIAN_WORKSPACE_ID=ws-...
|
||||
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
|
||||
```
|
||||
|
||||
## 配置
|
||||
@@ -181,17 +161,34 @@ export BAILIAN_WORKSPACE_ID=ws-...
|
||||
# 查看当前配置
|
||||
bl config show
|
||||
|
||||
# 设置默认值
|
||||
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
|
||||
bl config set --key default_text_model --value qwen-turbo
|
||||
bl config set --key timeout --value 600
|
||||
# 查看全部配置档
|
||||
bl config list
|
||||
|
||||
# 自更新到最新版本
|
||||
bl update
|
||||
# 切换配置档
|
||||
bl config use --name token-plan
|
||||
|
||||
# 将 CLI 界面切换为中文
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
配置文件位置:`~/.bailian/config.json`
|
||||
|
||||
## 更新
|
||||
|
||||
```bash
|
||||
bl update
|
||||
```
|
||||
|
||||
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群,获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
|
||||
|
||||
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
|
||||
|
||||
## 相关链接
|
||||
|
||||
| 资源 | 地址 |
|
||||
@@ -201,12 +198,5 @@ bl update
|
||||
| 通义千问模型列表 | https://help.aliyun.com/zh/model-studio/getting-started/models |
|
||||
| 阿里云百炼控制台 | https://bailian.console.aliyun.com/?source_channel=cli_github |
|
||||
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
|
||||
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
|
||||
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
|
||||
|
||||
## 更新日志
|
||||
|
||||
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
|
||||
|
||||
+125
-65
@@ -2,115 +2,175 @@
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 增加新的鉴权方式(OAuth、SSO、控制台回调登录)
|
||||
- 增加新的 token 来源(env / config / flag / 文件)
|
||||
- 调整凭证解析优先级
|
||||
- 改 `bl auth login` 流程
|
||||
- 增加新的鉴权域或 token 来源(env / config / flag / 文件)
|
||||
- 调整 API Key / Console token 解析优先级
|
||||
- 改 `bl auth login` / `auth status` / `auth logout` 流程
|
||||
- 改 runtime 对 command `auth` 的 gating 或 credential 注入
|
||||
|
||||
## 鉴权链路
|
||||
|
||||
```
|
||||
flag 优先 ─→ config 文件 ─→ env var
|
||||
│ │ │
|
||||
└──── resolveCredential() (core) ───┐
|
||||
│
|
||||
▼
|
||||
cli/utils/ensure-key.ts (启动时拦)
|
||||
命令注入 Authorization 头
|
||||
argv flags ─┐
|
||||
env var ──┼─ buildSources(flags) ─┐
|
||||
config ──┘ │
|
||||
├─ buildSettings(sources) → ctx.settings
|
||||
│
|
||||
├─ resolveApiKey(sources) → model-domain Client
|
||||
├─ resolveConsole(sources) → console-domain Client
|
||||
└─ resolveOpenApi(sources) → OpenAPI Client
|
||||
|
||||
defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx)
|
||||
```
|
||||
|
||||
凭证类型(`AuthMethod`):
|
||||
当前 command 鉴权域(`AuthRequirement`):
|
||||
|
||||
- `api-key` — DashScope SK(`sk-...`),走 Bearer 头
|
||||
- `access-token` — 控制台 OAuth 回调拿到的临时 token,走 Bearer + 不同 endpoint
|
||||
- `ak/sk` — Alibaba Cloud 标准 AK/SK,走 ROA 签名(只用于知识库)
|
||||
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
|
||||
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
|
||||
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
|
||||
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
|
||||
|
||||
### 双凭证并存(API Key + Console)
|
||||
### 多凭证并存
|
||||
|
||||
`~/.bailian/config.json` 可同时保存 `api_key` 与 `access_token`。**登录任一种方式不得删除另一种**(`bl auth login --api-key` / `--console` 只更新对应字段)。
|
||||
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
|
||||
|
||||
- `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`
|
||||
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
|
||||
- `bl auth logout` 清 `api_key` + `base_url` + `access_token` + `access_key_*`
|
||||
|
||||
解析分工:
|
||||
|
||||
- `resolveCredential()` — DashScope API 命令(`text chat`、`file upload` 等);config 里两者都有时 **优先 `api_key`**
|
||||
- `resolveConsoleGatewayCredential()` — 控制台网关(`app list`、`usage free`、`console call`);**只用** env/file 的 `access_token`,忽略 `api_key`
|
||||
- `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 的字段优先级。对 `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_*`,新写入只写短字段
|
||||
- `describeAuthState()` — `auth status` / banner / telemetry 使用的只读快照
|
||||
|
||||
必改调用点: 凡 `callConsoleGateway` 必须用 `resolveConsoleGatewayCredential`,不能误用 `resolveCredential`(否则 config 仅有 api_key 时会拿 sk- 打网关)。
|
||||
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘。
|
||||
|
||||
`bl auth logout --console` 只清 `access_token`;全量 `bl auth logout` 清两者。
|
||||
### 例外:agent 命令的分层鉴权与 SDK 凭证内存注入
|
||||
|
||||
`bl managed-agent *` 按调用链分两层:
|
||||
|
||||
- **离线命令** — `init`、`validate`、`state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
|
||||
- **联网命令** — `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()` — 调用 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. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider;随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint;离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
|
||||
|
||||
禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. core 层(类型 + 解析)
|
||||
|
||||
- [ ] `packages/core/src/types/command.ts`:
|
||||
- 如新增鉴权域,扩展 `AuthRequirement`
|
||||
- 更新 `credentialFlagDefs()` 暴露该域可见的 flag
|
||||
- 必要时新增 `*_AUTH_FLAGS`
|
||||
- `workspace_id` 是作用域字段而非 credential,不要把它放进 `ConsoleCredential`;读取方式按命令 `auth` 域区分:
|
||||
- `auth: "console"` 命令通过 `CONSOLE_AUTH_FLAGS` 自动获得 `--workspace-id`,由 `buildSettings()` 解析到 `settings.workspaceId`,命令统一从 `settings.workspaceId` 读取
|
||||
- `auth: "apiKey"`/`"openapi"`/`"none"` 命令如需 `--workspace-id`,必须自声明 flag;因它不会进入 credential/global flags,命令从 `ctx.flags.workspaceId` 读取(可回退到 `settings.workspaceId`)
|
||||
- [ ] `packages/core/src/auth/types.ts`:
|
||||
- 新增 `AuthMethod` 字面量
|
||||
- 新增 `ResolvedCredential` 字段(如 token 类型 / 过期时间)
|
||||
- 新增 credential 类型 / source / scope 字段
|
||||
- [ ] `packages/core/src/auth/resolver.ts`:
|
||||
- `resolveCredential()` 增加新分支
|
||||
- 控制台网关命令用 `resolveConsoleGatewayCredential()`(与 DashScope 解析分离)
|
||||
- 优先级注释保持清晰(数字标号)
|
||||
- [ ] `packages/core/src/auth/credentials.ts`:
|
||||
- 如果新方式需要持久化,加 `save*` / `load*` / `clear*`
|
||||
- 新增或调整 resolver,保持优先级注释清晰
|
||||
- 新增/调整 resolver hint 时保持产品无关,不要新增 `bl` / `kscli` 硬编码;当前遗留的 `bl auth login` hint 如被触碰,迁到 runtime `enhanceHint`
|
||||
- [ ] `packages/core/src/auth/store.ts`:
|
||||
- 如果新方式需要持久化,扩展 `AuthStore` / `AuthPersistPatch`
|
||||
- [ ] `packages/core/src/config/schema.ts`:
|
||||
- `Config` 接口加新字段(如 `fileAccessToken`、`accessTokenEnv`)
|
||||
- `ConfigFile` 接口加对应 disk 字段(snake_case)
|
||||
- `ConfigFile` 加 disk 字段(snake_case)
|
||||
- `Settings` 加运行时字段(如果命令需要读取)
|
||||
- [ ] `packages/core/src/config/loader.ts`:
|
||||
- `loadConfig()` 把 env / 文件读到 Config 上
|
||||
- `buildSources()` / `buildSettings()` 把 flag/env/file 读到正确层
|
||||
|
||||
### B. core 客户端
|
||||
### B. runtime 层
|
||||
|
||||
- [ ] `packages/core/src/client/http.ts`:
|
||||
- 不同 `credential.method` 走不同分支(参考已有 `access-token` 分支走 console gateway)
|
||||
- Authorization 头注入正确
|
||||
- [ ] `packages/runtime/src/create-cli.ts`:
|
||||
- parse flags 时纳入新的全局/凭证域 flag
|
||||
- `globalFlags` 与 `ownFlags` 分流正确
|
||||
- [ ] `packages/runtime/src/middleware.ts:authStage`:
|
||||
- 根据 `command.auth` 解析 credential 并注入 `ctx.client`
|
||||
- `settings.dryRun` 下是否允许缺 credential 的策略明确
|
||||
- [ ] `packages/runtime/src/error-handler.ts`:
|
||||
- AUTH hint 增强使用 `binName`,不要硬编码 `bl`
|
||||
- URL 从 `packages/runtime/src/urls.ts` import
|
||||
|
||||
### C. cli 层
|
||||
### C. command 层
|
||||
|
||||
- [ ] `packages/cli/src/utils/ensure-key.ts`:
|
||||
- 启动时检查新凭证方式是否已配置,缺的话提示
|
||||
- 如果是交互式 setup(类似 `bl auth login --console`),增加新分支
|
||||
- [ ] `packages/cli/src/commands/auth/login.ts`:
|
||||
- 新增 `--xxx` flag 触发新登录流程
|
||||
- 持久化到 config(调用 core 的 save 函数)
|
||||
- [ ] `packages/cli/src/commands/auth/status.ts`:
|
||||
- 分别显示 `api_key` / `access_token` 是否已配置,以及 DashScope vs 控制台网关各自生效的 credential
|
||||
- [ ] `packages/cli/src/output/status-bar.ts`:
|
||||
- 顶部状态条显示新凭证 method
|
||||
- [ ] `packages/commands/src/commands/auth/login.ts`:
|
||||
- 新增/调整登录 flag 与流程
|
||||
- 持久化只走 `ctx.authStore.login(...)`
|
||||
- [ ] `packages/commands/src/commands/auth/status.ts`:
|
||||
- 分别显示 model / console / openapi 鉴权状态,并 mask token
|
||||
- [ ] `packages/commands/src/commands/auth/logout.ts`:
|
||||
- 清理范围与双凭证并存规则一致
|
||||
- [ ] 新的业务命令设置正确 `auth`:
|
||||
- 模型域请求 → `auth: "apiKey"`
|
||||
- Console Gateway → `auth: "console"`
|
||||
- 阿里云 OpenAPI 请求 → `auth: "openapi"`
|
||||
- 本地/登录/配置 → `auth: "none"`
|
||||
|
||||
### D. main 启动逻辑
|
||||
|
||||
- [ ] 若新增命令**自行处理鉴权**或**不应在入口触发默认 API key 引导**,在对应 `defineCommand` 上设 `skipDefaultApiKeySetup: true`(见 `packages/core/src/types/command.ts`;`packages/cli/src/main.ts` 在 `registry.resolve` 后读取 `command.skipDefaultApiKeySetup`)
|
||||
|
||||
### E. 错误文案
|
||||
|
||||
- [ ] core 的 `BailianError` 鉴权失败 hint **保持通用**(不写 cli 命令名,见 [error-hint-change.md](error-hint-change.md))
|
||||
- [ ] cli 的 `enhanceHint` (error-handler.ts) 按 `ExitCode.AUTH` 注入新方式的 cli 命令引导
|
||||
|
||||
### F. 用户面文档
|
||||
### D. 用户面文档
|
||||
|
||||
- [ ] `README.md` / `README.zh.md` "Authentication" 段落
|
||||
- [ ] 各 `skills/<skill>/reference/` 通过 `pnpm run sync:skill-assets` 重建
|
||||
|
||||
### G. 测试
|
||||
### E. 测试
|
||||
|
||||
- [ ] `packages/cli/tests/e2e/auth.e2e.test.ts` 增加新方式的 happy / failure 路径
|
||||
- [ ] mask token 的输出格式不变(避免泄漏)
|
||||
- [ ] 如调整 resolver 优先级,补 core/runtime 单测覆盖 flag > env > file
|
||||
|
||||
## 完成后自查
|
||||
|
||||
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
|
||||
|
||||
```sh
|
||||
# 各种凭证组合
|
||||
unset DASHSCOPE_API_KEY DASHSCOPE_ACCESS_TOKEN
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts auth status
|
||||
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
||||
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts auth status
|
||||
|
||||
# flag 注入
|
||||
node packages/cli/src/main.ts auth status --api-key sk-xxx
|
||||
# flag 注入(凭证域 flag 只在对应业务命令可见,auth status 不接收)
|
||||
pnpm -F bailian-cli exec tsx src/main.ts text chat --message hi --api-key sk-xxx --dry-run
|
||||
pnpm -F bailian-cli exec tsx src/main.ts token-plan list-seats --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
|
||||
pnpm -F bailian-cli exec tsx src/main.ts auth login --open-api --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
|
||||
|
||||
# env 注入
|
||||
DASHSCOPE_ACCESS_TOKEN=xxx node packages/cli/src/main.ts auth status
|
||||
DASHSCOPE_API_KEY=sk-xxx pnpm -F bailian-cli exec tsx src/main.ts auth status
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID=ak-xxx ALIBABA_CLOUD_ACCESS_KEY_SECRET=sec-xxx pnpm -F bailian-cli exec tsx src/main.ts auth status
|
||||
```
|
||||
|
||||
Console 登录/网关相关改动:
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli exec tsx src/main.ts auth login --console
|
||||
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json --workspace-id ws-xxx
|
||||
```
|
||||
|
||||
注意:`usage stats --dry-run` 仍会先校验 workspace,必须传入 `--workspace-id`(或 `BAILIAN_WORKSPACE_ID` / config `workspace_id`)。
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 加了新 token 来源但忘了改 `resolveCredential` 优先级,实际不生效
|
||||
- ✗ `Config` 加字段但 `loadConfig` 没读 → 字段永远 undefined
|
||||
- ✗ `bl auth login` 写成功但 `bl auth status` 不识别(两边走的 storage path 不一致)
|
||||
- ✗ 加了新 token 来源但忘了改 resolver 优先级,实际不生效
|
||||
- ✗ `ConfigFile` / `Settings` 加字段但 `parseConfigFile` 或 `buildSettings` 没读
|
||||
- ✗ `auth login` 写成功但 `auth status` 不识别(两边走的 storage path 不一致)
|
||||
- ✗ token mask 显示完整 token,日志泄漏
|
||||
- ✗ `auth: "console"` 命令误用 `apiKey` 域,config 只有 API key 时会把 `sk-...` 发到网关
|
||||
- ✗ 新增 core resolver hint 时写死产品命令,导致 `kscli` 等入口提示错误
|
||||
|
||||
@@ -50,13 +50,13 @@ git diff --name-only <base>...<head>
|
||||
- [ ] **`package.json` 没破坏发布元数据**:`bin` / `exports` / `files` / `inlinedDependencies` 字段任何删除或改名都要单独评估
|
||||
- [ ] **公共依赖没被悄悄升级**:catalog / 根 lockfile 改动要列出来
|
||||
- [ ] **`package.json` version 没倒退**:目标分支已经更高时(如 main 1.0.3 vs head 1.0.0-beta.1),手动对齐版本号,不要被 head 覆盖
|
||||
- [ ] **全局表没冲突**:`registry.ts`、`defineCommand` 的 `skipDefaultApiKeySetup`(见 `packages/core/src/types/command.ts`)、`ExitCode` 三处新增项不和现有项冲突
|
||||
- [ ] **全局表没冲突**:`packages/cli/src/commands.ts` / `packages/kscli/src/main.ts` command map、`defineCommand({ auth })`、`GLOBAL_FLAGS` / `MODEL_AUTH_FLAGS` / `CONSOLE_AUTH_FLAGS` / `OPENAPI_AUTH_FLAGS`、`ExitCode` 新增项不和现有项冲突
|
||||
|
||||
## 清单 B:用户透出(用户可见的新东西必看)
|
||||
|
||||
- [ ] **新命令 / 新 flag** 已同步到用户面文档:
|
||||
- [README.md](README.md) + [README.zh.md](README.zh.md)(中英文都要,常漏 `_CN`)
|
||||
- (SKILL.md 已迁出本仓库,由 `npx add skills` 机制独立维护,不在本仓库 review 范围)
|
||||
- 各 `skills/<skill>/reference/` + 对应 `SKILL.md` 通过 `pnpm run sync:skill-assets` 更新并提交
|
||||
- [ ] **`bl <cmd> --help`** 文案完整:`description` / `examples` 都填了
|
||||
- [ ] **demo / quickstart**:用户可调用的新命令至少有一个示例
|
||||
- [ ] **行为变化的老命令**:在 commit message / CHANGELOG 注明用户感知的差异
|
||||
@@ -80,7 +80,7 @@ git diff --name-only <base>...<head>
|
||||
解冲突要点(merge 时不要漏):
|
||||
- <冲突文件> + <字段/段落> + <怎么取舍>
|
||||
↑ 放"合并那一刻才会出现"的细节,例如 package.json 的 files/scripts/devDependencies 各取并集、
|
||||
`skipDefaultApiKeySetup` 这类命令元数据两边都加项时不要丢一侧、pnpm-lock.yaml 直接 rm 后 pnpm install 重生等。
|
||||
command map / `auth` / 全局 flags 这类元数据两边都加项时不要丢一侧、pnpm-lock.yaml 直接 rm 后 pnpm install 重生等。
|
||||
建议修(可后置):
|
||||
- ...
|
||||
仅信息(无需动作,告知即可):
|
||||
@@ -94,11 +94,11 @@ git diff --name-only <base>...<head>
|
||||
|
||||
## 常见漏点(基于历史踩坑)
|
||||
|
||||
| 漏点 | 后果 |
|
||||
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `pnpm-workspace.yaml` 把 `packages/*` 收窄成显式列表 | 合并后目标分支的新子包不再被 workspace 识别,`pnpm install` 看似正常但子包失联 |
|
||||
| 源分支 version 比目标分支低,直接 merge 覆盖 | npm 上版本号回退,latest tag 错乱 |
|
||||
| `registry.ts` 注册新命令但忘了 [README](README.md) / [README.zh](README.zh.md) | 用户完全感知不到新功能 |
|
||||
| 共享 util 重构(抽公共函数)只改了一处调用方 | 其它调用方静默走旧分支,行为分裂 |
|
||||
| 不该跳过默认 API key 引导的命令误设 `skipDefaultApiKeySetup: true` | 安全风险,用户没配置 key 也能调付费 API |
|
||||
| `catalog.ts` / `skipDefaultApiKeySetup` 这类元数据两边都加项,解冲突时被合掉一侧 | 某个命令突然要求登录 / 某个新命令注册丢失,编译能过、回归不易察觉 |
|
||||
| 漏点 | 后果 |
|
||||
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `pnpm-workspace.yaml` 把 `packages/*` 收窄成显式列表 | 合并后目标分支的新子包不再被 workspace 识别,`pnpm install` 看似正常但子包失联 |
|
||||
| 源分支 version 比目标分支低,直接 merge 覆盖 | npm 上版本号回退,latest tag 错乱 |
|
||||
| `packages/cli/src/commands.ts` 注册新命令但忘了 [README](README.md) / [README.zh](README.zh.md) | 用户完全感知不到新功能 |
|
||||
| 共享 util 重构(抽公共函数)只改了一处调用方 | 其它调用方静默走旧分支,行为分裂 |
|
||||
| 命令 `auth` 域设错(如 Console Gateway 用了 `apiKey`) | 凭证域 flag/help/credential 注入都错,运行期才暴露 |
|
||||
| `packages/cli/src/commands.ts` / `packages/kscli/src/main.ts` 这类 map 两边都加项,解冲突时被合掉一侧 | 某个新命令注册丢失,编译能过、回归不易察觉 |
|
||||
|
||||
@@ -81,11 +81,12 @@ git merge-base --is-ancestor 12f2b1b 3fc54ae && echo "IN" || echo "NOT IN"
|
||||
光看 commit 还不够,要确认目标功能的代码 / 文件在 release commit 上真的存在:
|
||||
|
||||
```sh
|
||||
# 列出 release commit 下某目录的文件
|
||||
git ls-tree -r <releaseCommit> --name-only -- packages/cli/src/commands/
|
||||
# 列出 release commit 下命令实现与产品入口
|
||||
git ls-tree -r <releaseCommit> --name-only -- packages/commands/src/commands/
|
||||
git show <releaseCommit>:packages/cli/src/commands.ts | head
|
||||
|
||||
# 看 release commit 下某文件的内容
|
||||
git show <releaseCommit>:packages/cli/src/commands/console/call.ts | head
|
||||
git show <releaseCommit>:packages/commands/src/commands/console/call.ts | head
|
||||
```
|
||||
|
||||
特别注意被一行带过的"杂项" commit。本仓库历史踩过坑:`feat(cli): enhance output options and add new commands` 这种标题里藏了**新命令** + **新输出格式** + **logout 增强**三件事,粗看会全部漏掉。
|
||||
|
||||
+100
-32
@@ -1,30 +1,65 @@
|
||||
# CLI E2E 测试规范
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `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` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增/修改 `packages/cli/src` 下的 command(`commands/catalog.ts` 登记、`defineCommand` 实现、options/usage)
|
||||
- 新建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例
|
||||
- 为命令补 help / 缺参 / dry-run / 真实集成测试
|
||||
- 新增/修改 `packages/commands/src/commands` 下的 command 实现
|
||||
- 新增/修改 `packages/cli/src/commands.ts` 的 `bl` 命令路径 map
|
||||
- 新建或扩展 `packages/commands/tests/e2e/<topic>.e2e.test.ts`
|
||||
- 新增 bl 产品 path → `registry.smoke` 自动覆盖 leaf path;commands topic 测试在 `topic-routes.ts` 补最小路由
|
||||
|
||||
以上情况必须同步维护 `packages/cli/tests/e2e/<topic>.e2e.test.ts`。跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。
|
||||
跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。
|
||||
|
||||
> **规则**:共享 command 行为在 `commands/tests/e2e`;产品 map、identity、CLI-only 命令留在对应产品 `tests/e2e`。
|
||||
|
||||
## 文件与工具
|
||||
|
||||
- 路径:`packages/cli/tests/e2e/<kebab-topic>.e2e.test.ts`
|
||||
- 框架:`vite-plus/test`;子进程跑 CLI:`runCli` from `./helpers.ts`
|
||||
### commands E2E
|
||||
|
||||
- 路径:`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)
|
||||
|
||||
### 产品 smoke
|
||||
|
||||
- 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`;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)` 等显式超时
|
||||
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:分组 + --help,无密钥、无真实 API
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<group> 分组展示子命令帮助且成功退出", ...);
|
||||
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 / 真实集成;原有集成用例放最后、勿改逻辑
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
test("缺少 --<flag> 时退出为用法错误 (2)", ...);
|
||||
test("<cmd> --dry-run ...", ...); // 若适用
|
||||
@@ -32,57 +67,90 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
});
|
||||
```
|
||||
|
||||
## skip 条件(helpers.ts)
|
||||
## skip 条件(`e2e/gating`,commands helpers re-export)
|
||||
|
||||
| 场景 | 条件 |
|
||||
| ------------------- | ----------------------------------------------------- |
|
||||
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
|
||||
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
|
||||
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
|
||||
| 知识库 | `isKnowledgeE2EReady()` |
|
||||
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
|
||||
| 场景 | 条件 |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| 文本/搜索/配置 | `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**:`runCli(["image"])` → `exitCode === 0`,stdout+stderr 含子命令名
|
||||
2. **--help**:`runCli([..., "--help"])` → stderr 含主要 flags
|
||||
3. **缺参**:`--non-interactive` 且不传 required flag → `exitCode === 2`,stderr 匹配 `--flag|Missing required argument`
|
||||
4. **--dry-run**:仅当实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本,不入网
|
||||
5. **真实集成**:保留既有用例名称与断言;放在 skip 块**末尾**
|
||||
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`)
|
||||
- 分组 help 由产品 `registry.smoke` 负责,无需在 commands 重复
|
||||
|
||||
## 安全与例外
|
||||
|
||||
- **禁止真实破坏性操作**:`auth logout` 只用 `--dry-run`;`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 输出 → 更新对应断言
|
||||
|
||||
## 新增 command 检查清单
|
||||
|
||||
- [ ] `commands/catalog.ts` 登记 + `tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usage` / `options` / `examples`,跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/` 并提交
|
||||
- [ ] 顶层:分组 help + 子命令 `--help`(多子命令则各一条 help)
|
||||
- [ ] `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` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `pnpm test packages/cli/tests/e2e/<file>` 通过
|
||||
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
|
||||
|
||||
## 调试命令
|
||||
|
||||
```sh
|
||||
pnpm --filter bailian-cli-commands exec vp test packages/commands/tests/e2e/text-chat.e2e.test.ts
|
||||
pnpm --filter bailian-cli exec vp test packages/cli/tests/e2e/registry.smoke.e2e.test.ts
|
||||
pnpm --filter knowledge-studio-cli exec vp test packages/kscli/tests/e2e/registry.smoke.e2e.test.ts
|
||||
pnpm --filter bailian-cli-runtime exec vp test packages/runtime/tests/proxy.e2e.test.ts
|
||||
```
|
||||
|
||||
## 示例片段
|
||||
|
||||
```ts
|
||||
import { FOO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["foo", "bar", "--non-interactive"]);
|
||||
const { stderr, exitCode } = await runCommandE2e(FOO_ROUTES, ["foo", "bar", "--quiet"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--prompt|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("foo bar --dry-run 仅输出计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(FOO_ROUTES, [
|
||||
"foo",
|
||||
"bar",
|
||||
"--dry-run",
|
||||
"--prompt",
|
||||
"x",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
@@ -97,4 +165,4 @@ test("foo bar --dry-run 仅输出计划", async () => {
|
||||
- **E2E**:单条/少量调用、断言固定、可进 `vp test`(见上文 skip 条件)
|
||||
- **批量压测**:`packages/cli/tests/stress/run.mjs` + `targets/*.mjs`,并发 + 报告,**仅手动** `pnpm run test:stress -- <target>`
|
||||
|
||||
勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md)。
|
||||
勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md).
|
||||
|
||||
@@ -5,89 +5,135 @@
|
||||
- 增加新的 `bl xxx` 命令
|
||||
- 删除已有命令
|
||||
- 重命名命令(包括从单级 `bl x` 改成 `bl x y` 或反向)
|
||||
- 调整某个 shared command 在 `bl` / `kscli` 等产品入口里的暴露路径
|
||||
|
||||
## 命令路径与文件路径的对应规则
|
||||
## 命令实现与产品路径的关系
|
||||
|
||||
命令实现住在 `packages/commands`,产品路径由入口包决定。实现文件路径按能力组织,但不再等同于最终命令路径。
|
||||
|
||||
```
|
||||
单级命令(无 group): commands/<name>.ts ↔ bl <name>
|
||||
例: commands/update.ts ↔ bl update
|
||||
|
||||
两级命令(有 group): commands/<group>/<action>.ts ↔ bl <group> <action>
|
||||
例: commands/text/chat.ts ↔ bl text chat
|
||||
|
||||
三级命令(子组,慎用): commands/<group>/<sub>/<action>.ts ↔ bl <group> <sub> <action>
|
||||
例: commands/memory/profile/create.ts ↔ bl memory profile create
|
||||
仅当子组下有 ≥2 个 action 时合理(否则拍平到两级)
|
||||
实现文件:
|
||||
packages/commands/src/commands/knowledge/retrieve.ts
|
||||
↓ packages/commands/src/index.ts export { default as knowledgeRetrieve }
|
||||
产品入口:
|
||||
packages/cli/src/commands.ts "knowledge retrieve": knowledgeRetrieve ↔ bl knowledge retrieve
|
||||
packages/kscli/src/main.ts "retrieve": knowledgeRetrieve ↔ kscli retrieve
|
||||
```
|
||||
|
||||
文件路径与命令路径必须 1:1 对齐。
|
||||
常见路径形态:
|
||||
|
||||
```
|
||||
单级命令: packages/commands/src/commands/update.ts ↔ bl update
|
||||
两级命令: packages/commands/src/commands/text/chat.ts ↔ bl text chat
|
||||
子组命令: packages/commands/src/commands/memory/profile-get.ts ↔ bl memory profile get
|
||||
```
|
||||
|
||||
子组要慎用:只有子组下有 ≥2 个 action 时才合理,否则优先拍平到两级。
|
||||
|
||||
## CLI 命令注册架构(必读)
|
||||
|
||||
命令元数据以 **`catalog.ts` 为单一登记处**;`registry.ts` 只负责解析与打印 help,不再内嵌命令表或手写 Resources 列表。
|
||||
`packages/commands` 是命令库,只导出单个 command;不内置 path presets,不关心 `bl` / `kscli`。每个产品入口传入自己的 command map,`runtime` 负责解析、help、鉴权、遥测、执行。
|
||||
|
||||
```
|
||||
commands/<...>.ts defineCommand({ name, description, usage, options, examples, run })
|
||||
packages/commands/src/commands/<...>.ts
|
||||
defineCommand({ auth, flags, usageArgs, exampleArgs, validate, run })
|
||||
↓
|
||||
commands/catalog.ts export const commands: Record<string, Command>
|
||||
packages/commands/src/index.ts
|
||||
export { default as xxxCommand } from "./commands/...ts"
|
||||
↓
|
||||
┌────┴────┬──────────────────────┬─────────────────────┐
|
||||
↓ ↓ ↓ ↓
|
||||
registry.ts main.ts tools/generate-reference.ts export-schema.ts
|
||||
(解析/help) (入口) → skills/bailian-cli/reference/index.md + <group>.md
|
||||
┌──────────────────────────────┬──────────────────────────────┐
|
||||
│ packages/cli/src/commands.ts │ packages/kscli/src/main.ts │
|
||||
│ { "text chat": textChat } │ { "retrieve": knowledge... } │
|
||||
└──────────────┬───────────────┴──────────────┬───────────────┘
|
||||
↓ ↓
|
||||
createCli(commands, identity) → runtime registry/help/middleware
|
||||
↓
|
||||
tools/generate-reference.ts reads packages/cli/src/commands.ts
|
||||
```
|
||||
|
||||
- **`packages/cli/src/commands/catalog.ts`**: `import` 命令模块 + `"<path>": handler` 映射;**不** `import registry.ts`(避免构建时循环依赖)
|
||||
- **`packages/cli/src/commands/index.ts`**: `export { commands } from "./catalog.ts"`(给包内 re-export 用)
|
||||
- **`packages/cli/src/registry.ts`**: `import { commands } from "./commands/catalog.ts"`,建树、`resolve`、`printHelp`;Commands / Global Flags 从 `Command` 元数据与 `GLOBAL_OPTIONS` **动态生成**
|
||||
- **`tools/generate-reference.ts`**: pre-commit / `pnpm run sync:skill-assets` 时读 `catalog.ts`,写 `skills/bailian-cli/reference/index.md`(索引) + `skills/bailian-cli/reference/<一级命令>.md`(详情,勿手改)。该目录**纳入 git**,随 `npx skills add modelstudioai/cli` 分发
|
||||
- **`packages/commands/src/commands/<...>.ts`**:命令实现;`usageArgs` / `exampleArgs` 只写参数片段,不写 `bl` / `kscli` 前缀
|
||||
- **`packages/commands/src/index.ts`**:导出命令实现;新增命令必须在这里 re-export
|
||||
- **`packages/cli/src/commands.ts`**:`bl` 产品命令 map;新增/删除/重命名 `bl` 命令必须改这里
|
||||
- **`packages/kscli/src/main.ts`**:`kscli` 产品命令 map;只有该入口需要暴露/变更时才改
|
||||
- **`packages/runtime/src/registry.ts`**:通用 registry,从传入 map 建树;不要在这里登记业务命令
|
||||
- **`tools/generate-reference.ts`**:pre-commit / `pnpm run sync:skill-assets` 时读 `packages/cli/src/commands.ts`,按 `GROUP_OWNER_SKILL` 归属表分流写到各 `skills/<skill>/reference/index.md` + `<一级命令>.md`。未显式归属的一级组默认进 `bailian-cli`。各目录**纳入 git**,勿手改。新增一级命令组若应归领域 skill,记得改归属表。
|
||||
|
||||
已删除、勿再引用:`commands/help.ts`、`registry.ts` 内联 `new CommandRegistry({...})`、`printRootHelp` 手写命令行。
|
||||
已删除/勿再引用:旧的 `packages/cli/src/commands/catalog.ts`、旧的 `packages/cli/src/commands/index.ts` catalog re-export、`packages/cli/src/registry.ts`、`skipDefaultApiKeySetup`、`ensureApiKey` 启动拦截、`config/export-schema.ts`。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 代码层
|
||||
### A. 命令库
|
||||
|
||||
- [ ] **新建/删除/移动**对应的 `packages/cli/src/commands/<...>.ts` 文件
|
||||
- [ ] **`packages/cli/src/commands/catalog.ts`**:
|
||||
- 增删 `import xxx from "./.../xxx.ts"`
|
||||
- 在 `export const commands` 里增删 `"<group> <action>": xxx`(key 与 `defineCommand({ name })` 一致)
|
||||
- [ ] **不要**在 `registry.ts` 里重复登记命令(已从 catalog 读取)
|
||||
- [ ] 如果命令需要跳过入口的默认 DashScope API key 引导(`ensureApiKey`),在对应 `defineCommand` 上设 `skipDefaultApiKeySetup: true`(字段定义见 `packages/core/src/types/command.ts`;`main.ts` 根据已解析的 `command` 读取)
|
||||
- [ ] **`config/export-schema.ts`**: 若新命令不适合作为 agent tool,评估是否加入 `SKIP_PREFIXES`;该文件在 `run()` 内 `import("../catalog.ts")`,勿顶层 import catalog 以免循环依赖
|
||||
- [ ] 新建/删除/移动对应的 `packages/commands/src/commands/<...>.ts`
|
||||
- [ ] `defineCommand` 字段使用当前 schema:
|
||||
- `auth: "apiKey" | "console" | "openapi" | "none"`
|
||||
- `flags`(camelCase key,由 runtime 渲染为 kebab-case)
|
||||
- `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
|
||||
|
||||
### B. 文档层
|
||||
### B. 产品入口
|
||||
|
||||
- [ ] 运行 `pnpm run sync:skill-assets`(或正常 `git commit` 走 pre-commit),刷新 `skills/bailian-cli/reference/` 与 `SKILL.md` 的 `metadata.version` 并提交
|
||||
- [ ] `README.md` / `README.zh.md`: Quick Start、命令一览(用户向,与 help 对齐即可)
|
||||
- [ ] `skills/bailian-cli/SKILL.md`: 若安装说明或能力边界有变,同步更新
|
||||
- [ ] `packages/cli/src/commands.ts`:按需增删 `import` 与 `commands` map key
|
||||
- [ ] 新 map key 就是 `bl` 下的命令路径;重命名时全仓 grep 旧路径字符串
|
||||
- [ ] 如果 `kscli` 入口也要暴露/移除该能力,同步 `packages/kscli/src/main.ts`
|
||||
- [ ] 不要在 `packages/runtime/src/registry.ts` 或 `create-cli.ts` 里写业务命令表
|
||||
|
||||
### C. 测试层
|
||||
### C. 文档层
|
||||
|
||||
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/cli/tests/e2e/<topic>.e2e.test.ts`
|
||||
- [ ] 删除命令时一并删对应 e2e
|
||||
- [ ] 运行 `pnpm run sync:skill-assets`(或正常 `git commit` 走 pre-commit),刷新各 `skills/<skill>/reference/` 与 `SKILL.md` 的 `metadata.version` 并提交
|
||||
- [ ] `README.md` / `README.zh.md`:Quick Start、命令一览、认证说明(用户向,与 help 对齐)
|
||||
- [ ] 相关 `skills/<skill>/SKILL.md`:若安装说明或能力边界有变,同步更新;新一级命令组若属领域 skill,同步改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`
|
||||
- [ ] **拥有方** skill 的「When to use which command」(或等价路由表)补上新意图;hub `bailian-cli` 仅加/改 hand-off 行,**不要**把领域子命令与默认模型抄进 hub 表(约定见 [skill-change.md](skill-change.md))
|
||||
|
||||
### D. 重命名特殊处理
|
||||
### D. 测试层
|
||||
|
||||
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/commands/tests/e2e/<topic>.e2e.test.ts`
|
||||
- [ ] 同步 `packages/commands/tests/e2e/topic-routes.ts`(该 topic 的最小 path → export 映射)
|
||||
- [ ] bl 产品 path 变更由 `registry.smoke` 自动覆盖;kscli 变更同步 `kscli/src/commands.ts` 与 `registry.smoke`
|
||||
- [ ] 删除命令时一并删对应 commands e2e / README 示例 / reference / topic 路由条目
|
||||
- [ ] 如果 shared command 在不同入口路径下复用,至少确保 commands e2e 覆盖 `bl` path;`kscli` 入口改动需补对应 smoke 或说明不测 flat path live
|
||||
|
||||
### E. 重命名特殊处理
|
||||
|
||||
- [ ] 全仓 grep **旧命令名字符串**,确保以下位置全部更新:
|
||||
- `catalog.ts` 的 key
|
||||
- error hints(cli 层)
|
||||
- `skills/bailian-cli/reference/`(重建后检查并提交)
|
||||
- README 示例
|
||||
- 测试断言
|
||||
- `packages/cli/src/commands.ts` map key
|
||||
- `packages/kscli/src/commands.ts` map key(如适用)
|
||||
- 用户可见 hint / README / tests
|
||||
- `skills/*/reference/`(重建后检查并提交)
|
||||
- [ ] 检查 `usageArgs` / `exampleArgs` 没有硬编码旧的 `bl <path>` 前缀
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
pnpm run sync:skill-assets # reference/ + SKILL metadata.version 与 catalog / package.json 一致
|
||||
node packages/cli/src/main.ts <new-command> --help
|
||||
node packages/cli/src/main.ts # 根 help 列表含新命令
|
||||
vp test packages/cli/tests/e2e/<topic>.e2e.test.ts # 相关 e2e
|
||||
pnpm run sync:skill-assets
|
||||
pnpm -F bailian-cli exec tsx src/main.ts <new-command> --help
|
||||
pnpm -F bailian-cli exec tsx src/main.ts
|
||||
vp test packages/commands/tests/e2e/<topic>.e2e.test.ts
|
||||
```
|
||||
|
||||
如改了 `kscli` 入口:
|
||||
|
||||
```sh
|
||||
pnpm -F knowledge-studio-cli exec tsx src/main.ts <command> --help
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 只改了命令文件,忘了 **`catalog.ts`** → 命令不存在或 help 里没有
|
||||
- ✗ 手改 **`skills/bailian-cli/reference/*.md`** → 下次 generate 被覆盖;应改 `defineCommand` 后重新 generate 并提交
|
||||
- ✗ 在 `export-schema.ts` 顶层 `import catalog` → 可能与 registry 循环依赖
|
||||
- ✗ 只新增 `packages/commands/src/commands/...` 文件,忘了在 `packages/commands/src/index.ts` 导出
|
||||
- ✗ 只导出了命令实现,忘了在 `packages/cli/src/commands.ts` 暴露路径 → `bl --help` 看不到
|
||||
- ✗ 手改 `skills/*/reference/*.md` → 下次 generate 被覆盖;应改 command metadata 后重新 generate 并提交
|
||||
- ✗ 新一级命令组忘改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL` → reference 会落到 hub `bailian-cli`(未必是预期)
|
||||
- ✗ 只改 reference / hub,忘改拥有方 skill 路由表;或把领域命令明细重新抄回 `bailian-cli` SKILL → 与 [skill-change.md](skill-change.md) 分层冲突
|
||||
- ✗ 在 `usageArgs` / `exampleArgs` 写死 `bl text chat` → `kscli` 等入口复用时 help 错
|
||||
- ✗ Console Gateway 命令忘设 `auth: "console"` → console flags / credential 注入都不生效
|
||||
- ✗ 单 action 的子组是反模式,新增时优先拍平为两级
|
||||
|
||||
@@ -11,25 +11,27 @@
|
||||
|
||||
### A. 命令文件本身
|
||||
|
||||
- [ ] `packages/cli/src/commands/<group>/<action>.ts`:
|
||||
- `defineCommand({ options: [...] })` 数组里增删/改 `{ flag, description, type, required }`
|
||||
- `usage` 字段(如 `"bl text chat --message <text> [flags]"`)反映新签名
|
||||
- `examples` 数组覆盖新 flag 至少一个示例
|
||||
- `run()` 里读取 flag 的代码:
|
||||
- 类型转换正确(`type: "number"` 时 `flags.x as number`,`"array"` 时 `as string[]`)
|
||||
- 必填校验:`if (!flags.x) failIfMissing("x", ...)` 或交互式 prompt
|
||||
- 默认值 fallback
|
||||
- [ ] `packages/commands/src/commands/<group>/<action>.ts`:
|
||||
- `defineCommand({ flags: { ... } })` 里增删/改 camelCase flag key 与 `{ type, valueHint, description, required }`
|
||||
- `usageArgs` 字段只写参数片段(如 `"--message <text> [flags]"`),不写 `bl <path>`
|
||||
- `exampleArgs` 数组覆盖新 flag 至少一个示例,同样不写 bin/path 前缀
|
||||
- `run()` 里只从 `ctx.flags` 读取本命令 flag,从 `ctx.settings` 读取全局/config 解析结果
|
||||
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
|
||||
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
|
||||
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
|
||||
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。
|
||||
|
||||
### B. 鉴权 / 全局选项
|
||||
|
||||
- [ ] 如果是**全局 flag**(所有命令通用),改 `packages/core/src/types/command.ts` 的 `GLOBAL_OPTIONS`
|
||||
- [ ] 如果新 flag 影响 `Config`,改 `packages/core/src/config/schema.ts` 的 `Config` 接口
|
||||
- [ ] 如果对应 env var,改 `packages/core/src/config/loader.ts` 的 `loadConfig`
|
||||
- [ ] 如果是**全局 flag**(所有命令通用),改 `packages/core/src/types/command.ts` 的 `GLOBAL_FLAGS`
|
||||
- [ ] 如果是凭证域 flag,优先确认是否属于 `MODEL_AUTH_FLAGS` 或 `CONSOLE_AUTH_FLAGS`;不要在单个命令里重复声明
|
||||
- [ ] 如果新 flag 影响有效配置面,改 `packages/core/src/config/schema.ts` 的 `Settings` 接口
|
||||
- [ ] 如果对应 env var 或 config 文件字段,改 `packages/core/src/config/loader.ts` 的 `buildSettings`
|
||||
|
||||
### C. 文档层
|
||||
|
||||
- [ ] `README.md` / `README.zh.md` 如果在示例里展示了相关命令,补充新 flag
|
||||
- [ ] 跑 `pnpm --filter bailian-cli run generate:reference`,让 `skills/bailian-cli/reference/` 与命令一致(勿手改;改完提交)
|
||||
- [ ] 跑 `pnpm --filter bailian-cli run generate:reference`,让各 `skills/<skill>/reference/` 与命令一致(勿手改;改完提交)
|
||||
|
||||
### D. 测试层
|
||||
|
||||
@@ -44,13 +46,13 @@
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
node packages/cli/src/main.ts <command> --help # 看新 flag 出现在 Options
|
||||
node packages/cli/src/main.ts <command> --new-flag x # 实测一遍
|
||||
pnpm -F bailian-cli exec tsx src/main.ts <command> --help # 看新 flag 出现在 Flags
|
||||
pnpm -F bailian-cli exec tsx src/main.ts <command> --new-flag x # 实测一遍
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 加 `type: "number"` 但 `String(flags.x)` 触发 lint 警告(参考已修过的 memory/list.ts)
|
||||
- ✗ 加了 array 型 flag 但没考虑用户可能传多次
|
||||
- ✗ 改默认值忘记更新 description 里的 "(default: xxx)" 文案
|
||||
- ✗ Required flag 缺失时直接抛硬错而不是 prompt(交互友好性问题,参考已实现 prompt 的命令文件作为示例)
|
||||
- ✗ 在 `usageArgs` / `exampleArgs` 里写死 `bl <path>`,导致其它产品入口复用时 help 错
|
||||
- ✗ required flag 缺失又在 `run()` 里重复手写校验,与 parser/`validate` 的错误文案不一致
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Command Pack 维护
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增或移除 Command Pack 包
|
||||
- 调整包白名单、允许的命令前缀或协议字段
|
||||
- 修改 `plugin install/link/list/remove`
|
||||
- 修改 Command Pack 加载、隔离、兼容性或独立安装目录
|
||||
|
||||
## 分层边界
|
||||
|
||||
- `packages/core/src/types/command-pack.ts`:稳定的协议元数据和导出类型,不知道具体产品或白名单。
|
||||
- `packages/runtime/src/command-packs/`:所有 CLI 共用的加载、校验、API 适配、产品隔离安装目录和 manager 实现。
|
||||
- `packages/runtime/src/create-cli.ts`:始终接收静态 command map,按 `CliOptions.commandPacks` 统一合并 pack,并把已绑定产品 identity/policy 的 manager 注入 `ctx.commandPacks`。
|
||||
- `packages/commands/src/commands/plugin/`:普通共享管理命令,只依赖 `ctx.commandPacks`,不 import 任何产品 policy。
|
||||
- `packages/cli/src/command-pack-policy.ts`:`bl` 支持的包、命令前缀和凭据授权。
|
||||
- `kscli` 当前不传 `commandPacks`,使用 runtime 的默认空 policy。
|
||||
- 当前只有 `bl` 从 `bailian-cli-commands` 导入并登记 `plugin *`;使用默认空 policy 的产品不提前暴露管理命令。
|
||||
|
||||
不要把产品白名单写进 core/runtime,也不要通过扫描全局 `node_modules` 自动发现包。通用机制放 runtime,产品差异只由 policy 表达。
|
||||
|
||||
## 安全与兼容性清单
|
||||
|
||||
- [ ] 包名必须精确命中当前产品 policy 的 `supported`,命令路径必须位于该包允许的前缀。
|
||||
- [ ] 正式安装只接受包名加 version/tag;本地目录只走 `plugin link`。
|
||||
- [ ] npm 使用独立安装目录和 `--ignore-scripts`,不污染 CLI 自身依赖树。
|
||||
- [ ] npm 子进程只继承明确允许的 registry/config/cache/proxy/TLS 配置,不通配透传 pnpm 注入的 `npm_config_*`。
|
||||
- [ ] 安装目录按 `identity.npmPackage` 隔离,不能让一个产品安装/删除另一个产品的 pack。
|
||||
- [ ] 安装目录只隔离依赖位置,不隔离执行权限;Command Pack 必须视为 CLI 进程内的完全可信代码。
|
||||
- [ ] 入口 realpath 不能逃逸包根目录。
|
||||
- [ ] 加载前检查 `type`、`apiVersion`、`minCliVersion`;报告状态只使用 `loaded/failed`,具体原因写入 `error`。
|
||||
- [ ] 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。
|
||||
- [ ] 破坏协议前优先在适配层兼容;确实无法兼容时才提升 `apiVersion`。
|
||||
|
||||
## 测试与文档
|
||||
|
||||
- [ ] `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 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
|
||||
- [ ] 更新生成的 `skills/bailian-cli/reference/plugin.md`(或归属表指定的 skill reference);公开 `README.md` / `README.zh.md` 等正式对外发布时再补。
|
||||
|
||||
验证:
|
||||
|
||||
```sh
|
||||
vp test packages/runtime/tests/command-packs.test.ts
|
||||
vp test packages/cli/tests/e2e/command-packs.e2e.test.ts
|
||||
vp test packages/kscli/tests/e2e/command-packs.e2e.test.ts
|
||||
pnpm run sync:skill-assets
|
||||
vp check
|
||||
```
|
||||
+23
-19
@@ -11,46 +11,48 @@
|
||||
|
||||
```
|
||||
flag (--xxx) ─┐
|
||||
├─ loadConfig() 合并 ─→ Config(运行时单一对象)
|
||||
├─ buildSources() + buildSettings() ─→ Settings(命令读取面)
|
||||
env (XXX=yyy) ─┤
|
||||
│
|
||||
config 文件 ─┘
|
||||
~/.bailian/config.json
|
||||
```
|
||||
|
||||
优先级一般是 **flag > env > config 文件 > 默认值**,具体见 `core/config/loader.ts`。
|
||||
优先级一般是 **flag > env > config 文件 > 默认值**,具体见 `packages/core/src/config/loader.ts`。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 类型定义
|
||||
|
||||
- [ ] `packages/core/src/config/schema.ts`:
|
||||
- `Config`(运行时形状)加新字段
|
||||
- `Settings`(运行时有效配置面)加新字段
|
||||
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
|
||||
- `parseConfigFile()` 解析新字段
|
||||
- 如果是 enum 字段,加校验
|
||||
- 如果是数组字段,明确“缺失 / 空数组 / 非法值”的不同语义;安全策略字段的非法值必须 fail closed
|
||||
|
||||
### B. 加载逻辑
|
||||
|
||||
- [ ] `packages/core/src/config/loader.ts:loadConfig()`:
|
||||
- 加新字段的合并逻辑(`flags.x ?? process.env.XXX ?? file.x ?? default`)
|
||||
- [ ] `packages/core/src/config/loader.ts`:
|
||||
- `buildSources()` 如需新增来源,把 flag/file/env 纳入 sources
|
||||
- `buildSettings()` 加新字段的合并逻辑(`flags.x ?? process.env.XXX ?? file.x ?? default`)
|
||||
- 校验(数值范围、枚举合法性等)
|
||||
- 校验失败抛 `BailianError(USAGE)`
|
||||
|
||||
### C. 全局 flag(如果加的是 flag)
|
||||
|
||||
- [ ] `packages/core/src/types/command.ts:GLOBAL_OPTIONS` 数组
|
||||
- [ ] `registry.ts` 的 `buildGlobalFlagLines` 会**自动**从 `GLOBAL_OPTIONS` 生成 `bl --help` 与 `reference/index.md` 的全局 flag 段,无需手写
|
||||
- [ ] flag 的 type 标注(`boolean` / `number` / `array`),让 args.ts 正确解析
|
||||
- [ ] `packages/core/src/types/command.ts:GLOBAL_FLAGS`
|
||||
- [ ] `packages/runtime/src/registry.ts` 会**自动**从 `GLOBAL_FLAGS` 生成 root help;`tools/generate-reference.ts` 会生成 `reference/index.md` 的全局 flag 段
|
||||
- [ ] flag 的 type 标注(`switch` / `boolean` / `number` / `array` / `string`),让 `packages/runtime/src/args.ts` 正确解析
|
||||
- [ ] 改完全局 flag 后跑 `pnpm --filter bailian-cli run generate:reference`
|
||||
|
||||
### D. 命令使用方
|
||||
|
||||
- [ ] 用到新字段的命令文件直接读 `config.xxx`,不要重复解析
|
||||
- [ ] 用到新字段的命令文件直接读 `ctx.settings.xxx`,不要重复解析 env/config
|
||||
- [ ] 配置展示 / 修改命令同步:
|
||||
- `packages/cli/src/commands/config/show.ts` 显示新字段
|
||||
- `packages/cli/src/commands/config/set.ts` 允许 set
|
||||
- `packages/cli/src/commands/config/export-schema.ts` 在 schema 输出里
|
||||
- `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,24 +63,26 @@ config 文件 ─┘
|
||||
- [ ] 单测覆盖优先级:flag > env > file
|
||||
- [ ] 校验失败抛错(非法值)
|
||||
- [ ] 默认值正确
|
||||
- [ ] 数组配置覆盖 CLI 逗号/JSON 输入、Config UI 往返、去重和显式空数组
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 三个来源都试一遍
|
||||
node packages/cli/src/main.ts config show --output json | grep <new-field>
|
||||
XXX=value node packages/cli/src/main.ts config show --output json | grep <new-field>
|
||||
node packages/cli/src/main.ts config show --xxx value --output json | grep <new-field>
|
||||
pnpm -F bailian-cli exec tsx src/main.ts config show --output json | grep <new-field>
|
||||
XXX=value pnpm -F bailian-cli exec tsx src/main.ts config show --output json | grep <new-field>
|
||||
pnpm -F bailian-cli exec tsx src/main.ts config show --xxx value --output json | grep <new-field>
|
||||
|
||||
# 写到文件
|
||||
node packages/cli/src/main.ts config set --key <key> --value <value>
|
||||
# 写到文件(会改用户 HOME,必要时先用临时 HOME)
|
||||
pnpm -F bailian-cli exec tsx src/main.ts config set --key <key> --value <value>
|
||||
cat ~/.bailian/config.json
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ `Config` 接口加字段但 `loadConfig` 没填,运行时永远 undefined
|
||||
- ✗ `Settings` 接口加字段但 `buildSettings` 没填,运行时永远 undefined
|
||||
- ✗ `ConfigFile` 用 camelCase 字段名(disk schema 应该是 snake_case)
|
||||
- ✗ 全局 flag 没标 `type: "boolean"`,被当成需要值的 `--xxx <value>`
|
||||
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
|
||||
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
|
||||
- ✗ `config show` 不显示新字段,用户改了无法回查
|
||||
- ✗ UI 用 `String([])` 把显式空数组渲染为空串,保存后意外关闭安全策略
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Config Profile 与激活状态变更清单
|
||||
|
||||
适用于新增 Profile 预设、修改命名 Profile 选择规则、调整 `active_config`,或新增/修改 `bl config list/use/show/ui` 等 Profile 管理能力。
|
||||
|
||||
## 1. 保持存储边界
|
||||
|
||||
- Profile 业务字段继续由 `ConfigFile` / `CONFIG_FILE_KEYS` 管理。
|
||||
- `active_config` 是 `config.json` 顶层元数据,不得进入命名 Profile block,也不得被 `config set` 当作普通字段写入。
|
||||
- 识别命名 Profile 时必须排除业务字段和顶层元数据。
|
||||
- 旧配置缺少 `active_config` 时继续等价于激活 `default`。
|
||||
|
||||
## 2. 保持选择语义
|
||||
|
||||
```text
|
||||
显式 --config <name> > active_config > default
|
||||
```
|
||||
|
||||
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
|
||||
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
|
||||
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
|
||||
`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 时,仅在凭证实际落盘时
|
||||
创建和激活;`config set --config <name>` 可创建但不自动激活。
|
||||
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
|
||||
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
|
||||
- 按凭证域退出时必须清理该域的完整字段集合,例如 OpenAPI 同时清理 AK、SK 和 STS `security_token`。
|
||||
- 所有生产代码读取“当前配置”时优先经过 `buildSources()` 或携带解析后的 `configName`;直接调用无名称的 `readConfigFile()` / `writeConfigFile()` 只适用于明确操作顶层 default 的底层能力。
|
||||
|
||||
## 4. 保持状态一致性
|
||||
|
||||
- `config use` 只能激活已经存在的命名 Profile;`default` 始终有效。
|
||||
- 配置文件中的 `active_config` 指向不存在的 Profile 时返回 usage error,不静默回退。
|
||||
- 删除当前激活的命名 Profile 时,同一次落盘切回 `default`,不得留下悬空引用。
|
||||
- 配置写入继续使用临时文件 + rename,避免中断后留下半写文件。
|
||||
|
||||
## 5. 命令与展示联动
|
||||
|
||||
- 新增/重命名命令时同步 `packages/commands/src/index.ts` 和产品入口 `packages/cli/src/commands.ts`。
|
||||
- `config list` 标识所有 Profile 与当前激活项。
|
||||
- `config show`、`auth status` 只输出本次最终选择的 `config` 和 `config_file`,不重复携带激活状态。
|
||||
- `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`。
|
||||
- `config ui` 展示并可编辑完整 `ConfigFile`(含 `console_*`、`telemetry`),保存时按类型(数字/布尔/枚举)归一化写回;`config set` 仍只暴露较窄的 `VALID_KEYS`。UI 未管理的顶层元数据(如 `active_config`)不进入 Profile block,仍由写盘逻辑单独保留。
|
||||
- `config ui` 只读展示本地 agent 生态:Skills 跨全部 agent skill 目录(`~/.agents/skills` 及各 agent 的 `skills/`,含软链接)按 id 聚合并标注安装来源;MCP、Agents 从各 agent 本地配置读取。
|
||||
- `config ui` 提供 Assets 资产管理:扫描 `output_dir`(默认 `~/bailian-output`)下的 `images/videos/speech/omni` 分类及根目录散落文件,按分类与生成时间(mtime)标记,支持按分类筛选、内联预览(图/视频/音频)与删除单个文件;文件读取与删除均通过限定在输出目录内的路径校验(防目录穿越)。
|
||||
- 同步 E2E topic routes、Skill setup 和自动生成 reference。
|
||||
|
||||
## 6. 最小测试矩阵
|
||||
|
||||
- 旧配置无 `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` 明确失败。
|
||||
- 删除激活 Profile 后切回 `default`。
|
||||
- 登录、退出、`config set` 分别覆盖“当前激活项”和“显式不存在名称成功后创建”。
|
||||
- 显式 `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` 覆盖常见扩展名映射。
|
||||
|
||||
## 7. 完成检查
|
||||
|
||||
```sh
|
||||
pnpm run sync:skill-assets
|
||||
vp check
|
||||
vp test
|
||||
```
|
||||
|
||||
命令 E2E 会启动本地子进程,Config UI 测试还会监听 `127.0.0.1` 临时端口;受限沙箱内出现 `EPERM` 时,需要在允许本地进程和端口的环境中复跑。
|
||||
@@ -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)
|
||||
@@ -3,8 +3,8 @@
|
||||
## 触发条件
|
||||
|
||||
- 修改 `BailianError` 的 message 或 hint
|
||||
- 调整 cli 的 hint 增强逻辑(`enhanceHint`)
|
||||
- 改 ensure-key 的 setup 流程文案
|
||||
- 调整 runtime 的 hint 增强逻辑(`enhanceHint`)
|
||||
- 改 auth stage / resolver 的鉴权失败文案
|
||||
- 改任何抛错位置的分类(exitCode)
|
||||
|
||||
> 注意:`mapApiError` **不再做错误分类**(参见下方"边界原则")。如果你想给某种 HTTP 错误码加白名单分类,请先回到本文档读完"边界原则"再说。
|
||||
@@ -17,7 +17,7 @@
|
||||
| ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
|
||||
| 命令解析、缺 flag、参数校验 | **内部** | `BailianError(USAGE)` |
|
||||
| 文件 I/O(ENOENT/EACCES/...) | **内部** | `BailianError(GENERAL)` + errno-specific hint |
|
||||
| 本地 credentials 缺失(resolver/ensure-key/AK-SK 等) | **内部** | `BailianError(AUTH)` |
|
||||
| 本地 credentials 缺失(resolver / authStage 等) | **内部** | `BailianError(AUTH)` |
|
||||
| `fetch` 自身失败(DNS/TCP/TLS/proxy) | **内部** | `BailianError(NETWORK)` + 读 `err.cause.code` 给 errno-hint |
|
||||
| polling 客户端超时 | **内部** | `BailianError(TIMEOUT)` |
|
||||
| HTTP 4xx/5xx、HTTP 200 + 业务错码、async task FAILED | **服务** | `BailianError(GENERAL)`,**message 原样透传**,不分类、不替换 |
|
||||
@@ -39,13 +39,14 @@
|
||||
```
|
||||
core 抛出 BailianError(message, exitCode, hint, cause?)
|
||||
↓ 沿调用栈冒泡
|
||||
cli/main.ts: main().catch(handleError)
|
||||
runtime/create-cli.ts: dispatch().catch(handleError)
|
||||
↓
|
||||
cli/error-handler.ts:
|
||||
runtime/error-handler.ts:
|
||||
- 服务端错误(BailianError(GENERAL)) → text 直接打 message
|
||||
- 内部 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)
|
||||
@@ -62,36 +63,42 @@ process.exit(err.exitCode)
|
||||
|
||||
- ❌ 不要回退到"401 → AUTH、429 → QUOTA"那套白名单
|
||||
- ✅ message 把 status / apiCode / request_id 拼进去就够,exit 统一 GENERAL
|
||||
- 例外:CLI **自己**因为本地状态产生的 BailianError(resolver、ensure-key 等)可以用语义化 exitCode
|
||||
- 例外:CLI/runtime **自己**因为本地状态产生的 BailianError(resolver、authStage 等)可以用语义化 exitCode
|
||||
|
||||
### 3. core 的 hint 必须不含 cli 关切
|
||||
|
||||
- ❌ 不写 `bl xxx` 命令名
|
||||
- ❌ 不写控制台 URL 或 region
|
||||
- ❌ 不写渠道追踪参数(`source_channel=xxx`)
|
||||
- ❌ 新增/改动时不写 `bl xxx` 命令名
|
||||
- ❌ 新增/改动时不写 `kscli xxx` 等产品入口命令名
|
||||
- ❌ 新增/改动时不写控制台 URL 或 region
|
||||
- ❌ 新增/改动时不写渠道追踪参数(`source_channel=xxx`)
|
||||
- ✅ 只描述抽象做法(如 `"Set DASHSCOPE_API_KEY environment variable, or pass --api-key."`)
|
||||
- 当前遗留:`packages/core/src/auth/resolver.ts` 仍含 `bl auth login` hint;触碰鉴权错误时迁到 runtime `enhanceHint`
|
||||
|
||||
### 4. cli 端可以自由使用 cli 命令名 + URL
|
||||
### 4. runtime / 产品层可以使用入口名 + URL
|
||||
|
||||
- 命令文件、`error-handler.ts`、`utils/ensure-key.ts` 是 cli 层,内部可以写 `bl xxx`
|
||||
- URL 必须从 `packages/cli/src/urls.ts` import,不能硬编码
|
||||
- `packages/runtime/src/error-handler.ts` 通过 `binName` 渲染 `bl` / `kscli` 等入口名,不要硬编码
|
||||
- 产品入口 / README / E2E 可以写具体入口命令
|
||||
- shared command 实现不写 `bl` / `kscli` 前缀;`usageArgs` / `exampleArgs` 只写参数片段
|
||||
- URL 必须从 `packages/runtime/src/urls.ts` import,不能硬编码
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. core 改动(message / hint)
|
||||
|
||||
- [ ] `packages/core/src/errors/api.ts` 的 `mapApiError`:**保持透传形态**,不要加白名单分支
|
||||
- [ ] `packages/core/src/auth/resolver.ts` 改 throw 语句:hint 不含 cli 关切
|
||||
- [ ] 任何 core 文件 throw 的 BailianError:同上
|
||||
- [ ] `packages/core/src/auth/resolver.ts` 新增/改 throw 语句时:hint 不含 cli 关切;已有 `bl auth login` 遗留点被触碰时要收敛
|
||||
- [ ] 任何 core 文件新增/改 BailianError:同上
|
||||
|
||||
### B. cli 增强(`enhanceHint`)
|
||||
### B. runtime 增强(`enhanceHint`)
|
||||
|
||||
- [ ] `packages/cli/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为只有 resolver/ensure-key 等内部位置会发 AUTH)
|
||||
- [ ] `packages/runtime/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为 resolver / authStage 等内部位置会发 AUTH)
|
||||
- [ ] 命令名使用 `binName`,不要硬编码 `bl`
|
||||
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`
|
||||
|
||||
### C. cli 直接抛错(`ensure-key`、命令文件)
|
||||
### C. command / runtime 直接抛错
|
||||
|
||||
- [ ] cli 层抛 BailianError 时,hint 里可以放 cli 命令名,但 **URL 一律走 `urls.ts` import**
|
||||
- [ ] runtime 层抛 BailianError 时,hint 里可以放 `binName` 渲染的入口命令,但 **URL 一律走 `urls.ts` import**
|
||||
- [ ] `packages/commands` 作为 shared command 库,默认不硬编码产品 bin;如果确需用户操作提示,优先依赖 runtime error handler 或 `ctx.identity.binName`
|
||||
- [ ] 抛错位置如果**已经在调用服务端**,catch 时不要替换 message——重新评估是否需要 catch
|
||||
|
||||
### D. 文案一致性
|
||||
@@ -104,14 +111,14 @@ process.exit(err.exitCode)
|
||||
|
||||
```sh
|
||||
# 触发对应错误,看 text 输出
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive
|
||||
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message "x"
|
||||
|
||||
# 看 JSON 输出(应包含 cause 字段当 cause 存在时)
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive --output json
|
||||
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message "x" --output json
|
||||
|
||||
# 模拟网络层错误,验证 errno 透传
|
||||
DASHSCOPE_BASE_URL=https://nonexistent-host.invalid \
|
||||
node packages/cli/src/main.ts text chat --message hi
|
||||
pnpm -F bailian-cli exec tsx src/main.ts text chat --message hi
|
||||
# 预期:"Network request failed: ENOTFOUND ..." + Caused by 链
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 安装文档变更
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 修改根目录 `INSTALL.md` 的安装、鉴权或验证流程
|
||||
- 修改发布包 Node.js 要求、全局 flag 或安装文档引用的命令
|
||||
- 同步或发布 `https://bailian.aliyun.com/cli/install.md`
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. CLI 契约
|
||||
|
||||
- [ ] `INSTALL.md` 中的 `bl` 命令路径存在于 `packages/cli/src/commands.ts`
|
||||
- [ ] 示例 flag 属于 `GLOBAL_FLAGS`、命令鉴权域 flag 或命令自身 `flags`
|
||||
- [ ] Node.js 用户安装要求与 `packages/cli/package.json` 的 `engines.node` 一致,不使用根 `package.json` 的开发环境要求
|
||||
- [ ] 鉴权流程与 `packages/commands/src/commands/auth/` 的实际校验、保存和 Profile 激活行为一致
|
||||
|
||||
### B. 静态副本
|
||||
|
||||
- [ ] 将 `INSTALL.md` 同步到 `bailian-cli-static-resources/public/install.txt`
|
||||
- [ ] 使用 `cmp -s` 确认两份文档逐字节一致
|
||||
- [ ] 静态资源仓库单独创建分支、提交和发布,不把跨仓库改动遗漏在 CLI PR 之外
|
||||
|
||||
### C. 线上验证
|
||||
|
||||
- [ ] 发布后读取 `https://bailian.aliyun.com/cli/install.md`,确认内容来自最新静态副本
|
||||
- [ ] 带随机 query 参数复查,区分 CDN 缓存与源站未更新
|
||||
- [ ] 验证线上文档中的安装命令、Node.js 要求和配置验证段落,不只检查页面可访问
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli test -- tests/install-doc.test.ts
|
||||
cmp -s INSTALL.md ../bailian-cli-static-resources/public/install.txt
|
||||
curl -L -s "https://bailian.aliyun.com/cli/install.md?verify=$(date +%s)"
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- `--non-interactive` 已从 CLI 移除,但旧安装文档和静态副本仍把它当作全局 flag
|
||||
- 根 `package.json` 是开发工具链 Node.js 要求;用户安装要求以 `packages/cli/package.json` 为准
|
||||
- 静态仓库文件名是 `public/install.txt`,线上稳定地址是 `/cli/install.md`;只更新其中一侧不会自动证明发布成功
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
### A. 版本一致性
|
||||
|
||||
- [ ] `package.json` 的 `engines.node` 与 README 的 Node.js 徽章一致
|
||||
- [ ] 发布包(`cli` 等)的 `engines.node` 与 README 的 Node.js 徽章一致;根/e2e 开发要求(`>=22.12`)与 CONTRIBUTING 一致
|
||||
- [ ] `pnpm-lock.yaml` 同步生成(运行 `pnpm install`)
|
||||
- [ ] 三处 `tsconfig.json`(根 + cli + core)的 target / module 设置一致
|
||||
- [ ] 各源码包 `tsconfig.json`(根 + core + runtime + commands + cli + kscli)的 target / module 设置一致
|
||||
|
||||
### B. lint / format 规则改动
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
|
||||
### C. 构建配置
|
||||
|
||||
- [ ] `packages/cli/vite.config.ts` 和 `packages/core/vite.config.ts` 的 entry / external / dts 设置
|
||||
- [ ] cli 的 bundle 必须把 `bailian-cli-core` 当 **external**(不内联),确认 `dist/bailian.mjs` 第一行有 `from "bailian-cli-core"`
|
||||
- [ ] cli 的 bundle 第一行必须有 `#!/usr/bin/env node` shebang(`tools/release.mjs check` 会断言)
|
||||
- [ ] `packages/*/vite.config.ts` 的 entry / dts / exports 设置符合包类型:
|
||||
- library 包(core/runtime/commands):本地 `exports` 默认指向 `src/index.ts`;`publishConfig.exports` 覆盖发布入口为 `dist/index.mjs`;dts 产物正常生成
|
||||
- binary 包(cli/kscli):entry 指向 `src/main.ts`,有 shebang,`exports: true`
|
||||
- [ ] cli / kscli 的 bundle 必须把 workspace 包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands`)当 **external**(不内联),确认 dist 中仍是 package import
|
||||
- [ ] cli / kscli 的 binary bundle 第一行必须有 `#!/usr/bin/env node` shebang
|
||||
|
||||
### D. 依赖升级
|
||||
|
||||
- [ ] 检查 `bailian-cli-core` 在 cli 的 `dependencies` 里仍是 `"workspace:*"`(不要变成实际版本号 — `tools/release.mjs` 会拦)
|
||||
- [ ] 检查 workspace 内部依赖在 `dependencies` 里仍是 `"workspace:*"`(不要手改成实际版本号;发布时由 pack/publish 流程解析)
|
||||
- [ ] 升级后跑 `vp check && vp test`
|
||||
- [ ] 升级 `@types/node` 时注意 Node API 变化(如 fs.existsSync 行为)
|
||||
|
||||
@@ -40,11 +42,11 @@
|
||||
|
||||
- [ ] `.vite-hooks/pre-commit` 改动后,`pnpm install` 重新软链(走 `prepare: vp config`)
|
||||
- [ ] 增加 hook 时,确认在干净 clone 后能自动激活
|
||||
- [ ] pre-commit 会跑 `pnpm run sync:skill-assets`(先 build core,再 `generate:reference` + `sync:skill-version`)并 `git add` skill 资产,最后 `vp staged`
|
||||
- [ ] pre-commit 会跑 `pnpm run sync:skill-assets`(`generate:reference` 含格式化 + `sync:skill-version`,直接读源码、无需先 build)并 `git add` skill 资产,最后 `vp staged`
|
||||
|
||||
### F. CI / 发版工具
|
||||
|
||||
- [ ] `tools/release.mjs` 中如有版本/规则相关的硬编码,同步更新
|
||||
- [ ] `tools/release/` 中如有版本/规则相关的硬编码,同步更新
|
||||
- [ ] 比如 `secretPatterns` 添加新的敏感值识别
|
||||
|
||||
## 完成后自查
|
||||
@@ -54,13 +56,13 @@
|
||||
pnpm install --frozen-lockfile
|
||||
vp check
|
||||
vp test
|
||||
node tools/release.mjs check
|
||||
node tools/release/check.mjs
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 升级 Node engines 但忘了 README 徽章
|
||||
- ✗ 改 lint 规则后没全仓 `--fix`,新人 PR 报红一片
|
||||
- ✗ 改 cli 的 vite config 把 core 不小心打成 inline,bundle 体积暴涨
|
||||
- ✗ 改 cli/kscli 的 vite config 把 core/runtime/commands 不小心打成 inline,bundle 体积暴涨
|
||||
- ✗ Oxlint 配置改了但 IDE 缓存还是旧的(IDE 可能要重启 ts server)
|
||||
- ✗ 升级依赖一并升 lockfile,改动量大但没拆 commit
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
### Do
|
||||
|
||||
- 写清晰的 **must / must-not / 必查**,不写"建议"性语气
|
||||
- 用 file path + 具体 action 的句式(`packages/cli/src/commands/catalog.ts:增加 import 与 commands 条目`)
|
||||
- 用 file path + 具体 action 的句式(`packages/cli/src/commands.ts:增加产品命令 map 条目`)
|
||||
- 在每份场景末尾留**常见漏点**段,持续累积真实经验
|
||||
- 在跨场景的不变量上互相引用,不复制
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
|
||||
### A. 命令实现
|
||||
|
||||
- [ ] `packages/cli/src/commands/<group>/<action>.ts`:
|
||||
- [ ] `packages/commands/src/commands/<group>/<action>.ts`:
|
||||
- `--model` flag 的 description 里"default:"反映新默认值
|
||||
- 命令内部 `const model = (flags.model as string) || "<default>"` 的 fallback 字符串
|
||||
- 命令内部 `const model = flags.model || settings.defaultXxxModel || "<default>"` 的 fallback 字符串
|
||||
- 如果命令维护一个 supported-models 列表(如 `speech/synthesize.ts:MODEL_VOICES`),增删条目
|
||||
- 如果不同模型有不同 endpoint / 请求体形状,确保 `if (model.startsWith("xxx"))` 分支覆盖
|
||||
- [ ] 模型如有特殊 endpoint,看 `packages/core/src/client/endpoints.ts`
|
||||
- [ ] 如果新增的是某产品入口专属能力,确认 `packages/cli/src/commands.ts` 或其它入口 map 是否需要暴露/隐藏
|
||||
|
||||
### B. 类型层
|
||||
|
||||
@@ -25,7 +26,8 @@
|
||||
|
||||
### C. 命令手册
|
||||
|
||||
- [ ] 若 `--model` 的 description 含 default,改命令后跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/<group>.md` 并提交
|
||||
- [ ] 若 `--model` 的 description 含 default,改命令后跑 `pnpm --filter bailian-cli run generate:reference` 更新对应 `skills/<skill>/reference/<group>.md` 并提交
|
||||
- [ ] 同步**拥有该命令的领域 skill**「When to use which command」表中的 Default model(现主要是 `bailian-gen`;精调相关看 `bailian-finetune` 正文示例)。hub `bailian-cli` 已瘦身,一般**不必**再写领域默认模型(见 [skill-change.md](skill-change.md))
|
||||
|
||||
### D. 用户面文档
|
||||
|
||||
@@ -41,13 +43,14 @@
|
||||
|
||||
```sh
|
||||
# 默认模型走通
|
||||
node packages/cli/src/main.ts <command> --message "test"
|
||||
pnpm -F bailian-cli exec tsx src/main.ts <command> --message "test"
|
||||
# 显式指定新模型
|
||||
node packages/cli/src/main.ts <command> --model <new-model> --message "test"
|
||||
pnpm -F bailian-cli exec tsx src/main.ts <command> --model <new-model> --message "test"
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 改了命令默认模型,但 SKILL.md frontmatter 仍写老型号 → AI agent 调用时仍按老型号宣传
|
||||
- ✗ 改了命令默认模型,但 SKILL.md frontmatter 或领域路由表 Default model 仍写老型号 → AI agent 调用时仍按老型号宣传
|
||||
- ✗ 只改了 `reference/` / flag description,忘改 `bailian-gen`(等) SKILL 路由表
|
||||
- ✗ 废弃模型时只删了代码,e2e 测试还在跑,CI 红
|
||||
- ✗ 新模型 endpoint 不一致,但只改了 default,没加 endpoint 分支判断
|
||||
|
||||
+81
-38
@@ -1,70 +1,107 @@
|
||||
# 发布(npm publish)
|
||||
# 发布(npm + GitHub Release 二进制)
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 准备发布 channel(beta/mcp/plugin 等)或正式版到 npm
|
||||
- 准备打 git tag
|
||||
- 准备发布 channel(mcp/plugin 等)或正式版到 npm **与** GitHub Releases 二进制
|
||||
- 准备打 git tag(仅 stable)
|
||||
|
||||
## 发布方式:GitHub Actions + npm OIDC
|
||||
## 发布方式:GitHub Actions 总入口
|
||||
|
||||
发版**必须**通过 CI 完成,不要本地手动 `pnpm publish`。
|
||||
|
||||
入口:GitHub Actions → **Publish** workflow(`.github/workflows/publish.yml`)→ Run workflow。
|
||||
|
||||
**编排关系(重要):**
|
||||
|
||||
```text
|
||||
publish-stable.mjs / publish-channel.mjs ← 唯一发版入口
|
||||
├─ npm(pnpm publish)
|
||||
└─ binary(lib/binary-release
|
||||
→ binary-build
|
||||
→ gh-release
|
||||
→ oss-direct-upload)
|
||||
```
|
||||
|
||||
`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#发布)。
|
||||
|
||||
两种模式:
|
||||
|
||||
| 模式 | 用途 | 触发方式 |
|
||||
| ------- | ------------------------------ | -------------------------------------------------- |
|
||||
| channel | 发 channel 版本到指定 dist-tag | 选 mode=channel,填 dist-tag 名称(如 mcp/plugin) |
|
||||
| stable | 正式发版到 latest | 选 mode=stable,需 production environment 审批 |
|
||||
| 模式 | 用途 | 触发方式 |
|
||||
| ------- | --------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||
| channel | npm dist-tag +(仅 bailian-cli)二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channel,channel 填 **npm dist-tag** 名 |
|
||||
| stable | npm latest + GitHub Release `v<ver>` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 |
|
||||
|
||||
可选 flag:`--skip-binary`(仅发 npm,紧急逃生)。
|
||||
|
||||
### CDN 滚动指针(bailian-cli)
|
||||
|
||||
| 发布模式 | CDN 指针 | 本机安装 / 更新 |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------------------- |
|
||||
| channel | 始终覆盖 `sync-release.json` | `BAILIAN_CHANNEL=sync-release` / `install --channel sync-release` |
|
||||
| stable | `manifest.json`(+ `latest.json`) | 默认安装 / `bl update`(无 channel) |
|
||||
|
||||
workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` / `sync-release`),**不再**生成 `release-test.json` 这类旁路文件。
|
||||
|
||||
### channel 发布
|
||||
|
||||
1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 dist-tag 名(如 `mcp`)
|
||||
2. CI 自动:生成 `0.0.0-beta-<sha7>-<date>` 版本号 → 自检 → 构建 → 发布到指定 dist-tag
|
||||
1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名:
|
||||
- **`bailian-cli`**:npm 发到该 tag;二进制同时刷新 CDN `sync-release.json`(与 tag 名无关)。本机验证:`BAILIAN_CHANNEL=sync-release`
|
||||
- **`knowledge-studio-cli`**:仅 npm(自动跳过 binary,不碰 `sync-release.json`)
|
||||
2. CI 自动:生成 `0.0.0-beta-<sha7>-<YYYYMMDDHHMM>`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json
|
||||
3. 对应脚本:`tools/release/publish-channel.mjs`
|
||||
|
||||
### stable 发布
|
||||
|
||||
1. 确保 `packages/cli/package.json` 和 `packages/core/package.json` 已升到目标版本且一致
|
||||
2. 在 GitHub 触发 Publish workflow,mode 选 `stable`
|
||||
1. 确保当前 release tooling 覆盖的包(`tools/release/lib/packages.mjs`)已升到目标版本且一致;当前基础集合为 `packages/core` / `packages/runtime` / `packages/commands` / `packages/cli`,`knowledge-studio-cli` 发布会额外包含 `packages/kscli`
|
||||
2. 在 GitHub 触发 Publish workflow,package 选目标包集合,mode 选 `stable`
|
||||
3. 需要 production environment 审批人批准
|
||||
4. CI 自动:自检 → 构建 → 发布到 latest → 打 git tag
|
||||
5. 对应脚本:`tools/release/publish-stable.mjs`
|
||||
4. CI 自动:自检 → **npm 发到 latest** → **推送 git tag `v<ver>`** → **Bun 编二进制并创建/更新 GitHub Release** →(bailian-cli)维护 CDN **`manifest.json`** → 完成
|
||||
5. 如果所选发布集合的当前版本已全部存在于 npm,stable 发布会失败并提示先升级版本号;如果只有部分包已发布,CI 会继续补发缺失包
|
||||
6. 对应脚本:`tools/release/publish-stable.mjs`
|
||||
|
||||
## 自检(`tools/release/check.mjs`)
|
||||
|
||||
两种模式都会先跑 `check.mjs`,覆盖以下检查:
|
||||
|
||||
| 检查项 | 说明 |
|
||||
| -------------------------------- | ----------------------------------------- |
|
||||
| `pnpm install --frozen-lockfile` | lockfile 一致性 |
|
||||
| README 同步 | `packages/cli/README.md` 与根 README 一致 |
|
||||
| 版本号一致 | cli 与 core 的 version 字段相同 |
|
||||
| `workspace:*` 替换 | cli 对 core 的依赖解析为真实版本号 |
|
||||
| 构建 core + cli | `pnpm build` |
|
||||
| pnpm pack | 打 tarball |
|
||||
| publint | 包元数据校验 |
|
||||
| gitleaks | 敏感信息扫描 |
|
||||
| 检查项 | 说明 |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `pnpm install --frozen-lockfile` | lockfile 一致性 |
|
||||
| README 同步 | `packages/cli/README.md` 与根 README 一致 |
|
||||
| 版本号一致 | `tools/release/lib/packages.mjs` 中待发布包集合 version 相同 |
|
||||
| `workspace:*` 替换 | 发布包间 workspace 依赖解析为真实版本号 |
|
||||
| 构建 | 基础发布构建 core/runtime/commands 依赖和 cli;`--knowledge` 额外构建 `knowledge-studio-cli` |
|
||||
| 生成资产 | 重建各 `skills/<skill>/reference/`;非 channel 模式还同步各 `skills/*/SKILL.md` version(含 `bailian-protocol`) |
|
||||
| pnpm pack | 打 tarball |
|
||||
| publint | 包元数据校验 |
|
||||
| gitleaks | 敏感信息扫描 |
|
||||
|
||||
本地可以 dry-run 验证:
|
||||
|
||||
```sh
|
||||
node tools/release/publish-channel.mjs --channel test --dry-run
|
||||
node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
|
||||
```
|
||||
|
||||
## CI 基础设施
|
||||
|
||||
- **认证**:npm OIDC Trusted Publishing(无 token),需要 `id-token: write` 权限
|
||||
- **GitHub Release**:`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`(stable / channel 均需)
|
||||
- **Node 版本**:24(npm 11.5+ 才支持 OIDC token 交换)
|
||||
- **Bun**:`oven-sh/setup-bun`,版本钉死在 workflow 中
|
||||
- **Actions 版本**:checkout/setup-node/pnpm-action 均为 v6(Node 24 兼容)
|
||||
- **npm 配置**:两个包的 Trusted Publisher 都指向 `modelstudioai/cli` 的 `publish.yml`,environment 留空
|
||||
- **npm 配置**:当前 release tooling 发布的包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands` / `bailian-cli` / `knowledge-studio-cli`)的 Trusted Publisher 指向 `modelstudioai/cli` 的 `publish.yml`;新增发布包时同步 npm Trusted Publisher
|
||||
|
||||
## `check.mjs` 不覆盖的(手动确认)
|
||||
|
||||
### 版本号目标(仅 stable)
|
||||
|
||||
- [ ] `packages/cli/package.json` 和 `packages/core/package.json` 已升到目标版本
|
||||
- [ ] `tools/release/lib/packages.mjs` 覆盖的目标包集合已升到目标版本且一致
|
||||
- [ ] 源码包 `packages/core/package.json`、`packages/runtime/package.json`、`packages/commands/package.json`、`packages/cli/package.json`、`packages/kscli/package.json` 是否需要同步升版已人工确认;当前仓库通常保持五包版本一致
|
||||
- [ ] `tools/release/lib/packages.mjs` 的 `PACKAGES` 覆盖基础发布包;`KSCLI_PACKAGE` / `ALL_PACKAGES` 覆盖 `knowledge-studio-cli` 发布路径;如果新增发布包,同步 `publish-stable.mjs` / `publish-channel.mjs` 的 bump、publish、idempotency 逻辑和 `.github/workflows/publish.yml` 的 package 选项
|
||||
- [ ] pre-release 格式正确(`1.0.0-beta.0` / `1.0.0-rc.1`,**不要直接用 `1.0.0` 当 beta**)
|
||||
|
||||
### CHANGELOG(仅 stable)
|
||||
@@ -78,21 +115,27 @@ node tools/release/publish-channel.mjs --channel test --dry-run
|
||||
- [ ] `README.md` / `README.zh.md` 的 Quick Start 命令仍能跑通
|
||||
- [ ] README 的 Node.js 徽章版本与 `cli/package.json.engines.node` 一致
|
||||
- [ ] README 宣传的 bin 名称在 `cli/package.json.bin` 都真的注册
|
||||
- [ ] `LICENSE` 文件存在(根 + cli + core 各一份)
|
||||
- [ ] `packages/kscli/README.md` / `README.zh.md` 与 `knowledge-studio-cli` 的 bin、控制台 URL、认证方式一致
|
||||
- [ ] `LICENSE` 文件存在(根 + 当前实际发布包;新增发布包时补该包 LICENSE)
|
||||
|
||||
## 完成后
|
||||
|
||||
- [ ] 验证 npm 上能装:`npm view bailian-cli@<tag> version`
|
||||
- [ ] 试装一次:`npm i -g bailian-cli@<tag> && bl --version`
|
||||
- [ ] 验证 npm 上能装:`npm view bailian-cli@<tag> version`;如发布 `knowledge-studio-cli`,同时 `npm view knowledge-studio-cli@<tag> version`
|
||||
- [ ] 试装一次:`npm i -g bailian-cli@<tag> && bl --version`;如发布 `knowledge-studio-cli`,同时 `npm i -g knowledge-studio-cli@<tag> && kscli --version`
|
||||
|
||||
## 常见漏点(基于历史踩坑)
|
||||
|
||||
| 漏点 | 后果 |
|
||||
| -------------------------------------------------------- | -------------------------------------------------- |
|
||||
| cli 升版号但 core 没升 | check.mjs 会拦下 |
|
||||
| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 |
|
||||
| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 |
|
||||
| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` |
|
||||
| Node 徽章 `>=18`、engines `>=22.12` 不一致 | 用户在 Node 18 上 `npm i` 被 engine 警告或直接失败 |
|
||||
| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 |
|
||||
| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 |
|
||||
| 漏点 | 后果 |
|
||||
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli |
|
||||
| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 |
|
||||
| cli 升版号但 core 没升 | check.mjs 会拦下 |
|
||||
| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 |
|
||||
| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 |
|
||||
| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` |
|
||||
| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17`) | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 |
|
||||
| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 |
|
||||
| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 |
|
||||
| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 |
|
||||
| channel job 缺少 `contents: write` | `gh release create` 失败 |
|
||||
| stable 未先推 tag 就建 Release | `--verify-tag` 失败 |
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Skill 文案 / 路由 / 安装约定
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 改 `skills/*/SKILL.md` 的 description、路由表、consent、安全闸、hand-off、references 落款
|
||||
- 调整 `bailian-protocol` 与业务 skill 的关系,或业务 skill 之间的软 hand-off 约定
|
||||
- 新增 / 拆分 / 合并 `bailian-*` 业务 skill,或改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL` 归属(与命令增删改交叉时两边都看)
|
||||
- 给业务 skill 补安装说明、README,或统一「勿猜 flag → `reference/`」类约定
|
||||
|
||||
纯改生成物 `skills/*/reference/*.md`(由命令 metadata 驱动)→ 走 [command-add-remove.md](command-add-remove.md) / [command-flag-change.md](command-flag-change.md),**不要手改 reference**。
|
||||
|
||||
## 统一口径(安装)
|
||||
|
||||
1. **Supported install:** `bl skill init`(装齐 registry 中全部 `bailian-*`,含 `bailian-protocol`)
|
||||
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它
|
||||
3. **不要**在 frontmatter 写 `companions`,也不要对外说「companions = 安装器硬依赖」
|
||||
4. 子集安装:`bl skill add --name bailian-protocol,<skill>`;漏装 protocol 会导致相对路径 Read 失败
|
||||
5. **`bl skill add --all`:** 安装 registry 全量(含 `spark-video` 等非 bailian 技能);一键安装 / `bl update` 用 `skill init`,不要用 `--all`
|
||||
|
||||
## 概念图
|
||||
|
||||
```text
|
||||
bailian-protocol ← 共享协议(consent / 鉴权 / 版本 / 错误上报)
|
||||
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
|
||||
│
|
||||
┌───────┴────────┬────────────────┬──────────────────┬───────────────────┐
|
||||
bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
|
||||
(领域路由表) (领域工作流) (IaC 安全闸) (搜索路由+兜底)
|
||||
│ │ │ │
|
||||
└────────────────┼──────────────────┴─────────────────────┘
|
||||
▼ 软 hand-off(按 skill 名)
|
||||
bailian-cli(hub)
|
||||
hub 路由表:本职命令 + 领域 hand-off 行
|
||||
细节 → 各 skill reference/(生成)
|
||||
```
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 分层边界
|
||||
|
||||
- [ ] **整包装齐**:安装/升级文案主推 `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」指向句
|
||||
|
||||
### B. 文案与落款一致性
|
||||
|
||||
- [ ] 领域 skill(gen / finetune / managed-agent)路由或命令表后有指向 `reference/` 的句;文末 `## references`(protocol + reference)与家族对齐
|
||||
- [ ] description 含 WHAT + WHEN + 反触发;安装说明指向 `bl skill init`,不写 companions 必装
|
||||
- [ ] Quick examples 只演示本 skill 职责(hub 不示范 `bl image` / `bl video` 等)
|
||||
- [ ] 若改了安装方式:同步 `README.md` / `README.zh.md` / `INSTALL.md` / `skills/*/README*` / `skills/bailian-protocol/assets/setup.md` 中的 `bl skill init` / `bl skill add …` 示例(改 `INSTALL.md` 时按 [install-doc-change.md](install-doc-change.md) 同步静态页)
|
||||
|
||||
### C. 归属与生成
|
||||
|
||||
- [ ] 新一级命令组归属领域时:改 `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) 一并核对
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
pnpm run sync:skill-assets
|
||||
# 已发布版本试装
|
||||
bl skill init
|
||||
```
|
||||
|
||||
抽查:打开 `skills/bailian-cli/SKILL.md` 确认无领域子命令明细表、无 `companions`;打开对应领域 skill 确认有「勿猜 flag」与 hand-off。
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ hub 路由表再次抄回 image / video / finetune / managed-agent 明细 → token 膨胀且与领域 skill 双份漂移
|
||||
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 `bl skill add` 合同不符
|
||||
- ✗ 软 hand-off 写成硬路径 `../bailian-*/SKILL.md` 当执行前提 → 子集安装断链
|
||||
- ✗ 只改 SKILL、忘改 `GROUP_OWNER_SKILL` → reference 落错 skill
|
||||
- ✗ 手改 `skills/*/reference/*.md` → 下次 generate 被覆盖
|
||||
- ✗ 改默认模型只动 flag description / reference,忘改领域 SKILL「When to use which command」表(见 [model-add-remove.md](model-add-remove.md))
|
||||
@@ -115,19 +115,18 @@ pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3
|
||||
|
||||
### 子进程调用方式
|
||||
|
||||
- **实际执行**:`node packages/cli/src/main.ts <args>`,`cwd` 为 `packages/cli`
|
||||
- **实际执行**:仓库本地 `tsx src/main.ts <args>`,`cwd` 为 `packages/cli`
|
||||
- **禁止**用 `pnpm run dev` 跑子任务:`pnpm` 会向 stdout 打生命周期日志,污染 JSON 解析
|
||||
- **报告中的「完整命令」**:用 `pnpm run dev ...` 展示(`buildDisplayCommand`)
|
||||
|
||||
### 必须带的 CLI 参数(通用)
|
||||
|
||||
- `--non-interactive`
|
||||
- 除 `speech recognize` 外,压测子进程宜带 `--output json`(语音识别以 `--out` 文件为准 stdout 可能为纯文本)
|
||||
- 异步类命令带 `--timeout`、对应 `--poll-interval`
|
||||
|
||||
**禁止**对子进程加 `--quiet`(与 `--output json` 并存时可能丢 `urls` / `video_url`)。
|
||||
|
||||
**禁止**对视频相关子进程加 `--no-wait`;须阻塞到任务完成(及下载路径正确时落盘)。
|
||||
**禁止**对视频相关子进程加 `--async`;须阻塞到任务完成(及下载路径正确时落盘)。
|
||||
|
||||
### 成功 / 失败判定(概要)
|
||||
|
||||
@@ -192,8 +191,8 @@ pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3
|
||||
### 只改压测脚本时
|
||||
|
||||
- [ ] `lib/paths.mjs` 解析的 `CLI_PACKAGE` / `MONOREPO_ROOT` 仍正确
|
||||
- [ ] 子进程仍为 `node` + `src/main.ts`,未改回裸 `pnpm run dev` 执行任务
|
||||
- [ ] 未对子进程加 `--quiet`,视频未加 `--no-wait`
|
||||
- [ ] 子进程仍为仓库本地 `tsx` + `src/main.ts`,未改回裸 `pnpm run dev` 执行任务
|
||||
- [ ] 未对子进程加 `--quiet`,视频未加 `--async`
|
||||
- [ ] `parsers.mjs` 与文档中的成功判定一致
|
||||
- [ ] 根 `package.json` 仅保留 `test:stress` 入口指向 `run.mjs`
|
||||
- [ ] `node --check` 对相关 `.mjs` 通过,`pnpm run test:stress -- list` 可运行
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# 埋点变更
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 调整 AEM 命令事件、事件字段或参数 allowlist
|
||||
- 调整 `User-Agent`、`x-dashscope-source-config` 或其他后端渠道标识
|
||||
- 新增鉴权域、请求网关或绕开统一 Client 的网络出口
|
||||
- 排查命令量、成功率、版本、鉴权域或后端渠道数据不一致
|
||||
|
||||
## 当前数据流
|
||||
|
||||
三套鉴权对应三套请求域,但不代表三套网关使用相同的后端埋点。命令侧另有一套覆盖所有实际执行命令的 AEM 客户端事件,两者必须分开理解。
|
||||
|
||||
```text
|
||||
命令进入 run
|
||||
├─ telemetryStage
|
||||
│ ├─ ~/.bailian/telemetry.jsonl
|
||||
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
|
||||
│
|
||||
└─ confirmationStage
|
||||
└─ versionCheckStage → authStage
|
||||
├─ apiKey → DashScope / 模型域
|
||||
├─ console → Bailian Console Gateway
|
||||
├─ openapi → 阿里云 OpenAPI
|
||||
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
|
||||
```
|
||||
|
||||
### 1. 三套鉴权与埋点标识
|
||||
|
||||
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(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` | 有: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` |
|
||||
|
||||
`authMethod` 记录的是命令声明的鉴权域,不是凭证来源。它不会区分 API Key 来自 flag、env 还是 config。
|
||||
鉴权域是命令的准入门槛和主请求域,不保证命令内部只有一种网络出口;例如部分 `apiKey` 命令也可能读取匿名 Console 公共目录,Managed Agent 还可能访问其他 provider。
|
||||
|
||||
表中的后端埋点按该鉴权域的主要业务请求填写:
|
||||
|
||||
- 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. 后端渠道参数
|
||||
|
||||
当前 `x-dashscope-source-config` 结构为:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "bailian-cli",
|
||||
"tags": {
|
||||
"t1": "public",
|
||||
"t2": "bl 或 kscli",
|
||||
"t3": "实际 CLI 版本"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `t2` 取产品 `identity.binName`:完整 CLI 为 `bl`,Knowledge Studio CLI 为 `kscli`
|
||||
- `t3` 取产品 `identity.version`,由产品入口的 `package.json` 注入
|
||||
- `channel` 与 `t1` 是当前固定口径
|
||||
- `User-Agent` 是独立标识:`bl` 为 `bailian-cli/<version>`,`kscli` 为 `knowledge-studio-cli/<version>`
|
||||
|
||||
source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网络传输:
|
||||
|
||||
| 请求 | source-config |
|
||||
| ------------------------------------ | ------------- |
|
||||
| 模型 API、任务提交与轮询 | 有 |
|
||||
| Bailian MCP / OpenAPI | 有 |
|
||||
| DashScope 上传策略 `getPolicy` | 有 |
|
||||
| OSS 文件上传 | 无 |
|
||||
| 图片、视频、音频、转录结果下载 | 无 |
|
||||
| npm / 二进制更新检查、Skill registry | 无 |
|
||||
|
||||
当前已知例外:Pipeline runtime 自建的 `Identity.version` 为 `0.0.0-dev`,因此 Pipeline 内部模型请求的 `t3` 不代表产品包版本;现阶段不纳入本轮收敛。
|
||||
|
||||
### 3. 全命令 AEM 客户端埋点
|
||||
|
||||
`packages/runtime/src/middleware.ts` 的 `telemetryStage` 包裹确认闸门、`authStage` 与命令执行,因此成功、确认未通过、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`。确认闸门仍位于版本检查、鉴权和业务执行之前,不会因为埋点而放行高风险操作。
|
||||
|
||||
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`:
|
||||
|
||||
- 根帮助、子命令 `--help`、`--version`
|
||||
- 未识别命令、参数解析失败、缺少必填参数
|
||||
- `defineCommand.validate` 在 dispatch 阶段拒绝的请求
|
||||
|
||||
遥测默认开启;`DO_NOT_TRACK=1` 一票否决,配置文件 `telemetry: false` 也可关闭。关闭后本地和远端均不记录。
|
||||
|
||||
单条 `TrackingEvent` 当前包含:
|
||||
|
||||
- `command`、`timestamp`、`durationMs`、`success`
|
||||
- `cliVersion`、`nodeVersion`、`os`
|
||||
- `authMethod`
|
||||
- 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId`
|
||||
- 安全 allowlist 过滤后的 `params`
|
||||
|
||||
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
|
||||
|
||||
事件同时写入两处:
|
||||
|
||||
1. 本地 `~/.bailian/telemetry.jsonl`:权限 `0600`,超过 5 MB 后重建
|
||||
2. AEM:`pid=bailian-cli-node`,源码运行自动使用 `env=dev`,npm 安装或编译二进制使用 `env=prod`
|
||||
|
||||
底层 Node tracker 还会附加公共设备字段:OS 类型/版本、Node 应用名与版本、平台,以及由本机网络标识计算的 MD5 `device_id`。
|
||||
|
||||
当前 AEM 事件没有 `binName` 或 `clientName` 产品维度,并且 `bl`、`kscli` 共用 `pid=bailian-cli-node`。两边相同路径的 `config show`、`config set`、`update` 无法仅凭当前事件稳定区分产品;Knowledge 命令虽然因路径映射不同而表现为 `knowledge chat` 与 `chat`,也不应把命令路径当作长期产品标识。后端 source-config 的 `t2` 已能区分 `bl/kscli`,但这个维度尚未进入 AEM 客户端事件。
|
||||
|
||||
AEM 映射:
|
||||
|
||||
| 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。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 新增或调整命令
|
||||
|
||||
- [ ] `defineCommand({ auth })` 必须声明真实请求域;AEM 的 `authMethod` 直接读取该值
|
||||
- [ ] 新命令进入 `run` 后自动有基础事件,不得在命令内重复发送同名事件
|
||||
- [ ] 需要按产品分析 AEM 数据时,必须显式设计产品字段;不得从命令路径推断 `bl/kscli`
|
||||
- [ ] 只有可枚举、数值或布尔等低风险字段才可加入 `PARAM_ALLOWLIST`
|
||||
- [ ] 新增 console raw API flag 时只允许记录公开 API 名,不得记录请求 `data`
|
||||
|
||||
### B. 调整后端渠道参数
|
||||
|
||||
- [ ] 同时核对 `packages/core/src/client/http.ts`、`mcp.ts`、`instrumented-fetch.ts`、`client.ts` 与 `files/upload.ts`
|
||||
- [ ] 产品身份必须来自 `Identity`;不得从命令路径、环境变量或 `process.argv` 猜测
|
||||
- [ ] `bl` 与 `kscli` 必须分别验证 `binName`、`clientName`、`version`
|
||||
- [ ] OSS、结果文件、npm、二进制和 Skill 下载不得为了业务渠道统计新增 source-config
|
||||
- [ ] 改 URL / host 范围时同时执行 [URL / 渠道变更](url-change.md) 清单
|
||||
|
||||
### C. 调整 AEM 事件
|
||||
|
||||
- [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射
|
||||
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
|
||||
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
|
||||
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5`
|
||||
- [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口
|
||||
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
rg -n "trackingHeaders|x-dashscope-source-config|User-Agent" packages --glob '*.ts'
|
||||
rg -n "trackCommandExecution|PARAM_ALLOWLIST|buildRemoteAemOptions" packages/core packages/runtime --glob '*.ts'
|
||||
vp check
|
||||
vp test packages/core/tests packages/commands/tests/e2e/auth.e2e.test.ts
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 只看 AEM 命令事件,误以为它能替代网关侧请求渠道统计
|
||||
- ✗ 把 `authMethod` 当成实际凭证来源;它只是命令声明的鉴权域
|
||||
- ✗ 新增 bypass `fetch` 后漏掉应由网关消费的 source-config,或把它发给 OSS / npm / 第三方下载地址
|
||||
- ✗ 只改 `bl` 入口,导致 `kscli` 的产品名或版本标签错误
|
||||
- ✗ 把帮助、版本或参数校验失败算进“全部命令”;这些路径当前没有进入 telemetry middleware
|
||||
+18
-13
@@ -13,12 +13,15 @@
|
||||
core/config/schema.ts ← API endpoint / 文档站(region-aware)
|
||||
REGIONS{cn, us, intl} dashscope.aliyuncs.com 等
|
||||
DOCS_HOSTS{cn, us, intl} help.aliyun.com/zh/model-studio
|
||||
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (POP API)
|
||||
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (OpenAPI)
|
||||
|
||||
cli/src/urls.ts ← 用户面控制台 URL(cn-only)
|
||||
runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
|
||||
BAILIAN_CONSOLE_ROOT bailian.console.aliyun.com
|
||||
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
|
||||
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
|
||||
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
|
||||
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
|
||||
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
|
||||
|
||||
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
|
||||
@@ -29,14 +32,16 @@ core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||
### A. TS 源码(必须 import,不准硬编码)
|
||||
|
||||
- [ ] `packages/core/src/config/schema.ts` 是所有 API/docs 基址的源头
|
||||
- [ ] `packages/cli/src/urls.ts` 是所有用户面控制台 URL 的源头
|
||||
- [ ] `packages/runtime/src/urls.ts` 是所有用户面控制台 URL 的源头
|
||||
- [ ] 改完后 grep 验证:
|
||||
|
||||
```sh
|
||||
# 控制台 URL — 应只在 urls.ts 出现
|
||||
grep -rnE "https://bailian\.console\.aliyun\.com" packages/ --include="*.ts" \
|
||||
| grep -v "node_modules" | grep -v "/dist/"
|
||||
# 期望:只匹配 packages/cli/src/urls.ts
|
||||
# 期望:匹配 packages/runtime/src/urls.ts;
|
||||
# 当前遗留例外:packages/commands/src/commands/auth/login-console.ts(登录站点映射)、
|
||||
# packages/core/src/advisor/recommend.ts(模型文档 deep link)。触碰时优先收敛到统一 URL 模块。
|
||||
|
||||
# API endpoint — 应只在 schema.ts 和 upload.ts 出现
|
||||
grep -rnE "https://dashscope[a-z-]*\.aliyuncs\.com" packages/ --include="*.ts" \
|
||||
@@ -46,29 +51,29 @@ grep -rnE "https://dashscope[a-z-]*\.aliyuncs\.com" packages/ --include="*.ts" \
|
||||
|
||||
### B. 非 TS 文件(只能人工同步,无法 import)
|
||||
|
||||
- [ ] `skills/bailian-cli/reference/` 各 `<group>.md` 中 API/控制台 URL(`generate:reference` 重建后核对并提交)
|
||||
- [ ] `skills/*/reference/` 各 `<group>.md` 中 API/控制台 URL(`generate:reference` 重建后核对并提交)
|
||||
- [ ] `README.md` / `README.zh.md` 中所有 URL
|
||||
|
||||
### C. 渠道追踪参数
|
||||
|
||||
- [ ] **当前现状**:全仓不带 `source_channel=aliway` 等追踪参数
|
||||
- [ ] 如未来要恢复以收集分析数据,**统一评估再加回**(不要单点恢复造成不一致)
|
||||
- [ ] 全仓 grep `source_channel=`,确认无残留
|
||||
- [ ] **当前现状**:TS 源码不带 `source_channel=...`;README / package README 中保留 `cli_github` / `key_github` 等用户入口追踪参数
|
||||
- [ ] 如未来调整追踪参数,统一评估 README、`packages/cli/README*`、`packages/core/README*` 与 package homepage,不要单点改造成不一致
|
||||
- [ ] grep `source_channel=`,确认每个残留都属于预期用户面文档或已批准的追踪入口
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 验证错误 hint 不再泄漏旧 URL
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message x --non-interactive
|
||||
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message x
|
||||
# 看输出的 Get API Key URL 是否走新值
|
||||
|
||||
# 验证 banner / help
|
||||
node packages/cli/src/main.ts # banner
|
||||
node packages/cli/src/main.ts help # help 命令
|
||||
pnpm -F bailian-cli exec tsx src/main.ts # banner
|
||||
pnpm -F bailian-cli exec tsx src/main.ts help # help 命令
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 改了 `urls.ts` 但忘记同步 README(用户最先看到)
|
||||
- ✗ 在 cli 命令文件里 inline `https://bailian.console.aliyun.com/...` 而不是 `${API_KEY_PAGE}`
|
||||
- ✗ 改了 `urls.ts` / 登录站点 / 文档 deep link 但忘记同步 README(用户最先看到)
|
||||
- ✗ 在 runtime / command 文件里 inline `https://bailian.console.aliyun.com/...` 而不是从 `urls.ts` import
|
||||
- ✗ 在 core 的 hint 里写 URL(违反 [error-hint-change.md](error-hint-change.md) 不变量 1)
|
||||
|
||||
@@ -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)
|
||||
@@ -1,438 +0,0 @@
|
||||
# 模型训练 + 数据集 + 部署:最小闭环 CLI 设计
|
||||
|
||||
> 目标:一个 Qwen 文本模型 SFT 训练、数据集上传、模型部署的端到端最小链路。
|
||||
|
||||
---
|
||||
|
||||
## 一、命令概览
|
||||
|
||||
| 优先级 | 命令 | 映射 API | 用途 |
|
||||
| ------ | ----------------------------------- | --------------------------------------------- | ------------------------------- |
|
||||
| P0 | `bl dataset upload <path>` | `POST /api/v1/files` | 上传训练数据(含本地格式校验) |
|
||||
| P0 | `bl finetune create` | `POST /api/v1/fine-tunes` | 创建 SFT 训练任务(预填默认超参) |
|
||||
| P0 | `bl finetune status <job_id>` | `GET /api/v1/fine-tunes/{job_id}` | 查询训练状态 |
|
||||
| P0 | `bl deploy create` | `POST /api/v1/deployments` | 部署训练好的模型 |
|
||||
| P1 | `bl finetune logs <job_id>` | `GET /api/v1/fine-tunes/{job_id}/logs` | 拉取训练日志 |
|
||||
| P1 | `bl finetune checkpoints <job_id>` | `GET /api/v1/fine-tunes/{job_id}/checkpoints` | 查看/挑选 Checkpoint |
|
||||
| P1 | `bl deploy status <deployed_model>` | `GET /api/v1/deployments/{deployed_model}` | 查询部署状态 |
|
||||
| P1 | `bl deploy delete <deployed_model>` | `DELETE /api/v1/deployments/{deployed_model}` | 下线部署 |
|
||||
| P1 | `bl infer --model <deployed_model>` | 复用 `text chat` 通路 | 调用已部署模型 |
|
||||
|
||||
---
|
||||
|
||||
## 二、P0 命令详细设计
|
||||
|
||||
### 2.1 `bl dataset upload`
|
||||
|
||||
**定位:** 上传训练数据文件到百炼平台,获取 `file_id` 供训练任务引用。
|
||||
|
||||
#### CLI 签名
|
||||
|
||||
```
|
||||
bl dataset upload <path> [--purpose fine-tune] [--validate] [--no-validate]
|
||||
```
|
||||
|
||||
| Flag | 必填 | 默认值 | 说明 |
|
||||
| --------------- | ---- | ----------- | ------------------------------ |
|
||||
| `<path>` | 是 | — | 本地文件路径(.jsonl 或 .zip) |
|
||||
| `--purpose` | 否 | `fine-tune` | 文件用途标签 |
|
||||
| `--validate` | 否 | `true` | 上传前执行本地格式校验 |
|
||||
| `--no-validate` | 否 | — | 跳过本地校验 |
|
||||
|
||||
#### 本地格式校验规则(提交前拦截)
|
||||
|
||||
校验逻辑在 `packages/core` 实现(纯函数),CLI 调用后展示错误:
|
||||
|
||||
1. **文件格式检查**:仅允许 `.jsonl` 和 `.zip`(zip 内根目录必须有 `data.jsonl`)
|
||||
2. **JSONL 逐行校验**:
|
||||
- 每行可被 `JSON.parse`
|
||||
- 顶层必须包含 `messages` 数组
|
||||
- `messages` 中每项必须包含 `role`(枚举:`system` | `user` | `assistant`)和 `content`(非空字符串)
|
||||
- 至少包含一条 `user` + 一条 `assistant` 消息
|
||||
3. **数量校验**:SFT 训练至少需要上千条数据(给出 warning 而非 hard fail,阈值建议 ≥ 10 条 hard fail)
|
||||
4. **文件体积**:≤ 300MB
|
||||
|
||||
#### 校验失败输出示例
|
||||
|
||||
```
|
||||
✗ Validation failed:
|
||||
|
||||
Line 3: missing "messages" field
|
||||
Line 7: role "bot" is not valid (expected: system | user | assistant)
|
||||
Line 12: "content" is empty string
|
||||
|
||||
Fix 3 errors above and retry.
|
||||
```
|
||||
|
||||
#### API 调用
|
||||
|
||||
```
|
||||
POST https://dashscope.aliyuncs.com/api/v1/files
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <api-key>
|
||||
|
||||
Body:
|
||||
files: <binary>
|
||||
purpose: "fine-tune"
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"id": "file-xxxx",
|
||||
"bytes": 12345,
|
||||
"filename": "train.jsonl",
|
||||
"purpose": "fine-tune",
|
||||
"created_at": 1700000000
|
||||
}
|
||||
```
|
||||
|
||||
#### 输出
|
||||
|
||||
- 默认 text:`✓ Uploaded file-xxxx (12.3 KB) — use this ID in bl finetune create`
|
||||
- `--output json`:完整 response body
|
||||
- `--quiet`:仅输出 `file-xxxx`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `bl finetune create`
|
||||
|
||||
**定位:** 创建一个 SFT 训练任务。核心设计原则——**预填合理默认超参 + 提交前二次确认**,降低 OOM/超参不合理导致的训练失败率。
|
||||
|
||||
#### CLI 签名
|
||||
|
||||
```
|
||||
bl finetune create --model <model> --data <file_id> [hyperparams...]
|
||||
```
|
||||
|
||||
| Flag | 必填 | 默认值 | 说明 |
|
||||
| ------------------- | ---- | ------------ | -------------------------------------------- |
|
||||
| `--model` | 是 | — | 基座模型(如 `qwen3-8b`, `qwen3-14b`) |
|
||||
| `--data` | 是 | — | 训练数据 file_id(bl dataset upload 返回值) |
|
||||
| `--validation-data` | 否 | — | 验证数据 file_id |
|
||||
| `--epochs` | 否 | 3 | 训练轮次 (n_epochs) |
|
||||
| `--batch-size` | 否 | 按模型自动选 | 批大小 |
|
||||
| `--lr` | 否 | 按模型自动选 | 学习率 (learning_rate_multiplier) |
|
||||
| `--warmup-ratio` | 否 | 0.1 | warmup 比例 |
|
||||
| `--suffix` | 否 | — | 输出模型后缀名 |
|
||||
| `--yes` / `-y` | 否 | — | 跳过确认直接提交 |
|
||||
|
||||
#### 预填默认超参策略
|
||||
|
||||
| 基座模型 | batch_size | lr_multiplier | n_epochs | 备注 |
|
||||
| ---------- | ---------- | ------------- | -------- | ---------------- |
|
||||
| qwen3-8b | 4 | 1e-5 | 3 | 小模型可大 batch |
|
||||
| qwen3-14b | 2 | 5e-6 | 3 | 中模型防 OOM |
|
||||
| qwen3-32b+ | 1 | 2e-6 | 2 | 大模型保守设置 |
|
||||
|
||||
> 以上为建议默认值,用户显式传参时覆盖。具体映射表在 `packages/core/src/finetune/defaults.ts` 维护。
|
||||
|
||||
#### 提交前交互确认
|
||||
|
||||
非 `--yes` 模式下,显示任务摘要等待确认:
|
||||
|
||||
```
|
||||
┌─ Fine-tune Job Summary ──────────────────────┐
|
||||
│ Model: qwen3-8b │
|
||||
│ Training: file-abc123 (2,048 samples) │
|
||||
│ Validation: (none) │
|
||||
│ Epochs: 3 │
|
||||
│ Batch size: 4 │
|
||||
│ LR: 1e-5 │
|
||||
│ Warmup: 0.1 │
|
||||
│ Suffix: my-assistant │
|
||||
│ │
|
||||
│ Estimated cost: ~¥XX (based on token count) │
|
||||
└───────────────────────────────────────────────┘
|
||||
Proceed? [Y/n]
|
||||
```
|
||||
|
||||
#### API 调用
|
||||
|
||||
```
|
||||
POST https://dashscope.aliyuncs.com/api/v1/fine-tunes
|
||||
Authorization: Bearer <api-key>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"model": "qwen3-8b",
|
||||
"training_file_ids": ["file-abc123"],
|
||||
"validation_file_ids": [],
|
||||
"hyper_parameters": {
|
||||
"n_epochs": 3,
|
||||
"batch_size": 4,
|
||||
"learning_rate": "1e-5",
|
||||
"warmup_ratio": 0.1
|
||||
},
|
||||
"suffix": "my-assistant"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"job_id": "ft-xxxx",
|
||||
"status": "PENDING",
|
||||
"model": "qwen3-8b",
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
"training_file_ids": ["file-abc123"],
|
||||
"hyper_parameters": {...},
|
||||
"trained_model": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 输出
|
||||
|
||||
- text:`✓ Fine-tune job ft-xxxx created (PENDING). Track with: bl finetune status ft-xxxx`
|
||||
- json:完整 response body
|
||||
- quiet:`ft-xxxx`
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `bl finetune status`
|
||||
|
||||
**定位:** 查询训练任务状态,支持 `--wait` 轮询模式。
|
||||
|
||||
#### CLI 签名
|
||||
|
||||
```
|
||||
bl finetune status <job_id> [--wait] [--interval <seconds>]
|
||||
```
|
||||
|
||||
| Flag | 必填 | 默认值 | 说明 |
|
||||
| ------------ | ---- | ------ | ---------------- |
|
||||
| `<job_id>` | 是 | — | 任务 ID |
|
||||
| `--wait` | 否 | — | 持续轮询直到终态 |
|
||||
| `--interval` | 否 | 30 | 轮询间隔(秒) |
|
||||
|
||||
#### 状态机
|
||||
|
||||
```
|
||||
PENDING → RUNNING → SUCCEEDED
|
||||
↘ FAILED
|
||||
```
|
||||
|
||||
#### 输出(text 模式)
|
||||
|
||||
单次查询:
|
||||
|
||||
```
|
||||
Job: ft-xxxx
|
||||
Status: RUNNING (elapsed 12m)
|
||||
Model: qwen3-8b
|
||||
Output: (pending)
|
||||
```
|
||||
|
||||
`--wait` 模式(spinner + 实时刷新):
|
||||
|
||||
```
|
||||
⠋ ft-xxxx RUNNING [14:32 elapsed]
|
||||
✓ ft-xxxx SUCCEEDED — trained model: qwen3-8b:ft-xxxx-20250101
|
||||
Deploy with: bl deploy create --model qwen3-8b:ft-xxxx-20250101
|
||||
```
|
||||
|
||||
失败时:
|
||||
|
||||
```
|
||||
✗ ft-xxxx FAILED
|
||||
Error: OutOfMemory — try reducing --batch-size or using a smaller model
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 `bl deploy create`
|
||||
|
||||
**定位:** 将训练好的模型(或 checkpoint)部署为可调用的推理服务。
|
||||
|
||||
#### CLI 签名
|
||||
|
||||
```
|
||||
bl deploy create --model <model_name> [--plan <plan>] [--capacity <n>]
|
||||
```
|
||||
|
||||
| Flag | 必填 | 默认值 | 说明 |
|
||||
| ------------ | ---- | ---------- | ----------------------------------------------- |
|
||||
| `--model` | 是 | — | 待部署模型名称(finetune 产出的 trained_model) |
|
||||
| `--plan` | 否 | `standard` | 部署方案 |
|
||||
| `--capacity` | 否 | 依 plan | 并发容量 |
|
||||
| `--wait` | 否 | — | 等待部署就绪 |
|
||||
|
||||
#### API 调用
|
||||
|
||||
```
|
||||
POST https://dashscope.aliyuncs.com/api/v1/deployments
|
||||
Authorization: Bearer <api-key>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"model_name": "qwen3-8b:ft-xxxx-20250101",
|
||||
"plan": "standard",
|
||||
"capacity": 2
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"deployed_model": "qwen3-8b-ft-xxxx",
|
||||
"model_name": "qwen3-8b:ft-xxxx-20250101",
|
||||
"status": "PENDING",
|
||||
"created_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 输出
|
||||
|
||||
```
|
||||
✓ Deployment created: qwen3-8b-ft-xxxx (PENDING)
|
||||
Once RUNNING, call with: bl text chat --model qwen3-8b-ft-xxxx
|
||||
Check status: bl deploy status qwen3-8b-ft-xxxx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、P1 命令简要设计
|
||||
|
||||
### 3.1 `bl finetune logs <job_id>`
|
||||
|
||||
流式输出训练日志,支持 `--follow`(类似 `tail -f`)。输出 loss/step/epoch 信息。
|
||||
|
||||
### 3.2 `bl finetune checkpoints <job_id>`
|
||||
|
||||
列出可选 checkpoint(step, loss, eval metrics),支持 `--output json` 供脚本使用。可配合 `bl deploy create --model <checkpoint_model>` 部署指定 checkpoint。
|
||||
|
||||
### 3.3 `bl deploy status <deployed_model>`
|
||||
|
||||
查询部署状态及资源信息(PENDING → RUNNING → STOPPED/FAILED)。
|
||||
|
||||
### 3.4 `bl deploy delete <deployed_model>`
|
||||
|
||||
下线部署。需部署处于 RUNNING/STOPPED/FAILED 状态。交互确认或 `--yes` 跳过。
|
||||
|
||||
### 3.5 `bl infer --model <deployed_model>`
|
||||
|
||||
实际可复用已有 `bl text chat --model <deployed_model>` 通路,作为别名/快捷方式。P1 考虑是否有独立存在必要。
|
||||
|
||||
---
|
||||
|
||||
## 四、代码架构方案
|
||||
|
||||
按照 monorepo 分层约定(core 纯逻辑 / cli 是 UI):
|
||||
|
||||
### packages/core 新增模块
|
||||
|
||||
```
|
||||
packages/core/src/
|
||||
├── finetune/
|
||||
│ ├── index.ts # re-export
|
||||
│ ├── api.ts # createFineTune, getFineTune, getFineTuneLogs, getCheckpoints
|
||||
│ ├── defaults.ts # 模型 → 默认超参映射表
|
||||
│ └── types.ts # FineTuneJob, HyperParameters, CheckpointInfo 类型
|
||||
├── dataset/
|
||||
│ ├── index.ts
|
||||
│ ├── upload.ts # uploadDataset (multipart)
|
||||
│ ├── validate.ts # validateJsonl (纯函数,逐行校验)
|
||||
│ └── types.ts # DatasetFile, ValidationError 类型
|
||||
└── deploy/
|
||||
├── index.ts
|
||||
├── api.ts # createDeployment, getDeployment, deleteDeployment
|
||||
└── types.ts # Deployment, DeploymentStatus 类型
|
||||
```
|
||||
|
||||
### packages/cli 新增命令
|
||||
|
||||
```
|
||||
packages/cli/src/commands/
|
||||
├── dataset/
|
||||
│ └── upload.ts # bl dataset upload
|
||||
├── finetune/
|
||||
│ ├── create.ts # bl finetune create
|
||||
│ ├── status.ts # bl finetune status
|
||||
│ ├── logs.ts # bl finetune logs
|
||||
│ └── checkpoints.ts # bl finetune checkpoints
|
||||
└── deploy/
|
||||
├── create.ts # bl deploy create
|
||||
├── status.ts # bl deploy status
|
||||
└── delete.ts # bl deploy delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、关键设计决策
|
||||
|
||||
### 5.1 数据格式校验放在 CLI 侧(提交前拦截)
|
||||
|
||||
训练失败 TOP 原因中"数据格式错误"占比高。与其等服务端 10 分钟后返回 FAILED,不如 CLI 本地秒级校验:
|
||||
|
||||
- **validate.ts** 是纯函数,接收 ReadableStream/Buffer,返回 `ValidationError[]`
|
||||
- CLI 在 `dataset upload` 默认执行校验,`--no-validate` 允许跳过
|
||||
- 未来可扩展为独立命令 `bl dataset validate <path>`
|
||||
|
||||
### 5.2 超参预填 + 确认而非强制
|
||||
|
||||
- core 维护 `defaults.ts` 映射:`model → { batch_size, lr, epochs }`
|
||||
- CLI `finetune create` 未指定超参时自动填入
|
||||
- 提交前展示完整参数面板(非 --yes 模式),避免"我以为用了默认但其实没传"
|
||||
|
||||
### 5.3 费用感知(P1+)
|
||||
|
||||
- 图像/语音/视频训练费用远高于文本。MVP 阶段(Qwen 文本 SFT)费用可控
|
||||
- 后续扩展多模态时,在 confirm panel 中强化费用估算提示
|
||||
- `bl quota check` 已存在,可在 `finetune create` 内部集成余额预检
|
||||
|
||||
### 5.4 `bl infer` 是否独立存在
|
||||
|
||||
建议 P1 阶段**不新增** `bl infer`,而是让 `bl text chat --model <deployed_model>` 直接工作。部署完成后的引导文案中指明这个用法即可。减少命令膨胀。
|
||||
|
||||
---
|
||||
|
||||
## 六、最小闭环用户操作流
|
||||
|
||||
```bash
|
||||
# 1. 准备数据 → 上传(含校验)
|
||||
bl dataset upload ./train.jsonl
|
||||
# ✓ Uploaded file-abc123 (5.2 MB)
|
||||
|
||||
# 2. 创建训练任务(自动预填超参)
|
||||
bl finetune create --model qwen3-8b --data file-abc123
|
||||
# Shows summary panel → confirm → ✓ Job ft-xxxx created
|
||||
|
||||
# 3. 等待训练完成
|
||||
bl finetune status ft-xxxx --wait
|
||||
# ⠋ RUNNING [23:15] → ✓ SUCCEEDED: qwen3-8b:ft-xxxx-20250601
|
||||
|
||||
# 4. 部署模型
|
||||
bl deploy create --model qwen3-8b:ft-xxxx-20250601 --wait
|
||||
# ✓ Deployed: qwen3-8b-ft-xxxx (RUNNING)
|
||||
|
||||
# 5. 调用模型
|
||||
bl text chat --model qwen3-8b-ft-xxxx "你好,介绍一下你自己"
|
||||
# (正常推理输出)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实现顺序建议
|
||||
|
||||
```
|
||||
Phase 1 (P0 — 最小闭环):
|
||||
core: dataset/validate.ts → dataset/upload.ts → finetune/api.ts → deploy/api.ts
|
||||
cli: dataset upload → finetune create → finetune status → deploy create
|
||||
测试: 单元测试 validate.ts + e2e dry-run + 真实 API 端到端一次
|
||||
|
||||
Phase 2 (P1 — 可观测性):
|
||||
finetune logs → finetune checkpoints → deploy status → deploy delete
|
||||
费用估算集成
|
||||
|
||||
Phase 3 (后续):
|
||||
bl dataset validate (独立命令)
|
||||
bl dataset list (查看已上传)
|
||||
bl finetune list (查看历史任务)
|
||||
多模态 SFT 支持(图像/视频数据格式校验扩展)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、风险与 TODO
|
||||
|
||||
| 风险点 | 影响 | 缓解措施 |
|
||||
| ----------------- | ----------------- | --------------------------------------------- |
|
||||
| OOM 训练失败 | 用户浪费时间/金钱 | 保守默认超参 + batch_size 自适应模型大小 |
|
||||
| 数据格式错误 | 训练启动后才失败 | 本地校验拦截,启动秒级反馈 |
|
||||
| 部署等待时间长 | 用户困惑 | `--wait` + 预估时间提示 |
|
||||
| 费用超预期 | 账号欠费 | confirm panel 预估费用(P1 集成 quota check) |
|
||||
| API endpoint 变动 | 调用失败 | 端点集中管理在 core/client/endpoints.ts |
|
||||
+4
-1
@@ -16,16 +16,19 @@
|
||||
"ready": "vp check && vp run -r test && vp run -r build",
|
||||
"prepare": "vp config",
|
||||
"check": "vp check",
|
||||
"sync:skill-assets": "pnpm --filter \"bailian-cli^...\" run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
|
||||
"sync:skill-assets": "pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
|
||||
"dev": "pnpm -F bailian-cli-core dev",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"tsx": "catalog:",
|
||||
"vite-plus": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
@@ -2,4 +2,7 @@ node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
outputs/
|
||||
outputs/
|
||||
# agents
|
||||
agents.state.json
|
||||
.env
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user