Compare commits

..

113 Commits

Author SHA1 Message Date
clark-fc ad5c44d746 Merge pull request #166 from modelstudioai/feat/iteration1-w1-foundation
Feat/iteration1 w1 foundation
2026-08-17 14:08:56 +08:00
zeyu.fz 9450895a06 test(commands): 添加dry-run参数测试支持文件检测
- 增加--dry-run参数的测试用例
- 验证在dry-run模式下无支持文件的退出码和错误信息
- 确保无支持文件时正确返回错误码2
2026-08-17 13:59:05 +08:00
zeyu.fz e681263049 feat(cli): 增加知识库全生命周期管理命令
- 新增知识库管理命令实现创建、查询、更新、删除等功能
- 支持文档上传、本地目录扫描及OSS导入,覆盖完整文档生命周期
- 增加检索与问答服务的管理及部署操作
- 实现文档切片管理,支持添加、列表、更新和删除功能
- 引入数据中心管理命令,管理类目、文件和数据集
- 检索和问答支持指定服务版本参数,方便调试和版本控制
- 所有知识库命令同步支持kscli工具,提供更短命令路径
- 移除无效参数`bl knowledge search --query-history`,建议改用聊天命令传递历史
- 请求增加静态OpenAPI来源标识请求头,改进后台渠道归因
- 添加知识库端到端测试套件,覆盖多条使用场景
- 版本号统一更新至1.16.0,文档同步更新相关版本信息
2026-08-17 13:40:44 +08:00
gujieye ffc460156c Merge pull request #165 from modelstudioai/feat/cli-skill-sync
Feat/cli skill sync
2026-08-17 13:05:11 +08:00
zeyu.fz 70b50060cf Merge remote-tracking branch 'origin/main' into feat/iteration1-w1-foundation 2026-08-17 12:55:57 +08:00
故璃 3909a17da1 Merge branch 'main' into feat/cli-skill-sync 2026-08-17 12:51:26 +08:00
gujieye 5ed15d3a16 Merge pull request #164 from modelstudioai/feat/version-1.15.1
chore(release): prepare 1.15.1
2026-08-17 12:04:38 +08:00
故璃 640dd02bc5 chore(release): prepare 1.15.1 2026-08-17 11:55:36 +08:00
gujieye 6eeb8fe0cb Merge pull request #163 from modelstudioai/feat/version-1.15.1
docs(changelog): document 1.15.1
2026-08-17 11:38:42 +08:00
故璃 d0610a61dc docs(changelog): document 1.15.1 2026-08-17 11:25:17 +08:00
gujieye 57c2d98308 Merge pull request #160 from modelstudioai/feat/skill-init-simplify
feat: update skill init output
2026-08-17 11:01:35 +08:00
gujieye 78e6993475 Merge pull request #162 from modelstudioai/feat/model-command-update
feat: update model quota limit & add model permission command
2026-08-17 00:06:30 +08:00
故璃 79a0d2db9a fix(permission): drop explicit undefined return and make revoke --all e2e credential-independent 2026-08-16 23:48:19 +08:00
故璃 8e6af6c669 feat: update model quota limit & add model permission command 2026-08-16 19:49:23 +08:00
Gong Shiqi f7d32504ab Merge pull request #161 from modelstudioai/agent/release-1.15.0
chore(release): prepare 1.15.0
2026-08-15 14:51:34 +08:00
若麒 cdf94a8c89 chore(release): prepare 1.15.0 2026-08-15 14:33:55 +08:00
故璃 7461189007 Merge branch 'main' into feat/cli-skill-sync 2026-08-15 10:22:09 +08:00
故璃 4ccda5f929 feat: update skill init output 2026-08-15 10:15:00 +08:00
zeyu.fz 4086da572f docs(knowledge): 修改多处参数描述为“精确匹配”并完善错误处理说明
- 将 category-list、file-list、service-list 等命令参数的描述更新为强调“精确匹配”
- file-list 命令中 --name 参数改为匹配不含扩展名的精确文件名,并在备注中补充说明
- kb-stats 命令严格校验时间格式,增强无效格式报错及提示
- 丰富知识相关测试用例,增加对不存在 ID 的服务端错误传递和非零退出的正向断言
- 优化知识文档上传测试,支持跳过 node_modules/.git 文件夹及显示被跳过文件详情
- 修正知识搜索及聊天流程中未发布版本号引发的服务器拒绝场景测试
- 更新知识模块命令文档,补充参数要求和用法提示,提升用户指引明确度
2026-08-14 23:59:05 +08:00
Gong Shiqi ce4d66b736 Merge pull request #157 from modelstudioai/release/1.15.0
docs(changelog): document 1.14.2 through 1.15.0
2026-08-14 19:00:08 +08:00
若麒 196a0aa506 docs(changelog): document 1.14.2 through 1.15.0 2026-08-14 18:50:35 +08:00
gujieye a9b0a752a8 Merge pull request #155 from modelstudioai/feat/coding-plan-usage
feat: add coding plan usage
2026-08-14 17:36:47 +08:00
Gong Shiqi 2b7a0c742a Merge pull request #156 from modelstudioai/feat/text-chat-responses-api
feat(text): support Responses API
2026-08-14 17:21:40 +08:00
Gong Shiqi e5818e103c Merge pull request #145 from modelstudioai/feat/change-skills-install
Switch official skill install to bl skill init
2026-08-14 17:21:24 +08:00
若麒 7797940626 docs(skills): clarify update install channel 2026-08-14 17:14:23 +08:00
gujieye 5f1c97940d Merge branch 'main' into feat/coding-plan-usage 2026-08-14 17:05:53 +08:00
若麒 94ccab0898 feat(text): support Responses API 2026-08-14 17:02:29 +08:00
clh02467605 b895f88abb Merge remote-tracking branch 'origin/feat/change-skills-install' into feat/change-skills-install 2026-08-14 16:48:36 +08:00
clh02467605 7eedc05b99 docs: Modify the preferred installation method 2026-08-14 16:47:30 +08:00
若麒 f3c7b6fb10 docs(readme): add standalone installation options 2026-08-14 16:28:51 +08:00
若麒 eb196cb4a6 docs(readme): add standalone installation options 2026-08-14 16:26:47 +08:00
Gong Shiqi f1b6cacd7f Merge pull request #153 from modelstudioai/feat/mcp-support-sse
Add MCP classic SSE auto-fallback for Bailian and --url
2026-08-14 16:07:15 +08:00
故璃 9133b6bdd1 feat: add coding plan usage 2026-08-14 15:56:16 +08:00
clh02467605 98ba3279fa fix(runtime): expose errno in fetch-failed JSON cause.code 2026-08-14 15:27:26 +08:00
clh02467605 3b7c4cfabc Merge remote-tracking branch 'refs/remotes/origin/main' into feat/mcp-support-sse 2026-08-14 14:42:21 +08:00
clh02467605 d5d9fcb50f fix: fixed sse error 2026-08-14 14:41:37 +08:00
zeyu.fz d5c4bd3572 docs(knowledge): 优化知识库文档内容及CLI说明
- 修改表格/图片知识库必须提供`--doc-id`的描述,更准确表达要求
- 调整CLI命令文档中过期或不准确信息,说明集合删除暂不支持
- 更新chunk添加命令中`--doc-id`的说明,强调对所有知识库类型均必需
- 精简chunk删除命令备注,明确批量操作自动分批处理
- 优化文档删除命令描述,强调删除异步传播及输出行为
- 修正文档状态命令中错误提示用词,更清晰表达
- 文件列表命令修改说明,明确默认分类ID不解析
- 知识库信息命令删除过时备注,突出索引设置不可变
- 服务删除命令简洁描述幂等性和权限要求
- 服务列表命令调整对场景参数的描述,明确必传要求
2026-08-14 13:40:08 +08:00
zeyu.fz eb4f9af3e7 fix(knowledge): 修正 getConnector 不返回 fileConnectorConfig 问题
- 移除 collection-get 命令中对 fileConnectorConfig 字段的输出
- 文档中补充说明 getConnector 不返回 storeType、regionId、bucketName 等字段
- 更新类型定义,去除 RagConnectorInfo 中的 fileConnectorConfig 字段
- 明确这些字段只在创建连接时请求体中传入,查询时不可读取
2026-08-14 11:21:44 +08:00
clh02467605 3ea2931152 fix(mcp): harden SSE parsing, abort, and fallback matching 2026-08-14 11:11:47 +08:00
gujieye b402f3eacd Merge pull request #141 from sonicg83/codex/usage-token-plan-reset-times
fix(usage): handle missing Token Plan quota fields
2026-08-14 11:10:56 +08:00
zeyu.fz b7a4efe619 feat(speech): 支持同步Flash ASR模型和异步文件转录模型
- 新增同步Flash ASR模型请求流程,支持单音频文件识别
- 实现了对同步Flash模型不支持异步标志及参数的限制校验
- 异步文件转录模型支持单文件URL上传和语言参数细化
- 优化异步和同步鉴权域显示,丰富根帮助和分组帮助提示
- speech recognize增加dry-run测试覆盖多种识别场景
- free-tier自动停用功能优化,改用统一轮询函数处理批量请求
- 统一轮询逻辑,支持console和telemetry接口的异步任务完成判定
- 规范输出格式和错误提示,增强用户调试体验
- 版本升级到1.14.3,更新示例参数和模型ID引用
2026-08-14 11:07:21 +08:00
gujieye bedd59df27 Merge branch 'main' into codex/usage-token-plan-reset-times 2026-08-13 19:54:06 +08:00
故璃 39a488181e refactor(usage): align token-plan with --output convention and tolerant quota reading 2026-08-13 19:43:11 +08:00
若麒 4ec0f6828b fix(update): sync skills after binary upgrades 2026-08-13 19:14:49 +08:00
clh02467605 4dcec7d075 fix(mcp): fix SSE header timeout, 405 fallback matching, and parseSSE chunking 2026-08-13 18:32:54 +08:00
zeyu.fz 30f7525d50 docs(commands): 更新知识库分块命令中 --doc-id 的描述和注意事项
- 说明 --doc-id 在实际使用中为必需,避免服务器返回 HTTP 500 错误
- 明确指出应使用 doc list 命令中的文档级别 ID,拒绝使用 chunk list 中的每行 doc_id
- 新增说明向图片类型文档添加文本块会触发服务器错误,建议使用文本类型文档
- 对帮助文档中相关描述和备注进行了同步更新,增强使用指导性和准确性
2026-08-13 16:42:19 +08:00
Gong Shiqi daefc094ec Merge pull request #149 from modelstudioai/fix/fixed_issue_146
fix: support sync-flash and qwen3-filetrans ASR models in speech recognize
2026-08-13 16:26:32 +08:00
zeyu.fz aa38d5c670 fix(commands): 修复知识库创建时请求ID未传递问题
- 在知识库创建成功日志中添加请求ID信息
- 确保导入作业失败消息中包含请求追踪数据
- 改进日志详细程度,方便问题排查
2026-08-13 16:25:34 +08:00
zeyu.fz cc51164c2f fix(knowledge): 优化导入任务轮询逻辑与失败信息展示
- 添加函数判断所有文档是否达到终止状态,防止服务器无限保持运行状态
- 修改失败信息函数,展示失败和成功文档详情,方便用户了解整体情况
- 调整轮询任务完成条件,新增所有文档终止状态判断,提升轮询准确性
- 改进轮询状态显示,增加失败文档数量与总计信息,清晰反馈任务进展
2026-08-13 16:18:37 +08:00
clh02467605 ae0c2c1213 fix(speech): handle qwen3-filetrans singular result.transcription_url
Normalize async ASR transcription items so waiting mode downloads text and --out works without changing shared media task types.
2026-08-13 15:52:34 +08:00
clh02467605 01a62eb85b Merge remote-tracking branch 'refs/remotes/origin/main' into feat/mcp-support-sse 2026-08-13 15:40:04 +08:00
clh02467605 798ce596f6 fix(mcp): harden SSE fallback for Bailian and --url overrides 2026-08-13 15:37:32 +08:00
clh02467605 bd91e9d1c2 Merge remote-tracking branch 'refs/remotes/origin/main' into fix/fixed_issue_146
# Conflicts:
#	skills/bailian-gen/reference/index.md
#	skills/bailian-gen/reference/speech.md
2026-08-13 14:36:16 +08:00
clh02467605 e244771ee9 test(speech): harden flash ASR contract coverage and docs
Add SSE disable header, data-URI format inference, broader response text
parsing, HTTP contract e2e, pipeline routing tests, and ASR model selection
guidance in bailian-gen.
2026-08-13 14:25:47 +08:00
Gong Shiqi 94f9dbbe9e Merge pull request #151 from modelstudioai/feat/command-auth-help
feat(cli): show command authentication requirements in help
2026-08-13 13:38:28 +08:00
若麒 8a0dd70206 feat(cli): show command authentication requirements in help 2026-08-13 12:01:41 +08:00
clh02467605 9379da7a4c fix(speech): align flash vocabulary_id and qwen3-filetrans language params 2026-08-13 09:47:09 +08:00
clh02467605 ddcd564e61 test: dry-run realtime ASR usage-error e2e to skip auth in CI 2026-08-12 17:22:25 +08:00
gujieye 0e4dd4b824 Merge pull request #148 from modelstudioai/feat/usage_free_api
refactor(usage): consolidate shared poll logic; migrate freeTrial API…
2026-08-12 17:13:59 +08:00
clh02467605 241de61866 fix: support sync-flash and qwen3-filetrans ASR models in speech recognize
- Add asr-routes.ts with resolveAsrApi() to route models to the correct
  DashScope endpoint instead of always hitting asr/transcription
- Async filetrans: fun-asr / paraformer / *-filetrans → file_urls (plural)
- Async filetrans (qwen3): qwen3-asr-flash-filetrans* → file_url (singular)
- Sync flash (input-audio): fun-asr-flash* / qwen-audio-*-asr-flash → multimodal-generation
- Sync flash (qwen3): qwen3-asr-flash* → multimodal-generation + asr_options
- Realtime/streaming models now give a clear USAGE error instead of a
  confusing server-side "url error"
- Propagate same routing logic to pipeline speechRecognize step
- Add table-driven unit tests and dry-run e2e assertions
Fixes #146
2026-08-12 17:12:23 +08:00
故璃 61d9a74166 fix: 1.14.3 2026-08-12 17:05:18 +08:00
故璃 69eb759490 refactor(usage): consolidate shared poll logic; migrate freeTrial APIs to bailian-commerce
Dedup:
- shared.ts: extract generic pollConsoleUntilDone (request-builder callback
  absorbs each wrapper convention); pollTelemetryApi becomes a thin wrapper;
  add pollFreeTierBatch
- freetier.ts / stats.ts: drop inline duplicates of extractResponseData,
  polling, model-list paging, free-tier extractors and usage label maps;
  import from shared.ts (behaviour unchanged: freetier keeps its 20-poll
  budget, telemetry keeps 30)

Endpoint migration (broadscope-bailian.freeTrial -> bailian-commerce.freeTrial):
- queryFreeTierQuota, queryFreeTierOnlyStatus, batchActivateFreeTierOnly,
  batchDeactivateFreeTierOnly
- update the console call example and the gateway doc comment to match

Note: verified statically and via dry-run; live calls pending a fresh
console login (session expired).
2026-08-12 16:30:08 +08:00
clh02467605 313966d7a9 feat(mcp): add SSE support with fallback mechanism for MCP connections
- Add McpSseClient implementation for classic HTTP+SSE MCP protocol
- Implement connectBailianMcpWithFallback with Streamable HTTP to SSE fallback
- Add isStreamableHttpUnsupported helper to detect 405 streamableHttp errors
- Update activate-hint logic to handle WebSearch 405 streamableHttp cases
- Replace direct MCP client usage with connection manager in call/tools commands
- Add proper client cleanup with close() calls in finally blocks
- Export new MCP connection utilities and types from core client module
- Add comprehensive tests for SSE client and fallback behavior
2026-08-12 15:44:51 +08:00
故璃 d74d4efcd0 fix(dataset): align validation with the platform data-format rules doc
Reviewed against the official text-tuning data rules; fixes two confirmed
mismatches and fills enforcement gaps:

- thinking: exempt assistant messages carrying tool_calls from the
  THINK_TAG_NOT_LAST check — the spec's tool+thinking combo example puts
  <think> on a non-last assistant and was previously false-flagged
- DPO support matrix: reject image/video content items, tools, tool_calls
  and role:tool (DPO_UNSUPPORTED_ELEMENT); also scan chosen/rejected
- DPO: messages not ending with user upgraded warning -> error
- OpenAI migration: name/weight upgraded warning -> error (spec: must not
  carry); drop dead record-level name branch
- tool_call_id: unmatched tool response upgraded warning -> error
  (one-to-one per spec); new TOOL_CALL_NO_RESPONSE warning for orphan calls
- loss_weight: validate range at message level too; warn when placed on
  anything but the last assistant message (LOSS_WEIGHT_PLACEMENT)
- video params: fps/sample_fps must be within [0.1, 10]
  (INVALID_VIDEO_FPS); mode-mismatched params warned
  (VIDEO_PARAM_MODE_MISMATCH); video_start/video_end type-checked
- zip: skip macOS packaging metadata (__MACOSX/, .DS_Store, ._*) in
  filename constraints and image counting to stop false failures on
  Finder-created archives

Tests 45 -> 59 covering every new/changed rule, including a replica of the
spec's official tool+thinking example.
2026-08-12 10:09:10 +08:00
故璃 e7422bd2e5 fix(dataset): align validation rules with platform data format spec
- Support content array format [{text/image/video}] alongside legacy string
- Add tool role support with tool_calls structure and tool_call_id validation
- Add thinking tag placement check (only in last assistant message)
- Add OpenAI migration guards: warn on unsupported name/weight fields
- Add loss_weight range validation (0.0–1.0)
- Fix size limits: SFT/DPO 200MB, CPT 300MB, media ZIP 2GB
- Enforce data.jsonl at ZIP root (reject nested wrapping folders)
- Add ZIP filename constraints: charset [a-zA-Z0-9_-], length ≤120, uniqueness
- Add .tif to accepted image extensions
- Add DPO_LAST_MSG_NOT_USER warning when messages don't end with user role
- CPT profile now uses dedicated 300MB cap instead of shared default
- Expand unit tests from 19 to 45 covering all new validation paths
2026-08-12 07:48:11 +08:00
zeyu.fz 2d5c49b02e fix(knowledge): 优化quiet和format参数的输出逻辑
- 在file-get命令中,quiet模式下仅输出fileId,避免了多余的结果格式化
- 在kb-info命令中,quiet模式仅输出id,格式化输出逻辑得到简化
- 在kb-stats命令中,去除了quiet判断,确保非"text"格式下正确输出结果
- 在service-get命令中,quiet模式下只输出agent_id,格式化部分调整为独立判断
- 统一了各命令中针对quiet和format参数的处理流程,提升代码一致性和可读性
2026-08-11 20:05:44 +08:00
zeyu.fz 9bd8b60c22 refactor(knowledge-search): 移除对 query-history 功能的支持及相关代码
- 从文档中删除了 query-history 参数及示例
- 删除命令行接口中 query-history 相关 flag 定义
- 移除解析和传递 query-history 的逻辑代码
- 调整测试用例,去除对 query-history 的相关断言和测试
- 更新帮助文档,删除 query-history 相关说明和示例
- 精简接口类型定义,去除 query_history 字段
2026-08-11 19:44:00 +08:00
zeyu.fz 0369bd36b0 fix(knowledge): 修复内容文件读取时的编码和错误提示
- 为知识块添加和更新命令的读取内容文件函数添加了可选的inline flag参数
- 更新readUtf8TextFile以支持根据inline flag生成更友好的ENOENT错误提示
- 确保内容读取时使用严格的UTF-8编码解码方式
- 如果读取文件失败,提供文件路径和权限相关的详细错误信息
- 校验知识块内容长度时保持一致的错误处理逻辑
2026-08-11 17:48:21 +08:00
zeyu.fz 92a978af3c fix(knowledge): 验证并限制查询时间范围为过去时间
- 修改时间参数说明,明确要求起止时间必须为过去时间
- 添加起始时间未来时报错机制,避免无意义查询
- 截断结束时间未来时间至当前时间,保障监控接口正确响应
- 添加测试覆盖,验证时间范围边界行为及错误处理
- 补充对应端到端测试路由映射,完善测试用例组织结构
2026-08-11 17:39:40 +08:00
故璃 9749a11d76 fix(finetune): detect video-kf2v sub-variant from local dataset
finetune video create passed a fixed modality "video" to the profile
validator without probing the data for last_frame_path. This caused a
false KF2V_DATA_MISMATCH error when training kf2v models (wan2.2-kf2v-*)
with datasets that correctly contain last_frame_path.

Add sub-variant detection symmetric to the existing image-i2i upgrade:
when a local file is provided, detectModality() inspects the first record
and upgrades "video" → "video-kf2v" if last_frame_path is present.
2026-08-11 14:50:58 +08:00
故璃 2965080cb7 feat(skills): replace foreign skill dirs containing SKILL.md during fan-out
Previously, fan-out skipped any existing real directory not recorded in
the lock file, treating it as user content. This left stale skill copies
installed by other tools (e.g. npx skills add) permanently out of date.

Now: if the directory contains a SKILL.md, it is recognized as a skill
artifact and replaced with a symlink to the canonical dir. Directories
without SKILL.md are still preserved (user content safety boundary).
2026-08-11 14:29:10 +08:00
zeyu.fz 81959145d7 test(auth): 添加 openApiSource 头和相关测试字段
- 在请求和响应处理中新增 openApiSource 字段
- 更新 e2e 测试以包含 openApiSource 字段验证
- 确保请求头包含 x-dashscope-openapisource 信息
- 在测试数据中添加 openApiSource 的示例值 BailianCLI
2026-08-11 14:23:01 +08:00
zeyu.fz 4343fc87af feat(core): 添加并统一管理 x-dashscope-openapisource 请求头
- 在 headers.ts 中新增 OPEN_API_SOURCE 常量,作为静态的 OpenAPI 源标识
- 在 trackingHeaders 函数中添加 x-dashscope-openapisource 请求头
- 更新 client/index.ts 以导出 OPEN_API_SOURCE
- 在 instrumented-fetch.test.ts 中添加对应请求头的测试,确保其正确添加或省略
- 修改文档注释,明确 x-dashscope-openapisource 与 x-dashscope-source-config 的用途和区别
2026-08-11 14:20:44 +08:00
故璃 1c76749ee5 feat: update datalist validate 2026-08-11 13:33:31 +08:00
zeyu.fz 1b568e8d37 test(knowledge): 补全知识库相关命令参数并增加E2E测试覆盖
- 添加知识库列表、创建、删除及文件删除等命令路由
- 新增知识块、分类、文件相关参数的端到端测试,覆盖文件列表、分类列表、块新增更新及分页等功能
- 增加对知识文档状态上传、等待、轮询参数的实时测试及自清理逻辑
- 新增知识文档列表分页、过滤参数的E2E测试覆盖
- 扩展知识文档上传命令的轮询间隔参数测试,验证无错误
- 补充知识库删除命令的轮询间隔参数传递测试
- 增强知识库列表的分页、名称过滤测试用例
- 丰富知识服务命令的参数全覆盖测试,包括创建、更新、部署、删除及多版本描述等功能
- 添加知识检索命令的重新排序指令与过时参数的实时测试覆盖
2026-08-11 13:17:34 +08:00
故璃 5d1b7aac3a Merge branch 'main' into feat/cli-skill-sync 2026-08-11 10:47:45 +08:00
zeyu.fz e292b20d4b docs(knowledge): 添加知识库各类资源及操作命令手册
- 新增 Chunk 管理命令手册,涵盖添加、列出、更新、删除操作详解
- 新增数据中心集合与分类命令文档,介绍集合创建、查看,分类增删查等功能
- 新增文档管理命令,包含文档上传、导入 OSS、状态查询、删除及标签管理
- 新增数据中心文件管理文档,涵盖文件列表、详情查看、删除等命令说明
- 新增知识库管理命令手册,包含知识库创建、查看、更新、删除和监控
- 各命令均详细说明参数、输出格式及多模式支持(text/quiet/json)
- 提供丰富示例及注意事项,帮助用户正确使用相关命令
2026-08-11 01:02:49 +08:00
zeyu.fz 12e7a22195 test(knowledge): 增加文档相关命令的独立读回验证
- 在 knowledge doc delete 命令中添加异步删除的轮询验证,确保文档从服务器彻底移除
- 为 knowledge doc tag 添加标签设置后,独立调用 file get 验证标签正确应用
- 在知识库更新操作后,通过 info 命令独立验证更新是否成功保存
- 在文件删除和类别删除后,通过独立列表命令验证资源确实被清除
- 对知识块更新及排除标记修改,添加通过列表接口的内容验证步骤
- 对知识服务代理删除操作后,增加独立查询接口确保代理已彻底删除
- 补充 doc delete 备注,明确 doc_id 与 fileId 的区别及异步删除机制说明
- 增加 e2e 路由映射中缺失的 knowledge info 和 knowledge file get 命令支持
2026-08-10 17:37:21 +08:00
zeyu.fz 219d8be80a test(e2e): 修正知识库删除测试中文件名匹配逻辑
- 移除未使用的完整文件名变量
- 使用文件名主干(去掉扩展名)进行文档匹配判断
- 更新断言提示信息以反映主干文件名匹配
- 提升测试对文档名称匹配的准确性与鲁棒性
2026-08-10 16:29:14 +08:00
zeyu.fz ab766d44d3 test(commands): 添加知识文档列表的端到端测试步骤
- 在topic-routes测试文件中新增“knowledge doc list”步骤
- 该步骤用于验证导入的知识库文件是否可见
- 增强了知识库文件相关功能的测试覆盖率
2026-08-10 16:25:01 +08:00
zeyu.fz 1d9852805f fix(knowledge): 修正文档上传接口请求的字段名为 docIds
- 将请求体中的 dataSource.fileIds 改为扁平结构的 docIds 字段
- 移除嵌套的 dataSource 对象,显式指定 sourceType 字段
- 更新相关单元测试以匹配新的请求参数格式和字段名称
- 在知识库删除测试中增加了导入结果和最终状态的断言,确保导入流程完整
- 新增校验导入文件在知识库文档列表中正确显示
- 调整测试中对请求体结构的断言逻辑以适配改动
2026-08-10 16:24:00 +08:00
zeyu.fz 99a3dbae2d Merge remote-tracking branch 'origin/main' into feat/iteration1-w1-foundation 2026-08-10 15:26:18 +08:00
sonicg83 4d84af614b Merge branch 'modelstudioai:main' into codex/usage-token-plan-reset-times 2026-08-07 23:29:01 +08:00
gujieye 2389681ad6 Merge pull request #144 from modelstudioai/feat/add-version-tag
feat: add version 1.14.2
2026-08-07 18:02:08 +08:00
clh02467605 9ae5dc924d docs(cli): update skill installation command from add --name all to init
- Replace `bl skill add --name all` with `bl skill init` across documentation
- Update installation instructions in README, INSTALL, and agent skill guides
- Modify code references in update checker and UI components
- Adjust documentation links and cross-references accordingly
- Revise command examples in protocol and asset files
- Update versioning and setup instructions to reflect new command
- Modify HTML UI rendering for skill installation guidance
- Change internal command constants and execution calls
2026-08-07 17:56:30 +08:00
故璃 946b7029c6 feat: add version 1.14.2 2026-08-07 17:42:31 +08:00
clh02467605 d6cb075629 Merge remote-tracking branch 'refs/remotes/origin/main' into feat/change-skills-install
# Conflicts:
#	README.md
#	README.zh.md
#	packages/cli/README.md
#	packages/cli/README.zh.md
2026-08-07 17:37:43 +08:00
gujieye b9ecd5c43b Merge pull request #143 from modelstudioai/feat/skill-init-commend
feat: add skill init & opt commend flags
2026-08-07 17:26:21 +08:00
Gong Shiqi 978f332fea Merge pull request #142 from modelstudioai/docs/update-readme-and-agent-guides
docs: refresh READMEs and auth maintenance guidance
2026-08-07 17:24:52 +08:00
故璃 4502424200 feat: add skill init & opt commend flags 2026-08-07 17:17:27 +08:00
若麒 03839766bc docs: update READMEs 2026-08-07 17:15:26 +08:00
故璃 5007b9b574 feat: change output to json 2026-08-07 16:35:47 +08:00
若麒 1f8b9ace7e docs: refine auth maintenance guidance 2026-08-07 15:27:46 +08:00
故璃 8286a74fb6 test: remove sync pipeline verification marker 2026-08-07 14:40:33 +08:00
故璃 9eb2acbb65 test: trigger skills sync pipeline 2026-08-07 14:38:40 +08:00
故璃 1d35326c86 ci: read FC trigger url from variables 2026-08-07 14:36:42 +08:00
故璃 eb6c2b8e2a Merge branch 'feat/model-finetune-opt' into feat/cli-skill-sync 2026-08-07 14:26:38 +08:00
故璃 ebd6226a9f ci: rename trigger secret to FC_TRIGGER_URL 2026-08-07 14:25:49 +08:00
故璃 e25d3b0b8e ci: add workflow to publish skills to OSS 2026-08-07 14:13:47 +08:00
clh02467605 0e33c70e65 docs: update skill installation instructions to use bl skill add
- Replace all instances of `npx skills add modelstudioai/cli --all -g` with `bl skill add --name all`
- Update installation documentation in INSTALL.md, README.md, and related files
- Modify code references in config/inventory.ts, generate-reference.ts, and other files
- Update HTML UI messages to reflect new installation command
- Correct setup.md to include binary installation option and update subset install instructions
- Adjust versioning documentation to use new skill installation command
- Update all SKILL.md files with consistent installation instructions
2026-08-07 14:09:23 +08:00
故璃 3c64461cca feat: video finetune/deploy/invoke full pipeline + training cost calculation
- Add finetune video create subcommand (wan2.7/2.5/2.2 i2v + kf2v)
- Align video hyperparams with official docs (n_epochs=50, per-model batch_size/max_pixels)
- Add --last-frame flag to video generate for kf2v (image2video endpoint)
- Fix wan2.1-2.6 i2v input format (img_url instead of media[])
- Add training_cost field to finetune get/watch (catalog ft price, API-key domain only)
- Add --aigc-* flags to deploy create (optional, for video LoRA prompt config)
2026-08-07 07:03:01 +08:00
sonicg 24092b423c fix(usage): handle unavailable token plan quotas 2026-08-06 22:47:26 +08:00
故璃 f30fff9065 feat(finetune): clarify model flags; add price estimate & actual cost
- Rename for clarity: finetune --model → --base-model (create/price/
  capability/list); deploy create --model → --model-name, --name →
  --display-name
- Add `finetune price` (console domain) for pre-training cost estimate
  (sft/dpo/cpt)
- Add actual training cost (fee.ts) enriched into finetune get/watch
  from catalog price × reported usage
2026-08-06 15:08:02 +08:00
故璃 a7245c0f62 feat(deploy): add pause/resume commands; JSON-only output for dataset/finetune/deploy
- Add `bl deploy pause` and `bl deploy resume` (console domain, first
  console-auth commands in deploy group) via modelInstance start/stop APIs
- Add core deploy/lifecycle.ts with input-wrapped console gateway calls
- Switch all dataset/finetune/deploy commands to JSON-only output, removing
  text formatting logic
- Expose usage/charge_type in finetune get, model_name/expire_time in
  finetune checkpoints with near-expiry warning
- Update deploy delete hint to suggest `bl deploy pause`
2026-08-06 11:34:48 +08:00
sonicg 752a79e442 fix(usage): handle missing token plan reset times 2026-08-06 09:12:41 +08:00
sonicg 80bdcb83f6 feat(usage): add token plan usage view 2026-08-05 23:28:39 +08:00
zeyu.fz d9e8601a50 feat(knowledge): 支持上传目录路径并递归扫描文件
- 支持上传参数中传入目录路径,递归扫描子目录下文件
- 自动忽略 node_modules、.git 等常见工具目录
- 不支持的文件格式不会报错,跳过并列表提示
- 上传时校验扩展名和大小限制,支持批量文件上传
- 输出中增加跳过的文件列表,verbose 模式下显示详细文件名
- 测试覆盖目录上传、文件跳过和空目录等场景
- 更新相关文档,说明新支持的目录上传功能及注意事项
2026-08-05 22:29:39 +08:00
zeyu.fz e3bb5a7fa0 test(commands): 添加知识库统计接口的E2E测试
- 在topic-routes中新增knowledge stats路由映射
- 在j2-content-ops测试中添加知识库统计命令调用
- 验证接口返回的存储限制和请求速率监控数据结构有效
- 记录存储限制和请求窗口数量作为测试备注
- 引入parseStdoutJson辅助函数解析JSON输出
2026-08-05 20:13:42 +08:00
zeyu.fz 54da9aa29a chore(deps): 更新pnpm锁文件及包覆盖版本
- 调整smol-toml包版本位置
- 新增overrides字段,指向自定义vite和vitest包最新版本
- 保持其他依赖版本不变
- 确保包管理器锁文件一致性
2026-08-05 20:02:35 +08:00
zeyu.fz ef463e8d5d test(e2e): 优化检索结果标记召回判断并完善服务调优用例
- 新增节点召回标记判断函数 nodesRecallMarker,避免误判 marker 出现位置
- 将所有相关轮询断言替换为基于 nodesRecallMarker 的更严格判断
- 调整删除测试中对误伤判断的断言逻辑,确保准确检测召回标记
- 扩展服务调优用例,增加描述和温度参数调优测试,验证配置持久化
- 添加通过配置文件更新 kb_search_configs 并校验嵌套配置修改生效
- 部署后验证发布版本配置正确包含所有调优项
- 更新流程注释与断言提示,提升测试用例可读性和覆盖度
2026-08-05 19:56:52 +08:00
zeyu.fz 8ee2c378f5 test(knowledge): 添加多模态与表格型仓库 E2E 测试套件支持
- 新增多模态问答及检索服务的 live E2E 测试用例,支持基于图像参数的功能验证
- 补充表格库的 chunk add/list/delete 闭环测试,验证了 field channel 的必填项和读写一致性
- 增加图片库 chunk list 的 metadata 验证,确保 image_url 数组和可见性标志存在
- 实现带覆盖重导功能的 doc import-oss 测试,确认覆盖后 fileId 变更及旧文件失效
- 编写自有 OSS Bucket 的幂等复用集合创建和获取测试,确保服务端的 tag-based 访问控制支持
- 在 gating 中添加对各类长驻知识库及服务环境变量的就绪检测函数
2026-08-05 18:14:23 +08:00
zeyu.fz 9fc6434a26 Merge remote-tracking branch 'origin/main' into feat/iteration1-w1-foundation 2026-08-05 17:46:12 +08:00
zeyu.fz 43abf0aca5 feat(knowledge): 新增知识库管理及用户旅程端到端测试支持
- 增加test:journey脚本,覆盖知识库跨命令全链路用户旅程测试
- 在文档中新增Journey E2E章节,详细说明用户旅程测试定位及断言机制
- 完善commands模块,新增知识库相关命令包括知识库列表、信息、创建、更新、删除
- 新增知识库文档相关命令,如文档列表、状态、上传、删除、打标签及OSS导入
- 添加知识服务管理命令,支持列表、创建、更新、部署、删除及复制
- 支持知识块增删查改命令,完善知识点的灵活操作能力
- 实现数据中心分类管理命令,支持分类增删查操作
- 优化knowledge chat命令,增加workspace-id统一解析及agent-version版本控制
- 重构与知识库相关命令的导出与注册,完善CLI整体能力覆盖
- 新增命令详尽的帮助文档,包含参数说明、使用示例及错误边界
- 实现批量删除知识块的自动分批处理逻辑,易于操作大规模数据
- 添加必要的输入校验与安全提示,确保操作安全且符合规范
2026-08-05 12:02:32 +08:00
294 changed files with 28210 additions and 8361 deletions
+27
View File
@@ -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 }}"
+1 -1
View File
@@ -35,7 +35,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
packages/core/src/client/ # HTTP client / endpoints / console gateway
```
Skill / 命令手册随 `skills/bailian-*/``npx skills add modelstudioai/cli --all -g` 安装(整包装齐,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts``packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
Skill / 命令手册随 `skills/bailian-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `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)。
约定:
+73
View File
@@ -6,6 +6,79 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [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
+73
View File
@@ -6,6 +6,79 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [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
### 新增
+34 -31
View File
@@ -1,8 +1,38 @@
# 阿里云百炼CLI 安装说明(供 AI Agent 阅读)
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**使用二进制一键安装(无需 Node若环境已有 Node / 需要 Command Pack再用 npm。不要臆造版本号或路径;以用户环境为准。
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**在已有 Node.js**≥ 18.17.0**)的环境用 npm 安装;若没有可用 Node / npm再走二进制一键安装。不要臆造版本号或路径;以用户环境为准。
## 1. 推荐:二进制安装(无需 Node
## 1. 推荐:npm 安装(要求 **≥ 18.17.0**
1. `node -v` 确认版本 ≥ 18.17.0。
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn
3. 执行:
```bash
npm install -g bailian-cli
```
4. 校验:`bl --version`
安装 skillsCLI 内置,无需 Git / npx skills
```bash
bl skill init
```
**Supported** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
**Advanced / 按需子集:**
```bash
bl skill add --name bailian-protocol,bailian-gen
```
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
## 2. 备选:二进制安装(无需 Node
当环境没有 Node / npm或 Node 版本过低无法走 npm 时,使用二进制安装脚本。脚本安装 CLI 成功后会自动执行 `bl skill init`
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
@@ -37,36 +67,9 @@ bl --version
which bl # Windows: where.exe bl
```
> CDN / GitHub Release 未就绪或下载失败时,回退到下方 npm 安装
若自动 skill 安装失败,再手动执行:`bl skill init`
## 2. 备选npm 安装(要求 **≥ 18.17.0**
1. `node -v` 确认版本。
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn
3. 执行:
```bash
npm install -g bailian-cli
```
4. 校验:`bl --version`
可选 skills与 CLI 本体无关,按需):
```bash
npx skills add modelstudioai/cli --all -g
```
**Supported** 始终使用 `--all -g`,一次装齐整套 `bailian-*`(含共享协议 `bailian-protocol`。Agent Skills / `npx skills` **不会**按 metadata 自动拉依赖。
**Advanced / 不推荐:** 子集 `-s` 时 skills CLI 不会自动带上 `bailian-protocol`;若坚持子集,必须手动同时指定,例如:
```bash
# Advanced: you MUST include bailian-protocol yourself — installer does not pull it
npx skills add modelstudioai/cli -g -s bailian-protocol -s bailian-gen
```
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node回退到上方 npm 安装。
---
+90 -146
View File
@@ -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,29 +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.8-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 3.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 520s 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
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
> **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 text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), 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`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **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">
@@ -57,136 +45,93 @@ 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
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
**Agent install (recommended)**
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
> Binary install does not require Node.js. `npm install -g` remains fully supported.
**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
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 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 text 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 probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
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
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# 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." |
| 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"
```
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
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
@@ -194,26 +139,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). 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 OpenAPI AK/SK (Token Plan only)
### Alibaba Cloud OpenAPI AK/SK
Required for 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
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -222,18 +161,31 @@ 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 or a specific version
bl update
bl update --to 0.1.14
# Switch config profile
bl config use --name token-plan
```
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 |
@@ -245,11 +197,3 @@ Config file location: `~/.bailian/config.json`
| 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.
+90 -147
View File
@@ -22,29 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.8-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
> **注意:** 以下功能目前仅对中国站aliyun.com账号开放国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -54,137 +41,96 @@ _专为 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
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
**Agent 安装(推荐)**
# WindowsPowerShell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
# Node 用户 / 开发者(需要 Node.js >= 18.17
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
**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
# 或使用 Token Plan已内置 Base URL登录时自动测试 Key
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
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 text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
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 # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# 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) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL登录命令会先测试 Key通过后才保存并激活 `token-plan` 配置。
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
@@ -192,26 +138,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history``asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK(仅 Token Plan
### 阿里云 OpenAPI AK/SK
`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
# 方式一:登录命令(持久化到 ~/.bailian/config.json
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -220,20 +160,31 @@ 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 update --to 0.1.14
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.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" />
## 相关链接
| 资源 | 地址 |
@@ -245,11 +196,3 @@ bl update --to 0.1.14
| 获取 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)。
-574
View File
@@ -1,574 +0,0 @@
# bailian-cli 快速上手指南
> 本文档面向新加入项目的开发者,帮助你理解 monorepo 的整体架构、代码组织方式和日常开发流程。
> AI Agent 维护契约见根目录 [`AGENTS.md`](../AGENTS.md);各场景的详细清单见 [`docs/agents/`](agents/)。
---
## 1. 项目是什么
**bailian-cli** 是阿里云百炼DashScope / Model Studio平台的命令行工具让用户和 AI Agent 通过终端调用平台的全部 AI 能力:
- 文本/全模态对话、图像/视频生成与编辑、语音合成与识别
- 知识库检索、记忆管理、应用调用、MCP 集成
- 微调与部署、数据集管理、配额与业务空间
- 控制台能力(用量统计、限流提额、资产中心等)
仓库以 **pnpm monorepo** 组织,产出两个 npm 产品:
| 产品 | 包名 | 二进制 | 定位 |
| -------------- | ---------------------- | ---------------- | ------------------------------------ |
| 百炼全量 CLI | `bailian-cli` | `bl` / `bailian` | 暴露全部命令 |
| 知识库轻量 CLI | `knowledge-studio-cli` | `kscli` | 仅 config + knowledge 命令,路径拍平 |
---
## 2. 技术栈
| 类别 | 选型 |
| --------- | -------------------------------------------------------------------------------------------- |
| 语言 | TypeScriptstrict |
| 运行时 | Node.js ≥ 22.12 |
| 包管理 | pnpm 10 + workspace catalog |
| 构建/测试 | [vite-plus](https://github.com/voidzero-dev/vite-plus)`vp check` / `vp test` / `vp pack` |
| HTTP | undici经 core client 封装) |
| 模块 | ESM`"type": "module"` |
---
## 3. 核心架构:四层分层
项目按 **「纯逻辑 → 运行时框架 → 命令库 → 产品入口」** 严格分层,职责边界清晰:
```
┌─────────────────────────────────────────────────────────────────┐
│ 产品入口层 │
│ packages/cli (bl) packages/kscli (kscli) │
│ 决定命令路径 map、产品 identity、README、技能 reference │
└────────────────────────────┬────────────────────────────────────┘
│ createCli(commands, identity)
┌────────────────────────────▼────────────────────────────────────┐
│ 运行时框架层 packages/runtime (bailian-cli-runtime) │
│ 参数解析、命令树/registry、help、middleware、错误处理、输出 │
└────────────────────────────┬────────────────────────────────────┘
│ 调用 defineCommand 的 run()
┌────────────────────────────▼────────────────────────────────────┐
│ 命令库层 packages/commands (bailian-cli-commands) │
│ 96+ 命令实现;只导出 command不决定产品路径 │
└────────────────────────────┬────────────────────────────────────┘
│ client / settings / auth
┌────────────────────────────▼────────────────────────────────────┐
│ 纯逻辑层 packages/core (bailian-cli-core) │
│ 鉴权、配置、HTTP client、错误、类型、文件工具、领域 API │
└─────────────────────────────────────────────────────────────────┘
```
### 分层边界(必须遵守)
| 层 | 可以做 | 不能做 |
| --------------- | --------------------------- | ------------------------------------------------------------- |
| **core** | 纯库逻辑、HTTP、鉴权解析 | 依赖 runtime/commands硬编码 `bl`/`kscli`;调 `process.exit` |
| **runtime** | TTY、help、middleware、输出 | 写具体业务命令逻辑 |
| **commands** | 命令元数据 + `run` 实现 | 决定产品路径;在 usage 里写 bin 前缀 |
| **cli / kscli** | 命令路径 map、产品 identity | 不写命令业务逻辑 |
---
## 4. 包详解
### 4.1 `packages/core` — `bailian-cli-core`
纯逻辑层,被所有上层依赖。主要模块:
```
packages/core/src/
├── auth/ # API Key / Console token 解析与落盘
├── client/ # HTTP client、endpoints、MCP、流式解析
├── config/ # ~/.bailian/config.json、Settings、来源优先级
├── console/ # Console Gateway 调用
├── dataset/ # 数据集校验ChatML/DPO/CPT schema
├── finetune/ # 微调 API 与能力探测
├── deploy/ # 部署 API
├── advisor/ # 模型推荐(意图识别 + 召回)
├── errors/ # BailianError、UsageError、退出码
├── output/ # JSON/text 格式化(命令层也可用 runtime 的 emit
├── files/ # 本地文件上传、URL 解析
├── telemetry/ # 命令执行遥测
└── types/ # Command、FlagsDef、defineCommand
```
**关键类型** — 每个命令通过 `defineCommand` 声明:
```typescript
defineCommand({
description: "…",
auth: "apiKey" | "console" | "none",
flags: {
/* camelCase key → kebab-case CLI flag */
},
usageArgs: "--prompt <text> [flags]", // 不含 bl/kscli 前缀
exampleArgs: ['--prompt "hello"'],
validate: (flags) => string | undefined, // 跨 flag 校验
run: async (ctx) => {
/* ctx.client / ctx.flags / ctx.settings */
},
});
```
**Client** 是命令的网络入口,凭证已注入,命令层不碰 token
```typescript
ctx.client.requestJson({ path: "/…", method: "POST", body });
ctx.client.console({ product: "…", action: "…", params });
ctx.client.uploadFile(localPath);
ctx.client.mcp();
```
### 4.2 `packages/runtime` — `bailian-cli-runtime`
通用 CLI 框架,与具体业务无关。核心文件:
| 文件 | 职责 |
| ------------------ | ----------------------------------------------- |
| `create-cli.ts` | 入口工厂:`createCli(commands, identity).run()` |
| `registry.ts` | 从 `Record<string, AnyCommand>` 建树,动态 help |
| `args.ts` | 路径 + flag 解析 |
| `middleware.ts` | auth → telemetry → versionCheck → runCommand |
| `error-handler.ts` | 统一错误输出与退出码 |
| `urls.ts` | 用户面控制台 URL非 API endpoint |
| `output/` | 颜色、表格、进度条、banner |
| `pipeline/` | 多步 pipeline 编排(`bl pipeline run` |
**Middleware 流水线**(洋葱模型):
```
argv 解析
→ authStage 按 command.auth 注入 apiKey / console 凭证到 ctx.client
→ telemetryStage 记录命令执行
→ versionCheckStage 检查 npm 更新
→ runCommandStage 调用 command.run(ctx)
```
### 4.3 `packages/commands` — `bailian-cli-commands`
命令实现库,按**能力域**组织目录(≠ 最终 CLI 路径):
```
packages/commands/src/commands/
├── text/ # 文本对话
├── omni/ # 全模态对话
├── image/ # 图像生成/编辑
├── video/ # 视频生成/编辑/下载
├── speech/ # 语音合成/识别
├── vision/ # 图像/视频理解
├── knowledge/ # 知识库检索/搜索/对话
├── memory/ # 记忆管理
├── app/ # 应用调用
├── mcp/ # MCP 服务
├── auth/ # 登录/登出/状态
├── config/ # 配置读写
├── console/ # 通用 Console Gateway 调用
├── dataset/ # 数据集上传/校验
├── finetune/ # 微调任务
├── deploy/ # 模型部署
├── quota/ # 限流与提额
├── workspace/ # 业务空间
├── usage/ # 用量统计
├── advisor/ # 模型推荐
├── asset-center/ # 资产中心(新)
├── pipeline/ # Pipeline 编排
├── search/ # 联网搜索
├── file/ # 文件上传
├── token-plan/ # Token 计划
└── update.ts # 自更新
```
每个命令文件 `export default defineCommand(…)`,并在 `packages/commands/src/index.ts` 具名 re-export。
### 4.4 `packages/cli` — `bailian-cli``bl`
产品入口,极薄:
```typescript
// packages/cli/src/main.ts
createCli(commands, {
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
npmPackage: "bailian-cli",
}).run();
```
**命令路径由 `packages/cli/src/commands.ts` 决定**,例如:
```typescript
export const commands: Record<string, AnyCommand> = {
"text chat": textChat,
"asset-center list": assetList,
"finetune create": finetuneCreate,
update, // 单级命令 key 即路径
};
```
此文件还被 `tools/generate-reference.ts` 读取,生成 Agent Skill 参考文档。
### 4.5 `packages/kscli` — `knowledge-studio-cli``kscli`
轻量 RAG 产品,**复用同一套 commands**,但路径拍平:
```typescript
const commands = {
retrieve: knowledgeRetrieve, // ↔ bl knowledge retrieve
search: knowledgeSearch, // ↔ bl knowledge search
chat: knowledgeChat, // ↔ bl knowledge chat
"config show": configShow,
update,
};
```
同一个 `knowledgeRetrieve` 实现,在 `bl` 显示 `bl knowledge retrieve`,在 `kscli` 显示 `kscli retrieve`——路径完全由产品入口 map 的 key 决定。
---
## 5. 一次命令执行的完整链路
`bl text chat --message "hi"` 为例:
```mermaid
sequenceDiagram
participant User
participant main as cli/main.ts
participant createCli as runtime/create-cli.ts
participant registry as runtime/registry.ts
participant mw as middleware
participant cmd as commands/text/chat.ts
participant client as core/client
User->>main: bl text chat --message "hi"
main->>createCli: createCli(commands, identity).run(argv)
createCli->>registry: 解析路径 ["text","chat"]
registry-->>createCli: 匹配 textChat command
createCli->>mw: authStage → 注入 apiKey 到 client
mw->>cmd: run(ctx)
cmd->>client: requestJson / parseSSE
client-->>User: stdout 输出
```
**配置与凭证解析优先级**core 统一处理,命令不介入):
| 来源 | API Key | Console Token |
| ---- | ----------------------- | ------------------------------------- |
| 1 | `--api-key` flag | `~/.bailian/config.json` access_token |
| 2 | `DASHSCOPE_API_KEY` env | — |
| 3 | config.json `api_key` | — |
Console 命令额外有 `--console-region``--workspace-id` 等 flag由 runtime 按 `auth: "console"` 自动展示)。
---
## 6. 鉴权域
每个命令声明 `auth` 字段runtime 自动处理:
| auth 值 | 适用场景 | 凭证来源 | 网络方法 |
| ----------- | ------------------------------ | -------------------- | -------------------------------- |
| `"apiKey"` | DashScope API模型推理等 | API Key | `client.request` / `requestJson` |
| `"console"` | Console Gateway控制台能力 | Console access token | `client.console` |
| `"none"` | 纯本地config、update、help | 无 | 可选 credential-less client |
**规则**:调用 Console Gateway 的命令必须 `auth: "console"`,且**不要**重复声明 console 凭证域 flags。
---
## 7. 错误处理约定
CLI **只翻译自己能权威解释的错误**,服务端错误原样透传:
| 错误来源 | 处理 |
| ---------------------- | -------------------------------- |
| 缺参、flag 校验 | `UsageError` → 退出码 2 |
| 本地无凭证 | `BailianError(AUTH)` |
| 网络/DNS/TLS | `BailianError(NETWORK)` |
| HTTP 4xx/5xx、业务错码 | message **原样透传**,不二次包装 |
---
## 8. 开发工作流
### 8.1 环境准备
```bash
# 要求 Node >= 22.12, pnpm >= 10
pnpm install
# 格式化 + lint + 类型检查
pnpm run check # 或 vp check
# 本地跑 bltsx 直跑,无需 build
pnpm run bl -- text chat --help
pnpm run kscli -- search --help
# 全量测试
pnpm test # 或 vp test
# 构建所有包
pnpm run ready # check + test + build
```
### 8.2 新增一个 `bl` 命令(最小路径)
假设新增 `bl widget do`
**Step 1** — 实现命令(`packages/commands`
```bash
# 新建
packages/commands/src/commands/widget/do.ts
```
```typescript
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const FLAGS = {
name: { type: "string", valueHint: "<name>", description: "Widget name", required: true },
} satisfies FlagsDef;
export default defineCommand({
description: "Do something with a widget",
auth: "apiKey", // 或 "console" / "none"
flags: FLAGS,
usageArgs: "--name <name>",
exampleArgs: ['--name "demo"'],
async run(ctx) {
const data = await ctx.client.requestJson({ path: "/…", method: "POST", body: { } });
emitResult(ctx, data);
},
});
```
**Step 2** — 导出(`packages/commands/src/index.ts`
```typescript
export { default as widgetDo } from "./commands/widget/do.ts";
```
**Step 3** — 注册产品路径(`packages/cli/src/commands.ts`
```typescript
import { widgetDo } from "bailian-cli-commands";
// …
"widget do": widgetDo,
```
**Step 4** — E2E 测试(`packages/cli/tests/e2e/widget.e2e.test.ts`
见 [docs/agents/cli-e2e-tests.md](agents/cli-e2e-tests.md):至少覆盖分组 help、`--help`、缺参用例。
**Step 5** — 验证
```bash
vp check
vp test
pnpm run bl -- widget do --help
```
> 若 `kscli` 也需要暴露:在 `packages/kscli/src/main.ts` 的 map 里加 key。
> 技能 reference 会在 pre-commit 时由 `generate-reference.ts` 自动从 `commands.ts` 生成。
详细清单 → [docs/agents/command-add-remove.md](agents/command-add-remove.md)
### 8.3 给已有命令加 flag
→ [docs/agents/command-flag-change.md](agents/command-flag-change.md)
---
## 9. 测试体系
```
packages/cli/tests/
├── e2e/ # 33 个 e2e 测试文件
│ ├── helpers.ts # runCli、环境变量 readiness 判断
│ ├── global-setup.ts
│ └── <topic>.e2e.test.ts
└── stress/ # 多能力并发压测
├── run.mjs
└── targets/
```
**E2E 双层结构**(固定模式):
```typescript
// 层 1永远跑 — help / 分组,无需 API Key
describe("e2e: asset-center", () => {
test("asset-center 分组展示子命令帮助且成功退出", );
test("asset-center list --help 正常退出", );
});
// 层 2skipIf 缺凭证 — dry-run / 真实集成
describe.skipIf(!isConsoleE2EReady())("e2e: asset-centerConsole …)", () => {
test("缺少 --asset-id 时退出为用法错误 (2)", );
test("真实 list 流程", );
});
```
环境变量(常用):
| 变量 | 用途 |
| --------------------------------------------- | ------------------------- |
| `DASHSCOPE_API_KEY` | 模型 API 集成测试 |
| Console token`bl auth login --console` | 控制台命令测试 |
| `BAILIAN_E2E_*` | 各能力开关(视频/媒体等) |
压测:`pnpm run test:stress`
---
## 10. 命令能力地图(`bl` 全量)
当前 `packages/cli/src/commands.ts` 注册的命令组:
| 命令组 | 子命令示例 | auth 域 |
| -------------- | ------------------------------------------------------------------------------- | ---------------- |
| `auth` | login, status, logout | none / console |
| `text` | chat | apiKey |
| `omni` | (全模态对话) | apiKey |
| `image` | generate, edit | apiKey |
| `video` | generate, edit, ref, task get, download | apiKey |
| `vision` | describe | apiKey |
| `speech` | synthesize, recognize | apiKey |
| `knowledge` | retrieve, search, chat | apiKey |
| `memory` | add, search, list, update, delete, profile create/get | apiKey |
| `app` | call, list | apiKey / console |
| `mcp` | call, list, tools | apiKey |
| `search` | web | apiKey |
| `file` | upload | apiKey |
| `config` | show, set | none |
| `console` | call | console |
| `usage` | free, freetier, stats | console |
| `workspace` | list | console |
| `quota` | list, request, history, check | console |
| `dataset` | upload, list, get, delete, validate | console |
| `finetune` | create, list, get, cancel, delete, logs, checkpoints, export, watch, capability | console |
| `deploy` | create, list, get, models, scale, update, delete | console |
| `token-plan` | list-seats, create-key, assign-seats, add-member | console |
| `asset-center` | list, get, favorite, unfavorite, delete, download, stats, storage | console |
| `pipeline` | run, validate | apiKey |
| `advisor` | recommend | apiKey |
| `update` | (自更新) | none |
---
## 11. 非代码资产
```
tools/
├── generate-reference.ts # 从 cli/commands.ts → skills/bailian-cli/reference/
├── sync-skill-metadata.ts # 同步 SKILL.md 版本号
└── release/ # CI 发版自动化
skills/bailian-cli/ # Agent Skillnpx skills add modelstudioai/cli
.github/workflows/ # CI/CDpublish.yml 等)
docs/agents/ # 各维护场景的 AI 清单
```
根脚本:
```bash
pnpm run sync:skill-assets # build + 生成 reference + 同步版本
pnpm run release:check # 发版前校验
```
---
## 12. 发布
- 版本号:`packages/core``runtime``commands``cli``kscli` **保持同步**
- 发布范围:`tools/release/lib/packages.mjs` 定义
- `bailian-cli` 走常规定义发布;`knowledge-studio-cli``--knowledge` 通道
- 详见 [docs/agents/publish.md](agents/publish.md)
---
## 13. 关键文件速查
| 我想… | 看这里 |
| ------------------- | --------------------------------------- |
| 了解项目契约 | `AGENTS.md` |
| 改 `bl` 命令路径 | `packages/cli/src/commands.ts` |
| 写/改命令逻辑 | `packages/commands/src/commands/<域>/` |
| 导出命令 | `packages/commands/src/index.ts` |
| 改 CLI 框架行为 | `packages/runtime/src/` |
| 改 HTTP/鉴权/配置 | `packages/core/src/` |
| 改 kscli 路径 | `packages/kscli/src/main.ts` |
| 加 E2E 测试 | `packages/cli/tests/e2e/` |
| 改控制台 URL | `packages/runtime/src/urls.ts` |
| 改 API endpoint | `packages/core/src/client/endpoints.ts` |
| 改配置 schema | `packages/core/src/config/schema.ts` |
| 生成 Agent 参考文档 | `tools/generate-reference.ts` |
---
## 14. 场景导航(维护清单)
| 场景 | 文档 |
| ----------- | ------------------------------------------------------- |
| 命令增删改 | [command-add-remove.md](agents/command-add-remove.md) |
| E2E 测试 | [cli-e2e-tests.md](agents/cli-e2e-tests.md) |
| 加/改 flag | [command-flag-change.md](agents/command-flag-change.md) |
| 模型上下架 | [model-add-remove.md](agents/model-add-remove.md) |
| 错误文案 | [error-hint-change.md](agents/error-hint-change.md) |
| 鉴权扩展 | [auth-change.md](agents/auth-change.md) |
| 配置项扩展 | [config-add.md](agents/config-add.md) |
| 发布 | [publish.md](agents/publish.md) |
| 工具链/lint | [lint-toolchain.md](agents/lint-toolchain.md) |
---
## 15. 架构设计要点(读懂代码的钥匙)
1. **命令实现 ≠ 产品路径** — 同一 `knowledgeRetrieve` 可以是 `bl knowledge retrieve``kscli retrieve`
2. **defineCommand 是契约**`auth` + `flags` + `run(ctx)` 是命令的全部接口;凭证和网络细节下沉到 core/runtime。
3. **registry 从 map 建树**`"asset-center list"` 等 path 自动变成命令组help 动态生成。
4. **flags 用 camelCase 定义** — runtime 渲染为 `--kebab-case``ParsedFlags<typeof FLAGS>` 提供类型安全。
5. **dry-run 是全局 flag**`--dry-run` 在 auth stage 有例外处理,命令在 `run` 开头判断 `ctx.settings.dryRun`
6. **本地路径即 URL** — 所有接受 URL 的参数同时支持本地文件路径core `files/upload` 自动上传。
7. **Console Gateway 统一入口** — 控制台 API 走 `client.console({ product, action, params })`,不散落 raw fetch。
---
## 16. 本地配置速览
配置文件:`~/.bailian/config.json`
```json
{
"api_key": "sk-…",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"access_token": "…",
"console_region": "cn-beijing",
"console_site": "domestic"
}
```
常用环境变量:
| 变量 | 说明 |
| ---------------------------- | -------------------------- |
| `DASHSCOPE_API_KEY` | 模型 API Key |
| `DASHSCOPE_BASE_URL` | API Base URL |
| `BAILIAN_WORKSPACE_ID` | 业务空间 ID |
| `HTTP_PROXY` / `HTTPS_PROXY` | 代理runtime 启动时读取) |
登录:
```bash
bl auth login # API Key
bl auth login --console # Console token扫码
bl auth status
```
---
_文档版本:基于仓库当前结构(含 `asset-center`、`kscli``packages/rag` 已演进为 `packages/kscli`。_
+10 -3
View File
@@ -25,7 +25,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
当前 command 鉴权域(`AuthRequirement`):
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent/workspace
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
@@ -35,7 +35,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`
- `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_*`
@@ -78,6 +78,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- 如新增鉴权域,扩展 `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`:
- 新增 credential 类型 / source / scope 字段
- [ ] `packages/core/src/auth/resolver.ts`:
@@ -131,6 +134,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
## 完成后自查
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
```sh
# 各种凭证组合
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
@@ -150,9 +155,11 @@ 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
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 来源但忘了改 resolver 优先级,实际不生效
+10 -1
View File
@@ -6,6 +6,7 @@
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup`private`,不发布) |
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、livegated每用例最小路由 |
| **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、根 help |
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组identity`--version``search --help` path |
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
@@ -27,7 +28,7 @@
### commands E2E
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`spawn `harness/main.ts``routes` 为本 topic 最小 path → export 映射)
- fixtures`packages/commands/tests/e2e/fixtures/`
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map
@@ -78,6 +79,14 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
4. **真实集成**:放在 skip 块**末尾**
## 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`
+12 -11
View File
@@ -11,16 +11,17 @@
## 统一口径(安装)
1. **Supported install** `npx skills add modelstudioai/cli --all -g`(整包装齐,含 `bailian-protocol`
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它Agent Skills / `npx skills` **不会**按 frontmatter 自动拉依赖
1. **Supported install** `bl skill init`(装齐 registry 中全部 `bailian-*`,含 `bailian-protocol`
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它
3. **不要**在 frontmatter 写 `companions`也不要对外说「companions = 安装器硬依赖」
4. 子集安装`-s`)为 **advanced / 不推荐**skills CLI 不会自动带上 protocol;漏装会导致相对路径 Read 失败
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 / 鉴权 / 版本 / 错误上报)
▲ 靠 --all -g 与业务 skill 同装;非安装器强制 companions
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
┌───────┴────────┬────────────────┬──────────────────┐
bailian-gen bailian-finetune bailian-managed-agent
@@ -37,8 +38,8 @@ bailian-gen bailian-finetune bailian-managed-agent
### A. 分层边界
- [ ] **整包装齐**:安装/升级文案主推 `--all -g`;业务 skill **不**声明 `companions`
- [ ] **协议读取**CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `npx skills add modelstudioai/cli --all -g`
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
- [ ] **协议读取**CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
- [ ] **软 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」指向句
@@ -46,9 +47,9 @@ bailian-gen bailian-finetune bailian-managed-agent
### B. 文案与落款一致性
- [ ] 领域 skillgen / finetune / managed-agent路由或命令表后有指向 `reference/` 的句;文末 `## references`protocol + reference与家族对齐
- [ ] description 含 WHAT + WHEN + 反触发;安装说明指向 `--all -g`,不写 companions 必装
- [ ] 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` 中的 `npx skills add …` 示例(改 `INSTALL.md` 时按 [install-doc-change.md](install-doc-change.md) 同步静态页)
- [ ] 若改了安装方式:同步 `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. 归属与生成
@@ -60,8 +61,8 @@ bailian-gen bailian-finetune bailian-managed-agent
```sh
pnpm run sync:skill-assets
# 本地试装(测本仓库改动,勿只拉远端)
npx skills add "$(pwd)" --all -g -y
# 已发布版本试装
bl skill init
```
抽查:打开 `skills/bailian-cli/SKILL.md` 确认无领域子命令明细表、无 `companions`;打开对应领域 skill 确认有「勿猜 flag」与 hand-off。
@@ -69,7 +70,7 @@ npx skills add "$(pwd)" --all -g -y
## 常见漏点
- ✗ hub 路由表再次抄回 image / video / finetune / managed-agent 明细 → token 膨胀且与领域 skill 双份漂移
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 Agent Skills / `npx skills` 合同不符
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 `bl skill add` 合同不符
- ✗ 软 hand-off 写成硬路径 `../bailian-*/SKILL.md` 当执行前提 → 子集安装断链
- ✗ 只改 SKILL、忘改 `GROUP_OWNER_SKILL` → reference 落错 skill
- ✗ 手改 `skills/*/reference/*.md` → 下次 generate 被覆盖
+248
View File
@@ -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 IDCLI 自动分批。
- 如果某批失败,操作停止,已删除的批次会在错误 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)
+268
View File
@@ -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)
+344
View File
@@ -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)
+157
View File
@@ -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)
+340
View File
@@ -0,0 +1,340 @@
# 知识库管理命令手册
知识库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> (--doc-id <id> | --category-id <id>) [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
| `--name <text>` | string | 是 | 知识库名称1-20 字符,工作区内唯一) |
| `--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 字符
- `--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 --doc-id file-xxx --workspace-id ws-xxx
# 从分类导入并等待导入完成
bl knowledge create --name demo --category-id cate-xxx --wait
# 指定向量模型和切片大小
bl knowledge create --name my-kb --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`CLI 会先查询知识库名称和文档数量作为确认摘要。
**示例**
```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)
+822
View File
@@ -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` 可跳过确认,适用于自动化脚本。
### 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 --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 --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` |
+218
View File
@@ -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)
+401
View File
@@ -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 | 是 | 服务agentID |
| `--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 字符
**输出**
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 | 是 | 服务agentID |
| `--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-writeCLI 先读取当前 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 | 是 | 服务agentID |
| `--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 | 是 | 服务agentID |
| `--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 | 是 | 源服务agentID |
**输出**
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)
+1
View File
@@ -21,6 +21,7 @@
"bl": "pnpm -F bailian-cli dev",
"kscli": "pnpm -F knowledge-studio-cli dev",
"test": "vp test",
"test:journey": "vp test packages/commands/tests/e2e/knowledge/journeys",
"release:check": "node tools/release/check.mjs",
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
"test:stress": "node packages/cli/tests/stress/run.mjs"
+90 -146
View File
@@ -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,29 +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.8-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 3.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 520s 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
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
> **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 text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), 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`)
- **Asset center** — Browse and manage model-generated assets (`asset-center list/get/download`), favorites and recycle bin (`favorite`/`delete`), and storage quota (`stats`/`storage`)
- **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">
@@ -57,136 +45,93 @@ 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
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
**Agent install (recommended)**
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
> Binary install does not require Node.js. `npm install -g` remains fully supported.
**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
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 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 text 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 probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
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
# Asset center — browse, download, and manage model-generated assets (requires console login)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# 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." |
| 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"
```
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
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
@@ -194,26 +139,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`, `asset-center *`). 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 OpenAPI AK/SK (Token Plan only)
### Alibaba Cloud OpenAPI AK/SK
Required for 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
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -222,18 +161,31 @@ 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 or a specific version
bl update
bl update --to 0.1.14
# Switch config profile
bl config use --name token-plan
```
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 |
@@ -245,11 +197,3 @@ Config file location: `~/.bailian/config.json`
| 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.
+90 -147
View File
@@ -22,29 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.8-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
> **注意:** 以下功能目前仅对中国站aliyun.com账号开放国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **资产中心** — 管理模型生成资产(`asset-center list/get/download`)、收藏与回收站(`favorite`/`delete`)、容量统计(`stats`/`storage`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -54,137 +41,96 @@ _专为 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
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
**Agent 安装(推荐)**
# WindowsPowerShell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
# Node 用户 / 开发者(需要 Node.js >= 18.17
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留。
**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
# 或使用 Token Plan已内置 Base URL登录时自动测试 Key
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
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 text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
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 # 查看提额历史记录
# 资产中心 — 浏览、下载与管理模型生成资产(需控制台登录)
bl asset-center list --type IMAGE
bl asset-center get <asset-id> --include-download-url
bl asset-center download --id <asset-id>
bl asset-center stats
bl asset-center storage
# 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) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL登录命令会先测试 Key通过后才保存并激活 `token-plan` 配置。
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
@@ -192,26 +138,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history``asset-center *`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK(仅 Token Plan
### 阿里云 OpenAPI AK/SK
`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
# 方式一:登录命令(持久化到 ~/.bailian/config.json
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -220,20 +160,31 @@ 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 update --to 0.1.14
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.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" />
## 相关链接
| 资源 | 地址 |
@@ -245,11 +196,3 @@ bl update --to 0.1.14
| 获取 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)。
+160
View File
@@ -0,0 +1,160 @@
# 迭代一设计 · doc 组命令
> 命令:`doc upload` / `doc list` / `doc status` / `doc delete` / `doc tag` / `doc import-oss`
> 公共约定见 [README.md](README.md)。
## doc upload — 上传本地文件入库(编排命令)
**说明**:本迭代最复杂命令。把"本地文件 → 数据中心 →(可选)导入知识库"封装为一条命令替代构建期最高频的控制台操作S2.2 痛点:高)。对标竞品 add-file。
**编排四步**
| 步 | API | 输入 | 输出 |
| ---------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------- |
| 1 申请租约 | `POST /api/v1/connector/dash/applyFileUploadLease` | `category`(类目ID) + `fileName` + `sizeBytes`(字符串!) + `contentMd5`(Base64) | `leaseId` + `param.url/method/headers` |
| 2 OSS 上传 | `PUT {param.url}` | 文件二进制 + `param.headers`(含 `x-bailian-extra``Content-Type` | HTTP 200 |
| 3 注册文件 | `POST /api/v1/connector/dash/addFile` | `leaseId` + `category` + `parser: "AUTO_SELECT"` + `tags?` | `fileId` |
| 4 导入(可选,传 `--index-id` 时) | `POST /api/v1/indices/rag/index/job/create` | `indexId` + `dataSource: { sourceType: "DATA_CENTER_FILE", fileIds }` | `ingestionId` |
坑位(实现注释必须标注):
- `sizeBytes` 必须字符串;`contentMd5` = `crypto.createHash("md5").update(buf).digest("base64")`
- 租约/注册的类目参数名是 `category`,不是 `categoryId`
- 第 4 步 body 是嵌套 `dataSource: { sourceType, fileIds }`(实测;公开文档的平铺 `documentIds` 会报 `Index.InvalidParameter`
- **第 4 步必须显式传 `sourceType`不传会导入整个数据中心API 文档明示的默认行为)**
- 步骤 2 走 OSS 域名不走 DashScope 网关,用原生 fetch 而非 ctx.client无 Bearer 头);失败归类 NETWORK
**Flags**
| flag | 类型 | 必填 | 说明 |
| -------------------------------------------------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------- |
| `--file <path>` | array | 是 | 本地文件路径,可重复;扩展名与大小按产品支持范围预校验(见下方格式白名单) |
| `--index-id <id>` | string | 否 | 注册后立即导入该知识库(触发第 4 步,多文件合并为一个 job |
| `--category-id <id>` | string | 否 | 目标类目缺省自动解析默认类目listCategory 取 `isDefault: true`),解析失败报 GENERAL + hint 显式传 `--category-id` |
| `--tag <text>` | array | 否 | addFile tags可重复 |
| `--wait` / `--poll-interval <s>` / `--timeout <s>` | — | 否 | 与 `--index-id` 联用,轮询 job status 至终态 |
**validate**`--wait``--index-id` → USAGE文件不存在/不可读 → GENERAL + errno hint沿用错误边界规范
**格式白名单与大小预校验**(依据 data/documents.md「支持的格式」读文件前拦截避免白传 OSS
| 类型 | 扩展名 | 硬限(超限 USAGE |
| ------ | -------------------------- | ----------------------------------------------------- |
| 文档 | .doc .docx .ppt .pptx .pdf | 150 MB |
| 表格 | .xls .xlsx | 10 MB产品为“建议值”超限降级为 stderr 警告不拦截) |
| 图片 | .png .jpg .jpeg .bmp .gif | 20 MB尺寸约束不做客户端校验留服务端 |
| 纯文本 | .md .txt .html | 10 MB同表格警告不拦截 |
- 扩展名不在白名单 → USAGE错误信息列出支持格式白名单常量独立导出便于后续随产品更新
- 开放问题create-kb.md 提及 .csv 但 documents.md 格式表未列——文档口径不一致,实现前向产品确认;确认前 .csv 暂入白名单(服务端拒绝会透传)
**输出**
- text每文件一行 `<fileName> <fileId> registered`;有导入时追加 `job: <ingestionId>`--wait 结束追加终态
- json`{ files: [{path, fileId}], index_id?, ingestion_id?, final_status? }`(编排命令无单一响应可透传,输出自定义稳定结构)
- quiet仅 fileId 每行一个
**实现方案**
- 文件 `doc-upload.ts`;多文件串行执行 1-3 步(首版不并发,避免 OSS 限流复杂化),全部注册成功后合并执行第 4 步
- 部分失败语义:任一文件步骤 1-3 失败即中止并报错,已成功的 fileId 列入错误 hint幂等重传代价低
- 默认类目解析结果进程内缓存(多文件只查一次)
- dry-run不读文件内容size/md5 以占位符表示),输出四步编排计划 `{ steps: [{step, endpoint, request}] }`
**测试方案**
- help / 缺 `--file` exitCode 2 / `--wait``--index-id` exitCode 2
- 文件不存在 → 非零退出 + ENOENT hint`.zip` 扩展名 → USAGE 列出支持格式
- dry-run断言 steps 长度(带/不带 --index-id 为 4/3、lease 请求 `sizeBytes` 为字符串类型、job 请求含 `sourceType: "DATA_CENTER_FILE"`
- live上传 1KB 临时 md 文件 → 断言 fileId 前缀 `file_` → afterAll doc delete + 数据中心 deleteFile 清理
## doc list — 查询知识库文档列表
**说明**:列出库内文档及解析/索引状态,含 FAILED 发现S2.3 / S5.2)。
**API**`GET /api/v1/indices/rag/index/files`query string`index_id` + `page_num`(注意本接口是 page_num+ `page_size`(默认 10最大 100
**Flags**`--index-id` 必填;`--page-number` / `--page-size`
**输出**
- text每行 `doc_id status doc_name doc_type size`status=FAILED 行红色高亮TTY尾行 `total: N`
- json 透传quiet 仅 doc_id
**实现/测试**:单 API 直映射(`doc-list.ts`dry-run 断言 query 参数名为 `page_num`live 断言 rows 结构与 doc_id 前缀。
## doc status — 查询导入任务状态
**说明**:查导入任务进度,`--wait` 阻塞至终态供脚本串行S2.3 痛点:高L3 验收FAILED 时非零 exit code
**API**`GET /api/v1/indices/rag/index_job/status`query string`index_id` + `job_id`**双必填,仅传其一服务端返回 SystemError客户端前置双校验拦截**+ 分页参数。
**Flags**
| flag | 必填 | 说明 |
| -------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------- |
| `--index-id <id>` | 是 | 知识库 ID |
| `--job-id <id>` | 是 | 导入任务 IDkb create / doc upload 返回的 ingestionId也见 doc list 的 ingestion_id |
| `--page-number` / `--page-size` | 否 | 任务含大量文档时分页 |
| `--wait` / `--poll-interval <s>`(默认 5) / `--timeout <s>`(默认 600) | 否 | 轮询至终态 |
**行为**
- 终态 FINISH → exit 0FAILED → `BailianError(GENERAL)` 透传服务端 message含文档级失败明细摘要exit 1
- `--wait` 超时 → TIMEOUT(5)
- 已知行为:库无进行中任务时接口可能返回 SystemError——hint 引导 "check ingestion_id via doc list"
**输出**text 顶部任务总状态 + 文档级状态列表FAILED 高亮json 透传。
**测试方案**help / 缺任一必填(两条用例)/ dry-run 断言 query 含两个 id / live配合 upload 用例拿真实 job 轮询到 FINISH`--wait --timeout 1` 对慢任务断言 exitCode 5若不稳定则仅静态覆盖超时路径live 标记 skip 原因)。
## doc delete — 删除文档【危险操作】
**说明**从知识库删除文档及其全部切片S5.1 内容更新循环)。
**API**`POST /api/v1/indices/rag/index/delete_file`body `{ index_id, doc_ids }`snake_case。响应 `data.deleted[]` 为实际删除列表。
**Flags**`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`
**实现方案**`doc-delete.ts`;确认摘要含 index_id + doc_id 列表≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
**测试方案**help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY 无 `--yes` exitCode 2 / live 配合 upload 清理链。
## doc tag — 批量更新文档标签
**说明**批量打标支撑标签过滤检索S2.4)。
**API**`POST /api/v1/connector/dash/batchUpdateFileTag``fileInfos`1-20 项,每项 `fileId` + `tags`,单标签 ≤32 字符、单文件 ≤100 个、总长 ≤700+ `updateMode`OVERWRITE/APPEND
**Flags**
| flag | 必填 | 说明 |
| --------------- | ---- | ------------------------------------------------------------------------- |
| `--doc-id <id>` | 是 | 可重复1-20 个(客户端预校验),映射 fileInfos[].fileId |
| `--tag <text>` | 是 | 可重复,应用到所有 `--doc-id`(首版同一组标签批量打;异构标签用多次调用) |
| `--mode <m>` | 否 | choices: `overwrite`/`append`,默认 `append`(追加比覆盖安全,作为缺省) |
**实现/测试**`doc-tag.ts` 单 API 直映射客户端预校验标签长度约束USAGE 前置拦截dry-run 断言 `updateMode: "APPEND"` 大写映射与 fileInfos 结构live 打标后 listFile/describeFile 验证回读。
## doc import-oss — 从授权 OSS 批量导入
**说明**:从已 SLR 授权的 OSS Bucket 批量导入数据中心(大客户批量场景)。
**API**`POST /api/v1/connector/dash/addFilesFromAuthorizedOss`。必填 `categoryId/categoryType/ossBucket/ossRegionId/fileDetails`1-10 项,每项 `fileName+ossKey`)。返回 `data.fileIds`
**Flags**
| flag | 必填 | 说明 |
| -------------------- | ---- | -------------------------------------------- |
| `--bucket <name>` | 是 | 映射 ossBucket |
| `--region <id>` | 是 | 映射 ossRegionId如 cn-beijing |
| `--oss-key <key>` | 是 | 可重复1-10 个fileName 取 key 的 basename |
| `--category-id <id>` | 否 | 缺省走默认类目解析(复用 upload 的解析函数) |
| `--tag <text>` | 否 | 可重复≤10 |
| `--overwrite` | 否 | switch映射 overWriteFileByOssKey |
固定值:`categoryType: "UNSTRUCTURED"``parser` 不暴露(默认 AUTO_SELECT审慎原则——DASH_QWEN_VL_PARSER 等需配 parserConfig使用方式未验证
**错误边界**SLR 未授权的服务端权限错误原样透传hint 附 RAM 控制台确认 `AliyunServiceRoleForBailian` 的指引(该指引来自 API 文档 Note属可权威解释范围
**实现/测试**`doc-import-oss.ts` 单 API 直映射dry-run 断言 fileDetails 结构与 fileName 派生逻辑live 依赖 OSS 授权环境gating 追加 `BAILIAN_E2E_OSS_BUCKET` 环境变量,无则 skip。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.14.1",
"version": "1.16.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+96 -18
View File
@@ -33,6 +33,37 @@ import {
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
knowledgeKbList,
knowledgeKbInfo,
knowledgeDocList,
knowledgeDocStatus,
knowledgeDocUpload,
knowledgeKbCreate,
knowledgeKbUpdate,
knowledgeKbDelete,
knowledgeDocDelete,
knowledgeDocTag,
knowledgeServiceList,
knowledgeServiceGet,
knowledgeServiceCreate,
knowledgeServiceUpdate,
knowledgeServiceDeploy,
knowledgeServiceDelete,
knowledgeServiceCopy,
knowledgeChunkAdd,
knowledgeChunkList,
knowledgeChunkUpdate,
knowledgeChunkDelete,
knowledgeKbStats,
knowledgeCategoryList,
knowledgeCategoryAdd,
knowledgeCategoryDelete,
knowledgeFileList,
knowledgeFileGet,
knowledgeFileDelete,
knowledgeCollectionCreate,
knowledgeCollectionGet,
knowledgeDocImportOss,
mcpCall,
mcpList,
mcpTools,
@@ -45,15 +76,20 @@ import {
usageFreetier,
usageStats,
usageSummary,
usageTokenPlan,
usageCodingPlan,
pipelineRun,
pipelineValidate,
advisorRecommend,
modelList,
workspaceList,
quotaList,
quotaRequest,
quotaUpdate,
quotaHistory,
quotaCheck,
permissionList,
permissionGrant,
permissionRevoke,
datasetUpload,
datasetList,
datasetGet,
@@ -62,6 +98,7 @@ import {
finetuneTextCreate,
finetuneAudioCreate,
finetuneImageCreate,
finetuneVideoCreate,
finetuneList,
finetuneGet,
finetuneCancel,
@@ -71,6 +108,7 @@ import {
finetuneExport,
finetuneWatch,
finetuneCapability,
finetunePrice,
deployTextCreate,
deployAudioCreate,
deployImageCreate,
@@ -80,18 +118,12 @@ import {
deployScale,
deployUpdate,
deployDelete,
deployPause,
deployResume,
tokenPlanListSeats,
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
assetList,
assetGet,
assetFavorite,
assetUnfavorite,
assetDelete,
assetDownload,
assetStats,
assetStorage,
workspaceInit,
pluginInstall,
pluginLink,
@@ -101,6 +133,7 @@ import {
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentPlan,
@@ -159,6 +192,39 @@ export const commands: Record<string, AnyCommand> = {
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
"knowledge chat": knowledgeChat,
"knowledge list": knowledgeKbList,
"knowledge info": knowledgeKbInfo,
"knowledge create": knowledgeKbCreate,
"knowledge update": knowledgeKbUpdate,
"knowledge delete": knowledgeKbDelete,
"knowledge doc list": knowledgeDocList,
"knowledge doc status": knowledgeDocStatus,
"knowledge doc upload": knowledgeDocUpload,
"knowledge doc delete": knowledgeDocDelete,
"knowledge doc tag": knowledgeDocTag,
"knowledge service list": knowledgeServiceList,
"knowledge service get": knowledgeServiceGet,
"knowledge service create": knowledgeServiceCreate,
"knowledge service update": knowledgeServiceUpdate,
"knowledge service deploy": knowledgeServiceDeploy,
"knowledge service delete": knowledgeServiceDelete,
"knowledge service copy": knowledgeServiceCopy,
"knowledge chunk add": knowledgeChunkAdd,
"knowledge chunk list": knowledgeChunkList,
"knowledge chunk update": knowledgeChunkUpdate,
"knowledge chunk delete": knowledgeChunkDelete,
"knowledge stats": knowledgeKbStats,
"knowledge doc import-oss": knowledgeDocImportOss,
// Data-center commands live under knowledge (no separate connector namespace);
// the user-facing term for connector is "collection".
"knowledge collection create": knowledgeCollectionCreate,
"knowledge collection get": knowledgeCollectionGet,
"knowledge category list": knowledgeCategoryList,
"knowledge category add": knowledgeCategoryAdd,
"knowledge category delete": knowledgeCategoryDelete,
"knowledge file list": knowledgeFileList,
"knowledge file get": knowledgeFileGet,
"knowledge file delete": knowledgeFileDelete,
"mcp call": mcpCall,
"mcp list": mcpList,
"mcp tools": mcpTools,
@@ -171,15 +237,20 @@ export const commands: Record<string, AnyCommand> = {
"usage freetier": usageFreetier,
"usage stats": usageStats,
"usage summary": usageSummary,
"usage token-plan": usageTokenPlan,
"usage coding-plan": usageCodingPlan,
"pipeline run": pipelineRun,
"pipeline validate": pipelineValidate,
"advisor recommend": advisorRecommend,
"model list": modelList,
"workspace list": workspaceList,
"quota list": quotaList,
"quota request": quotaRequest,
"quota update": quotaUpdate,
"quota history": quotaHistory,
"quota check": quotaCheck,
"permission list": permissionList,
"permission grant": permissionGrant,
"permission revoke": permissionRevoke,
"dataset upload": datasetUpload,
"dataset list": datasetList,
"dataset get": datasetGet,
@@ -188,6 +259,7 @@ export const commands: Record<string, AnyCommand> = {
"finetune text create": finetuneTextCreate,
"finetune audio create": finetuneAudioCreate,
"finetune image create": finetuneImageCreate,
"finetune video create": finetuneVideoCreate,
"finetune list": finetuneList,
"finetune get": finetuneGet,
"finetune cancel": finetuneCancel,
@@ -197,6 +269,7 @@ export const commands: Record<string, AnyCommand> = {
"finetune export": finetuneExport,
"finetune watch": finetuneWatch,
"finetune capability": finetuneCapability,
"finetune price": finetunePrice,
"deploy text create": deployTextCreate,
"deploy audio create": deployAudioCreate,
"deploy image create": deployImageCreate,
@@ -206,18 +279,12 @@ export const commands: Record<string, AnyCommand> = {
"deploy scale": deployScale,
"deploy update": deployUpdate,
"deploy delete": deployDelete,
"deploy pause": deployPause,
"deploy resume": deployResume,
"token-plan list-seats": tokenPlanListSeats,
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"asset-center list": assetList,
"asset-center get": assetGet,
"asset-center favorite": assetFavorite,
"asset-center unfavorite": assetUnfavorite,
"asset-center delete": assetDelete,
"asset-center download": assetDownload,
"asset-center stats": assetStats,
"asset-center storage": assetStorage,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
@@ -227,6 +294,7 @@ export const commands: Record<string, AnyCommand> = {
"skill update": skillUpdate,
"skill remove": skillRemove,
"skill list": skillList,
"skill init": skillInit,
"managed-agent init": managedAgentInit,
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
@@ -245,3 +313,13 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent session events": managedAgentSessionEvents,
"managed-agent skill-list": managedAgentSkillList,
};
/**
* Runtime-only aliases for renamed commands: dispatched by the CLI (merged in
* main.ts) but kept out of the canonical map so generate-reference.ts only
* documents the canonical path.
*/
export const commandAliases: Record<string, AnyCommand> = {
// Pre-migration name of "quota update".
"quota request": quotaUpdate,
};
+12 -9
View File
@@ -1,5 +1,5 @@
import { createCli } from "bailian-cli-runtime";
import { commands } from "./commands.ts";
import { commandAliases, commands } from "./commands.ts";
import { commandPackPolicy } from "./command-pack-policy.ts";
import pkg from "../package.json" with { type: "json" };
@@ -10,11 +10,14 @@ const quickStartTasks = [
"Help me analyze this video and write a Xiaohongshu-style post",
] as const;
void createCli(commands, {
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
npmPackage: "bailian-cli",
quickStartTasks,
commandPacks: commandPackPolicy,
}).run();
void createCli(
{ ...commands, ...commandAliases },
{
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
npmPackage: "bailian-cli",
quickStartTasks,
commandPacks: commandPackPolicy,
},
).run();
@@ -1,131 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts";
describe("e2e: asset-center", () => {
test("asset-center 分组展示子命令帮助且成功退出", async () => {
const { stdout, stderr, exitCode } = await runCli(["asset-center"]);
expect(exitCode, stderr).toBe(0);
const output = `${stdout}\n${stderr}`;
expect(output).toContain("list");
expect(output).toContain("storage");
expect(output).not.toContain("oss");
});
test("asset-center list --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--type");
expect(stderr).toContain("--recycle-bin");
expect(stderr).toContain("bl asset-center list");
});
test("asset-center get --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "get", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--asset-id");
});
test("asset-center favorite --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--id");
});
test("asset-center delete --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "delete", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--permanent");
});
test("asset-center download --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "download", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--id");
expect(stderr).not.toMatch(/(^|\s)--out(\s|$)/);
});
test("asset-center stats --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "stats", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--sync-failed");
});
test("asset-center storage --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "storage", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("bl asset-center storage");
});
});
describe.skipIf(!isConsoleE2EReady())("e2e: asset-centerConsole", () => {
test("asset-center get 缺少 --asset-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "get", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--asset-id|Missing required argument/i);
});
test("asset-center favorite 缺少 --id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "favorite", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--id|Missing required argument/i);
});
test("asset-center download 缺少 --id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["asset-center", "download", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--id|Missing required argument/i);
});
test("asset-center list --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"list",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
api?: string;
data?: { deleteStatus?: string };
}>(stdout);
expect(data.api).toContain("listModelGeneratedAsset");
expect(data.data?.deleteStatus).toBe("NORMAL");
});
test("asset-center stats --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"stats",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ api?: string }>(stdout);
expect(data.api).toContain("countModelGeneratedAsset");
});
test("asset-center storage --dry-run 输出请求参数", async () => {
const { stdout, stderr, exitCode } = await runCli([
"asset-center",
"storage",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ api?: string }>(stdout);
expect(data.api).toContain("getStorageQuota");
});
test("【console】asset-center list 真实调用或鉴权失败优雅退出", async () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID;
const args = ["asset-center", "list", "--output", "json", "--page-size", "1"];
if (workspaceId) args.push("--workspace-id", workspaceId);
const result = await runCli(args);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
});
});
@@ -7,10 +7,15 @@ const commandPaths = Object.keys(commands).sort();
const groupPaths = deriveGroupPaths(commandPaths);
describe("e2e: bl registry smoke", () => {
test("根帮助展示 bl 与全局 flag", async () => {
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
const { stderr, exitCode } = await runCli(["--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/\bbl\b/i);
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
expect(stderr).toMatch(/token-plan create-key\s+\[AK\/SK\]\s+Create a Token Plan API key/);
expect(stderr).toMatch(/config show\s+\[No Auth\]\s+Display current configuration/);
expect(stderr).toMatch(/--base-url/);
expect(stderr).toMatch(/--console-region/);
expect(stderr).toMatch(/--console-site/);
@@ -18,6 +23,24 @@ describe("e2e: bl registry smoke", () => {
expect(stderr).not.toMatch(/^\s*--region\s/m);
});
test("分组帮助按叶子命令展示不同鉴权域", async () => {
const { stderr, exitCode } = await runCli(["app", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
});
test.each([
[["text", "chat"], "API Key"],
[["app", "list"], "Console"],
[["token-plan", "list-seats"], "AK/SK"],
[["config", "show"], "No Auth"],
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain(`Authentication: ${authLabel}`);
});
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
expect(exitCode, stderr).toBe(0);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.14.1",
"version": "1.16.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
@@ -1,434 +0,0 @@
# 资产中心 CLI 命令树设计
> 本文档定义 `bl asset` 命令族的路径结构、help 层级、flags 概览与示例。
> 技术实现细节见 [DESIGN.md](./DESIGN.md)API 字段见 [api-doc.md](./api-doc.md)。
## 1. 命名原则
| 原则 | 说明 |
| ------------ | ------------------------------------------------------------------------- |
| 产品路径前缀 | `asset`(不用 `asset-center`,与 `deploy` / `dataset` 等产品域一致) |
| 层级深度 | 最多三级:`asset <group> <action>` |
| 子组条件 | 仅当子组下 ≥ 2 个 action 时使用子组(见 AGENTS.md |
| bin 前缀 | `usageArgs` / `exampleArgs` 不写 `bl`help 由 runtime 按路径补全 |
| 鉴权 | 全部 `auth: "console"`;自动可见 `--console-region` 等 CONSOLE_AUTH_FLAGS |
---
## 2. 命令树总览
```
bl asset
├── list # 分页查询资产列表
├── get <asset-id> # 查询单个资产详情
├── favorite # 收藏资产
├── unfavorite # 取消收藏
├── delete # 删除资产(默认软删到回收站)
├── restore # 从回收站恢复
├── download # 获取下载链接 / 可选落盘
├── stats # 资产数量统计
├── storage # 存储容量与配额
├── models # [P1] 模型列表(辅助筛选)
│ └── list
├── service # [P1/P2] 服务开通状态
│ ├── status
│ ├── enable # [P2]
│ └── disable # [P2]
```
---
## 3. 产品入口注册 Map
`packages/cli/src/commands.ts` 中预期注册camelCase export → kebab path
| Map Key | Export 名(建议) | Phase |
| ------------------------- | --------------------- | ----- |
| `"asset list"` | `assetList` | 1 |
| `"asset get"` | `assetGet` | 1 |
| `"asset favorite"` | `assetFavorite` | 1 |
| `"asset unfavorite"` | `assetUnfavorite` | 1 |
| `"asset delete"` | `assetDelete` | 1 |
| `"asset restore"` | `assetRestore` | 1 |
| `"asset download"` | `assetDownload` | 1 |
| `"asset stats"` | `assetStats` | 1 |
| `"asset storage"` | `assetStorage` | 1 |
| `"asset models list"` | `assetModelsList` | 2 |
| `"asset service status"` | `assetServiceStatus` | 2 |
| `"asset service enable"` | `assetServiceEnable` | 3 |
| `"asset service disable"` | `assetServiceDisable` | 3 |
---
## 4. Help 层级预览
### 4.1 顶层分组
```
$ bl asset
Asset management commands for Bailian Asset Center.
Commands:
list List model-generated assets
get Get asset details by ID
favorite Mark assets as favorites
unfavorite Remove assets from favorites
delete Delete assets (soft delete by default)
restore Restore soft-deleted assets
download Get asset download URLs
stats Count assets by type
storage View storage quota and usage
models Model configuration helpers
service Asset center service subscription
Run `bl asset <command> --help` for details.
```
---
## 5. 各命令规格
以下 `usageArgs` 为命令 metadata 中的值(不含 global flags。Global flags`--output``--dry-run``--quiet` 等)与 console flags`--workspace-id` 等)由 runtime 自动追加到 help。
---
### 5.1 Phase 1 命令
#### `bl asset list`
```
Description: List model-generated assets with filters and cursor pagination
Usage: bl asset list [flags]
Flags:
--type <type> Asset type: IMAGE, VIDEO, AUDIO
--model <name> Filter by model name
--keyword <text> Filter by asset name (substring)
--favorited Show only favorited assets
--recycle-bin Show soft-deleted assets (recycle bin)
--sync-status <status> OSS sync status filter
--begin-time <datetime> Filter by generate time start (ISO_LOCAL_DATE_TIME)
--end-time <datetime> Filter by generate time end
--include-download-url Include signed download URLs
--include-thumbnail Include thumbnail URLs
--thumbnail-width <px> Thumbnail width
--thumbnail-height <px> Thumbnail height
--page-size <n> Page size (default: 10, max: 100)
--next-token <token> Cursor for next page
--pre-token <token> Cursor for previous page
Examples:
bl asset list
bl asset list --type IMAGE --model qwen-image-3.0
bl asset list --favorited --page-size 20
bl asset list --recycle-bin
bl asset list --keyword landscape --output json
```
**PRD 映射:** #1 查看资产列表
---
#### `bl asset get <asset-id>`
```
Description: Get full details of a model-generated asset
Usage: bl asset get <asset-id> [flags]
Arguments:
<asset-id> Asset ID to query
Flags:
--asset-id <id> Asset ID (alternative to positional)
--include-download-url Include signed download URL
--include-thumbnail Include thumbnail URL
--thumbnail-width <px> Thumbnail width
--thumbnail-height <px> Thumbnail height
Examples:
bl asset get asset-001
bl asset get asset-001 --include-download-url --output json
```
**PRD 映射:** #2 查看资产详情
---
#### `bl asset favorite`
```
Description: Add assets to favorites
Usage: bl asset favorite --id <asset-id> [--id <asset-id>...]
Flags:
--id <asset-id> Asset ID (repeatable, max 100, required)
Examples:
bl asset favorite --id asset-001
bl asset favorite --id asset-001 --id asset-002
```
**PRD 映射:** #3 收藏
---
#### `bl asset unfavorite`
```
Description: Remove assets from favorites
Usage: bl asset unfavorite --id <asset-id> [--id <asset-id>...]
Flags:
--id <asset-id> Asset ID (repeatable, max 100, required)
Examples:
bl asset unfavorite --id asset-001
bl asset unfavorite --id asset-001 --id asset-002
```
**PRD 映射:** #3 取消收藏
---
#### `bl asset delete`
```
Description: Delete assets (soft delete to recycle bin by default)
Usage: bl asset delete --id <asset-id> [--id <asset-id>...] [flags]
Flags:
--id <asset-id> Asset ID (repeatable, max 100, required)
--permanent Permanently delete (cannot be restored)
Examples:
bl asset delete --id asset-001
bl asset delete --id asset-001 --id asset-002
bl asset delete --id asset-001 --permanent
```
**PRD 映射:** #4 删除资产、#5 批量删除
---
#### `bl asset restore`
```
Description: Restore soft-deleted assets from recycle bin
Usage: bl asset restore --id <asset-id> [--id <asset-id>...]
Flags:
--id <asset-id> Asset ID (repeatable, max 100, required)
Examples:
bl asset restore --id asset-001
bl asset restore --id asset-001 --id asset-002
```
**PRD 映射:** 补充能力(配合回收站)
---
#### `bl asset download`
```
Description: Get signed download URLs for assets
Usage: bl asset download --id <asset-id> [--id <asset-id>...] [--out <path>]
Flags:
--id <asset-id> Asset ID (repeatable, max 100, required)
--out <path> Save file to path (only when exactly one --id)
Examples:
bl asset download --id asset-001
bl asset download --id asset-001 --out ./image.png
bl asset download --id asset-001 --id asset-002 --output json
```
**PRD 映射:** #6 下载资产
---
#### `bl asset stats`
```
Description: Count model-generated assets by type
Usage: bl asset stats [flags]
Flags:
--type <type> Filter by asset type
--model <name> Filter by model name
--keyword <text> Filter by asset name
--favorited Count only favorited assets
--recycle-bin Count soft-deleted assets
--sync-failed Also count assets with failed OSS sync
--begin-time <datetime> Filter by generate time start
--end-time <datetime> Filter by generate time end
Examples:
bl asset stats
bl asset stats --sync-failed
bl asset stats --type IMAGE --output json
```
**PRD 映射:** #7 查看资产统计
**text 输出示例:**
```
Total: 200
Image: 150
Video: 30
Audio: 20
Sync failed: 5 # 仅 --sync-failed 时出现
```
---
#### `bl asset storage`
```
Description: View storage quota, usage, and overage pricing
Usage: bl asset storage [flags]
Examples:
bl asset storage
bl asset storage --output json
```
**PRD 映射:** #14 查看容量信息
**text 输出示例:**
```
Used: 1.2 GB
Free quota: 5.0 GB
Overage: ¥0.12/GB/month
```
---
### 5.3 Phase 2/3 可选命令
#### `bl asset models list`
```
Description: List managed models grouped by asset type
Usage: bl asset models list
Examples:
bl asset models list --output json
```
用途:配合 `bl asset list --model` 时查阅可用 modelId。
---
#### `bl asset service status`
```
Description: Check whether asset center service is enabled
Usage: bl asset service status
Examples:
bl asset service status
```
---
---
## 6. PRD 覆盖矩阵
| PRD # | 功能 | CLI 命令 | Phase | 状态 |
| ----- | ------------- | ------------------------------- | ----- | ---------------------------------- |
| 1 | 查看资产列表 | `asset list` | 1 | ✅ 可开发 |
| 2 | 查看资产详情 | `asset get` | 1 | ✅ 可开发 |
| 3 | 收藏/取消收藏 | `asset favorite` / `unfavorite` | 1 | ✅ 可开发 |
| 4 | 删除资产 | `asset delete` | 1 | ✅ 可开发 |
| 5 | 批量删除 | `asset delete`(多 `--id` | 1 | ✅ 可开发 |
| 6 | 下载资产 | `asset download` | 1 | ✅ 可开发 |
| 7 | 查看资产统计 | `asset stats` | 1 | ✅ 可开发(转存失败用 workaround |
| 14 | 查看容量信息 | `asset storage` | 1 | ✅ 可开发 |
---
## 7. 典型工作流
### 7.1 首次使用
```bash
bl auth login --console
bl config set workspace_id ws-xxxxx
bl asset service status # 可选:确认已开通
bl asset storage # 查看容量
```
### 7.2 浏览与筛选
```bash
bl asset list
bl asset list --type IMAGE --model qwen-image-3.0 --keyword landscape
bl asset list --favorited
bl asset list --recycle-bin
bl asset get asset-001 --include-download-url
bl asset stats
bl asset stats --sync-failed
```
### 7.3 资产管理
```bash
bl asset favorite --id asset-001
bl asset unfavorite --id asset-001
bl asset delete --id asset-001
bl asset delete --id asset-001 --id asset-002
bl asset restore --id asset-001
bl asset download --id asset-001 --out ./image.png
```
### 7.5 脚本翻页JSON
```bash
# 第一页
bl asset list --page-size 50 --output json
# 后续页(使用响应中的 nextToken
bl asset list --page-size 50 --next-token 1000 --output json
```
---
## 8. 与现有命令的风格对齐
| 参考命令 | 对齐点 |
| ------------------------------ | -------------------------------------------------- |
| `bl app list` | console gateway 调用、dry-run 输出 `{ api, data }` |
| `bl dataset list` | text 表格 + json items 结构 |
| `bl deploy list/get/create` | 产品域子命令命名、多级 path |
| `bl memory profile get/create` | 三级 path 子组 |
| `bl quota list` | `zeldaHttp.*` API 名、响应 extract |
| `bl video download` | `--out` 落盘 |
| `bl usage stats` | `requireWorkspaceId`、console E2E 模式 |
---
## 9. 变更记录
| 日期 | 版本 | 说明 |
| ---------- | ---- | ---------------------------------------------- |
| 2026-07-09 | 0.1 | 初稿命令树、PRD 映射、分 Phase 规格 |
| 2026-08-07 | 0.2 | 取消 OSS 转存命令(`oss *` / `transfer list` |
@@ -1,408 +0,0 @@
# 资产中心 CLI 设计文档
> 本文档描述 `bl asset` 命令族的技术设计方案,供开发、评审与联调使用。
> 接口字段细节见同目录 [api-doc.md](./api-doc.md);命令路径与 help 结构见 [COMMAND-TREE.md](./COMMAND-TREE.md)。
## 1. 背景与目标
### 1.1 背景
百炼资产中心Asset Center提供模型生成资产的存储、检索、收藏、删除与容量管理能力。产品 PRD 要求 CLI 覆盖以下模块:
| 模块 | PRD 能力 |
| -------- | ----------------------------------------------- |
| 资产管理 | 列表、详情、收藏/取消收藏、删除、批量删除、下载 |
| 资产统计 | 总量、按类型统计 |
| 容量 | 已用容量、免费额度、超额单价 |
后端接口通过 **Zelda HTTP 网关** 暴露Base Path 为 `/zelda/api/v1/bailian/asset`,详见 [api-doc.md](./api-doc.md)。
### 1.2 目标
- 在 `packages/commands` 实现可复用命令库,由 `packages/cli/src/commands.ts` 注册为 `bl asset ...` 产品路径
- 遵循 monorepo 分层约定:`commands` 不写产品 bin 前缀Console Gateway 命令统一 `auth: "console"`
- 服务端错误原样透传CLI 仅对参数校验、缺凭证、网络失败等内部错误发出语义化 `BailianError`
- 支持 `--dry-run``--output json`、text 表格输出等现有 CLI 惯例
### 1.3 非目标
- 不在 `rag` 入口暴露(首期与 `deploy` / `finetune` 一致,仅 `bl`
- 不暴露 `sendMqMessage` 等内部 MQ 接口
- 不在 `core` / `runtime` 层硬编码 `bl` 命令名或控制台 URL
---
## 2. PRD → API → CLI 映射
### 2.1 资产管理
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
| ----- | ------------- | ------------------------------------------- | --------------------------------------------- | ------------------------------------------ |
| 1 | 查看资产列表 | `bl asset list` | `listModelGeneratedAsset` | 游标分页;支持类型/模型/关键词/收藏/回收站 |
| 2 | 查看资产详情 | `bl asset get <asset-id>` | `getModelGeneratedAsset` | positional 或 `--asset-id` |
| 3 | 收藏/取消收藏 | `bl asset favorite` / `bl asset unfavorite` | `batchFavoriteAsset` / `batchUnfavoriteAsset` | 单 ID 也走 batch长度 1 |
| 4 | 删除资产 | `bl asset delete` | `batchDeleteAsset` | 默认 `SOFT_DELETE`(移入回收站) |
| 5 | 批量删除 | `bl asset delete` | `batchDeleteAsset` | `--id` 可重复,最多 100 |
| 6 | 下载资产 | `bl asset download` | `batchGetAssetDownloadUrl` | 默认输出 URL单资产可选 `--out` 落盘 |
**建议补充API 已有、PRD 未写):**
| 能力 | CLI 命令 | API Action |
| ------------ | ------------------ | ------------------- |
| 从回收站恢复 | `bl asset restore` | `batchRestoreAsset` |
### 2.2 资产统计
| PRD # | 能力 | CLI 命令 | API Action | 备注 |
| ----- | ------------ | ---------------- | -------------------------- | --------------------------------------- |
| 7 | 查看资产统计 | `bl asset stats` | `countModelGeneratedAsset` | 输出 total / image / video / audio 计数 |
**转存失败数PRD 子项):**
- API 支持 `syncOssDataStatus=SYNC_FAILED` 筛选,但无独立 `failureCount` 字段
- **Phase 1 方案**`bl asset stats --sync-failed` 额外发起一次 count 查询,输出 `sync_failed_count`
- **Phase 3 备选**:等后端在 stats 响应中增加专用字段后收敛
### 2.4 容量
| PRD # | 能力 | CLI 命令 | API Action |
| ----- | ------------ | ------------------ | ----------------- |
| 14 | 查看容量信息 | `bl asset storage` | `getStorageQuota` |
### 2.5 可选扩展API 有、PRD 未列)
| CLI 命令 | API Action | 优先级 |
| ------------------------------------- | ------------------------------- | -------------------- |
| `bl asset service status` | `checkAssetServiceSubscription` | P1 |
| `bl asset service enable` / `disable` | `subscribeAssetService` | P2 |
| `bl asset models list` | `listModels` | P1配合 list 筛选) |
---
## 3. 架构与分层
### 3.1 在 monorepo 中的位置
```
packages/commands/src/commands/asset-center/*.ts ← 命令实现(本目录)
↓ export
packages/commands/src/index.ts
↓ import + map key
packages/cli/src/commands.ts ← "asset list": assetList, ...
packages/runtime (createCli / authStage / registry)
```
约定:
- 实现文件按能力组织在本目录
- `usageArgs` / `exampleArgs` 不含 `bl` 前缀
- 所有 asset 命令 `auth: "console"`;不重复声明 `CONSOLE_AUTH_FLAGS`runtime 自动注入)
### 3.2 目录结构
```
asset-center/
├── api-doc.md # 后端 API 文档(已有)
├── DESIGN.md # 本文档
├── COMMAND-TREE.md # 命令树与 help 结构
├── types.ts # TypeScript 类型ModelGeneratedAssetItem 等)
├── utils.ts # 公共请求构建、API 调用、响应解析
├── list.ts
├── get.ts
├── favorite.ts
├── unfavorite.ts
├── delete.ts
├── download.ts
├── stats.ts
└── storage.ts
```
### 3.3 共享层 `utils.ts`
参考 `token-plan/utils.ts``usage/stats.ts``requireWorkspaceId` 模式。
#### 3.3.1 API 名称约定
`quota/list.ts``zeldaHttp.dashscopeModel./zelda/api/v1/...` 类似,资产中心预期为:
```typescript
const ASSET_SERVICE = "bailianAsset"; // ⚠️ 编码前需 spike 确认
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
function assetApi(action: string): string {
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
}
```
编码第一步用 `bl console call --api <name> --data '{...}'` 验证实际注册名。
#### 3.3.2 公共请求体
所有接口继承 `AssetHttpBaseRequest`(见 api-doc §公共请求参数):
| 字段 | CLI 来源 | 状态 |
| ---------------- | --------------------------------------------------------- | ---------- |
| `workspace` | `settings.workspaceId``--workspace-id` / env / config | ✅ 已有 |
| `tenantId` | 待定 | ⚠️ 需确认 |
| `mainAccountUid` | 待定 | ⚠️ 需确认 |
| `apiSource` | 固定 `"CLI"` | 实现时写入 |
| `aliYunUid` 等 | 网关 session 注入或省略 | 待确认 |
`requireWorkspaceId(settings, binName)` 在缺少 workspace 时抛出 `BailianError(GENERAL)`hint 指向 `bl workspace list`
#### 3.3.3 调用封装
```typescript
async function callAssetApi<T>(
ctx: CommandRunContext,
action: string,
body: Record<string, unknown>,
): Promise<T> {
const payload = { ...buildBaseRequest(ctx), ...body };
const raw = await ctx.client.console(assetApi(action), payload);
return extractAssetResponse<T>(raw);
}
```
#### 3.3.4 响应解析
Console Gateway 响应可能存在多层嵌套(参考 `quota/list.ts``extractResponseData`
1. 剥 gateway 外层:`data``DataV2``data` → ...
2. 到达业务 `Result<T>``{ success, code, message, data }`
3. 若 `success === false`:抛 `BailianError(GENERAL, message)`**不翻译、不替换** message
4. 成功时返回 `data` 字段
---
## 4. 命令实现规范
### 4.1 通用模式
每个命令文件遵循:
```typescript
export default defineCommand({
description: "...",
auth: "console",
usageArgs: "...",
flags: { ... },
exampleArgs: ["...", "--output json"],
validate(ctx) { /* 跨 flag 条件校验 */ },
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ api: assetApi("..."), data: { ... } }, format);
return;
}
const data = await callAssetApi(ctx, "actionName", { ... });
// text 表格 或 emitResult(json)
},
});
```
参考实现:`app/list.ts`console + dry-run`dataset/list.ts`(表格输出)、`video/download.ts`(落盘)。
### 4.2 分页模型(`asset list`
**与 `app list` 不同**:资产列表使用 **id 游标分页**,不是 page/pageSize 页码模式。
| Flag | API 字段 | 说明 |
| -------------- | ----------- | -------------------------- |
| `--page-size` | `pageSize` | 默认 10最大 100 |
| `--next-token` | `nextToken` | 下一页游标(来自上次响应) |
| `--pre-token` | `preToken` | 上一页游标 |
JSON 输出保留 `nextToken` / `preToken` / `hasNext` / `hasPre`,便于脚本翻页。
### 4.3 批量 ID 传参
批量操作favorite / unfavorite / delete / restore / download统一
```typescript
id: {
type: "array",
valueHint: "<asset-id>",
description: "Asset ID(s) to operate on (repeatable, max 100)",
required: true,
}
```
CLI 用法:`--id asset-001 --id asset-002` 或多次重复。实现时在 `validate` 中校验 `ids.length <= 100`
### 4.4 输出格式
| 命令 | text 默认 | json |
| ---------- | -------------------------------------------------------------- | ----------------------------------------- |
| `list` | 表格assetId / type / name / model / favorited / generateTime | items + pagination |
| `get` | 关键字段摘要 | 完整 item |
| `stats` | 数字摘要 | `{ total_count, image_count, ... }` |
| `storage` | 人类可读字节 + 单价 | 原始 quota 字段 |
| 写操作 | 一行确认affectedCount | `{ success, affected_count }` |
| `download` | URL 列表或 saved 路径 | `{ items: [{ asset_id, download_url }] }` |
使用 `formatTable``dataset/list.ts`)、`formatBytes``video/download.ts`)、`emitResult` / `emitBare`
### 4.5 条件校验(`validate`
| 命令 | 规则 |
| ---------- | --------------------------------------------------------- |
| `delete` | `--permanent` 映射 `PERMANENT_DELETE`;默认 `SOFT_DELETE` |
| 所有 batch | `assetIdList.length <= 100` |
---
## 5. 关键命令 Flag 详设
### 5.1 `bl asset list`
| Flag | 类型 | API 映射 | 说明 |
| ------------------------ | ---------------- | ---------------------------- | ------------------------------------------------------------ |
| `--type` | string (choices) | `assetType` | `IMAGE` / `VIDEO` / `AUDIO` |
| `--model` | string | `modelName` | PRD「按模型筛选」 |
| `--keyword` | string | `assetName` | PRD「关键词」是否同时搜 description 待产品确认 |
| `--favorited` | switch | `favorited: true` | 仅看收藏 |
| `--recycle-bin` | switch | `deleteStatus: SOFT_DELETED` | 仅看回收站 |
| `--sync-status` | string (choices) | `syncOssDataStatus` | `NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
| `--begin-time` | string | `beginTime` | ISO_LOCAL_DATE_TIME |
| `--end-time` | string | `endTime` | ISO_LOCAL_DATE_TIME |
| `--include-download-url` | switch | `includeDownloadUrl` | |
| `--include-thumbnail` | switch | `includeThumbnail` | |
| `--thumbnail-width` | number | `thumbnailWidth` | 配合 thumbnail |
| `--thumbnail-height` | number | `thumbnailHeight` | 配合 thumbnail |
| `--page-size` | number | `pageSize` | |
| `--next-token` | number | `nextToken` | |
| `--pre-token` | number | `preToken` | |
### 5.2 `bl asset get`
| 参数/Flag | 说明 |
| ------------------------------------------ | --------------------------------------- |
| `<asset-id>` | positionalprimary |
| `--asset-id` | 与 positional 二选一positional 优先) |
| `--include-download-url` | |
| `--include-thumbnail` | |
| `--thumbnail-width` / `--thumbnail-height` | |
### 5.3 `bl asset delete`
| Flag | 说明 |
| ------------- | --------------------------------------------------------- |
| `--id` | array, required, max 100 |
| `--permanent` | switch → `deleteType: PERMANENT_DELETE`;默认 SOFT_DELETE |
### 5.4 `bl asset download`
| Flag | 说明 |
| ------- | ----------------------------------------------------- |
| `--id` | array, required |
| `--out` | 仅当 `--id` 恰好 1 个时有效;调用 `downloadFile` 落盘 |
### 5.5 `bl asset stats`
| Flag | 说明 |
| ------------------------------------- | ---------------------------------------------- |
| (无 filter | 默认 `deleteStatus: NORMAL` |
| `--recycle-bin` | `deleteStatus: SOFT_DELETED` |
| `--sync-failed` | 额外查询 `syncOssDataStatus: SYNC_FAILED` 计数 |
| `--type` / `--model` / `--keyword` 等 | 与 list 相同筛选维度(可选) |
---
## 6. 风险与待确认项
### 6.1 P0 — 编码前必须对齐
| # | 问题 | 影响 | 建议动作 |
| --- | ------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- |
| 1 | Console API 注册名(`zeldaHttp.{service}./zelda/api/v1/bailian/asset/*` | 无法调用 | `bl console call` spike与后端确认 service 名 |
| 2 | `tenantId` / `mainAccountUid` 由谁填充 | 所有接口必填 | 确认网关是否从 session 自动注入;否则扩展 config 或新增解析 API |
### 6.2 P1 — 产品设计
| # | 问题 | 建议默认 |
| --- | -------------------- | ----------------------------------------------- |
| 4 | 关键词搜索范围 | 仅 `assetName`;后续可加 `--search-description` |
| 5 | PRD 只提 image/video | CLI 暴露 IMAGE/VIDEO/AUDIO与 API 一致) |
| 6 | 下载行为 | 默认输出 URL单 ID + `--out` 落盘 |
| 7 | 永久删除 | 提供 `--permanent`help 注明不可恢复 |
| 8 | 服务未开通 | 不预检查;失败时透传服务端 message |
| 9 | 收藏命令形态 | 两个命令 `favorite` / `unfavorite`(语义清晰) |
---
## 7. 错误处理
遵循 [AGENTS.md](../../../../../../AGENTS.md) 错误边界:
| 场景 | 处理 |
| --------------------------------- | --------------------------------------------- |
| 缺 `--workspace-id` | `BailianError(GENERAL)` + hint |
| 缺 console token | authStage 抛 `BailianError(AUTH)` |
| flag 校验失败 | `UsageError` (exit 2) |
| HTTP 4xx/5xx / 业务 success=false | `BailianError(GENERAL)`message **原样透传** |
| batch ID > 100 | `UsageError` |
Console 未登录参考 `mcp/list.ts`:检测 `BailianGateway.Login.NotLogined` 时 hint 指向 `bl auth login --console`
---
## 8. 测试策略
新建 `packages/cli/tests/e2e/asset.e2e.test.ts`,遵循 [cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)。
### 8.1 不 skip 层
- `bl asset` 分组 help
- 各子命令 `--help`
- 缺参 → exit 2
### 8.2 Console skip 层(`isConsoleE2EReady()`
- 各命令 `--dry-run` 输出 api + data
- 真实 `asset list` / `asset storage` 集成(需已开通资产中心的工作空间)
环境:`BAILIAN_E2E=1` + console `access_token` + `BAILIAN_WORKSPACE_ID`
---
## 9. 注册与文档变更清单
| 文件 | 变更 |
| -------------------------------------------------- | ------------------- |
| `packages/commands/src/commands/asset-center/*.ts` | 新建 |
| `packages/commands/src/index.ts` | export |
| `packages/cli/src/commands.ts` | 注册 map |
| `packages/cli/tests/e2e/asset.e2e.test.ts` | 新建 |
| `skills/bailian-cli/reference/` | pre-commit 自动生成 |
| `README.md` / `README.zh.md` | 发版前补充命令一览 |
---
## 10. 分期实施
### Phase 1 — 核心资产P0
```
asset list | get | favorite | unfavorite | delete | restore | download | stats | storage
```
**前置:** §6.1 #1 #2 确认。
### Phase 2+ — 可选扩展
```
asset models list | service status | service enable/disable
```
> OSS 转存相关命令(`asset-center oss *` / `transfer list`)已取消,不再排期。
## 11. 参考
- 命令注册:[docs/agents/command-add-remove.md](../../../../../../docs/agents/command-add-remove.md)
- E2E 规范:[docs/agents/cli-e2e-tests.md](../../../../../../docs/agents/cli-e2e-tests.md)
- Console 命令样例:`packages/commands/src/commands/app/list.ts`
- 游标/表格:`packages/commands/src/commands/quota/list.ts`
- workspace 必填:`packages/commands/src/commands/usage/stats.ts`
- 文件落盘:`packages/commands/src/commands/video/download.ts`
@@ -1,35 +0,0 @@
# Asset Center 命令测试报告 — Phase 2
- **测试时间**: 2026-07-10 09:07:09 (UTC)
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
- **测试 IMAGE**: `asset_98175cbf83294f7b8ada86657623dcf3`
- **测试 VIDEO**: `asset_df026105d2274ff9b8c824058fa23d60`
- **策略**: 可逆写操作favorite/unfavorite 往返download 到 /tmp 后删除;其余只读
- **汇总**: 16 通过 / 0 失败 / 16 总计
> Phase 1 报告见同目录 [TEST-REPORT.md](./TEST-REPORT.md)24 项 dry-run + 只读基础验证)
## Phase 2 测试结果
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
| --- | ---- | ------------------------------------------ | -------- | ------- | ---- | ------- | -------------------------------------------------------------------------------------------------------- |
| 1 | 下载 | `asset-center download (IMAGE)` | 真实调用 | ✅ PASS | 0 | 20045ms | saved /tmp/asset-center-test-asset_98175cbf83294f7b8ada86657623dcf3.png (1449847 bytes, reported 1.4 MB) |
| 2 | 查询 | `asset-center get --include-download-url` | 真实调用 | ✅ PASS | 0 | 21226ms | download_url present |
| 3 | 查询 | `asset-center list --include-download-url` | 真实调用 | ✅ PASS | 0 | 19057ms | items contain download_url |
| 4 | 查询 | `asset-center list --next-token` | 真实调用 | ✅ PASS | 0 | 19584ms | page2=3 items, overlap=0, has_pre=true |
| 5 | 统计 | `asset-center stats --type IMAGE` | 真实调用 | ✅ PASS | 0 | 18830ms | image=7, total=7 |
| 6 | 统计 | `asset-center stats --sync-failed` | 真实调用 | ✅ PASS | 0 | 19084ms | total=27, sync_failed=0 |
| 7 | 查询 | `asset-center list --recycle-bin` | 真实调用 | ✅ PASS | 0 | 19314ms | 0 soft-deleted item(s) |
| 8 | 输出 | `asset-center list (text)` | 真实调用 | ✅ PASS | 0 | 17840ms | 4 lines table output |
| 9 | 收藏 | `asset-center favorite (真实)` | 真实调用 | ✅ PASS | 0 | 22082ms | affected=1 |
| 10 | 收藏 | `get 验证 favorited=true` | 真实调用 | ✅ PASS | 0 | 18865ms | favorited=true ✓ |
| 11 | 查询 | `list --favorited 含测试资产` | 真实调用 | ✅ PASS | 0 | 22120ms | found in favorited list |
| 12 | 收藏 | `asset-center unfavorite (真实)` | 真实调用 | ✅ PASS | 0 | 22133ms | affected=1 |
| 13 | 收藏 | `get 验证 favorited=false (恢复)` | 真实调用 | ✅ PASS | 0 | 27797ms | favorited=false ✓ |
| 14 | 收藏 | `favorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 22947ms | affected=2 |
| 15 | 收藏 | `unfavorite 批量 (--id x2)` | 真实调用 | ✅ PASS | 0 | 15385ms | affected=2 |
| 16 | 边界 | `get 不存在的 asset-id` | 真实调用 | ✅ PASS | 1 | 12207ms | exit 1, 服务端错误原样透传: "资产不存在" |
## 边界行为说明
查询不存在的 `asset-id` 时,服务端返回业务错误 **「资产不存在」**CLI 按约定 **原样透传**exit code 1不会替换为本地文案。这与 AGENTS.md 错误处理边界一致。
@@ -1,36 +0,0 @@
# Asset Center 命令测试报告
- **测试时间**: 2026-07-10 08:41:06 (UTC)
- **Workspace**: `llm-0xvms4kqhbqjlg8s`
- **样本 Asset ID**: `asset_df026105d2274ff9b8c824058fa23d60`
- **策略**: 只读命令真实调用;写操作/下载一律 `--dry-run`
- **汇总**: 24 通过 / 0 失败 / 24 总计
## 测试结果
| # | 分类 | 命令 | 模式 | 状态 | Exit | 耗时 | 结果摘要 |
| --- | ---- | ---------------------------------- | -------- | ------- | ---- | ------- | -------------------------------------------------------------- |
| 1 | 查询 | `asset-center list` | 真实调用 | ✅ PASS | 0 | 18962ms | 3 item(s), next=94 |
| 2 | 查询 | `asset-center list --type IMAGE` | 真实调用 | ✅ PASS | 0 | 16411ms | 2 item(s), next=90 |
| 3 | 查询 | `asset-center get` | 真实调用 | ✅ PASS | 0 | 16541ms | {gmtModified, aliyunUid, generateTime, aliyunMainId} |
| 4 | 统计 | `asset-center stats` | 真实调用 | ✅ PASS | 0 | 14226ms | total=27 |
| 5 | 统计 | `asset-center storage` | 真实调用 | ✅ PASS | 0 | 18622ms | {used_storage_size, free_storage_quota, extra_storage_price} |
| 9 | 查询 | `asset-center list --dry-run` | dry-run | ✅ PASS | 0 | 23855ms | dry-run → /zelda/api/v1/bailian/asset/listModelGeneratedAsset |
| 10 | 查询 | `asset-center get --dry-run` | dry-run | ✅ PASS | 0 | 17398ms | dry-run → /zelda/api/v1/bailian/asset/getModelGeneratedAsset |
| 11 | 收藏 | `asset-center favorite` | dry-run | ✅ PASS | 0 | 16471ms | dry-run → /zelda/api/v1/bailian/asset/batchFavoriteAsset |
| 12 | 收藏 | `asset-center unfavorite` | dry-run | ✅ PASS | 0 | 19724ms | dry-run → /zelda/api/v1/bailian/asset/batchUnfavoriteAsset |
| 13 | 删除 | `asset-center delete` | dry-run | ✅ PASS | 0 | 20612ms | dry-run → /zelda/api/v1/bailian/asset/batchDeleteAsset |
| 14 | 下载 | `asset-center download` | dry-run | ✅ PASS | 0 | 20348ms | dry-run → /zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl |
| 15 | 统计 | `asset-center stats --dry-run` | dry-run | ✅ PASS | 0 | 14898ms | dry-run → /zelda/api/v1/bailian/asset/countModelGeneratedAsset |
| 16 | 统计 | `asset-center storage --dry-run` | dry-run | ✅ PASS | 0 | 14305ms | dry-run → /zelda/api/v1/bailian/asset/getStorageQuota |
| 21 | 校验 | `asset-center get (缺 asset-id)` | 参数校验 | ✅ PASS | 2 | 15202ms | Error: Missing required flag: --asset-id |
| 22 | 校验 | `asset-center favorite (缺 --id)` | 参数校验 | ✅ PASS | 2 | 16697ms | Error: Missing required flag: --id |
| 23 | 校验 | `asset-center download (缺 --out)` | 参数校验 | ✅ PASS | 2 | 18803ms | Error: Missing required flag: --out |
## 模式说明
| 模式 | 说明 |
| -------- | -------------------------------------------------- |
| 真实调用 | 只读 API不修改数据 |
| dry-run | 输出 `{ api, data, gateway }` 请求体,不发起写操作 |
| 参数校验 | 预期 exit code 2用法错误 |
@@ -1,624 +0,0 @@
# BailianAssetZeldaHttpService API 文档
通过 Zelda 网关调用 bailian-asset HTTP 接口文档。
## 基础信息
- **Base Path**: `/zelda/api/v1/bailian/asset`
- **Method**: POST
- **Content-Type**: `application/json`
- **Accept**: `application/json`
## 统一响应格式
所有接口返回 `Result<T>` 结构:
```json
{
"requestId": "string",
"success": true,
"code": "string",
"message": "string",
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
| --------- | ------- | ---------------------- |
| requestId | String | 请求唯一ID |
| success | Boolean | 是否成功 |
| code | String | 错误码(失败时返回) |
| message | String | 错误信息(失败时返回) |
| data | Object | 业务数据(成功时返回) |
## 公共请求参数(基类字段)
所有接口请求体均继承自 `AssetHttpBaseRequest`,包含以下公共字段:
| 字段 | 类型 | 必填 | 说明 |
| -------------- | ------ | ---- | ---------------------------------------------- |
| requestId | String | 否 | 请求唯一ID |
| apiSource | String | 否 | 调用入口渠道,如 OpenAPI、CloudSDK |
| tenantId | String | 是 | 内部租户ID |
| workspace | String | 是 | 业务空间ID |
| aliYunUid | String | 否 | 阿里云子账号ID |
| mainAccountUid | String | 是 | 阿里云主账号ID |
| callerType | String | 否 | 账号类型partner/customer/sub/AssumedRoleUser |
| callerParentId | Long | 否 | 调用者所属主账号ID |
| accessKeyId | String | 否 | STS认证用户AccessKeyId |
| securityToken | String | 否 | STS认证扮演者的STS Token |
---
## 1. 开通/关闭资产中心服务
**POST** `/zelda/api/v1/bailian/asset/subscribeAssetService`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------------ | ---- | --------------------------------------- |
| action | String(Enum) | 是 | 操作类型:`ENABLE`-开通,`DISABLE`-关闭 |
### 响应 data
| 字段 | 类型 | 说明 |
| ------ | ------------ | ------------ |
| status | String(Enum) | 当前服务状态 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"action": "ENABLE"
}
```
---
## 8. 批量收藏资产
**POST** `/zelda/api/v1/bailian/asset/batchFavoriteAsset`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ------------ | ---- | ---------------------------------- |
| assetIdList | List<String> | 是 | 待收藏的资产ID列表长度不超过 100 |
### 响应 data
| 字段 | 类型 | 说明 |
| ------------- | ------- | -------------------- |
| success | Boolean | 是否收藏成功 |
| affectedCount | Integer | 实际被收藏的资产数量 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetIdList": ["asset-001", "asset-002", "asset-003"]
}
```
---
## 9. 批量取消收藏资产
**POST** `/zelda/api/v1/bailian/asset/batchUnfavoriteAsset`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ------------ | ---- | -------------------------------------- |
| assetIdList | List<String> | 是 | 待取消收藏的资产ID列表长度不超过 100 |
### 响应 data
| 字段 | 类型 | 说明 |
| ------------- | ------- | ------------------------ |
| success | Boolean | 是否取消收藏成功 |
| affectedCount | Integer | 实际被取消收藏的资产数量 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetIdList": ["asset-001", "asset-002"]
}
```
---
## 10. 批量删除资产
**POST** `/zelda/api/v1/bailian/asset/batchDeleteAsset`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ------------ | ---- | ----------------------------------------------------------- |
| assetIdList | List<String> | 是 | 待删除的资产ID列表长度不超过 100 |
| deleteType | String(Enum) | 是 | 删除类型:`SOFT_DELETE`-软删除,`PERMANENT_DELETE`-彻底删除 |
### 响应 data
| 字段 | 类型 | 说明 |
| ------------- | ------- | -------------------- |
| success | Boolean | 是否删除成功 |
| affectedCount | Integer | 实际被删除的资产数量 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetIdList": ["asset-001", "asset-002"],
"deleteType": "SOFT_DELETE"
}
```
---
## 11. 批量恢复软删除资产
**POST** `/zelda/api/v1/bailian/asset/batchRestoreAsset`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ------------ | ---- | ---------------------------------- |
| assetIdList | List<String> | 是 | 待恢复的资产ID列表长度不超过 100 |
### 响应 data
| 字段 | 类型 | 说明 |
| ------------- | ------- | -------------------- |
| success | Boolean | 是否恢复成功 |
| affectedCount | Integer | 实际被恢复的资产数量 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetIdList": ["asset-001", "asset-002"]
}
```
---
## 12. 分页查询模型生成资产
**POST** `/zelda/api/v1/bailian/asset/listModelGeneratedAsset`
采用 id 游标分页,默认 pageSize=10最大 100。
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ------------------- | ------------ | ---- | ---------------------------------------------------------------------------------- |
| preToken | Long | 否 | 向前翻页游标 |
| nextToken | Long | 否 | 向后翻页游标(查询下一页时传入上一次响应的 nextToken |
| pageSize | Integer | 否 | 每页大小,默认 10最大 100 |
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL默认 false |
| thumbnailWidth | Integer | 否 | 缩放图宽度像素includeThumbnail=true 时生效 |
| thumbnailHeight | Integer | 否 | 缩放图高度像素includeThumbnail=true 时生效 |
| softDeleteTimeOrder | String(Enum) | 否 | 软删除时间排序方式:`ASC`-正序,`DESC`-倒序;仅在 deleteStatus=SOFT_DELETED 时有效 |
| assetType | String(Enum) | 否 | 资产类型:`IMAGE`-图片,`VIDEO`-视频,`AUDIO`-音频 |
| favorited | Boolean | 否 | 是否被收藏 |
| assetName | String | 否 | 资产名称(子串模糊匹配) |
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
| trusted | Boolean | 否 | 是否可信 |
| modelType | String | 否 | 生成资产的模型类型 |
| modelName | String | 否 | 生成资产的模型型号 |
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态:`NOT_SYNCED` / `SYNC_SUCCESS` / `SYNC_FAILED` |
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态`NOT_SYNCED` / `IN_SYNCING` / `SYNC_SUCCESS` / `SYNC_FAILED` |
| deleteStatus | String(Enum) | 否 | 删除状态:`NORMAL` / `SOFT_DELETED` / `PERMANENTLY_DELETED` |
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME`2023-10-25T14:30:00` |
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME`2023-10-25T14:30:00` |
### 响应 data
| 字段 | 类型 | 说明 |
| --------- | ----------------------------- | ------------ |
| dataList | List<ModelGeneratedAssetItem> | 资产列表 |
| preToken | Long | 前一页游标 |
| nextToken | Long | 下一页游标 |
| hasNext | Boolean | 是否有下一页 |
| hasPre | Boolean | 是否有前一页 |
**ModelGeneratedAssetItem 结构:**
| 字段 | 类型 | 说明 |
| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| id | Long | 主键 ID分页游标 token |
| gmtCreate | Date | 创建时间 |
| gmtModified | Date | 修改时间 |
| workspaceId | String | 工作空间ID |
| tenantId | String | 租户ID |
| aliyunUid | String | 阿里云子账号ID |
| aliyunMainId | String | 阿里云主账号ID |
| assetId | String | 资产 ID |
| assetType | String | 资产类型IMAGE/VIDEO/AUDIO |
| assetSource | String | 资产来源MODEL_GENERATED/OFFICIAL/USER_UPLOADED |
| favorited | Boolean | 是否被收藏 |
| assetName | String | 资产名称 |
| assetDescription | String | 资产描述 |
| assetSize | Long | 资产大小(字节) |
| md5 | String | 资产 MD5 |
| ossBucket | String | 资产所在 OSS Bucket |
| ossKey | String | 资产在 OSS bucket 中的 key |
| region | String | 工作空间地域 |
| ossRegion | String | 资产所在 OSS bucket 的地域 |
| trusted | Boolean | 是否可信 |
| modelType | String | 模型类型 |
| modelName | String | 模型型号 |
| syncWhiteListStatus | String | 同步白名单状态 |
| syncOssDataStatus | String | 同步 OSS 数据状态 |
| deleteStatus | String | 删除状态NORMAL/SOFT_DELETED/PERMANENTLY_DELETED |
| generateTime | Long | 资产生成时间戳(毫秒) |
| softDeleteDays | Integer | 已被软删除的天数(仅当 deleteStatus=SOFT_DELETED 且请求 softDeleteTimeOrder 时返回) |
| originalOssUrl | String | 原始 OSS URL |
| downloadUrl | String | 文件下载链接(仅当请求 includeDownloadUrl=true 时返回) |
| thumbnailUrl | String | 资产缩放图 URL仅当请求 includeThumbnail=true 时返回;视频返回首帧缩放图,图片返回缩放图,音频返回 null |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"pageSize": 20,
"includeDownloadUrl": true,
"includeThumbnail": true,
"thumbnailWidth": 200,
"thumbnailHeight": 200,
"assetType": "IMAGE",
"favorited": true,
"beginTime": "2024-01-01T00:00:00",
"endTime": "2024-12-31T23:59:59"
}
```
### 响应示例
```json
{
"success": true,
"data": {
"dataList": [
{
"id": 1001,
"assetId": "asset-001",
"assetType": "IMAGE",
"assetName": "generated_image_01.png",
"assetDescription": "A landscape painting",
"favorited": true,
"generateTime": 1700000000000
}
],
"nextToken": 1000,
"hasNext": true,
"hasPre": false
}
}
```
---
## 13. 统计模型生成资产数量
**POST** `/zelda/api/v1/bailian/asset/countModelGeneratedAsset`
查询条件与 `listModelGeneratedAsset` 一致(不需要分页参数),按资产类型分组返回数量。
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ------------------- | ------------ | ---- | ------------------------------------------------ |
| assetType | String(Enum) | 否 | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
| favorited | Boolean | 否 | 是否被收藏 |
| assetName | String | 否 | 资产名称(子串模糊匹配) |
| assetDescription | String | 否 | 资产描述(子串模糊匹配) |
| trusted | Boolean | 否 | 是否可信 |
| modelType | String | 否 | 模型类型 |
| modelName | String | 否 | 模型型号 |
| syncWhiteListStatus | String(Enum) | 否 | 同步白名单状态 |
| syncOssDataStatus | String(Enum) | 否 | 同步OSS数据状态 |
| deleteStatus | String(Enum) | 否 | 删除状态 |
| beginTime | String | 否 | 资产生成时间起始(含),格式 ISO_LOCAL_DATE_TIME |
| endTime | String | 否 | 资产生成时间截止(含),格式 ISO_LOCAL_DATE_TIME |
### 响应 data
| 字段 | 类型 | 说明 |
| ---------- | ---- | ---------------- |
| imageCount | Long | 图片类型资产数量 |
| videoCount | Long | 视频类型资产数量 |
| audioCount | Long | 音频类型资产数量 |
| totalCount | Long | 总资产数量 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"deleteStatus": "NORMAL"
}
```
### 响应示例
```json
{
"success": true,
"data": {
"imageCount": 150,
"videoCount": 30,
"audioCount": 20,
"totalCount": 200
}
}
```
---
## 14. 批量获取资产下载链接
**POST** `/zelda/api/v1/bailian/asset/batchGetAssetDownloadUrl`
一次最多获取 100 个资产的下载链接。
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ----------- | ------------ | ---- | ------------------------------------------ |
| assetIdList | List<String> | 是 | 待获取下载链接的资产ID列表长度不超过 100 |
### 响应 data
| 字段 | 类型 | 说明 |
| ----- | -------------------------- | -------------------------------- |
| items | List<AssetDownloadUrlItem> | 资产下载链接列表,按请求顺序返回 |
**AssetDownloadUrlItem 结构:**
| 字段 | 类型 | 说明 |
| ----------- | ------ | ---------------------------------------------------------- |
| assetId | String | 资产 ID |
| downloadUrl | String | 资产下载链接(带签名);资产不存在或缺少 OSS 信息时为 null |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetIdList": ["asset-001", "asset-002", "asset-003"]
}
```
---
## 15. 查询模型生成资产详情
**POST** `/zelda/api/v1/bailian/asset/getModelGeneratedAsset`
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ------------------ | ------- | ---- | ------------------------------------------------ |
| assetId | String | 是 | 待查询的资产 ID |
| includeDownloadUrl | Boolean | 否 | 是否返回文件下载链接,默认 false |
| includeThumbnail | Boolean | 否 | 是否返回资产缩放图 URL默认 false |
| thumbnailWidth | Integer | 否 | 缩放图宽度像素includeThumbnail=true 时生效 |
| thumbnailHeight | Integer | 否 | 缩放图高度像素includeThumbnail=true 时生效 |
### 响应 data
| 字段 | 类型 | 说明 |
| ---- | ----------------------- | ------------------------ |
| item | ModelGeneratedAssetItem | 资产详情结构同第12节 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"assetId": "asset-001",
"includeDownloadUrl": true,
"includeThumbnail": true,
"thumbnailWidth": 200,
"thumbnailHeight": 200
}
```
---
## 16. 获取存储额度与用量
**POST** `/zelda/api/v1/bailian/asset/getStorageQuota`
### 请求参数
仅需公共参数(`workspace``tenantId` 必填)。
### 响应 data
| 字段 | 类型 | 说明 |
| ----------------- | ------ | --------------------------------------------- |
| freeStorageQuota | Long | 平台免费存储额度(单位:字节) |
| usedStorageSize | Long | 当前用户已使用的存储量(单位:字节) |
| extraStoragePrice | String | 超出免费额度的费用说明(如 "¥0.12元/GB/月" |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890"
}
```
---
## 19. 通用 MQ 消息发送
**POST** `/zelda/api/v1/bailian/asset/sendMqMessage`
向指定的 RocketMQ Producer 发送 JSON 格式的消息。producerType 对应 `EnumRocketMqProducerType` 枚举的 code 值mainAccountUid 作为消息路由 key。
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
| ------------ | ------ | ---- | --------------------------------------------------------------------------------------------------- |
| producerType | String | 是 | 生产者类型:`WHITE_LIST_ASSET_PRODUCER` / `OSS_DATA_HANDEL_PRODUCER` / `ORIGIN_ASSET_INFO_PRODUCER` |
| messageBody | String | 是 | JSON 格式的消息体字符串 |
| messageKey | String | 否 | 消息 key可选为空时默认使用 mainAccountUid |
### 响应 data
| 字段 | 类型 | 说明 |
| ------- | ------- | ------------ |
| success | Boolean | 是否发送成功 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890",
"producerType": "ORIGIN_ASSET_INFO_PRODUCER",
"messageBody": "{\"time\":1700000000000,\"modelId\":\"model-abc\",\"type\":\"IMAGE\",\"workspace\":\"ws-xxxxx\",\"ossUrl\":\"oss://my-bucket/path/to/asset.png\"}"
}
```
---
## 21. 查询模型列表
**POST** `/zelda/api/v1/bailian/asset/listModels`
返回当前服务管理的模型配置列表按资产类型分组包含每个模型的ID及是否可信标识。
### 请求参数
仅需公共参数。
### 响应 data
| 字段 | 类型 | 说明 |
| ----------- | ---------------- | ---------------------------- |
| modelGroups | List<ModelGroup> | 按资产类型分组的模型配置列表 |
**ModelGroup 结构:**
| 字段 | 类型 | 说明 |
| --------- | --------------- | ------------------------------------- |
| assetType | String(Enum) | 资产类型:`IMAGE` / `VIDEO` / `AUDIO` |
| models | List<ModelItem> | 该类型下管理的模型列表 |
**ModelItem 结构:**
| 字段 | 类型 | 说明 |
| ------- | ------- | -------------- |
| modelId | String | 模型ID |
| trusted | Boolean | 该模型是否可信 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890"
}
```
### 响应示例
```json
{
"success": true,
"data": {
"modelGroups": [
{
"assetType": "IMAGE",
"models": [
{ "modelId": "qwen-image-3.0", "trusted": true },
{ "modelId": "qwen-image-3.0-pro", "trusted": true }
]
},
{
"assetType": "VIDEO",
"models": [
{ "modelId": "wan2.7-t2v", "trusted": true },
{ "modelId": "wan2.7-i2v", "trusted": true }
]
}
]
}
}
```
---
## 22. 查询用户是否已开通资产中心服务
**POST** `/zelda/api/v1/bailian/asset/checkAssetServiceSubscription`
查询当前用户是否已开通资产中心服务。
### 请求参数
仅需公共参数(`mainAccountUid` 必填)。
### 响应 data
| 字段 | 类型 | 说明 |
| ------- | ------- | ------------------------------------------------- |
| enabled | Boolean | 是否已开通资产中心服务true-已开通false-未开通 |
### 请求示例
```json
{
"workspace": "ws-xxxxx",
"tenantId": "123456",
"mainAccountUid": "1234567890"
}
```
### 响应示例
```json
{
"success": true,
"data": {
"enabled": true
}
}
```
@@ -1,67 +0,0 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import type { AssetBatchResponse } from "./types.ts";
import {
ASSET_API,
ASSET_ID_FLAG,
callAssetApi,
dryRunPayload,
validateAssetIds,
} from "./utils.ts";
const DELETE_FLAGS = {
...ASSET_ID_FLAG,
permanent: {
type: "switch",
description: "Permanently delete assets (cannot be restored)",
},
} satisfies FlagsDef;
/**
* `bl asset-center delete`
*
* --permanent --id 100
*/
export default defineCommand({
description: "Delete assets (soft delete to recycle bin by default)",
auth: "console",
usageArgs: "--id <asset-id> [--id <asset-id>...] [flags]",
flags: DELETE_FLAGS,
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002", "--id asset-001 --permanent"],
validate(flags) {
return validateAssetIds(flags.id);
},
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const assetIdList = flags.id;
const deleteType = flags.permanent ? "PERMANENT_DELETE" : "SOFT_DELETE";
const body = { assetIdList, deleteType };
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.batchDeleteAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetBatchResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.batchDeleteAsset,
body,
);
const verb = flags.permanent ? "Permanently deleted" : "Deleted";
if (settings.quiet || format === "text") {
emitBare(`${verb} ${data.affectedCount ?? assetIdList.length} asset(s).`);
} else {
emitResult(
{ affected_count: data.affectedCount ?? assetIdList.length, delete_type: deleteType },
format,
);
}
},
});
@@ -1,76 +0,0 @@
import {
defineCommand,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
import type { AssetDownloadResponse } from "./types.ts";
import { ASSET_API, callAssetApi, dryRunPayload } from "./utils.ts";
const DOWNLOAD_FLAGS = {
id: {
type: "string",
valueHint: "<asset-id>",
description: "Asset ID to get download URL for",
required: true,
},
} satisfies FlagsDef;
/**
* `bl asset-center download` ID
*
* batchGetAssetDownloadUrl download URL
*/
export default defineCommand({
description: "Get a signed download URL for an asset by ID",
auth: "console",
usageArgs: "--id <asset-id>",
flags: DOWNLOAD_FLAGS,
exampleArgs: ["--id asset-001", "--id asset-001 --output json", "--id asset-001 --quiet"],
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const assetId = flags.id;
const body = { assetIdList: [assetId] };
if (settings.dryRun) {
emitResult(
{
asset_id: assetId,
action: "download",
...dryRunPayload(settings, identity.binName, ASSET_API.batchGetAssetDownloadUrl, body),
},
format,
);
return;
}
const data = await callAssetApi<AssetDownloadResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.batchGetAssetDownloadUrl,
body,
);
const url = data.items?.[0]?.downloadUrl;
if (!url) {
throw new BailianError(`No download URL available for ${assetId}.`, ExitCode.GENERAL);
}
if (settings.quiet) {
emitBare(url);
return;
}
if (format === "json") {
emitResult({ asset_id: assetId, download_url: url }, format);
return;
}
emitBare(`${padEnd("AssetId", 16)} ${assetId}`);
emitBare(`${padEnd("DownloadUrl", 16)} ${url}`);
},
});
@@ -1,54 +0,0 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import type { AssetBatchResponse } from "./types.ts";
import {
ASSET_API,
ASSET_ID_FLAG,
callAssetApi,
dryRunPayload,
validateAssetIds,
} from "./utils.ts";
/**
* `bl asset-center favorite`
*
* --id 100 batchFavoriteAsset
*/
export default defineCommand({
description: "Add assets to favorites",
auth: "console",
usageArgs: "--id <asset-id> [--id <asset-id>...]",
flags: ASSET_ID_FLAG,
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
validate(flags) {
return validateAssetIds(flags.id);
},
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const assetIdList = flags.id;
const body = { assetIdList };
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.batchFavoriteAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetBatchResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.batchFavoriteAsset,
body,
);
if (settings.quiet || format === "text") {
emitBare(`Favorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
} else {
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
}
},
});
@@ -1,95 +0,0 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
import type { AssetGetResponse } from "./types.ts";
import { ASSET_API, callAssetApi, dryRunPayload, formatGenerateTime } from "./utils.ts";
const GET_FLAGS = {
assetId: {
type: "string",
valueHint: "<id>",
description: "Asset ID to query",
required: true,
},
includeDownloadUrl: {
type: "switch",
description: "Include signed download URL",
},
includeThumbnail: {
type: "switch",
description: "Include thumbnail URL",
},
thumbnailWidth: {
type: "number",
valueHint: "<px>",
description: "Thumbnail width in pixels",
},
thumbnailHeight: {
type: "number",
valueHint: "<px>",
description: "Thumbnail height in pixels",
},
} satisfies FlagsDef;
/**
* `bl asset-center get` ID
*
* --include-download-url / --include-thumbnail URL
*/
export default defineCommand({
description: "Get full details of a model-generated asset",
auth: "console",
usageArgs: "--asset-id <id> [flags]",
flags: GET_FLAGS,
exampleArgs: [
"--asset-id asset-001",
"--asset-id asset-001 --include-download-url --output json",
],
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const assetId = flags.assetId;
const body: Record<string, unknown> = { assetId };
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
if (flags.includeThumbnail) body.includeThumbnail = true;
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.getModelGeneratedAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetGetResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.getModelGeneratedAsset,
body,
);
const item = data.item;
if (!item) {
emitBare("Asset not found.");
return;
}
if (format === "json") {
emitResult(item, format);
return;
}
emitBare(`${padEnd("AssetId", 16)} ${item.assetId ?? "-"}`);
emitBare(`${padEnd("Type", 16)} ${item.assetType ?? "-"}`);
emitBare(`${padEnd("Name", 16)} ${item.assetName ?? "-"}`);
emitBare(`${padEnd("Description", 16)} ${item.assetDescription ?? "-"}`);
emitBare(`${padEnd("Model", 16)} ${item.modelName ?? "-"}`);
emitBare(`${padEnd("Favorited", 16)} ${item.favorited ? "yes" : "no"}`);
emitBare(`${padEnd("Generated", 16)} ${formatGenerateTime(item.generateTime)}`);
if (item.downloadUrl) emitBare(`${padEnd("DownloadUrl", 16)} ${item.downloadUrl}`);
if (item.thumbnailUrl) emitBare(`${padEnd("ThumbnailUrl", 16)} ${item.thumbnailUrl}`);
},
});
@@ -1,150 +0,0 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, formatTable } from "bailian-cli-runtime";
import type { AssetListResponse, ModelGeneratedAssetItem } from "./types.ts";
import {
ASSET_API,
ASSET_LIST_FILTER_FLAGS,
buildListFilterBody,
callAssetApi,
dryRunPayload,
formatGenerateTime,
} from "./utils.ts";
const LIST_FLAGS = {
...ASSET_LIST_FILTER_FLAGS,
includeDownloadUrl: {
type: "switch",
description: "Include signed download URLs in the response",
},
includeThumbnail: {
type: "switch",
description: "Include thumbnail URLs in the response",
},
thumbnailWidth: {
type: "number",
valueHint: "<px>",
description: "Thumbnail width in pixels",
},
thumbnailHeight: {
type: "number",
valueHint: "<px>",
description: "Thumbnail height in pixels",
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 10, max: 100)",
},
nextToken: {
type: "number",
valueHint: "<token>",
description: "Cursor for the next page",
},
preToken: {
type: "number",
valueHint: "<token>",
description: "Cursor for the previous page",
},
} satisfies FlagsDef;
function normalizeItem(item: ModelGeneratedAssetItem) {
return {
asset_id: item.assetId ?? "",
asset_type: item.assetType ?? "",
asset_name: item.assetName ?? "",
model_name: item.modelName ?? "",
favorited: item.favorited ?? false,
generate_time: item.generateTime,
download_url: item.downloadUrl,
thumbnail_url: item.thumbnailUrl,
};
}
/**
* `bl asset-center list`
*
* /////OSS /
* --next-token / --pre-token URL
*/
export default defineCommand({
description: "List model-generated assets with filters and cursor pagination",
auth: "console",
usageArgs: "[flags]",
flags: LIST_FLAGS,
exampleArgs: [
"",
"--type IMAGE --model qwen-image-3.0",
"--favorited --page-size 20",
"--recycle-bin",
"--keyword landscape --output json",
],
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const pageSize = flags.pageSize ?? 10;
const body: Record<string, unknown> = {
...buildListFilterBody(flags),
pageSize,
};
if (flags.includeDownloadUrl) body.includeDownloadUrl = true;
if (flags.includeThumbnail) body.includeThumbnail = true;
if (flags.thumbnailWidth !== undefined) body.thumbnailWidth = flags.thumbnailWidth;
if (flags.thumbnailHeight !== undefined) body.thumbnailHeight = flags.thumbnailHeight;
if (flags.nextToken !== undefined) body.nextToken = flags.nextToken;
if (flags.preToken !== undefined) body.preToken = flags.preToken;
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.listModelGeneratedAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetListResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.listModelGeneratedAsset,
body,
);
const items = (data.dataList ?? []).map(normalizeItem);
if (format === "json") {
emitResult(
{
items,
pre_token: data.preToken,
next_token: data.nextToken,
has_next: data.hasNext,
has_pre: data.hasPre,
},
format,
);
return;
}
if (items.length === 0) {
emitBare("No assets found.");
return;
}
const headers = ["ASSET_ID", "TYPE", "NAME", "MODEL", "FAVORITED", "GENERATED"];
const rows = items.map((item) => [
item.asset_id,
item.asset_type,
item.asset_name,
item.model_name,
item.favorited ? "yes" : "-",
formatGenerateTime(item.generate_time),
]);
for (const line of formatTable(headers, rows)) emitBare(line);
const parts: string[] = [];
if (data.hasPre) parts.push("has previous page");
if (data.hasNext) parts.push(`next token: ${data.nextToken}`);
if (parts.length > 0) emitBare(`\n${parts.join("; ")}`);
},
});
@@ -1,86 +0,0 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
import type { AssetCountResponse } from "./types.ts";
import {
ASSET_API,
ASSET_LIST_FILTER_FLAGS,
buildListFilterBody,
callAssetApi,
dryRunPayload,
} from "./utils.ts";
const STATS_FLAGS = {
...ASSET_LIST_FILTER_FLAGS,
syncFailed: {
type: "switch",
description: "Also count assets with failed OSS sync",
},
} satisfies FlagsDef;
/**
* `bl asset-center stats`
*
* list --sync-failed OSS
*/
export default defineCommand({
description: "Count model-generated assets by type",
auth: "console",
usageArgs: "[flags]",
flags: STATS_FLAGS,
exampleArgs: ["", "--sync-failed", "--type IMAGE --output json"],
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const body = buildListFilterBody(flags);
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.countModelGeneratedAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetCountResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.countModelGeneratedAsset,
body,
);
let syncFailedCount: number | undefined;
if (flags.syncFailed) {
const failed = await callAssetApi<AssetCountResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.countModelGeneratedAsset,
{ ...body, syncOssDataStatus: "SYNC_FAILED" },
);
syncFailedCount = failed.totalCount ?? 0;
}
if (format === "json") {
emitResult(
{
total_count: data.totalCount ?? 0,
image_count: data.imageCount ?? 0,
video_count: data.videoCount ?? 0,
audio_count: data.audioCount ?? 0,
...(syncFailedCount !== undefined ? { sync_failed_count: syncFailedCount } : {}),
},
format,
);
return;
}
emitBare(`${padEnd("Total", 14)} ${data.totalCount ?? 0}`);
emitBare(`${padEnd("Image", 14)} ${data.imageCount ?? 0}`);
emitBare(`${padEnd("Video", 14)} ${data.videoCount ?? 0}`);
emitBare(`${padEnd("Audio", 14)} ${data.audioCount ?? 0}`);
if (syncFailedCount !== undefined) {
emitBare(`${padEnd("Sync failed", 14)} ${syncFailedCount}`);
}
},
});
@@ -1,47 +0,0 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare, padEnd } from "bailian-cli-runtime";
import type { AssetStorageQuotaResponse } from "./types.ts";
import { ASSET_API, callAssetApi, dryRunPayload, formatStorageBytes } from "./utils.ts";
/**
* `bl asset-center storage`
*/
export default defineCommand({
description: "View storage quota, usage, and overage pricing",
auth: "console",
usageArgs: "[flags]",
exampleArgs: ["", "--output json"],
async run(ctx) {
const { settings, identity } = ctx;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(dryRunPayload(settings, identity.binName, ASSET_API.getStorageQuota, {}), format);
return;
}
const data = await callAssetApi<AssetStorageQuotaResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.getStorageQuota,
{},
);
if (format === "json") {
emitResult(
{
used_storage_size: data.usedStorageSize,
free_storage_quota: data.freeStorageQuota,
extra_storage_price: data.extraStoragePrice,
},
format,
);
return;
}
emitBare(`${padEnd("Used", 14)} ${formatStorageBytes(data.usedStorageSize)}`);
emitBare(`${padEnd("Free quota", 14)} ${formatStorageBytes(data.freeStorageQuota)}`);
emitBare(`${padEnd("Overage", 14)} ${data.extraStoragePrice ?? "-"}`);
},
});
@@ -1,72 +0,0 @@
export type AssetType = "IMAGE" | "VIDEO" | "AUDIO";
export type AssetDeleteStatus = "NORMAL" | "SOFT_DELETED" | "PERMANENTLY_DELETED";
export type AssetSyncOssStatus = "NOT_SYNCED" | "IN_SYNCING" | "SYNC_SUCCESS" | "SYNC_FAILED";
export type AssetDeleteType = "SOFT_DELETE" | "PERMANENT_DELETE";
export interface AssetHttpBaseRequest {
workspace: string;
tenantId?: string;
mainAccountUid?: string;
apiSource?: string;
}
export interface ModelGeneratedAssetItem {
id?: number;
assetId?: string;
assetType?: string;
assetName?: string;
assetDescription?: string;
favorited?: boolean;
assetSize?: number;
modelType?: string;
modelName?: string;
deleteStatus?: string;
syncOssDataStatus?: string;
generateTime?: number;
downloadUrl?: string;
thumbnailUrl?: string;
gmtCreate?: string;
gmtModified?: string;
}
export interface AssetListResponse {
dataList?: ModelGeneratedAssetItem[];
preToken?: number;
nextToken?: number;
hasNext?: boolean;
hasPre?: boolean;
}
export interface AssetGetResponse {
item?: ModelGeneratedAssetItem;
}
export interface AssetBatchResponse {
success?: boolean;
affectedCount?: number;
}
export interface AssetDownloadUrlItem {
assetId?: string;
downloadUrl?: string | null;
}
export interface AssetDownloadResponse {
items?: AssetDownloadUrlItem[];
}
export interface AssetCountResponse {
imageCount?: number;
videoCount?: number;
audioCount?: number;
totalCount?: number;
}
export interface AssetStorageQuotaResponse {
freeStorageQuota?: number;
usedStorageSize?: number;
extraStoragePrice?: string;
}
@@ -1,54 +0,0 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import type { AssetBatchResponse } from "./types.ts";
import {
ASSET_API,
ASSET_ID_FLAG,
callAssetApi,
dryRunPayload,
validateAssetIds,
} from "./utils.ts";
/**
* `bl asset-center unfavorite`
*
* --id 100 batchUnfavoriteAsset
*/
export default defineCommand({
description: "Remove assets from favorites",
auth: "console",
usageArgs: "--id <asset-id> [--id <asset-id>...]",
flags: ASSET_ID_FLAG,
exampleArgs: ["--id asset-001", "--id asset-001 --id asset-002"],
validate(flags) {
return validateAssetIds(flags.id);
},
async run(ctx) {
const { settings, identity, flags } = ctx;
const format = detectOutputFormat(settings.output);
const assetIdList = flags.id;
const body = { assetIdList };
if (settings.dryRun) {
emitResult(
dryRunPayload(settings, identity.binName, ASSET_API.batchUnfavoriteAsset, body),
format,
);
return;
}
const data = await callAssetApi<AssetBatchResponse>(
ctx.client,
settings,
identity.binName,
ASSET_API.batchUnfavoriteAsset,
body,
);
if (settings.quiet || format === "text") {
emitBare(`Unfavorited ${data.affectedCount ?? assetIdList.length} asset(s).`);
} else {
emitResult({ affected_count: data.affectedCount ?? assetIdList.length }, format);
}
},
});
@@ -1,209 +0,0 @@
import {
BailianError,
ExitCode,
effectiveConsoleGatewayConfig,
type Client,
type FlagsDef,
type ParsedFlags,
type Settings,
} from "bailian-cli-core";
import type { AssetHttpBaseRequest, AssetSyncOssStatus, AssetType } from "./types.ts";
const ASSET_SERVICE = "dashscopeModel";
const ASSET_BASE = "/zelda/api/v1/bailian/asset";
export const MAX_ASSET_BATCH_SIZE = 100;
export const ASSET_API = {
listModelGeneratedAsset: assetApi("listModelGeneratedAsset"),
getModelGeneratedAsset: assetApi("getModelGeneratedAsset"),
batchFavoriteAsset: assetApi("batchFavoriteAsset"),
batchUnfavoriteAsset: assetApi("batchUnfavoriteAsset"),
batchDeleteAsset: assetApi("batchDeleteAsset"),
batchGetAssetDownloadUrl: assetApi("batchGetAssetDownloadUrl"),
countModelGeneratedAsset: assetApi("countModelGeneratedAsset"),
getStorageQuota: assetApi("getStorageQuota"),
} as const;
export const ASSET_ID_FLAG = {
id: {
type: "array",
valueHint: "<asset-id>",
description: "Asset ID(s) to operate on (repeatable, max 100)",
required: true,
},
} satisfies FlagsDef;
export const ASSET_LIST_FILTER_FLAGS = {
type: {
type: "string",
valueHint: "<type>",
description: "Asset type: IMAGE, VIDEO, or AUDIO",
choices: ["IMAGE", "VIDEO", "AUDIO"] as const,
},
model: {
type: "string",
valueHint: "<name>",
description: "Filter by model name",
},
keyword: {
type: "string",
valueHint: "<text>",
description: "Filter by asset name (substring match)",
},
favorited: {
type: "switch",
description: "Show or count only favorited assets",
},
recycleBin: {
type: "switch",
description: "Show or count soft-deleted assets (recycle bin)",
},
syncStatus: {
type: "string",
valueHint: "<status>",
description: "OSS sync status filter",
choices: ["NOT_SYNCED", "IN_SYNCING", "SYNC_SUCCESS", "SYNC_FAILED"] as const,
},
beginTime: {
type: "string",
valueHint: "<datetime>",
description: "Filter by generate time start (ISO_LOCAL_DATE_TIME)",
},
endTime: {
type: "string",
valueHint: "<datetime>",
description: "Filter by generate time end (ISO_LOCAL_DATE_TIME)",
},
} satisfies FlagsDef;
type AssetListFilterFlags = ParsedFlags<typeof ASSET_LIST_FILTER_FLAGS>;
function assetApi(action: string): string {
return `zeldaHttp.${ASSET_SERVICE}.${ASSET_BASE}/${action}`;
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
export function extractAssetResponse<T>(result: unknown): T {
const raw = result as Record<string, unknown>;
const data = getNestedRecord(raw, "data");
if (!data) {
throw new BailianError("Unexpected empty response from asset API.", ExitCode.GENERAL);
}
const dataV2 = getNestedRecord(data, "DataV2");
const payload = dataV2
? (getNestedRecord(getNestedRecord(dataV2, "data") ?? dataV2, "data") ??
getNestedRecord(dataV2, "data") ??
dataV2)
: (getNestedRecord(data, "data") ?? data);
if (payload.success === false) {
const message =
typeof payload.message === "string" && payload.message.length > 0
? payload.message
: typeof payload.code === "string"
? payload.code
: "Asset API request failed.";
throw new BailianError(message, ExitCode.GENERAL);
}
if (payload.data !== undefined) {
return payload.data as T;
}
return payload as T;
}
export function requireWorkspaceId(settings: Settings, binName: string): string {
if (settings.workspaceId) return settings.workspaceId;
throw new BailianError(
`workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or \`${binName} config set workspace_id <id>\`.`,
ExitCode.GENERAL,
`Run \`${binName} workspace list\` to view available workspaces.`,
);
}
export function buildBaseRequest(settings: Settings, binName: string): AssetHttpBaseRequest {
// workspace 由 CLI 注入tenantId / mainAccountUid 由 Console 网关从登录 session 自动填充,
// CLI 侧无需也不应手动解析阿里云账号 ID。
return {
workspace: requireWorkspaceId(settings, binName),
apiSource: "CLI",
};
}
export function buildListFilterBody(flags: AssetListFilterFlags): Record<string, unknown> {
const body: Record<string, unknown> = {};
if (flags.type) body.assetType = flags.type as AssetType;
if (flags.model) body.modelName = flags.model;
if (flags.keyword) body.assetName = flags.keyword;
if (flags.favorited) body.favorited = true;
if (flags.recycleBin) {
body.deleteStatus = "SOFT_DELETED";
} else {
body.deleteStatus = "NORMAL";
}
if (flags.syncStatus) body.syncOssDataStatus = flags.syncStatus as AssetSyncOssStatus;
if (flags.beginTime) body.beginTime = flags.beginTime;
if (flags.endTime) body.endTime = flags.endTime;
return body;
}
export function validateAssetIds(ids: string[] | undefined): string | undefined {
if (!ids || ids.length === 0) {
return "At least one --id is required.";
}
if (ids.length > MAX_ASSET_BATCH_SIZE) {
return `At most ${MAX_ASSET_BATCH_SIZE} asset IDs are allowed per request.`;
}
return undefined;
}
export async function callAssetApi<T>(
client: Client,
settings: Settings,
binName: string,
api: string,
body: Record<string, unknown>,
): Promise<T> {
const payload = { ...buildBaseRequest(settings, binName), ...body };
const raw = await client.console(api, payload);
return extractAssetResponse<T>(raw);
}
export function dryRunPayload(
settings: Settings,
binName: string,
api: string,
body: Record<string, unknown>,
): Record<string, unknown> {
return {
api,
data: { ...buildBaseRequest(settings, binName), ...body },
...effectiveConsoleGatewayConfig(settings),
};
}
export function formatGenerateTime(ts?: number): string {
if (ts == null) return "-";
return new Date(ts).toISOString().replace("T", " ").slice(0, 19);
}
export function formatStorageBytes(bytes?: number): string {
if (bytes == null) return "-";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
@@ -1,5 +1,5 @@
// Read-only discovery of locally installed AI tooling, surfaced by `config ui`:
// - Agent skills installed under ~/.agents/skills (via `npx skills add`).
// - Agent skills installed under ~/.agents/skills (via `bl skill add`).
// - MCP servers declared in each coding agent's local config file.
// - Coding agent frameworks and whether the bailian-cli provider is wired in.
//
@@ -128,8 +128,8 @@ function countFiles(dir: string, budget = 500): number {
}
/**
* Skill directories to scan, keyed by the module that owns them. `npx skills
* add --all` fans skills out into each installed agent, so the same skill can
* Skill directories to scan, keyed by the module that owns them. `bl skill init` /
* `bl skill add` fans skills out into each installed agent, so the same skill can
* live in several of these roots at once.
*/
function skillRoots(home: string): Array<{ source: string; dir: string }> {
@@ -548,7 +548,7 @@ export const PAGE_HTML = `<!doctype html>
<section id="view-skills" class="view">
<div class="view-head">
<h2 class="view-title">Installed <span class="grad">Skills</span></h2>
<p class="view-sub">Agent skills discovered across every local agent module (~/.agents/skills plus each agent's skills folder). Installed via <code style="font-family:var(--mono)">npx skills add</code>.</p>
<p class="view-sub">Agent skills discovered across every local agent module (~/.agents/skills plus each agent's skills folder). Installed via <code style="font-family:var(--mono)">bl skill add</code>.</p>
</div>
<div class="toolbar"><input id="skillSearch" class="search" type="search" placeholder="Search skills…" autocomplete="off"><button id="addSkillBtn" class="btn-dark" type="button">+ Add skill</button></div>
<div id="skillsBody"><div class="loading">Loading</div></div>
@@ -1444,7 +1444,7 @@ export const PAGE_HTML = `<!doctype html>
function renderSkills() {
var body = document.getElementById('skillsBody');
var pager = document.getElementById('skillsPager');
if (!SKILLS.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills installed.', 'Install with <code>npx skills add modelstudioai/cli --all -g</code>'); return; }
if (!SKILLS.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills installed.', 'Install with <code>bl skill init</code>'); return; }
var list = SKILLS.filter(function (s) { return skillMatches(s, SKILL_Q); });
if (!list.length) { pager.innerHTML = ''; renderEmpty(body, 'No skills match "' + SKILL_Q + '".', ''); return; }
var info = pageSlice(list, SKILL_PAGE, getPageSize('skills')); SKILL_PAGE = info.page;
@@ -25,7 +25,7 @@ export default defineCommand({
},
},
exampleArgs: [
`--api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
`--api zeldaEasy.bailian-commerce.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
`--api some.api.name --data '{"key":"value"}' --console-region cn-beijing`,
],
async run(ctx) {
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, deleteDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
fileId: {
@@ -19,20 +19,18 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "dataset.delete", file_id: fileId }, format);
emitResult({ action: "dataset.delete", file_id: fileId }, "json");
return;
}
const response = await deleteDataset(ctx.client, fileId);
if (settings.quiet || format === "text") {
emitBare(`Deleted ${fileId}.`);
emitRequestId(response.request_id, settings.quiet);
if (settings.quiet) {
emitBare(fileId);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
+7 -17
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, getDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const GET_FLAGS = {
fileId: {
@@ -19,10 +19,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "dataset.get", file_id: fileId }, format);
emitResult({ action: "dataset.get", file_id: fileId }, "json");
return;
}
@@ -45,19 +44,10 @@ export default defineCommand({
description: file.description ?? "",
};
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
if (settings.quiet) {
emitBare(item.file_id);
} else {
emitResult({ ...item, request_id: response.request_id }, "json");
}
// text / quiet
emitBare(`file_id: ${item.file_id}`);
emitBare(`name: ${item.name}`);
emitBare(`size: ${item.size}`);
if (item.md5) emitBare(`md5: ${item.md5}`);
if (item.purpose) emitBare(`purpose: ${item.purpose}`);
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
if (item.description) emitBare(`description: ${item.description}`);
emitRequestId(response.request_id, settings.quiet);
},
});
+7 -19
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, listDatasets, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDatasets, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -23,7 +23,6 @@ export default defineCommand({
exampleArgs: ["", "--purpose fine-tune", "--purpose evaluation --page-size 20", "--output json"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -33,7 +32,7 @@ export default defineCommand({
page_size: flags.pageSize,
purpose: flags.purpose,
},
format,
"json",
);
return;
}
@@ -46,7 +45,6 @@ export default defineCommand({
const files = response.data?.files ?? [];
const total = response.data?.total;
// Normalize to consistent structure for both text/json output.
const items = files.map((item) => ({
file_id: item.file_id ?? "",
name: item.name ?? "",
@@ -54,20 +52,10 @@ export default defineCommand({
purpose: item.purpose ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
if (settings.quiet) {
for (const item of items) emitBare(item.file_id);
} else {
emitResult({ items, total, request_id: response.request_id }, "json");
}
// text / quiet
if (items.length === 0) {
emitBare("No dataset files found.");
return;
}
const headers = ["FILE_ID", "NAME", "SIZE", "PURPOSE"];
const rows = items.map((i) => [i.file_id, i.name, i.size, i.purpose]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
},
});
@@ -1,23 +1,23 @@
import {
defineCommand,
detectOutputFormat,
uploadDataset,
validateDataset,
parseDatasetSchemaFlag,
formatIssue,
MAX_DATASET_BYTES,
MAX_CPT_BYTES,
MAX_MEDIA_ZIP_BYTES,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const UPLOAD_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: "Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image)",
description: "Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT, ≤2GB media zip)",
required: true,
},
purpose: {
@@ -29,7 +29,7 @@ const UPLOAD_FLAGS = {
type: "string",
valueHint: "<s>",
description:
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
},
noValidate: {
type: "switch",
@@ -45,7 +45,7 @@ export default defineCommand({
description: "Upload a dataset file (.jsonl or .zip) to Bailian",
auth: "apiKey",
usageArgs:
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image>] [--no-validate] [--full-validate]",
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image|video>] [--no-validate] [--full-validate]",
flags: UPLOAD_FLAGS,
exampleArgs: [
"--file train.jsonl",
@@ -58,13 +58,14 @@ export default defineCommand({
],
notes: [
"Supports .jsonl (text) and .zip (audio/image archives with a data.jsonl",
"manifest). Five record schemas are recognized: chatml = {messages:[...]}",
"manifest). Six record schemas are recognized: chatml = {messages:[...]}",
'(SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."}',
'(continual pre-training, raw text); tts = {wav_fn:"train/xxx.wav",',
'text:"..."} (audio fine-tuning); image = {img_path:"..."} (image',
"generation). With no --schema, a record carrying wav_fn is validated as",
"TTS, img_path as image, chosen/rejected as DPO, text (no messages) as CPT,",
"otherwise ChatML. Upload cap: 300MB text, 1GB image. Upload uses the",
"generation); video = {first_frame_path:...} (video generation). With no",
"--schema, a record carrying wav_fn is validated as TTS, img_path as image,",
"chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML.",
"Upload cap: 200MB SFT/DPO text, 300MB CPT, 2GB media zip. Upload uses the",
"OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is",
"persisted (the DashScope-native /api/v1/files drops it).",
],
@@ -73,19 +74,15 @@ export default defineCommand({
const filePath = flags.file;
const purpose = flags.purpose || "fine-tune";
const schema = parseDatasetSchemaFlag(flags.schema);
if (schema === "video") {
throw new BailianError(
`--schema video is not supported.`,
ExitCode.USAGE,
`Supported schemas: chatml, dpo, cpt, tts, image.`,
);
}
const format = detectOutputFormat(settings.output);
// Image schema allows larger ZIPs (1 GB vs 300 MB for text).
const isMediaSchema = schema === "image";
// Size caps differ per training type: SFT/DPO 200MB, CPT 300MB, media ZIP 2GB.
const isMediaSchema = schema === "image" || schema === "video";
const maxBytes = isMediaSchema
? MAX_MEDIA_ZIP_BYTES
: schema === "cpt"
? MAX_CPT_BYTES
: MAX_DATASET_BYTES;
if (!flags.noValidate) {
const maxBytes = isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES;
const result = await validateDataset(filePath, {
fullValidate: flags.fullValidate,
schema,
@@ -125,11 +122,11 @@ export default defineCommand({
action: "dataset.upload",
file: filePath,
purpose,
max_bytes: isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES,
max_bytes: maxBytes,
validate: !flags.noValidate,
schema: schema ?? "auto",
},
format,
"json",
);
return;
}
@@ -142,11 +139,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(file.file_id);
} else if (format === "text") {
emitBare(`Uploaded ${file.name} → file_id=${file.file_id}`);
emitRequestId(request_id, settings.quiet);
} else {
emitResult({ ...file, request_id }, format);
emitResult({ ...file, request_id }, "json");
}
},
});
@@ -1,26 +1,13 @@
import {
defineCommand,
detectOutputFormat,
validateDataset,
parseDatasetSchemaFlag,
formatIssue,
BailianError,
ExitCode,
type ValidationResult,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
function formatStats(result: ValidationResult): string[] {
const out: string[] = [];
if (result.stats.totalRecords !== undefined) out.push(`records: ${result.stats.totalRecords}`);
if (result.stats.sampledRecords !== undefined)
out.push(`sampled: ${result.stats.sampledRecords}`);
if (result.stats.bytes !== undefined) out.push(`bytes: ${result.stats.bytes}`);
if (result.stats.durationMs !== undefined) out.push(`took: ${result.stats.durationMs}ms`);
return out;
}
const VALIDATE_FLAGS = {
file: {
type: "string",
@@ -36,7 +23,7 @@ const VALIDATE_FLAGS = {
type: "string",
valueHint: "<s>",
description:
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
},
} satisfies FlagsDef;
@@ -44,13 +31,14 @@ export default defineCommand({
description: "Locally validate a dataset file (.jsonl or .zip) without uploading",
// 纯本地校验,不触网、不需 API key与 `pipeline validate` 一致)。
auth: "none",
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image>]",
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image|video>]",
flags: VALIDATE_FLAGS,
exampleArgs: [
"--file train.jsonl",
"--file dpo.jsonl --schema dpo",
"--file cpt.jsonl --schema cpt",
"--file audio.zip --schema tts",
"--file wan-i2v-training-dataset.zip --schema video",
"--file eval.jsonl --full-validate",
"--file train.jsonl --output json",
],
@@ -60,27 +48,20 @@ export default defineCommand({
"Schemas: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen,",
'rejected}; cpt = {text:"..."} (continual pre-training, raw text);',
'tts = {wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning);',
'image = {img_path:"..."} (image generation). With no --schema, a record',
"carrying wav_fn is validated as TTS, img_path as image, chosen/rejected",
"as DPO, text (no messages) as CPT, otherwise ChatML. Pass --schema to",
"require a specific shape on every record. ZIP archives (.zip) are",
"validated structurally (data.jsonl present, media references resolve) in",
"addition to per-record content checks. Use --full-validate to JSON.parse",
"every line.",
'image = {img_path:"..."} (image generation);',
'video = {first_frame_path:"...", video_path:"..."} (video generation,',
"i2v first-frame or kf2v first+last-frame with last_frame_path). With no",
"--schema, a record carrying wav_fn is validated as TTS, img_path as image,",
"first_frame_path/video_path as video, chosen/rejected as DPO, text (no",
"messages) as CPT, otherwise ChatML. Pass --schema to require a specific",
"shape on every record. ZIP archives (.zip) are validated structurally",
"(data.jsonl present, media references resolve) in addition to per-record",
"content checks. Use --full-validate to JSON.parse every line.",
],
async run(ctx) {
const { settings, flags } = ctx;
const filePath = flags.file;
const schema = parseDatasetSchemaFlag(flags.schema);
if (schema === "video") {
throw new BailianError(
`--schema video is not supported.`,
ExitCode.USAGE,
`Supported schemas: chatml, dpo, cpt, tts, image.`,
);
}
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
{
@@ -89,38 +70,17 @@ export default defineCommand({
full: flags.fullValidate,
schema: schema ?? "auto",
},
format,
"json",
);
return;
}
const result = await validateDataset(filePath, { fullValidate: flags.fullValidate, schema });
if (format === "json") {
// For json output we always emit the structured result, exit code conveys validity.
emitResult(result, format);
} else if (settings.quiet) {
if (settings.quiet) {
emitBare(result.valid ? "ok" : "fail");
} else {
const status = result.valid ? "PASSED" : "FAILED";
emitBare(`Dataset validation ${status} for ${result.filePath}`);
const stats = formatStats(result);
if (stats.length) emitBare(` ${stats.join(" · ")}`);
if (result.errors.length) {
emitBare(`Errors (${result.errors.length}):`);
for (const error of result.errors.slice(0, 20)) emitBare(formatIssue(error));
if (result.errors.length > 20) {
emitBare(` … and ${result.errors.length - 20} more.`);
}
}
if (result.warnings.length) {
emitBare(`Warnings (${result.warnings.length}):`);
for (const warning of result.warnings.slice(0, 10)) emitBare(formatIssue(warning));
if (result.warnings.length > 10) {
emitBare(` … and ${result.warnings.length - 10} more.`);
}
}
emitResult(result, "json");
}
if (!result.valid) {
+25 -39
View File
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
createDeployment,
pickPlanStrategy,
STRATEGIES,
@@ -11,16 +10,16 @@ import {
type CommandContext,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const CREATE_FLAGS = {
model: {
modelName: {
type: "string",
valueHint: "<name>",
description: "Model name (catalog model or fine-tuned output) (required)",
valueHint: "<model_name>",
description: "Model to deploy — fine-tuned output name or catalog model (required)",
required: true,
},
name: {
displayName: {
type: "string",
valueHint: "<display_name>",
description: "Console display name for the deployment (required)",
@@ -64,7 +63,7 @@ const CREATE_FLAGS = {
} satisfies FlagsDef;
const CREATE_USAGE =
"--model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
"--model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
const CREATE_NOTES = [
"Plan defaults to `lora` (Token-billed) for text/image and `mu` (model-unit-",
@@ -78,14 +77,11 @@ const CREATE_NOTES = [
"Use `bl deploy models --source base` to inspect available templates.",
"After creation, status starts at PENDING and transitions to RUNNING.",
"Invoke the deployed model with: bl text chat --model <deployed_model>",
"WARNING: --model is overloaded across commands and refers to DIFFERENT",
"values. `bl deploy <modality> create --model` takes the exported model_name",
"(e.g. `qwen3-8b-ft-...`), but the create response also returns a",
"`deployed_model` field (the deployment instance id, e.g.",
"`qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use",
"the `deployed_model` from the create response — NOT the `model_name` you",
"passed to `deploy <modality> create`. Do not reuse the value across the two",
"commands.",
"NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).",
"The create response also returns a `deployed_model` field — the deployment",
"instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference",
"(`bl text chat --model <deployed_model>`) and lifecycle commands",
"(`deploy get/scale/pause/resume/delete --deployed-model <id>`).",
];
/**
@@ -119,10 +115,9 @@ async function runCreate(
ctx: CommandContext<typeof CREATE_FLAGS>,
): Promise<void> {
const { identity, settings, flags } = ctx;
const model = flags.model as string;
const name = flags.name as string;
const model = flags.modelName as string;
const name = flags.displayName as string;
const plan = (flags.plan as string | undefined) || defaultDeployPlan(modality);
const format = detectOutputFormat(settings.output);
// Plan-specific behaviour is owned by core `plans.ts`. The strategy resolves
// the plan-specific body fragment (mu may auto-pick a template from the
@@ -146,7 +141,7 @@ async function runCreate(
};
if (settings.dryRun) {
emitResult({ action: "deploy.create", body }, format);
emitResult({ action: "deploy.create", body }, "json");
return;
}
@@ -155,17 +150,8 @@ async function runCreate(
if (settings.quiet) {
emitBare(deployment?.deployed_model ?? "");
} else if (format === "text") {
emitBare(`Created deployment.`);
if (deployment?.deployed_model) emitBare(` deployed_model: ${deployment.deployed_model}`);
if (deployment?.status) emitBare(` status: ${deployment.status}`);
if (deployment?.plan) emitBare(` plan: ${deployment.plan}`);
emitBare(
`\nNext: track readiness with: ${identity.binName} deploy get --deployed-model ${deployment?.deployed_model ?? "<id>"}`,
);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
}
@@ -176,10 +162,10 @@ export const deployTextCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-qwen-sft --name my-sft-test",
"--model qwen3.6-flash-2026-04-16 --name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
"--model qwen3-8b --name my-qwen3-mu --plan mu",
"--model qwen3-8b --name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
"--model-name my-qwen-sft --display-name my-sft-test",
"--model-name qwen3.6-flash-2026-04-16 --display-name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
"--model-name qwen3-8b --display-name my-qwen3-mu --plan mu",
"--model-name qwen3-8b --display-name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("text", flags),
@@ -193,9 +179,9 @@ export const deployAudioCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-cosyvoice-ft --name my-tts",
"--model my-cosyvoice-ft --name my-tts --deploy-spec dps-xxxx --capacity 1",
"--model my-cosyvoice-ft --name my-tts --dry-run",
"--model-name my-cosyvoice-ft --display-name my-tts",
"--model-name my-cosyvoice-ft --display-name my-tts --deploy-spec dps-xxxx --capacity 1",
"--model-name my-cosyvoice-ft --display-name my-tts --dry-run",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("audio", flags),
@@ -209,9 +195,9 @@ export const deployImageCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-wan-ft --name my-wan",
"--model my-wan-ft --name my-wan-mu --plan mu",
"--model my-wan-ft --name my-wan --dry-run",
"--model-name my-wan-ft --display-name my-wan",
"--model-name my-wan-ft --display-name my-wan-mu --plan mu",
"--model-name my-wan-ft --display-name my-wan --dry-run",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("image", flags),
@@ -1,13 +1,12 @@
import {
defineCommand,
detectOutputFormat,
deleteDeployment,
getDeployment,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
deployedModel: {
@@ -38,10 +37,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, format);
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, "json");
return;
}
@@ -55,7 +53,8 @@ export default defineCommand({
if (status && status !== "STOPPED" && status !== "FAILED") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only STOPPED / FAILED deployments can be deleted. ` +
`Stop it first via the platform console, or pass --skip-precheck to attempt deletion anyway.`,
`Run \`bl deploy pause --deployed-model ${deployedModel}\` to pause it first, ` +
`or pass --skip-precheck to attempt deletion anyway.`,
ExitCode.USAGE,
);
}
@@ -69,11 +68,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
emitBare(`Deleted ${deployedModel}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
+5 -18
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, getDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const GET_FLAGS = {
deployedModel: {
@@ -22,10 +22,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "deploy.get", deployed_model: deployedModel }, format);
emitResult({ action: "deploy.get", deployed_model: deployedModel }, "json");
return;
}
@@ -33,7 +32,7 @@ export default defineCommand({
const deployment = response.output ?? response.data;
if (!deployment) {
emitBare(`No data returned for ${deployedModel}`);
emitResult({ deployed_model: deployedModel, request_id: response.request_id }, "json");
return;
}
@@ -57,18 +56,6 @@ export default defineCommand({
if (deployment.gmt_create) item.created_at = deployment.gmt_create;
if (deployment.gmt_modified) item.updated_at = deployment.gmt_modified;
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
}
// text / quiet — fixed-width label column for alignment
const label = (key: string) => `${key}:`.padEnd(18);
for (const [key, value] of Object.entries(item)) {
if (value === "" || value === undefined) continue;
const display = typeof value === "string" ? value : JSON.stringify(value);
emitBare(`${label(key)}${display}`);
}
emitRequestId(response.request_id, settings.quiet);
emitResult({ ...item, request_id: response.request_id }, "json");
},
});
+4 -31
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listDeployments,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDeployments, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -28,13 +23,12 @@ export default defineCommand({
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const status = flags.status || undefined;
if (settings.dryRun) {
emitResult(
{ action: "deploy.list", page: flags.page, page_size: flags.pageSize, status },
format,
"json",
);
return;
}
@@ -57,27 +51,6 @@ export default defineCommand({
created_at: item.gmt_create ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet
if (items.length === 0) {
emitBare("No deployments found.");
return;
}
const headers = ["DEPLOYED_MODEL", "MODEL_NAME", "STATUS", "PLAN", "CAPACITY", "CREATED_AT"];
const rows = items.map((item) => [
item.deployed_model,
item.model_name,
item.status,
item.plan,
item.capacity,
item.created_at,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
+49 -102
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listDeployableModels,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDeployableModels, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const MODELS_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -39,7 +34,6 @@ export default defineCommand({
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
// Default version to v1.0 — without it, the API returns the legacy catalog
// (only old fine-tune outputs). Pass --catalog-version "" to opt out.
const version = flags.catalogVersion === "" ? undefined : (flags.catalogVersion ?? "v1.0");
@@ -54,7 +48,7 @@ export default defineCommand({
version,
model_source: modelSource,
},
format,
"json",
);
return;
}
@@ -72,102 +66,55 @@ export default defineCommand({
// Two response shapes:
// - custom (fine-tuned): top-level supported_plans: string[]
// - base (catalog): plans: [{plan, templates?, cu_specs?}]
// For json: surface the deployment-relevant fields preserved as a tree, so
// Surface the deployment-relevant fields preserved as a tree, so
// downstream tooling can drive `bl deploy <modality> create --deploy-spec <…>`
// without a second round-trip. For text: keep the compact one-line summary.
if (format === "json") {
const items = models.map((model) => {
const out: Record<string, unknown> = {
model_name: model.model_name ?? "",
};
if (model.base_model) out.base_model = model.base_model;
if (model.model_source) out.model_source = model.model_source;
if (model.supported_plans && model.supported_plans.length > 0) {
out.supported_plans = model.supported_plans;
}
if (model.plans && model.plans.length > 0) {
out.plans = model.plans.map((plan) => {
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
if (plan.cu_specs && plan.cu_specs.length > 0) {
planEntry.cu_specs = plan.cu_specs;
}
if (plan.templates && plan.templates.length > 0) {
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
// Drop noisy/redundant: template_source, template_type,
// template_version, deploy_spec (typically == template_id).
planEntry.templates = plan.templates.map((template) => {
const tpl: Record<string, unknown> = {};
if (template.template_id) tpl.template_id = template.template_id;
if (template.template_name) tpl.template_name = template.template_name;
if (template.charge_type) tpl.charge_type = template.charge_type;
// Flatten roles.unified for the common COUPLED case.
const unified = template.roles?.unified;
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
if (unified?.capacity_unit_per_instance !== undefined)
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
// Preserve split-role configs (SEPERATED) as-is so callers
// can still drive prefill/decode sizing.
if (template.roles?.prefill || template.roles?.decode) {
tpl.roles = {
prefill: template.roles?.prefill,
decode: template.roles?.decode,
};
}
if (template.template_desc) tpl.template_desc = template.template_desc;
return tpl;
});
}
return planEntry;
});
}
return out;
});
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet — keep the compact single-line summary table.
const textItems = models.map((model) => {
let plansSummary = "";
if (model.supported_plans && model.supported_plans.length > 0) {
plansSummary = model.supported_plans.join(",");
} else if (model.plans && model.plans.length > 0) {
plansSummary = model.plans
.map((plan) => {
const planName = plan.plan ?? "?";
if (plan.templates && plan.templates.length > 0) {
return `${planName}(${plan.templates.length}t)`;
}
if (plan.cu_specs && plan.cu_specs.length > 0) {
return `${planName}(${plan.cu_specs.join("/")})`;
}
return planName;
})
.join(",");
} else {
plansSummary = "-";
}
return {
// without a second round-trip.
const items = models.map((model) => {
const out: Record<string, unknown> = {
model_name: model.model_name ?? "",
base_model: model.base_model ?? "",
source: model.model_source ?? "",
plans: plansSummary,
};
if (model.base_model) out.base_model = model.base_model;
if (model.model_source) out.model_source = model.model_source;
if (model.supported_plans && model.supported_plans.length > 0) {
out.supported_plans = model.supported_plans;
}
if (model.plans && model.plans.length > 0) {
out.plans = model.plans.map((plan) => {
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
if (plan.cu_specs && plan.cu_specs.length > 0) {
planEntry.cu_specs = plan.cu_specs;
}
if (plan.templates && plan.templates.length > 0) {
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
// Drop noisy/redundant: template_source, template_type,
// template_version, deploy_spec (typically == template_id).
planEntry.templates = plan.templates.map((template) => {
const tpl: Record<string, unknown> = {};
if (template.template_id) tpl.template_id = template.template_id;
if (template.template_name) tpl.template_name = template.template_name;
if (template.charge_type) tpl.charge_type = template.charge_type;
// Flatten roles.unified for the common COUPLED case.
const unified = template.roles?.unified;
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
if (unified?.capacity_unit_per_instance !== undefined)
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
// Preserve split-role configs (SEPERATED) as-is so callers
// can still drive prefill/decode sizing.
if (template.roles?.prefill || template.roles?.decode) {
tpl.roles = {
prefill: template.roles?.prefill,
decode: template.roles?.decode,
};
}
if (template.template_desc) tpl.template_desc = template.template_desc;
return tpl;
});
}
return planEntry;
});
}
return out;
});
if (textItems.length === 0) {
emitBare("No deployable models found.");
return;
}
const headers = ["MODEL_NAME", "BASE_MODEL", "SOURCE", "PLANS"];
const rows = textItems.map((item) => [
item.model_name,
item.base_model,
item.source,
item.plans,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
@@ -0,0 +1,85 @@
import {
defineCommand,
stopModelService,
listIndependentDeployedModels,
findDeploymentEntry,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const PAUSE_FLAGS = {
deployedModel: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
required: true,
},
skipPrecheck: {
type: "switch",
description: "Skip the local RUNNING/PENDING status precheck",
},
} satisfies FlagsDef;
/**
* `bl deploy pause` pause a running deployment.
*
* Takes the model service offline so it no longer serves inference requests.
* For mu/ptu plans, billing stops while paused.
* Precheck: status must be RUNNING or PENDING.
*/
export default defineCommand({
description: "Pause a running model deployment (stops billing for mu/ptu)",
auth: "console",
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: PAUSE_FLAGS,
exampleArgs: [
"--deployed-model dep-...",
"--deployed-model dep-... --skip-precheck",
"--deployed-model dep-... --dry-run",
],
notes: [
"While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.",
"Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.",
],
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
if (settings.dryRun) {
emitResult({ action: "deploy.pause", deployed_model: deployedModel }, "json");
return;
}
// Precheck: verify the deployment is in a pausable state.
if (!flags.skipPrecheck) {
try {
const entries = await listIndependentDeployedModels(ctx.client);
const entry = findDeploymentEntry(entries, deployedModel);
if (entry) {
const status = (entry.status ?? "").toUpperCase();
if (status && status !== "RUNNING" && status !== "PENDING") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only RUNNING / PENDING deployments can be paused. ` +
`Pass --skip-precheck to attempt the pause anyway.`,
ExitCode.USAGE,
);
}
}
// If entry not found in list, proceed — the server will surface the real error.
} catch (error) {
if (error instanceof BailianError) throw error;
// If the list call itself failed, proceed and let the API call surface the error.
}
}
const response = await stopModelService(ctx.client, deployedModel);
if (settings.quiet) {
emitBare(deployedModel);
} else {
emitResult({ deployed_model: deployedModel, action: "pause", ...response }, "json");
}
},
});
@@ -0,0 +1,84 @@
import {
defineCommand,
startModelService,
listIndependentDeployedModels,
findDeploymentEntry,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const RESUME_FLAGS = {
deployedModel: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
required: true,
},
skipPrecheck: {
type: "switch",
description: "Skip the local STOPPED status precheck",
},
} satisfies FlagsDef;
/**
* `bl deploy resume` resume a paused deployment.
*
* Brings the model service back online so it can serve inference requests.
* Precheck: status must be STOPPED.
*/
export default defineCommand({
description: "Resume a paused model deployment (brings service back online)",
auth: "console",
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: RESUME_FLAGS,
exampleArgs: [
"--deployed-model dep-...",
"--deployed-model dep-... --skip-precheck",
"--deployed-model dep-... --dry-run",
],
notes: [
"Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.",
"For mu/ptu plans, billing resumes once the service is back online.",
],
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
if (settings.dryRun) {
emitResult({ action: "deploy.resume", deployed_model: deployedModel }, "json");
return;
}
// Precheck: verify the deployment is in a resumable state.
if (!flags.skipPrecheck) {
try {
const entries = await listIndependentDeployedModels(ctx.client);
const entry = findDeploymentEntry(entries, deployedModel);
if (entry) {
const status = (entry.status ?? "").toUpperCase();
if (status && status !== "STOPPED") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only STOPPED deployments can be resumed. ` +
`Pass --skip-precheck to attempt the resume anyway.`,
ExitCode.USAGE,
);
}
}
// If entry not found in list, proceed — the server will surface the real error.
} catch (error) {
if (error instanceof BailianError) throw error;
// If the list call itself failed, proceed and let the API call surface the error.
}
}
const response = await startModelService(ctx.client, deployedModel);
if (settings.quiet) {
emitBare(deployedModel);
} else {
emitResult({ deployed_model: deployedModel, action: "resume", ...response }, "json");
}
},
});
+4 -15
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
scaleDeployment,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, scaleDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const SCALE_FLAGS = {
deployedModel: {
@@ -52,7 +47,6 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
const body: Record<string, unknown> = {};
if (flags.capacity !== undefined) body.capacity = flags.capacity;
@@ -60,21 +54,16 @@ export default defineCommand({
if (flags.outputTpm !== undefined) body.output_tpm = flags.outputTpm;
if (settings.dryRun) {
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, format);
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, "json");
return;
}
const response = await scaleDeployment(ctx.client, deployedModel, body);
const deployment = response.output ?? response.data;
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
const cap = deployment?.capacity !== undefined ? ` (capacity=${deployment.capacity})` : "";
emitBare(`Scaled ${deployedModel}${cap}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
updateDeployment,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, updateDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const UPDATE_FLAGS = {
deployedModel: {
@@ -48,31 +43,22 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
const body: Record<string, unknown> = {};
if (flags.rpmLimit !== undefined) body.rpm_limit = flags.rpmLimit;
if (flags.tpmLimit !== undefined) body.tpm_limit = flags.tpmLimit;
if (settings.dryRun) {
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, format);
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, "json");
return;
}
const response = await updateDeployment(ctx.client, deployedModel, body);
const deployment = response.output ?? response.data;
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
const parts: string[] = [];
if (deployment?.rpm_limit !== undefined) parts.push(`rpm_limit=${deployment.rpm_limit}`);
if (deployment?.tpm_limit !== undefined) parts.push(`tpm_limit=${deployment.tpm_limit}`);
const summary = parts.length ? ` (${parts.join(", ")})` : "";
emitBare(`Updated ${deployedModel}${summary}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, cancelFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, cancelFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const CANCEL_FLAGS = {
jobId: {
@@ -23,24 +23,18 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.cancel", job_id: jobId }, format);
emitResult({ action: "finetune.cancel", job_id: jobId }, "json");
return;
}
const response = await cancelFineTune(ctx.client, jobId);
const job = response.output ?? response.data;
if (settings.quiet) {
emitBare(jobId);
} else if (format === "text") {
const status = job?.status ? ` (status=${job.status})` : "";
emitBare(`Cancelled ${jobId}${status}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,15 +1,13 @@
import {
defineCommand,
detectOutputFormat,
fetchModelList,
fetchModelListAll,
fetchModelCapability,
listSupportedTrainingTypes,
modelSupportsTrainingType,
isTrainingTypeCli,
trainingTypeMethodVariant,
TRAINING_TYPES_CLI,
callConsoleGateway,
effectiveConsoleGatewayConfig,
anonymousConsoleCall,
UsageError,
type Settings,
type ModelCapability,
@@ -17,8 +15,6 @@ import {
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const PAGE_SIZE = 50;
/**
* Page through every foundation-model page (listFoundationModels, public no
* console login needed, so the gateway is called anonymously). Returns raw
@@ -26,36 +22,12 @@ const PAGE_SIZE = 50;
* for filtering.
*/
async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapability[]> {
const eff = effectiveConsoleGatewayConfig(settings);
const call = (api: string, data: Record<string, unknown>) =>
callConsoleGateway(
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
settings.timeout,
{ api, data },
);
const first = await fetchModelList(call, { pageNo: 1, pageSize: PAGE_SIZE });
const all = [...first.models];
const totalPages = Math.ceil(first.total / PAGE_SIZE);
for (let pageNo = 2; pageNo <= totalPages; pageNo++) {
const result = await fetchModelList(call, { pageNo, pageSize: PAGE_SIZE });
all.push(...result.models);
}
const all = await fetchModelListAll(anonymousConsoleCall(settings));
return all as ModelCapability[];
}
const VARIANT_LABEL: Record<string, string> = {
full: "full-parameter",
lora: "LoRA",
};
function describeTrainingType(value: string): string {
if (!isTrainingTypeCli(value)) return value;
const { method, variant } = trainingTypeMethodVariant(value);
return `${VARIANT_LABEL[variant] ?? variant} ${method.toUpperCase()}`;
}
const CAPABILITY_FLAGS = {
model: {
baseModel: {
type: "string",
valueHint: "<m>",
description: "List training types supported by this base model.",
@@ -71,31 +43,31 @@ export default defineCommand({
description:
"Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)",
auth: "none",
usageArgs: "--model <m> | --training-type <t>",
usageArgs: "--base-model <m> | --training-type <t>",
flags: CAPABILITY_FLAGS,
exampleArgs: [
"--model qwen3-8b",
"--base-model qwen3-8b",
"--training-type sft-lora",
"--training-type cpt --output json",
"--training-type sft --quiet",
],
notes: [
"Exactly one of --model / --training-type is required.",
"Exactly one of --base-model / --training-type is required.",
"Training-type values use the `<method>` / `<method>-lora` convention:",
"sft | sft-lora | dpo | dpo-lora | cpt. (cpt has no -lora variant server-side.)",
"Queries listFoundationModels, a public API — no console login needed.",
],
validate: (f) => {
if (f.model && f.trainingType)
return "--model and --training-type are mutually exclusive; pass one.";
if (!f.model && !f.trainingType) return "one of --model / --training-type is required.";
if (f.baseModel && f.trainingType)
return "--base-model and --training-type are mutually exclusive; pass one.";
if (!f.baseModel && !f.trainingType)
return "one of --base-model / --training-type is required.";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const model = flags.model || undefined;
const model = flags.baseModel || undefined;
const trainingType = flags.trainingType || undefined;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -104,7 +76,7 @@ export default defineCommand({
model,
training_type: trainingType,
},
format,
"json",
);
return;
}
@@ -113,7 +85,7 @@ export default defineCommand({
if (model) {
const capability = await fetchModelCapability(settings, model);
if (!capability) {
emitBare(`No foundation model found matching "${model}".`);
emitResult({ model, error: `No foundation model found matching "${model}".` }, "json");
return;
}
const supported = listSupportedTrainingTypes(capability);
@@ -121,23 +93,15 @@ export default defineCommand({
for (const value of supported) emitBare(value);
return;
}
if (format !== "text") {
emitResult(
{
model: capability.model ?? model,
supported,
supports: capability.supports,
trainingTypes: capability.trainingTypes,
},
format,
);
return;
}
emitBare(`${capability.model ?? model}`);
emitBare(supported.length ? "Supported training types:" : "No supported training types.");
for (const value of supported) {
emitBare(` ${value.padEnd(10)} ${describeTrainingType(value)}`);
}
emitResult(
{
model: capability.model ?? model,
supported,
supports: capability.supports,
trainingTypes: capability.trainingTypes,
},
"json",
);
return;
}
@@ -162,20 +126,15 @@ export default defineCommand({
for (const entry of matched) emitBare(entry.model);
return;
}
if (format !== "text") {
emitResult(
{
training_type: trainingType,
method,
variant,
count: matched.length,
models: matched,
},
format,
);
return;
}
emitBare(`Models supporting ${trainingType} (${method} / ${variant}): ${matched.length}`);
for (const entry of matched) emitBare(` ${entry.model}`);
emitResult(
{
training_type: trainingType,
method,
variant,
count: matched.length,
models: matched,
},
"json",
);
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listCheckpoints,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listCheckpoints, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const CHECKPOINTS_FLAGS = {
jobId: {
@@ -15,6 +10,8 @@ const CHECKPOINTS_FLAGS = {
},
} satisfies FlagsDef;
const EXPIRY_WARN_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours
export default defineCommand({
description: "List checkpoints produced by a fine-tune job",
auth: "apiKey",
@@ -22,16 +19,15 @@ export default defineCommand({
flags: CHECKPOINTS_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
notes: [
"Use the returned `checkpoint` value with `finetune export` to publish",
"a deployable model.",
"`model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model-name`.",
"Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.",
],
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.checkpoints", job_id: jobId }, format);
emitResult({ action: "finetune.checkpoints", job_id: jobId }, "json");
return;
}
@@ -44,22 +40,26 @@ export default defineCommand({
checkpoint: item.checkpoint ?? item.checkpoint_id ?? "",
step: item.step !== undefined ? String(item.step) : "",
status: item.status ?? "",
model_name: item.model_name ?? "",
expire_time: item.expire_time ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
emitResult({ items, total, request_id: response.request_id }, "json");
// text / quiet
if (items.length === 0) {
emitBare("No checkpoints found.");
return;
// Near-expiry warning: check if any non-expired checkpoint is within 72h of expiry.
const now = Date.now();
const expiringSoon = items.filter((item) => {
if (!item.expire_time) return false;
const deadline = new Date(item.expire_time).getTime();
if (Number.isNaN(deadline)) return false;
const remaining = deadline - now;
return remaining > 0 && remaining < EXPIRY_WARN_THRESHOLD_MS;
});
if (expiringSoon.length > 0) {
process.stderr.write(
`\n[warning] ${expiringSoon.length} checkpoint(s) will expire within 72 hours. ` +
"Export or deploy before expiry to avoid losing the model artifact.\n",
);
}
const headers = ["CHECKPOINT", "STEP", "STATUS"];
const rows = items.map((i) => [i.checkpoint, i.step, i.status]);
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
},
});
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
createFineTune,
getDataset,
uploadDataset,
@@ -27,7 +26,7 @@ import {
} from "bailian-cli-core";
import { existsSync, statSync } from "fs";
import { basename } from "path";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
/**
* A `--datasets` / `--validations` token is treated as a local file to upload
@@ -208,7 +207,7 @@ async function uploadResolvedLocal(
}
/** The modality a `finetune <modality> create` subcommand is bound to. */
type CommandModality = "text" | "audio" | "image";
type CommandModality = "text" | "audio" | "image" | "video";
/**
* Flags shared by every `finetune <modality> create` subcommand: what to train
@@ -216,10 +215,10 @@ type CommandModality = "text" | "audio" | "image";
* output. Every modality's model consumes these.
*/
const COMMON_FLAGS = {
model: {
baseModel: {
type: "string",
valueHint: "<model>",
description: "Base model to fine-tune",
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
required: true,
},
datasets: {
@@ -317,13 +316,41 @@ const IMAGE_FLAGS = {
} satisfies FlagsDef;
const TEXT_USAGE =
"--model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
"--base-model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
const AUDIO_USAGE =
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
const IMAGE_USAGE =
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
/**
* Video (Wan i2v/kf2v) flags: exposes the three hyper-parameters that the
* video API supports and users may want to override. Defaults are model-specific
* (resolved by the sft-lora profile: wan2.7 batch_size 1 / max_pixels 102400,
* wan2.5 4 / 36864, wan2.2 4 / 262144).
*/
const VIDEO_FLAGS = {
...COMMON_FLAGS,
nEpochs: {
type: "number",
valueHint: "<n>",
description: "Training epochs (default: 50)",
},
batchSize: {
type: "number",
valueHint: "<n>",
description: "Batch size (default: model-specific, 1 for wan2.7, 4 for wan2.5/2.2)",
},
learningRate: {
type: "string",
valueHint: "<str>",
description: 'Learning rate as a string to preserve precision (default: "2e-5")',
},
} satisfies FlagsDef;
const VIDEO_USAGE =
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>]";
const COMMON_NOTES = [
"Creating a job uploads any local datasets and consumes training quota.",
@@ -383,7 +410,7 @@ async function runCreate<F extends FlagsDef>(
): Promise<void> {
const { identity, settings } = ctx;
const flags = ctx.flags as Record<string, unknown>;
const model = flags.model as string;
const model = flags.baseModel as string;
const datasetsRaw = flags.datasets as string;
// CosyVoice audio fine-tuning accepts exactly one training file
@@ -441,6 +468,10 @@ async function runCreate<F extends FlagsDef>(
if (detected === "image-i2i") modality = "image-i2i";
}
}
if (commandModality === "video" && firstLocalPath && !settings.dryRun) {
const detected = await detectModality(firstLocalPath);
if (detected === "video-kf2v") modality = "video-kf2v";
}
const training = await analyzeDatasetTokens(
settings,
@@ -606,8 +637,6 @@ async function runCreate<F extends FlagsDef>(
if (modelName) body.model_name = modelName;
if (suffix) body.finetuned_output_suffix = suffix;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
const pending = [
...training.localPaths.map((path) => ({ field: "datasets", path })),
@@ -617,7 +646,7 @@ async function runCreate<F extends FlagsDef>(
pending.length > 0
? { action: "finetune.create", body, pending_uploads: pending }
: { action: "finetune.create", body },
format,
"json",
);
return;
}
@@ -627,16 +656,8 @@ async function runCreate<F extends FlagsDef>(
if (settings.quiet) {
if (job?.job_id) emitBare(job.job_id);
} else if (format === "text") {
if (job?.job_id) {
emitBare(`Created fine-tune job: ${job.job_id}`);
if (job.status) emitBare(`Status: ${job.status}`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
}
} else {
emitResult(response, format);
emitResult(response, "json");
}
}
@@ -647,14 +668,14 @@ export const finetuneTextCreate = defineCommand({
usageArgs: TEXT_USAGE,
flags: TEXT_FLAGS,
exampleArgs: [
"--model qwen3-8b --datasets file-xxx",
"--model qwen3-8b --datasets ./train.jsonl",
"--model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
"--model qwen3-8b --datasets file-aaa,./extra.jsonl",
"--model qwen3-8b --datasets ./train.jsonl --training-type sft",
'--model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
"--model qwen3-8b --datasets file-xxx --output json",
"--model qwen3-8b --datasets file-xxx --dry-run",
"--base-model qwen3-8b --datasets file-xxx",
"--base-model qwen3-8b --datasets ./train.jsonl",
"--base-model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
"--base-model qwen3-8b --datasets file-aaa,./extra.jsonl",
"--base-model qwen3-8b --datasets ./train.jsonl --training-type sft",
'--base-model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
"--base-model qwen3-8b --datasets file-xxx --output json",
"--base-model qwen3-8b --datasets file-xxx --dry-run",
],
notes: TEXT_NOTES,
run: (ctx) => runCreate("text", ctx),
@@ -667,11 +688,11 @@ export const finetuneAudioCreate = defineCommand({
usageArgs: AUDIO_USAGE,
flags: AUDIO_FLAGS,
exampleArgs: [
"--model cosyvoice-v3-flash --datasets ./audio.zip",
"--model cosyvoice-v3-flash --datasets file-xxx",
"--model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
"--model cosyvoice-v3-flash --datasets file-xxx --output json",
"--model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip",
"--base-model cosyvoice-v3-flash --datasets file-xxx",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
"--base-model cosyvoice-v3-flash --datasets file-xxx --output json",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
],
notes: AUDIO_NOTES,
run: (ctx) => runCreate("audio", ctx),
@@ -684,13 +705,38 @@ export const finetuneImageCreate = defineCommand({
usageArgs: IMAGE_USAGE,
flags: IMAGE_FLAGS,
exampleArgs: [
"--model wan2.7-image-pro --datasets ./images.zip",
"--model wan2.7-image-pro --datasets file-xxx",
"--model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
"--model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
"--model wan2.7-image-pro --datasets file-xxx --output json",
"--model wan2.7-image-pro --datasets ./images.zip --dry-run",
"--base-model wan2.7-image-pro --datasets ./images.zip",
"--base-model wan2.7-image-pro --datasets file-xxx",
"--base-model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
"--base-model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
"--base-model wan2.7-image-pro --datasets file-xxx --output json",
"--base-model wan2.7-image-pro --datasets ./images.zip --dry-run",
],
notes: IMAGE_NOTES,
run: (ctx) => runCreate("image", ctx),
});
const VIDEO_NOTES = [
...COMMON_NOTES,
"Video generation training (Wan i2v/kf2v) runs efficient_sft with model-",
"specific defaults: wan2.7 (batch_size=1, max_pixels=102400), wan2.5/2.2",
"(batch_size=4, max_pixels per model). Override with --batch-size/--n-epochs.",
"Datasets are .zip archives with data.jsonl + frame images + videos.",
"Recommended: ≥10 training samples, 20-100 for stable results.",
];
/** `bl finetune video create` — fine-tune a video generation model. Datasets are `.zip`. */
export const finetuneVideoCreate = defineCommand({
description: "Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft)",
auth: "apiKey",
usageArgs: VIDEO_USAGE,
flags: VIDEO_FLAGS,
exampleArgs: [
"--base-model wan2.7-i2v --datasets file-xxx",
"--base-model wan2.7-i2v --datasets ./i2v-data.zip",
"--base-model wan2.2-kf2v-flash --datasets file-xxx --n-epochs 100",
"--base-model wan2.7-i2v --datasets file-xxx --dry-run",
],
notes: VIDEO_NOTES,
run: (ctx) => runCreate("video", ctx),
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, deleteFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
jobId: {
@@ -23,10 +23,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.delete", job_id: jobId }, format);
emitResult({ action: "finetune.delete", job_id: jobId }, "json");
return;
}
@@ -34,11 +33,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(jobId);
} else if (format === "text") {
emitBare(`Deleted ${jobId}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
exportCheckpoint,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, exportCheckpoint, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const EXPORT_FLAGS = {
jobId: {
@@ -39,11 +34,10 @@ export default defineCommand({
"explicit export is the canonical path for non-best checkpoints.",
],
async run(ctx) {
const { identity, settings, flags } = ctx;
const { settings, flags } = ctx;
const jobId = flags.jobId;
const checkpoint = flags.checkpoint;
const modelName = flags.modelName;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -53,7 +47,7 @@ export default defineCommand({
checkpoint,
model_name: modelName,
},
format,
"json",
);
return;
}
@@ -64,14 +58,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(exported);
} else if (format === "text") {
emitBare(`Exported ${jobId} / ${checkpoint} → model_name=${exported}`);
emitBare(
`Next: ${identity.binName} deploy text create --model ${exported} --name <display-name>`,
);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -0,0 +1,105 @@
/**
* Best-effort actual training fee calculation using the model catalog's
* "ft" (fine-tune) price entry. Pure API-key domain no console auth needed.
*
* The model catalog (`listFoundationModels` via public gateway) returns a
* `prices[]` array **only when `queryPrice: true` is passed** (the same flag
* `fetchModelDetail` uses). Combined with the job's `output.usage` (actual
* consumed tokens, present on SUCCEEDED / CANCELED), this gives the exact
* training cost without any console-domain login.
*/
import {
callConsoleGateway,
effectiveConsoleGatewayConfig,
unwrapResponse,
MODEL_LIST_API,
type Settings,
type ModelPriceInfo,
} from "bailian-cli-core";
export interface ActualFee {
cost: number;
unitPrice: number;
priceUnit: string;
}
/**
* Fetch the model's training price from the public catalog gateway.
* Uses the same anonymous gateway path as `fetchModelCapability` (no console
* token required), but adds `queryPrice: true` to include the prices array.
*/
async function fetchTrainingPrice(
settings: Settings,
model: string,
): Promise<ModelPriceInfo | null> {
const eff = effectiveConsoleGatewayConfig(settings);
const result = await callConsoleGateway(
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
settings.timeout,
{
api: MODEL_LIST_API,
data: {
input: {
pageNo: 1,
pageSize: 10,
group: true,
model,
queryPrice: true,
querySampleCode: false,
queryGroupByModel: true,
queryQuota: false,
queryQpmInfo: false,
queryApplyStatus: false,
queryPermissions: false,
queryActivationStatus: false,
},
},
},
);
const responseData = unwrapResponse(result as Record<string, unknown>);
const list = (responseData.list as Record<string, unknown>[]) ?? [];
// The response is grouped; find the exact model in items.
for (const group of list) {
const items = (group.items as Record<string, unknown>[]) ?? [];
for (const item of items) {
if (item.model === model) {
const prices = (item.prices as ModelPriceInfo[]) ?? [];
return prices.find((entry) => entry.type === "ft") ?? null;
}
}
// Flat response fallback (no items nesting).
if (group.model === model) {
const prices = (group.prices as ModelPriceInfo[]) ?? [];
return prices.find((entry) => entry.type === "ft") ?? null;
}
}
return null;
}
/**
* Compute the actual training fee from the model catalog's "ft" price entry.
* Returns null when the price is unavailable (network error, model not in
* catalog, or no "ft" entry). Never throws.
*
* Only uses the public model catalog (model metadata) does NOT call
* console-domain pricing APIs (modelCenter.getModelPrice). Models whose
* catalog entry lacks a "ft" price (e.g. CosyVoice) will simply omit the
* training_cost field until the platform adds it to the catalog.
*/
export async function computeActualFee(
settings: Settings,
model: string,
usageTokens: number,
): Promise<ActualFee | null> {
try {
const ftEntry = await fetchTrainingPrice(settings, model);
const unitPrice = Number(ftEntry?.price);
if (!Number.isFinite(unitPrice) || unitPrice <= 0) return null;
const priceUnit = ftEntry?.priceUnit ?? "每百万tokens";
// Catalog price is yuan per million tokens.
const cost = (usageTokens / 1_000_000) * unitPrice;
return { cost: Number(cost.toFixed(4)), unitPrice, priceUnit };
} catch {
return null;
}
}
+29 -32
View File
@@ -1,5 +1,6 @@
import { defineCommand, detectOutputFormat, getFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { computeActualFee } from "./fee.ts";
const GET_FLAGS = {
jobId: {
@@ -17,12 +18,11 @@ export default defineCommand({
flags: GET_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
async run(ctx) {
const { identity, settings, flags } = ctx;
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.get", job_id: jobId }, format);
emitResult({ action: "finetune.get", job_id: jobId }, "json");
return;
}
@@ -30,18 +30,24 @@ export default defineCommand({
const job = response.output ?? response.data;
if (!job) {
emitBare(`No data returned for ${jobId}`);
emitResult({ job_id: jobId, error: "No data returned" }, "json");
return;
}
const hp = job.hyper_parameters;
const hyperParameters = job.hyper_parameters;
const hyperParts: string[] = [];
if (hp?.n_epochs !== undefined) hyperParts.push(`n_epochs=${hp.n_epochs}`);
if (hp?.batch_size !== undefined) hyperParts.push(`batch_size=${hp.batch_size}`);
if (hp?.learning_rate !== undefined) hyperParts.push(`learning_rate=${hp.learning_rate}`);
if (hp?.max_length !== undefined) hyperParts.push(`max_length=${hp.max_length}`);
if (hyperParameters?.n_epochs !== undefined)
hyperParts.push(`n_epochs=${hyperParameters.n_epochs}`);
if (hyperParameters?.batch_size !== undefined)
hyperParts.push(`batch_size=${hyperParameters.batch_size}`);
if (hyperParameters?.learning_rate !== undefined)
hyperParts.push(`learning_rate=${hyperParameters.learning_rate}`);
if (hyperParameters?.max_length !== undefined)
hyperParts.push(`max_length=${hyperParameters.max_length}`);
const item = {
const usageTokens = typeof job.usage === "number" ? job.usage : undefined;
const item: Record<string, unknown> = {
job_id: job.job_id ?? jobId,
base_model: job.model ?? "",
status: job.status ?? "",
@@ -53,29 +59,20 @@ export default defineCommand({
model_name: job.model_name ?? "",
created_at: job.create_time ?? job.gmt_create ?? "",
updated_at: job.end_time ?? job.gmt_modified ?? "",
usage_tokens: usageTokens ?? "",
charge_type: typeof job.charge_type === "string" ? job.charge_type : "",
};
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
// Actual fee: only when the platform reports a concrete token count
// (SUCCEEDED / CANCELED). Best-effort — silently omitted on lookup failure.
if (usageTokens !== undefined && usageTokens > 0 && job.model) {
const fee = await computeActualFee(settings, job.model, usageTokens);
if (fee) {
item.training_cost = fee.cost;
item.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
// text / quiet
emitBare(`job_id: ${item.job_id}`);
if (item.base_model) emitBare(`base_model: ${item.base_model}`);
if (item.status) emitBare(`status: ${item.status}`);
if (item.training_type) emitBare(`training_type: ${item.training_type}`);
if (item.training_files.length) emitBare(`training_files: ${item.training_files.join(", ")}`);
if (item.validation_files.length)
emitBare(`validation_files: ${item.validation_files.join(", ")}`);
if (item.hyper_params) emitBare(`hyper_params: ${item.hyper_params}`);
if (item.output_model)
emitBare(
`output_model: ${item.output_model} (→ ${identity.binName} deploy text create --model)`,
);
if (item.model_name) emitBare(`model_name: ${item.model_name}`);
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
if (item.updated_at) emitBare(`updated_at: ${item.updated_at}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ ...item, request_id: response.request_id }, "json");
},
});
+24 -47
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, listFineTunes, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listFineTunes, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -13,71 +13,48 @@ const LIST_FLAGS = {
valueHint: "<s>",
description: "Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED)",
},
baseModel: {
type: "string",
valueHint: "<model>",
description: "Filter by base model ID (server-side)",
},
} satisfies FlagsDef;
export default defineCommand({
description: "List fine-tune jobs",
auth: "apiKey",
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>]",
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>] [--base-model <model>]",
flags: LIST_FLAGS,
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
exampleArgs: ["", "--status RUNNING", "--base-model qwen3-8b", "--page-size 20"],
async run(ctx) {
const { identity, settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const { settings, flags } = ctx;
const pageNo = flags.page;
const pageSize = flags.pageSize;
const status = flags.status || undefined;
const model = flags.baseModel || undefined;
if (settings.dryRun) {
emitResult({ action: "finetune.list", page: pageNo, page_size: pageSize, status }, format);
emitResult(
{ action: "finetune.list", page: pageNo, page_size: pageSize, status, model },
"json",
);
return;
}
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status });
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status, model });
const payload = response.output ?? response.data;
const jobs = payload?.jobs ?? [];
const total = payload?.total;
const items = jobs.map((item) => ({
job_id: item.job_id ?? "",
base_model: item.model ?? "",
status: item.status ?? "",
training_type: item.training_type ?? "",
output_model: item.finetuned_output ?? "",
created_at: item.create_time ?? item.gmt_create ?? "",
const items = jobs.map((job) => ({
job_id: job.job_id ?? "",
base_model: job.model ?? "",
status: job.status ?? "",
training_type: job.training_type ?? "",
output_model: job.finetuned_output ?? "",
created_at: job.create_time ?? job.gmt_create ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet
if (items.length === 0) {
emitBare("No fine-tune jobs found.");
return;
}
const headers = [
"JOB_ID",
"BASE_MODEL",
"STATUS",
"TRAINING_TYPE",
"OUTPUT_MODEL",
"CREATED_AT",
];
const rows = items.map((i) => [
i.job_id,
i.base_model,
i.status,
i.training_type,
i.output_model,
i.created_at,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitBare(
`Tip: OUTPUT_MODEL is the input for \`${identity.binName} deploy text create --model\``,
);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
+15 -44
View File
@@ -1,25 +1,24 @@
import {
defineCommand,
detectOutputFormat,
getFineTuneLogs,
type Client,
type FineTuneLogEntry,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
/**
* Render a single log entry as a single line (mirrors the flatten logic used
* for non-search text output: prefer common fields, fall back to JSON).
* Render a single log entry as a single line (used for search matching:
* prefer common fields, fall back to JSON).
*/
function renderEntry(entry: FineTuneLogEntry | string): string {
if (typeof entry === "string") return entry;
const record = entry as Record<string, unknown>;
const ts = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
const timestamp = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
const level = (record.level ?? "") as string;
const msg = (record.message ?? record.msg ?? record.log ?? "") as string;
if (msg || ts || level) {
return [ts, level, msg].filter(Boolean).join("\t");
const message = (record.message ?? record.msg ?? record.log ?? "") as string;
if (message || timestamp || level) {
return [timestamp, level, message].filter(Boolean).join("\t");
}
return JSON.stringify(entry);
}
@@ -48,16 +47,16 @@ async function fetchAllLogs(
let total = 0;
// Hard cap to avoid an unbounded loop if the server misreports `total`.
const maxPages = 200;
for (let i = 0; i < maxPages; i++) {
for (let page = 0; page < maxPages; page++) {
const response = await getFineTuneLogs(client, jobId, { pageNo, pageSize });
const payload = response.output ?? response.data;
const page = payload?.logs ?? [];
const logs = payload?.logs ?? [];
total = payload?.total ?? total;
if (page.length === 0) break;
entries.push(...page);
if (logs.length === 0) break;
entries.push(...logs);
// Stop once we've collected everything the server claims exists.
if (total && entries.length >= total) break;
if (page.length < pageSize) break;
if (logs.length < pageSize) break;
pageNo++;
}
return { entries, total };
@@ -110,7 +109,6 @@ export default defineCommand({
const pageSize = flags.pageSize;
const search = flags.search || undefined;
const tail = flags.tail;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -122,7 +120,7 @@ export default defineCommand({
search,
tail,
},
format,
"json",
);
return;
}
@@ -147,18 +145,6 @@ export default defineCommand({
const result =
tailApplied !== undefined ? scanned.slice(scanned.length - tailApplied) : scanned;
if (settings.quiet || format === "text") {
if (result.length === 0) {
emitBare(search ? `No logs matched "${search}".` : "No logs returned.");
return;
}
for (const entry of result) emitBare(renderEntry(entry));
const parts: string[] = [`${result.length} shown`];
if (matched !== undefined) parts.push(`matched ${matched}`);
parts.push(`of ${entries.length}` + (total ? ` (total ${total})` : ""));
emitBare(`\n${parts.join(", ")}`);
return;
}
emitResult(
{
...(matched !== undefined ? { matched } : {}),
@@ -168,28 +154,13 @@ export default defineCommand({
...(tailApplied !== undefined ? { tail: tailApplied } : {}),
logs: result,
},
format,
"json",
);
return;
}
// Default: single page, verbatim response.
const response = await getFineTuneLogs(ctx.client, jobId, { pageNo, pageSize });
const payload = response.output ?? response.data;
const logs = payload?.logs ?? [];
if (settings.quiet || format === "text") {
if (logs.length === 0) {
emitBare("No logs returned.");
return;
}
for (const entry of logs) {
emitBare(renderEntry(entry));
}
if (payload?.total !== undefined) emitBare(`\nTotal: ${payload.total}`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
}
emitResult(response, "json");
},
});
@@ -0,0 +1,139 @@
import {
defineCommand,
fetchTrainingModelPrice,
estimateSftDpoTokens,
estimateCptTokens,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const PRICE_FLAGS = {
baseModel: {
type: "string",
valueHint: "<model>",
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
required: true,
},
datasets: {
type: "string",
valueHint: "<ids>",
description: "Training dataset file IDs, comma-separated (required)",
required: true,
},
trainingType: {
type: "string",
valueHint: "<type>",
description: "Training type: sft | dpo | cpt (default: sft)",
},
nEpochs: {
type: "number",
valueHint: "<n>",
description: "Number of training epochs (default: 3)",
},
} satisfies FlagsDef;
const SUPPORTED_TRAINING_TYPES = ["sft", "dpo", "cpt"];
// Fixed hyper-parameters used for estimation. Only n_epochs materially affects
// the estimate; the rest are held at representative defaults (not exposed as
// flags to keep the command surface minimal).
const ESTIMATE_BATCH_SIZE = 16;
const ESTIMATE_MAX_LENGTH = 8192;
const DEFAULT_N_EPOCHS = 3;
export default defineCommand({
description: "Estimate the training cost for a fine-tune job (token billing)",
auth: "console",
usageArgs: "--base-model <model> --datasets <ids> [--training-type <type>] [--n-epochs <n>]",
flags: PRICE_FLAGS,
exampleArgs: [
"--base-model qwen3-8b --datasets file-ft-xxx",
"--base-model qwen3-8b --datasets file-ft-xxx,file-ft-yyy --n-epochs 2",
"--base-model qwen3-8b --datasets file-ft-xxx --training-type cpt",
],
notes: [
"Estimate only — the server computes token usage from the datasets; final cost is subject to the bill.",
"Covers token billing for sft / dpo / cpt. Training-unit (MTU) billing is not supported by this command.",
"Hyper-parameters other than --n-epochs are fixed at representative defaults for estimation.",
],
async run(ctx) {
const { settings, flags } = ctx;
const model = flags.baseModel;
const datasetIds = flags.datasets
.split(",")
.map((datasetId) => datasetId.trim())
.filter(Boolean);
const trainingType = (flags.trainingType ?? "sft").toLowerCase();
const nEpochs = flags.nEpochs ?? DEFAULT_N_EPOCHS;
if (!SUPPORTED_TRAINING_TYPES.includes(trainingType)) {
throw new BailianError(
`Unsupported training type "${trainingType}". Supported: ${SUPPORTED_TRAINING_TYPES.join(", ")}.`,
ExitCode.USAGE,
);
}
if (datasetIds.length === 0) {
throw new BailianError("--datasets must contain at least one file ID.", ExitCode.USAGE);
}
if (settings.dryRun) {
emitResult(
{ action: "finetune.price", model, datasets: datasetIds, trainingType, nEpochs },
"json",
);
return;
}
// Unit price (yuan per 千Token).
const priceInfo = await fetchTrainingModelPrice(ctx.client, model);
const unitPrice = Number(priceInfo.price);
if (!Number.isFinite(unitPrice)) {
throw new BailianError(
`No training price found for model "${model}".`,
ExitCode.GENERAL,
undefined,
{ rawResponse: JSON.stringify(priceInfo) },
);
}
// Per-epoch token estimate (min/max range).
const estimate =
trainingType === "cpt"
? await estimateCptTokens(ctx.client, model, datasetIds.join(","), nEpochs)
: await estimateSftDpoTokens(ctx.client, datasetIds, {
nEpochs,
batchSize: ESTIMATE_BATCH_SIZE,
maxLength: ESTIMATE_MAX_LENGTH,
});
const minPerEpoch = estimate.estimatedDatasetConsumedTokensMinPerEpoch ?? 0;
const maxPerEpoch = estimate.estimatedDatasetConsumedTokensMaxPerEpoch ?? 0;
const mixedMinPerEpoch = estimate.estimatedMixedConsumedTokensMinPerEpoch ?? 0;
const mixedMaxPerEpoch = estimate.estimatedMixedConsumedTokensMaxPerEpoch ?? 0;
const minTokens = (minPerEpoch + mixedMinPerEpoch) * nEpochs;
const maxTokens = (maxPerEpoch + mixedMaxPerEpoch) * nEpochs;
// price is yuan per 1000 tokens.
const minFee = (minTokens / 1000) * unitPrice;
const maxFee = (maxTokens / 1000) * unitPrice;
emitResult(
{
model,
training_type: trainingType,
n_epochs: nEpochs,
unit_price: unitPrice,
price_unit: priceInfo.priceUnit ?? "千Token",
estimated_tokens: { min: minTokens, max: maxTokens },
estimated_fee_yuan: {
min: Number(minFee.toFixed(4)),
max: Number(maxFee.toFixed(4)),
},
disclaimer: "Server-side estimate; final cost is subject to the bill.",
},
"json",
);
},
});
@@ -1,12 +1,12 @@
import {
defineCommand,
detectOutputFormat,
getFineTune,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { computeActualFee } from "./fee.ts";
const DEFAULT_INTERVAL_SEC = 10;
const MIN_INTERVAL_SEC = 1;
@@ -103,7 +103,6 @@ export default defineCommand({
const follow = flags.follow;
const intervalSec = Math.max(MIN_INTERVAL_SEC, flags.interval ?? DEFAULT_INTERVAL_SEC);
const pollTimeoutSec = flags.pollTimeout;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -114,7 +113,7 @@ export default defineCommand({
interval: intervalSec,
timeout: pollTimeoutSec,
},
format,
"json",
);
return;
}
@@ -132,16 +131,24 @@ export default defineCommand({
if (settings.quiet) {
// Just the status word — ideal for `status=$(... finetune watch ... --quiet)`.
emitBare(status || "UNKNOWN");
} else if (format === "text") {
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
if (status === "SUCCEEDED") emitBare(`${jobId} ${status}`);
emitRequestId(response.request_id, settings.quiet);
} else {
// json: a compact, purpose-built status probe.
emitResult(
{ job_id: jobId, status: status || "UNKNOWN", terminal, request_id: response.request_id },
format,
);
const output: Record<string, unknown> = {
job_id: jobId,
status: status || "UNKNOWN",
terminal,
request_id: response.request_id,
};
// Enrich terminal output with actual fee when usage is reported.
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
if (terminal && usageTokens && usageTokens > 0 && job?.model) {
output.usage_tokens = usageTokens;
const fee = await computeActualFee(settings, job.model as string, usageTokens);
if (fee) {
output.training_cost = fee.cost;
output.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
emitResult(output, "json");
}
if (terminal && status !== "SUCCEEDED") {
@@ -168,18 +175,28 @@ export default defineCommand({
const job = response.output ?? response.data;
const status = String(job?.status ?? "").toUpperCase();
if (format === "text" && !settings.quiet && status !== lastStatus) {
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
if (!settings.quiet && status !== lastStatus) {
process.stderr.write(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}\n`);
lastStatus = status;
}
if (TERMINAL_STATUSES.has(status)) {
const elapsed = Date.now() - startedAt;
if (format !== "text" || settings.quiet) {
emitResult(response, format);
} else if (status === "SUCCEEDED") {
emitBare(`\n✓ ${jobId} ${status} (elapsed ${formatElapsed(elapsed)})`);
emitRequestId(response.request_id, settings.quiet);
if (settings.quiet) {
emitBare(status || "UNKNOWN");
} else {
// Enrich the raw response with actual fee when usage is available.
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
const enriched: Record<string, unknown> = { ...response };
if (usageTokens && usageTokens > 0 && job?.model) {
const fee = await computeActualFee(settings, job.model as string, usageTokens);
if (fee) {
enriched.training_cost = fee.cost;
enriched.usage_tokens = usageTokens;
enriched.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
emitResult(enriched, "json");
}
if (status !== "SUCCEEDED") {
throw new BailianError(
@@ -205,7 +222,7 @@ export default defineCommand({
// Any other error (including the BailianError thrown above) propagates to
// the central handler.
if (controller.signal.aborted) {
emitBare("\nInterrupted.");
process.stderr.write("\nInterrupted.\n");
return;
}
throw error;
@@ -0,0 +1,79 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagAddCategoryResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CATEGORY_ADD_FLAGS = {
name: {
type: "string",
valueHint: "<text>",
description: "Category name (1-20 chars)",
required: true,
},
parentId: {
type: "string",
valueHint: "<id>",
description: "Create as a sub-category of this category",
},
collectionId: {
type: "string",
valueHint: "<id>",
description: "Create under this collection (defaults to the platform collection)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Create a data-center category",
auth: "apiKey",
usageArgs: "--name <text> [flags]",
flags: CATEGORY_ADD_FLAGS,
notes: ["Use categories to organize data-center files by business domain."],
exampleArgs: ["--name product-docs --workspace-id ws-xxx", "--name sub --parent-id cate-xxx"],
validate(flags) {
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// categoryType fixed to UNSTRUCTURED (the only valid value for knowledge-base creation today)
const body = {
categoryName: flags.name,
categoryType: "UNSTRUCTURED",
...(flags.parentId ? { parentCategoryId: flags.parentId } : {}),
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addCategory);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagAddCategoryResponse>({
path: endpoint,
method: "POST",
body,
});
const categoryId = response.data?.categoryId;
if (settings.quiet) {
emitBare(categoryId ?? "");
return;
}
if (format === "text") {
emitBare(`created: ${categoryId ?? "-"} (${flags.name})`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,65 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagConnectorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CATEGORY_DELETE_FLAGS = {
categoryId: {
type: "string",
valueHint: "<id>",
description: "Category ID to delete",
required: true,
},
yes: { type: "switch", description: "Skip the confirmation prompt" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Delete a data-center category",
auth: "apiKey",
usageArgs: "--category-id <id> [flags]",
flags: CATEGORY_DELETE_FLAGS,
notes: [
"Behavior for categories containing files or sub-categories is server-defined — the server error is passed through as-is.",
],
exampleArgs: ["--category-id cate-xxx --workspace-id ws-xxx", "--category-id cate-xxx --yes"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = { categoryId: flags.categoryId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteCategory);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
await confirmDangerousAction(
`Delete category ${flags.categoryId}\nThis cannot be undone.`,
flags.yes ?? false,
);
const response = await ctx.client.requestJson<
RagConnectorResponse<Record<string, unknown> | undefined>
>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`deleted: ${flags.categoryId}`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,98 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagListCategoryResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts";
const CATEGORY_LIST_FLAGS = {
collectionId: {
type: "string",
valueHint: "<id>",
description: "Filter by exact collection ID",
},
parentId: {
type: "string",
valueHint: "<id>",
description: "List sub-categories of this exact parent category",
},
name: {
type: "string",
valueHint: "<text>",
description: "Filter by category name (exact match, unlike the knowledge base list)",
},
nextToken: {
type: "string",
valueHint: "<token>",
description: "Cursor for the next page (from previous output)",
},
maxResult: {
type: "number",
valueHint: "<n>",
description: "Items per page (default: 20)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "List data-center categories",
auth: "apiKey",
usageArgs: "[flags]",
flags: CATEGORY_LIST_FLAGS,
notes: [
"Categories marked [default] are where files land when no category is specified.",
"Pagination is cursor-based: reuse the printed next token to continue.",
],
exampleArgs: ["--workspace-id ws-xxx", "--name my-category", "--next-token <token>"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// type fixed to UNSTRUCTURED, not exposed as a flag (the only valid value today); note: maxResult is singular
const body = {
type: "UNSTRUCTURED",
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
...(flags.parentId ? { parentId: flags.parentId } : {}),
...(flags.name ? { categoryName: flags.name } : {}),
...(flags.nextToken ? { nextToken: flags.nextToken } : {}),
...(flags.maxResult !== undefined ? { maxResult: flags.maxResult } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listCategory);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagListCategoryResponse>({
path: endpoint,
method: "POST",
body,
});
const categories = response.data?.categoryList ?? [];
if (settings.quiet) {
for (const category of categories) emitBare(category.categoryId ?? "");
return;
}
if (format === "text") {
if (categories.length === 0) {
emitBare("No categories found.");
} else {
for (const category of categories) {
const defaultMark = category.isDefault ? " [default]" : "";
emitBare(truncateLine(`${category.categoryId} ${category.categoryName}${defaultMark}`));
}
}
const nextToken = response.data?.nextToken;
if (nextToken) emitBare(`next: --next-token ${nextToken}`);
} else {
emitResult(response, format);
}
},
});
@@ -13,6 +13,7 @@ import {
type KnowledgeChatStreamChunk,
} from "bailian-cli-core";
import { ansi, emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CHAT_FLAGS = {
message: {
@@ -27,11 +28,15 @@ const CHAT_FLAGS = {
description: "Q&A service ID (find in console knowledge Q&A page)",
required: true,
},
// 知识库走 workspace 专属域名,--workspace-id 属命令自有 flag(console 凭证域不适用)。
workspaceId: {
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
// flag here (the console credential scope does not apply).
...WORKSPACE_FLAG,
// Named to avoid the runtime-reserved global --version flag
agentVersion: {
type: "string",
valueHint: "<id>",
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
valueHint: "<version>",
description:
"Service version to call: beta (draft for debugging) or a published number; default is the latest published version",
},
image: {
type: "array",
@@ -146,6 +151,7 @@ export default defineCommand({
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.',
"`--agent-version beta` calls the draft config for debugging before it is deployed.",
],
exampleArgs: [
'--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
@@ -168,14 +174,7 @@ export default defineCommand({
messages = [{ role: "user", content: "" }];
}
const workspaceId = flags.workspaceId || settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
);
}
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// API only supports SSE; streamOutput controls whether to print tokens in real-time
@@ -199,6 +198,9 @@ export default defineCommand({
parameters: {
agent_options: {
agent_id: flags.agentId,
// Omitted flag → field not sent (default behavior unchanged); the value is
// not validated — the set of versions is server-side state
...(flags.agentVersion ? { agent_version: flags.agentVersion } : {}),
},
},
stream: true,
@@ -0,0 +1,165 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
import { readUtf8TextFile } from "./upload-support.ts";
const CHUNK_ADD_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description:
"Owning document ID from the doc list command; required in practice for all knowledge base types",
},
content: {
type: "string",
valueHint: "<text>",
description: "Chunk body text, up to 6000 chars (document-type); alternative to --content-file",
},
contentFile: {
type: "string",
valueHint: "<path>",
description: "Read chunk body from a UTF-8 plain text file (.md/.txt etc.)",
},
title: {
type: "string",
valueHint: "<text>",
description: "Chunk title, up to 50 chars (document-type)",
},
imageUrl: {
type: "array",
valueHint: "<url>",
description: "Chunk image URL (repeatable, up to 10; document-type)",
},
field: {
type: "array",
valueHint: "<key=value>",
description:
"Arbitrary field entry (repeatable) for table/image knowledge bases where keys are Excel column headers; mutually exclusive with content/title/image flags",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Parse --field key=value: split on the first =, value may contain = */
export function parseFieldEntries(entries: string[]): Record<string, string> {
const field: Record<string, string> = {};
for (const entry of entries) {
const separatorIndex = entry.indexOf("=");
if (separatorIndex <= 0) {
throw new BailianError(`--field must be key=value, got: ${entry}`, ExitCode.USAGE);
}
field[entry.slice(0, separatorIndex)] = entry.slice(separatorIndex + 1);
}
return field;
}
export default defineCommand({
description: "Add a chunk directly to a knowledge base",
auth: "apiKey",
usageArgs: "--index-id <id> (--content <text> | --field <k=v>) [flags]",
flags: CHUNK_ADD_FLAGS,
notes: [
"Document / table / image knowledge bases are supported; audio-video ones are not.",
"--doc-id is required in practice for all knowledge base types. Use the document-level id from the doc list command; the per-row doc_id in chunk list output is not accepted.",
"Image-type documents do not support text chunks. Target a text-type document (docx/pdf/txt) instead.",
"The API is idempotent but rate-limited to 10 calls per second — throttle batch scripts.",
"The response carries no chunk id; list chunks afterwards to find the new one.",
"For table/image knowledge bases use --field with Excel column headers as keys; values are passed through as strings.",
],
exampleArgs: [
'--index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx',
"--index-id idx-xxx --field 列A=v1 --field 列B=v2",
],
validate(flags) {
const hasConvenience =
flags.content !== undefined ||
flags.contentFile !== undefined ||
flags.title !== undefined ||
!!flags.imageUrl?.length;
const hasField = !!flags.field?.length;
if (hasConvenience && hasField) {
return "--field is mutually exclusive with --content/--content-file/--title/--image-url";
}
if (!hasConvenience && !hasField) {
return "Provide chunk content via --content/--content-file or --field entries";
}
if (flags.content !== undefined && flags.contentFile !== undefined) {
return "Use either --content or --content-file, not both";
}
if (flags.content !== undefined && flags.content.length > 6000) {
return "--content must be at most 6000 characters";
}
if (flags.title !== undefined && flags.title.length > 50) {
return "--title must be at most 50 characters";
}
if (flags.imageUrl !== undefined && flags.imageUrl.length > 10) {
return "--image-url accepts at most 10 entries";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// dry-run also reads the file and parses --field (rehearsal semantics)
let field: Record<string, unknown>;
if (flags.field?.length) {
field = parseFieldEntries(flags.field);
} else {
const content =
flags.contentFile !== undefined
? readUtf8TextFile(flags.contentFile, "--content")
: flags.content;
if (typeof content === "string" && content.length > 6000) {
throw new BailianError("Chunk content must be at most 6000 characters", ExitCode.USAGE);
}
field = {
...(content !== undefined ? { content } : {}),
...(flags.title !== undefined ? { title: flags.title } : {}),
...(flags.imageUrl?.length ? { image_urls: flags.imageUrl } : {}),
};
}
const body = {
pipelineId: flags.indexId,
...(flags.docId ? { dataId: flags.docId } : {}),
field,
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkCreate);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
body,
});
// The response carries no chunk_id — quiet mode exits 0 silently on success
if (settings.quiet) return;
if (format === "text") {
emitBare(`chunk created (pipeline: ${flags.indexId})`);
emitBare("List chunks to find the new chunk id.");
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,105 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
type FlagsDef,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CHUNK_DELETE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
chunkId: {
type: "array",
valueHint: "<id>",
description: "Chunk ID to delete (repeatable; batches of 10 are sent automatically)",
required: true,
},
yes: { type: "switch", description: "Skip the confirmation prompt" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** The server caps each request at 10 chunk ids — the client batches automatically (bulk delete is where the CLI beats the console) */
export function splitIntoBatches(chunkIds: string[], batchSize = 10): string[][] {
const batches: string[][] = [];
for (let batchStart = 0; batchStart < chunkIds.length; batchStart += batchSize) {
batches.push(chunkIds.slice(batchStart, batchStart + batchSize));
}
return batches;
}
export default defineCommand({
description: "Delete chunks from a knowledge base (irreversible)",
auth: "apiKey",
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
flags: CHUNK_DELETE_FLAGS,
notes: ["Accepts at most 10 chunk ids per call; larger sets are batched automatically."],
exampleArgs: [
"--index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx",
"--index-id idx-xxx --chunk-id chunk-a --yes",
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const batches = splitIntoBatches(flags.chunkId);
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkDelete);
if (settings.dryRun) {
emitResult(
{
endpoint,
batches: batches.map((batchIds) => ({
request: { pipelineId: flags.indexId, chunkIds: batchIds },
})),
},
format,
);
return;
}
await confirmDangerousAction(
`Delete ${flags.chunkId.length} chunk(s) from knowledge base ${flags.indexId} in ${batches.length} batch(es).\nChunks are permanently removed. This cannot be undone.`,
flags.yes ?? false,
);
// Sequential batches; any batch failure aborts, listing already-deleted batches in the error
let deletedCount = 0;
for (const batchIds of batches) {
try {
await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
body: { pipelineId: flags.indexId, chunkIds: batchIds },
});
deletedCount += batchIds.length;
} catch (error) {
if (deletedCount > 0 && error instanceof BailianError && !error.hint) {
throw new BailianError(
error.message,
error.exitCode,
`${deletedCount} chunk(s) in earlier batches were already deleted.`,
{ cause: error, api: error.api, rawResponse: error.rawResponse },
);
}
throw error;
}
}
if (settings.quiet) return;
if (format === "text") {
emitBare(`deleted: ${deletedCount} chunk(s) in ${batches.length} batch(es)`);
return;
}
emitResult({ deleted_count: deletedCount, batches: batches.length }, format);
},
});
@@ -0,0 +1,101 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagChunkListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
const CHUNK_LIST_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description: "Only show chunks belonging to this document",
},
...PAGE_FLAGS,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "List chunks in a knowledge base with content and status",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: CHUNK_LIST_FLAGS,
notes: [
"Use metadata._id as the chunk id and metadata.doc_id as the document id in chunk update/delete commands.",
"Page size defaults to 20 (server default), max 100.",
],
exampleArgs: [
"--index-id idx-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --doc-id file-xxx --page-size 50",
],
validate(flags) {
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
return "--page-size must be between 1 and 100";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Gotcha: this endpoint's pagination keys are pageNum/pageSize (camelCase, in the body)
const body = {
indexId: flags.indexId,
pageNum: flags.pageNumber ?? 1,
pageSize: flags.pageSize ?? 20,
...(flags.docId ? { docId: flags.docId } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkList);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagChunkListResponse>({
path: endpoint,
method: "POST",
body,
});
const nodes = response.data?.nodes ?? [];
if (settings.quiet) {
// chunk ids only, for piping into chunk update/delete
for (const node of nodes) emitBare(node.metadata?._id ?? "");
return;
}
if (format === "text") {
if (nodes.length === 0) {
emitBare("No chunks found.");
} else {
for (const node of nodes) {
const metadata = node.metadata ?? {};
const statusPart = metadata._chunk_status_message
? ` status: ${metadata._chunk_status_message}`
: "";
const excludedPart =
metadata.is_displayed_chunk_content === false ? " [excluded from retrieval]" : "";
emitBare(
`[chunk] ${metadata._id ?? "?"} (doc: ${metadata.doc_name ?? "?"}, doc_id: ${metadata.doc_id ?? "?"})${statusPart}${excludedPart}`,
);
const contentText = metadata.content ?? node.text ?? "";
emitBare(` ${contentText.length > 200 ? `${contentText.slice(0, 200)}` : contentText}`);
}
}
emitBare(`total: ${response.data?.total ?? nodes.length}`);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,175 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type Client,
type FlagsDef,
type RagChunkListResponse,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
import { readUtf8TextFile } from "./upload-support.ts";
const CHUNK_UPDATE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
chunkId: {
type: "string",
valueHint: "<id>",
description: "Chunk ID (metadata._id from the chunk list output)",
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description: "Document ID owning the chunk (metadata.doc_id from the chunk list output)",
required: true,
},
content: {
type: "string",
valueHint: "<text>",
description: "New chunk content, 10-6000 chars; alternative to --content-file",
},
contentFile: {
type: "string",
valueHint: "<path>",
description: "Read new content from a UTF-8 plain text file (.md/.txt etc.)",
},
title: {
type: "string",
valueHint: "<text>",
description: "Chunk title, 0-50 chars (empty string clears it; omit to keep unchanged)",
},
exclude: { type: "switch", description: "Exclude this chunk from retrieval" },
include: { type: "switch", description: "Include this chunk in retrieval (default)" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** When only toggling include/exclude, read back the current content first (the API requires content — hide that quirk from users) */
async function fetchChunkContent(
client: Client,
workspaceId: string,
indexId: string,
chunkId: string,
docId: string,
): Promise<string> {
const maxPages = 10;
for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
const response = await client.requestJson<RagChunkListResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.chunkList),
method: "POST",
body: { indexId, docId, pageNum, pageSize: 100 },
});
const nodes = response.data?.nodes ?? [];
const match = nodes.find((node) => node.metadata?._id === chunkId);
const matchContent = match?.metadata?.content ?? match?.text;
if (typeof matchContent === "string") return matchContent;
if (nodes.length < 100) break;
}
throw new BailianError(
`Chunk not found: ${chunkId}`,
ExitCode.GENERAL,
"Check the chunk id via the chunk list command.",
);
}
export default defineCommand({
description: "Update chunk content or toggle its retrieval visibility",
auth: "apiKey",
usageArgs: "--index-id <id> --chunk-id <id> --doc-id <id> [flags]",
flags: CHUNK_UPDATE_FLAGS,
notes: [
"Content must be 10-6000 characters and within the knowledge base's max chunk size.",
"--content-file expects a UTF-8 plain text file; document formats (.docx/.pdf) are not parsed here.",
"Toggling --exclude/--include without new content re-submits the existing content automatically.",
],
exampleArgs: [
'--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text"',
"--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude",
],
validate(flags) {
if (flags.content !== undefined && flags.contentFile !== undefined) {
return "Use either --content or --content-file, not both";
}
if (flags.exclude && flags.include) return "--exclude and --include are mutually exclusive";
const hasContent = flags.content !== undefined || flags.contentFile !== undefined;
if (!hasContent && !flags.exclude && !flags.include && flags.title === undefined) {
return "Nothing to update — pass --content/--content-file, --title, --exclude or --include";
}
// Content lower-bound is enforced here (not deferred to run) so dry-run and
// missing-flag diagnostics surface the same error as the live request.
if (flags.content !== undefined && (flags.content.length < 10 || flags.content.length > 6000)) {
return "--content must be 10-6000 characters";
}
if (flags.title !== undefined && flags.title.length > 50) {
return "--title must be at most 50 characters";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// dry-run also reads the file and validates (rehearsal semantics); the read-back
// request is only made outside dry-run and when no new content is given
let content =
flags.contentFile !== undefined
? readUtf8TextFile(flags.contentFile, "--content")
: flags.content;
if (content !== undefined && (content.length < 10 || content.length > 6000)) {
throw new BailianError("Chunk content must be 10-6000 characters", ExitCode.USAGE);
}
if (content === undefined) {
if (settings.dryRun) {
content = "<current-content (fetched at run time)>";
} else {
content = await fetchChunkContent(
ctx.client,
workspaceId,
flags.indexId,
flags.chunkId,
flags.docId,
);
}
}
const body = {
pipelineId: flags.indexId,
chunkId: flags.chunkId,
dataId: flags.docId,
content,
// Without exclude/include the chunk stays retrievable (safe default)
isDisplayedChunkContent: !flags.exclude,
...(flags.title !== undefined ? { title: flags.title } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkUpdate);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`updated: ${flags.chunkId}`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,113 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagAddConnectorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const COLLECTION_CREATE_FLAGS = {
name: {
type: "string",
valueHint: "<text>",
description: "Collection name",
required: true,
},
description: {
type: "string",
valueHint: "<text>",
description: "Collection description (required by the server)",
required: true,
},
storeType: {
type: "string",
valueHint: "<type>",
description: "Storage: platform (managed) or custom (your own OSS bucket)",
},
ossRegion: {
type: "string",
valueHint: "<id>",
description: "OSS region id (required with --store-type custom)",
},
ossBucket: {
type: "string",
valueHint: "<name>",
description: "OSS bucket name (required with --store-type custom)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Create a FILE data collection",
auth: "apiKey",
usageArgs: "--name <text> --description <text> [flags]",
flags: COLLECTION_CREATE_FLAGS,
notes: [
"Store type defaults to platform (managed storage); custom uses your authorized OSS bucket.",
"Custom buckets must carry the bucket tag bailian-connector-access=ReadAndWrite (Bailian's tag-based access control); without it the server rejects creation with a misleading 'setBucketCORS failed' error.",
"There is no collection delete API — create collections deliberately.",
],
exampleArgs: [
"--name my-collection --description 'team docs' --workspace-id ws-xxx",
"--name oss-coll --description 'own bucket' --store-type custom --oss-region cn-beijing --oss-bucket my-bucket",
],
validate(flags) {
// Server rejects names longer than 20 characters ("Connector name is longer than 20")
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
const storeType = flags.storeType ?? "platform";
if (storeType !== "platform" && storeType !== "custom") {
return "--store-type must be platform or custom";
}
if (storeType === "custom" && (!flags.ossRegion || !flags.ossBucket)) {
return "--store-type custom requires --oss-region and --oss-bucket";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const storeType = (flags.storeType ?? "platform").toUpperCase();
// The server contract still uses connector* fields; only the CLI-facing term is collection.
// CUSTOM fields are regionId/bucketName per api/connector/add-connector.md (live-verified;
// the earlier ossRegionId/ossBucket naming was an implementation error, rejected with InvalidParameter).
const body = {
connectorType: "FILE",
connectorName: flags.name,
description: flags.description,
fileConnectorConfig: {
storeType,
...(storeType === "CUSTOM"
? { regionId: flags.ossRegion, bucketName: flags.ossBucket }
: {}),
},
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addConnector);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagAddConnectorResponse>({
path: endpoint,
method: "POST",
body,
});
const collectionId = response.data?.connectorId;
if (settings.quiet) {
emitBare(collectionId ?? "");
return;
}
if (format === "text") {
emitBare(`created: ${collectionId ?? "-"} (${flags.name}, ${storeType})`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,75 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagGetConnectorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const COLLECTION_GET_FLAGS = {
collectionId: {
type: "string",
valueHint: "<id>",
description: "Collection ID; alternative to --name",
},
name: {
type: "string",
valueHint: "<text>",
description: "Collection name; alternative to --collection-id",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Show data collection details",
auth: "apiKey",
usageArgs: "(--collection-id <id> | --name <text>) [flags]",
flags: COLLECTION_GET_FLAGS,
exampleArgs: ["--collection-id conn-xxx --workspace-id ws-xxx", "--name my-collection"],
validate(flags) {
if (!flags.collectionId && !flags.name) return "Pass --collection-id or --name";
if (flags.collectionId && flags.name) return "Use either --collection-id or --name, not both";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// The server contract still uses connector* fields; only the CLI-facing term is collection
const body = {
...(flags.collectionId ? { connectorId: flags.collectionId } : {}),
...(flags.name ? { connectorName: flags.name } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.getConnector);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagGetConnectorResponse>({
path: endpoint,
method: "POST",
body,
});
const collection = response.data;
if (settings.quiet) {
emitBare(collection?.connectorId ?? "");
return;
}
if (format === "text") {
emitBare(`id: ${collection?.connectorId ?? "-"}`);
emitBare(`name: ${collection?.connectorName ?? "-"}`);
emitBare(`description: ${collection?.description ?? "-"}`);
// getConnector does not return fileConnectorConfig (storeType/regionId/bucketName);
// these fields are only available on the create request body.
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,96 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagDeleteFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const DOC_DELETE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
docId: {
type: "array",
valueHint: "<id>",
description: "Document ID to delete (repeatable)",
required: true,
},
yes: { type: "switch", description: "Skip the confirmation prompt" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary: list all doc_ids up to 5, otherwise show the first 5 + total count */
function buildDeleteSummary(indexId: string, docIds: string[]): string {
const listed =
docIds.length <= 5
? docIds.join("\n ")
: `${docIds.slice(0, 5).join("\n ")}\n ... (${docIds.length} documents total)`;
return `Delete ${docIds.length} document(s) from knowledge base ${indexId}:\n ${listed}\nDocuments and all their chunks are permanently removed from the index. This cannot be undone.`;
}
export default defineCommand({
description: "Delete documents and their chunks from a knowledge base",
auth: "apiKey",
usageArgs: "--index-id <id> --doc-id <id> [flags]",
flags: DOC_DELETE_FLAGS,
notes: [
"Removes documents from the knowledge base index only; the source files remain in the data center.",
"Use the doc_id from `knowledge doc list --quiet`, not the fileId from `knowledge doc upload`. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.",
"Deletion may take up to ~30s to propagate — the document may still appear in the doc list briefly.",
"The output lists the ids actually deleted.",
],
exampleArgs: [
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// snake_case: body { index_id, doc_ids }
const body = { index_id: flags.indexId, doc_ids: flags.docId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDeleteFile);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
await confirmDangerousAction(
buildDeleteSummary(flags.indexId, flags.docId),
flags.yes ?? false,
);
const response = await ctx.client.requestJson<RagDeleteFileResponse>({
path: endpoint,
method: "POST",
body,
});
// Output follows the server's data.deleted list
const deleted = response.data?.deleted ?? [];
if (settings.quiet) {
for (const docId of deleted) emitBare(docId);
return;
}
if (format === "text") {
emitBare(`deleted: ${deleted.length} document(s)`);
for (const docId of deleted) emitBare(` ${docId}`);
if (deleted.length !== flags.docId.length) {
process.stderr.write(
`Warning: requested ${flags.docId.length} deletion(s) but the server reported ${deleted.length}.\n`,
);
}
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,117 @@
import { basename } from "node:path";
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagOssImportResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const DOC_IMPORT_OSS_FLAGS = {
bucket: {
type: "string",
valueHint: "<name>",
description: "Authorized OSS bucket name",
required: true,
},
region: {
type: "string",
valueHint: "<id>",
description: "OSS region id (e.g. cn-beijing)",
required: true,
},
ossKey: {
type: "array",
valueHint: "<key>",
description: "OSS object key to import (repeatable, 1-10 per call)",
required: true,
},
categoryId: {
type: "string",
valueHint: "<id>",
description: "Target data-center category (default: the default category)",
},
tag: {
type: "array",
valueHint: "<text>",
description: "File tag applied to every imported file (repeatable, up to 10)",
},
overwrite: {
type: "switch",
description: "Overwrite files previously imported from the same OSS keys",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Batch import files from an authorized OSS bucket into the data center",
auth: "apiKey",
usageArgs: "--bucket <name> --region <id> --oss-key <key> [flags]",
flags: DOC_IMPORT_OSS_FLAGS,
notes: [
"The bucket must be authorized to the platform service role beforehand; permission errors from the server are passed through with a pointer to check AliyunServiceRoleForBailian in the RAM console.",
"File names are derived from the OSS key basename.",
"--overwrite replaces the previously imported file and issues a NEW fileId (the old one becomes invalid) — verified live.",
],
exampleArgs: [
"--bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx",
"--bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite",
],
validate(flags) {
if (flags.ossKey.length > 10) return "--oss-key accepts at most 10 entries per call";
if (flags.tag !== undefined && flags.tag.length > 10) {
return "--tag accepts at most 10 entries";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// categoryType fixed to UNSTRUCTURED; parser not exposed as a flag (defaults to AUTO_SELECT)
const body = {
categoryId: flags.categoryId ?? "default",
categoryType: "UNSTRUCTURED",
ossBucket: flags.bucket,
ossRegionId: flags.region,
fileDetails: flags.ossKey.map((ossKey) => ({ fileName: basename(ossKey), ossKey })),
...(flags.tag?.length ? { tags: flags.tag } : {}),
...(flags.overwrite ? { overWriteFileByOssKey: true } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addFilesFromAuthorizedOss);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagOssImportResponse>({
path: endpoint,
method: "POST",
body,
});
// Live-verified shape: results come back as addFileResultList (the docs' flat
// fileIds field is not returned); per-file status is SUCCESS on success
const results = response.data?.addFileResultList ?? [];
const fileIds = results
.map((result) => result.fileId)
.filter((fileId): fileId is string => !!fileId);
if (settings.quiet) {
for (const fileId of fileIds) emitBare(fileId);
return;
}
if (format === "text") {
emitBare(`imported: ${fileIds.length} file(s)`);
for (const result of results) {
emitBare(` ${result.fileId ?? "-"} ${result.status ?? "-"} ${result.ossKey ?? ""}`);
}
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,83 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagIndexFilesResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, ansi } from "bailian-cli-runtime";
import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
const DOC_LIST_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
...PAGE_FLAGS,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "List documents in a knowledge base with parse/index status",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: DOC_LIST_FLAGS,
notes: [
"Documents with status FAILED are highlighted in text mode — use the import job status command to inspect failures.",
"Page size defaults to 10 (server default), max 100.",
],
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx", "--index-id idx-xxx --page-size 100"],
validate(flags) {
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
return "--page-size must be between 1 and 100";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Gotcha: this endpoint's page parameter is page_num (not page_number)
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
url.searchParams.set("index_id", flags.indexId);
url.searchParams.set("page_num", String(flags.pageNumber ?? 1));
url.searchParams.set("page_size", String(flags.pageSize ?? 10));
const endpoint = url.toString();
if (settings.dryRun) {
emitResult({ endpoint, request: null }, format);
return;
}
const response = await ctx.client.requestJson<RagIndexFilesResponse>({
path: endpoint,
method: "GET",
});
const rows = response.data?.rows ?? [];
if (settings.quiet) {
for (const row of rows) emitBare(row.doc_id ?? "");
return;
}
if (format === "text") {
const styles = ansi(process.stdout);
if (rows.length === 0) {
emitBare("No documents found.");
} else {
for (const row of rows) {
const line = truncateLine(
[row.doc_id, row.status, row.doc_name, row.doc_type ?? "-", row.size ?? "-"].join(" "),
);
emitBare(row.status === "FAILED" ? styles.red(line) : line);
}
}
emitBare(`total: ${response.data?.total_count ?? rows.length}`);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,124 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type RagIndexJobStatusResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, ansi } from "bailian-cli-runtime";
import {
resolveWorkspaceId,
PAGE_FLAGS,
WORKSPACE_FLAG,
failedImportDocs,
importJobFailureMessage,
pollImportJob,
} from "./shared.ts";
const DOC_STATUS_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
jobId: {
type: "string",
valueHint: "<id>",
description: "Import job ID (ingestionId returned by import commands)",
required: true,
},
...PAGE_FLAGS,
wait: { type: "switch", description: "Poll until the job reaches a terminal state" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 5)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
function printStatus(response: RagIndexJobStatusResponse): void {
const styles = ansi(process.stdout);
emitBare(`status: ${response.data?.ingestion_status ?? "UNKNOWN"}`);
for (const doc of response.data?.rows ?? []) {
const docState = doc.code ?? doc.status ?? "?";
const line = ` ${doc.doc_id ?? "?"} ${docState} ${doc.doc_name ?? ""}`;
emitBare(docState.includes("FAILED") ? styles.red(line) : line);
}
}
export default defineCommand({
description: "Check knowledge base import job status",
auth: "apiKey",
usageArgs: "--index-id <id> --job-id <id> [flags]",
flags: DOC_STATUS_FLAGS,
notes: [
"Both --index-id and --job-id are required (passing only one returns SystemError).",
"If you see a SystemError, the job may not exist — check the ingestion id in the document list output.",
"Overall job states are PENDING / RUNNING / COMPLETED; per-document failures (for example PARSE_FAILED) exit non-zero with the error message passed through.",
],
exampleArgs: [
"--index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --job-id job-xxx --wait --poll-interval 10",
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Both required flags are enforced by the parser up front; parameters go in
// the query string (they are ignored in the body)
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexJobStatus));
url.searchParams.set("index_id", flags.indexId);
url.searchParams.set("job_id", flags.jobId);
if (flags.pageNumber !== undefined) {
url.searchParams.set("page_number", String(flags.pageNumber));
}
if (flags.pageSize !== undefined) {
url.searchParams.set("page_size", String(flags.pageSize));
}
const endpoint = url.toString();
if (settings.dryRun) {
emitResult({ endpoint, request: null }, format);
return;
}
let response: RagIndexJobStatusResponse;
if (flags.wait) {
// Reuse the shared polling (timeout → TIMEOUT(5)); failure detection happens
// uniformly after return, based on per-document status
response = await pollImportJob(ctx.client, settings, {
statusUrl: endpoint,
intervalSec: flags.pollInterval ?? 5,
});
} else {
response = await ctx.client.requestJson<RagIndexJobStatusResponse>({
path: endpoint,
method: "GET",
});
}
// Any per-document failure means a non-zero exit; the server message is passed through verbatim
if (failedImportDocs(response).length > 0) {
throw new BailianError(
importJobFailureMessage(response, "Import job reported document failures."),
ExitCode.GENERAL,
);
}
if (settings.quiet) {
emitBare(response.data?.ingestion_status ?? "UNKNOWN");
return;
}
if (format === "text") {
printStatus(response);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,88 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagBatchUpdateTagResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const DOC_TAG_FLAGS = {
docId: {
type: "array",
valueHint: "<id>",
description: "Data-center file ID to tag (repeatable, 1-20 per call)",
required: true,
},
tag: {
type: "array",
valueHint: "<text>",
description: "Tag applied to every --doc-id (repeatable, each up to 32 chars)",
required: true,
},
mode: {
type: "string",
valueHint: "<mode>",
description: "Update mode: append (default) or overwrite",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Batch update tags on data-center files",
auth: "apiKey",
usageArgs: "--doc-id <id> --tag <text> [flags]",
flags: DOC_TAG_FLAGS,
notes: [
"The same tag set is applied to every --doc-id; run the command multiple times for different tag sets.",
"Server limits: up to 100 tags per file, total tag length up to 700 chars, tag up to 32 chars.",
],
exampleArgs: [
"--doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx",
"--doc-id file-a --doc-id file-b --tag final --mode overwrite",
],
validate(flags) {
if (flags.docId.length > 20) return "--doc-id accepts at most 20 ids per call";
if (flags.mode !== undefined && flags.mode !== "append" && flags.mode !== "overwrite") {
return "--mode must be append or overwrite";
}
// Hard limits stated by the API contract: each tag ≤32 chars; ≤100 tags per file; total length ≤700
if (flags.tag.length > 100) return "At most 100 tags per file";
const overlongTag = flags.tag.find((tag) => tag.length > 32);
if (overlongTag) return `Tag exceeds 32 characters: ${overlongTag}`;
const totalLength = flags.tag.reduce((sum, tag) => sum + tag.length, 0);
if (totalLength > 700) return "Total tag length exceeds 700 characters";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = {
fileInfos: flags.docId.map((fileId) => ({ fileId, tags: flags.tag })),
updateMode: (flags.mode ?? "append").toUpperCase(),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.batchUpdateFileTag);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagBatchUpdateTagResponse>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`tagged: ${flags.docId.length} file(s) with [${flags.tag.join(", ")}]`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,322 @@
// Orchestration command: local file → data center → (optional) import into a knowledge base.
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { basename } from "node:path";
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type RagUploadLeaseResponse,
type RagAddFileResponse,
type RagJobCreateResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
resolveWorkspaceId,
WORKSPACE_FLAG,
failedImportDocs,
importJobFailureMessage,
importJobStatus,
importJobStatusUrl,
pollImportJob,
withPartialSuccessHint,
} from "./shared.ts";
import { checkUploadFile, expandUploadPaths } from "./upload-support.ts";
const DOC_UPLOAD_FLAGS = {
file: {
type: "array",
valueHint: "<path>",
description:
"Local file or directory path (repeatable). Directories are scanned recursively; unsupported formats are skipped",
required: true,
},
indexId: {
type: "string",
valueHint: "<id>",
description: "Import into this knowledge base after registration (one job for all files)",
},
categoryId: {
type: "string",
valueHint: "<id>",
description: "Target data-center category; defaults to the workspace default category",
},
tag: {
type: "array",
valueHint: "<text>",
description: "File tag (repeatable), applied to every uploaded file",
},
wait: {
type: "switch",
description: "Poll the import job to a terminal state (needs --index-id)",
},
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 5)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
interface UploadedFile {
path: string;
fileId: string;
}
export default defineCommand({
description:
"Upload local files or directories to the data center and optionally import into a knowledge base",
auth: "apiKey",
usageArgs: "--file <path> [flags]",
flags: DOC_UPLOAD_FLAGS,
notes: [
"Pipeline: apply upload lease → PUT to OSS → register file → (with --index-id) create import job.",
"Without --category-id the workspace default category is resolved automatically.",
"Directories are scanned recursively; node_modules, .git, and similar are skipped automatically.",
"Multiple files are processed sequentially; on failure, already-registered file ids are listed in the error hint.",
],
exampleArgs: [
"--file ./a.md --workspace-id ws-xxx",
"--file ./a.md --file ./b.pdf --index-id idx-xxx --wait",
"--file ./docs/ --workspace-id ws-xxx",
"--file ./docs/ --dry-run --verbose",
],
validate(flags) {
if (flags.wait && !flags.indexId) return "--wait requires --index-id";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Expand directories into individual file paths; unsupported extensions are
// collected into `skipped` rather than throwing (directory-scan semantics)
const { files: expandedFiles, skipped } = expandUploadPaths(flags.file);
if (expandedFiles.length === 0) {
throw new BailianError(
"No supported files found",
ExitCode.USAGE,
`Supported formats: .pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`,
);
}
// Local pre-flight validation also runs in dry-run (rehearsal semantics: surface
// file problems early); exceeding a soft limit only warns
const checkedFiles = expandedFiles.map((filePath) => {
const checked = checkUploadFile(filePath);
if (checked.warning) process.stderr.write(`Warning: ${checked.warning}\n`);
return { filePath, sizeBytes: checked.sizeBytes };
});
if (settings.dryRun) {
// dry-run does not read file contents (md5 shown as a placeholder)
const categoryPlaceholder = flags.categoryId ?? "default";
const steps = checkedFiles.flatMap((checkedFile) => [
{
step: "applyFileUploadLease",
endpoint: ragEndpoint(workspaceId, RAG_PATHS.applyFileUploadLease),
request: {
category: categoryPlaceholder,
fileName: basename(checkedFile.filePath),
sizeBytes: String(checkedFile.sizeBytes), // gotcha: must be a string
contentMd5: "<md5-base64>",
} as unknown,
},
{
step: "ossPut",
endpoint: "<lease.param.url>",
request: { method: "PUT", headers: "<lease.param.headers>" } as unknown,
},
{
step: "addFile",
endpoint: ragEndpoint(workspaceId, RAG_PATHS.addFile),
request: {
leaseId: "<leaseId>",
category: categoryPlaceholder,
parser: "AUTO_SELECT",
...(flags.tag?.length ? { tags: flags.tag } : {}),
} as unknown,
},
]);
if (flags.indexId) {
steps.push({
step: "createImportJob",
endpoint: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate),
request: {
indexId: flags.indexId,
// Live-verified: the field name is docIds (not documentIds as in the
// public docs); omitting sourceType would import the entire data center.
sourceType: "DATA_CENTER_FILE",
docIds: ["<fileId>"],
} as unknown,
});
}
emitResult({ steps, skipped }, format);
return;
}
// Default category: the literal "default" is accepted by lease/addFile
// (verified against the live API), so no listCategory resolution is needed
const categoryId = flags.categoryId ?? "default";
// Multiple files run steps 1-3 sequentially (no concurrency in this version,
// to avoid OSS rate-limit complexity)
const uploaded: UploadedFile[] = [];
for (const checkedFile of checkedFiles) {
try {
const fileBuffer = readFileSync(checkedFile.filePath);
const contentMd5 = createHash("md5").update(fileBuffer).digest("base64");
// 1) Apply for an upload lease (gotcha: the category parameter is named
// category, not categoryId; sizeBytes must be a string)
const lease = await ctx.client.requestJson<RagUploadLeaseResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.applyFileUploadLease),
method: "POST",
body: {
category: categoryId,
fileName: basename(checkedFile.filePath),
sizeBytes: String(checkedFile.sizeBytes),
contentMd5,
},
});
const leaseId = lease.data?.leaseId;
const leaseParam = lease.data?.param;
if (!leaseId || !leaseParam?.url) {
throw new BailianError(
`Upload lease response missing leaseId/url for ${checkedFile.filePath}`,
ExitCode.GENERAL,
);
}
// 2) OSS upload: goes to the OSS host, not the DashScope gateway — native fetch without a Bearer header
let ossResponse: Response;
try {
ossResponse = await fetch(leaseParam.url, {
method: leaseParam.method ?? "PUT",
headers: leaseParam.headers,
body: fileBuffer,
});
} catch (error) {
const causeCode = (error as { cause?: { code?: string } }).cause?.code;
throw new BailianError(
`OSS upload failed for ${basename(checkedFile.filePath)}`,
ExitCode.NETWORK,
causeCode ? `Network error (${causeCode}).` : undefined,
{ cause: error },
);
}
if (!ossResponse.ok) {
const ossBody = await ossResponse.text().catch(() => "");
throw new BailianError(
`OSS upload rejected (HTTP ${ossResponse.status}) for ${basename(checkedFile.filePath)}${ossBody ? `: ${ossBody.slice(0, 300)}` : ""}`,
ExitCode.GENERAL,
);
}
// 3) Register the file
const added = await ctx.client.requestJson<RagAddFileResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.addFile),
method: "POST",
body: {
leaseId,
category: categoryId,
parser: "AUTO_SELECT",
...(flags.tag?.length ? { tags: flags.tag } : {}),
},
});
const fileId = added.data?.fileId;
if (!fileId) {
throw new BailianError(
`addFile response missing fileId for ${checkedFile.filePath}`,
ExitCode.GENERAL,
);
}
uploaded.push({ path: checkedFile.filePath, fileId });
} catch (error) {
// Partial-failure semantics: abort with an error, listing already-registered
// fileIds in the hint (re-uploading is cheap and idempotent)
if (uploaded.length > 0) {
throw withPartialSuccessHint(
error,
`Already registered: ${uploaded.map((item) => item.fileId).join(", ")}`,
);
}
throw error;
}
}
// 4) Optional import (merged into a single job after all files are registered)
let ingestionId: string | undefined;
let finalStatus: string | undefined;
if (flags.indexId) {
const job = await ctx.client.requestJson<RagJobCreateResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate),
method: "POST",
body: {
indexId: flags.indexId,
// Live-verified: the field name is docIds (not documentIds as in the
// public docs); omitting sourceType would import the entire data center.
sourceType: "DATA_CENTER_FILE",
docIds: uploaded.map((item) => item.fileId),
},
});
ingestionId = job.data?.ingestionId;
if (flags.wait && ingestionId) {
const statusResponse = await pollImportJob(ctx.client, settings, {
statusUrl: importJobStatusUrl(workspaceId, flags.indexId, ingestionId).toString(),
intervalSec: flags.pollInterval ?? 5,
});
finalStatus = importJobStatus(statusResponse);
// Job finished but some documents failed to parse → non-zero exit, server message passed through verbatim
if (failedImportDocs(statusResponse).length > 0) {
throw new BailianError(
importJobFailureMessage(statusResponse, "Import job reported document failures."),
ExitCode.GENERAL,
`Registered file ids: ${uploaded.map((item) => item.fileId).join(", ")}`,
);
}
}
}
if (settings.quiet) {
for (const item of uploaded) emitBare(item.fileId);
return;
}
if (format === "text") {
for (const item of uploaded) {
emitBare(`${basename(item.path)} ${item.fileId} registered`);
}
if (ingestionId) emitBare(`job: ${ingestionId}`);
if (finalStatus) emitBare(`status: ${finalStatus}`);
// Summary line: always show counts; list skipped files only with --verbose
const summaryParts = [`Uploaded ${uploaded.length} file${uploaded.length !== 1 ? "s" : ""}`];
if (skipped.length > 0) {
summaryParts.push(`skipped ${skipped.length} unsupported`);
}
emitBare(`\n${summaryParts.join(", ")}.`);
if (settings.verbose && skipped.length > 0) {
emitBare("Skipped files:");
for (const skippedPath of skipped) {
emitBare(` ${basename(skippedPath)}`);
}
}
return;
}
// An orchestration command has no single response to pass through — emit a custom stable shape
emitResult(
{
files: uploaded.map((item) => ({ path: item.path, fileId: item.fileId })),
skipped,
...(flags.indexId ? { index_id: flags.indexId } : {}),
...(ingestionId ? { ingestion_id: ingestionId } : {}),
...(finalStatus ? { final_status: finalStatus } : {}),
},
format,
);
},
});
@@ -0,0 +1,88 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagConnectorResponse,
type RagDescribeFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const FILE_DELETE_FLAGS = {
fileId: {
type: "string",
valueHint: "<id>",
description: "Data-center file ID to delete",
required: true,
},
yes: { type: "switch", description: "Skip the confirmation prompt" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup (file name/size); failure degrades to id-only */
async function buildDeleteSummary(
client: Client,
workspaceId: string,
fileId: string,
): Promise<string> {
let infoPart = "";
try {
const detail = await client.requestJson<RagDescribeFileResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.describeFile),
method: "POST",
body: { fileId },
});
if (detail.data?.fileName) infoPart = ` name: ${detail.data.fileName}`;
} catch {
// Degrade gracefully: a failed lookup does not block confirmation
}
return `Delete data-center file ${fileId}${infoPart}\nPERMANENT: if the file is referenced by knowledge bases, their document indexes break too. This differs from removing a document from one knowledge base.`;
}
export default defineCommand({
description: "Permanently delete a file from the data center",
auth: "apiKey",
usageArgs: "--file-id <id> [flags]",
flags: FILE_DELETE_FLAGS,
notes: [
"Irreversible. If knowledge bases reference this file, their related document indexes become invalid.",
"To remove a document from a single knowledge base only, use the document delete command instead.",
],
exampleArgs: ["--file-id file-xxx --workspace-id ws-xxx", "--file-id file-xxx --yes"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = { fileId: flags.fileId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteFile);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const summary = flags.yes
? ""
: await buildDeleteSummary(ctx.client, workspaceId, flags.fileId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<
RagConnectorResponse<Record<string, unknown> | undefined>
>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`deleted: ${flags.fileId}`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,67 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagDescribeFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const FILE_GET_FLAGS = {
fileId: {
type: "string",
valueHint: "<id>",
description: "Data-center file ID",
required: true,
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Show data-center file details (size, MD5, tags, timestamps)",
auth: "apiKey",
usageArgs: "--file-id <id> [flags]",
flags: FILE_GET_FLAGS,
exampleArgs: ["--file-id file-xxx --workspace-id ws-xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = { fileId: flags.fileId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.describeFile);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagDescribeFileResponse>({
path: endpoint,
method: "POST",
body,
});
const file = response.data;
if (settings.quiet) {
emitBare(file?.fileId ?? "");
return;
}
if (format !== "text") {
emitResult(response, format);
return;
}
emitBare(`id: ${file?.fileId ?? "-"}`);
emitBare(`name: ${file?.fileName ?? "-"}`);
emitBare(`type: ${file?.fileType ?? "-"}`);
emitBare(`size: ${file?.sizeBytes ?? "-"}`);
emitBare(`status: ${file?.status ?? "-"}`);
emitBare(`parser: ${file?.parser ?? "-"}`);
emitBare(`category: ${file?.category ?? "-"}`);
emitBare(`uploaded: ${file?.uploadTime ?? "-"}`);
const tags = Array.isArray(file?.tags) ? file.tags.join(", ") : (file?.tags ?? "-");
emitBare(`tags: ${tags || "-"}`);
},
});
@@ -0,0 +1,104 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagListFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts";
const FILE_LIST_FLAGS = {
categoryId: {
type: "string",
valueHint: "<id>",
description: "Category to list (find ids via the category list command); exact match",
required: true,
},
name: {
type: "string",
valueHint: "<text>",
description: "Filter by exact file name without its extension (a.md → pass a)",
},
fileId: {
type: "array",
valueHint: "<id>",
description: "Filter by exact file ID (repeatable)",
},
nextToken: {
type: "string",
valueHint: "<token>",
description: "Cursor for the next page (from previous output)",
},
maxResult: {
type: "number",
valueHint: "<n>",
description: "Items per page",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "List files in a data-center category",
auth: "apiKey",
usageArgs: "--category-id <id> [flags]",
flags: FILE_LIST_FLAGS,
notes: [
"A real category id is required — the default value is not resolved here. Find the id via the category list command.",
"--name matches the exact file name without its extension (for a.md pass a); partial keywords return no results.",
"Pagination is cursor-based: reuse the printed next token to continue.",
],
exampleArgs: [
"--category-id cate-xxx --workspace-id ws-xxx",
"--category-id cate-xxx --name report",
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = {
categoryId: flags.categoryId,
...(flags.name ? { fileName: flags.name } : {}),
...(flags.fileId?.length ? { fileIds: flags.fileId } : {}),
...(flags.nextToken ? { nextToken: flags.nextToken } : {}),
...(flags.maxResult !== undefined ? { maxResult: flags.maxResult } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listFile);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagListFileResponse>({
path: endpoint,
method: "POST",
body,
});
const files = response.data?.fileList ?? [];
if (settings.quiet) {
for (const file of files) emitBare(file.fileId ?? "");
return;
}
if (format === "text") {
if (files.length === 0) {
emitBare("No files found.");
} else {
for (const file of files) {
emitBare(
truncateLine(
[file.fileId, file.status ?? "-", file.fileName, file.sizeBytes ?? "-"].join(" "),
),
);
}
}
const nextToken = response.data?.nextToken;
if (nextToken) emitBare(`next: --next-token ${nextToken}`);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,166 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type RagCreateIndexV2Response,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
resolveWorkspaceId,
WORKSPACE_FLAG,
failedImportDocs,
importJobFailureMessage,
importJobStatus,
importJobStatusUrl,
pollImportJob,
} from "./shared.ts";
const KB_CREATE_FLAGS = {
name: {
type: "string",
valueHint: "<text>",
description: "Knowledge base name (1-20 chars, unique in workspace)",
required: true,
},
docId: {
type: "array",
valueHint: "<id>",
description:
"Data-center file id to import (repeatable); mutually exclusive with --category-id",
},
categoryId: {
type: "array",
valueHint: "<id>",
description:
"Import every file under this category (repeatable); mutually exclusive with --doc-id",
},
embeddingModel: {
type: "string",
valueHint: "<name>",
description: "Embedding model name (default: text-embedding-v4)",
},
chunkSize: {
type: "number",
valueHint: "<n>",
description: "Chunk size in characters (default: 600, recommended 300-800)",
},
wait: { type: "switch", description: "Poll the initial import job to a terminal state" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 5)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** sourceType/docIds/categoryIds derivation, centralized for unit testing (gotcha: the parameter is docIds, not fileIds) */
export function buildDataSourceFields(flags: { docId?: string[]; categoryId?: string[] }): {
sourceType: string;
docIds?: string[];
categoryIds?: string[];
dataSources: Array<{ sourceType: string }>;
} {
if (flags.docId?.length) {
return {
sourceType: "DATA_CENTER_FILE",
docIds: flags.docId,
dataSources: [{ sourceType: "DATA_CENTER_FILE" }],
};
}
return {
sourceType: "DATA_CENTER_CATEGORY",
categoryIds: flags.categoryId,
dataSources: [{ sourceType: "DATA_CENTER_CATEGORY" }],
};
}
export default defineCommand({
description: "Create a knowledge base and import data-center files or categories",
auth: "apiKey",
usageArgs: "--name <text> (--doc-id <id> | --category-id <id>) [flags]",
flags: KB_CREATE_FLAGS,
notes: [
"Structure/sink types are fixed to the default document knowledge base (unstructured, BUILT_IN storage).",
"Returns the knowledge base id (pipelineId) and the initial import job id (ingestionId).",
"Use the import job status command (or --wait) to track the initial import.",
],
exampleArgs: [
"--name demo --doc-id file-xxx --workspace-id ws-xxx",
"--name demo --category-id cate-xxx --wait",
],
validate(flags) {
if (flags.name.length < 1 || flags.name.length > 20) return "--name must be 1-20 characters";
const hasDocIds = !!flags.docId?.length;
const hasCategoryIds = !!flags.categoryId?.length;
if (hasDocIds && hasCategoryIds) return "Use either --doc-id or --category-id, not both";
if (!hasDocIds && !hasCategoryIds)
return "Provide --doc-id or --category-id as the data source";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Fixed values, not exposed as flags in this version: structureType unstructured, sinkType BUILT_IN.
// Note: the public docs' example uses sinkType DEFAULT, but BUILT_IN is what works against the live API.
const body = {
name: flags.name,
structureType: "unstructured",
sinkType: "BUILT_IN",
embeddingModelName: flags.embeddingModel ?? "text-embedding-v4",
chunkSize: flags.chunkSize ?? 600,
...buildDataSourceFields(flags),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexCreateV2);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagCreateIndexV2Response>({
path: endpoint,
method: "POST",
body,
});
const pipelineId = response.data?.pipelineId;
const ingestionId = response.data?.ingestionId;
let finalStatus: string | undefined;
if (flags.wait && pipelineId && ingestionId) {
const statusResponse = await pollImportJob(ctx.client, settings, {
statusUrl: importJobStatusUrl(workspaceId, pipelineId, ingestionId).toString(),
intervalSec: flags.pollInterval ?? 5,
});
finalStatus = importJobStatus(statusResponse);
// Job finished but some documents failed to parse → non-zero exit, server message
// passed through verbatim (the knowledge base was created; its id goes in the hint)
if (failedImportDocs(statusResponse).length > 0) {
throw new BailianError(
importJobFailureMessage(statusResponse, "Initial import reported document failures."),
ExitCode.GENERAL,
`Knowledge base created: ${pipelineId}`,
{ api: { requestId: statusResponse.request_id } },
);
}
}
if (settings.quiet) {
emitBare(pipelineId ?? "");
return;
}
if (format === "text") {
emitBare(`index_id: ${pipelineId ?? "-"}`);
if (ingestionId) emitBare(`ingestion_id: ${ingestionId}`);
if (finalStatus) emitBare(`status: ${finalStatus}`);
emitBare("Next: check the import job status, then search against this knowledge base.");
return;
}
emitResult(finalStatus ? { ...response, final_status: finalStatus } : response, format);
},
});
@@ -0,0 +1,98 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagIndexFilesResponse,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
import { fetchIndexDetail } from "./kb-info.ts";
const KB_DELETE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
yes: { type: "switch", description: "Skip the confirmation prompt" },
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup: name + document count; any lookup failure degrades to id-only (never blocks deletion) */
async function buildDeleteSummary(
ctx: { client: Parameters<typeof fetchIndexDetail>[0] },
workspaceId: string,
indexId: string,
): Promise<string> {
let namePart = "";
let docCountPart = "";
try {
const detail = await fetchIndexDetail(ctx.client, workspaceId, indexId);
namePart = ` name: ${detail.name}`;
} catch {
// Degrade gracefully: a missing name does not block confirmation
}
try {
const filesUrl = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
filesUrl.searchParams.set("index_id", indexId);
filesUrl.searchParams.set("page_num", "1");
filesUrl.searchParams.set("page_size", "1");
const files = await ctx.client.requestJson<RagIndexFilesResponse>({
path: filesUrl.toString(),
method: "GET",
});
const totalCount = files.data?.total_count;
if (typeof totalCount === "number") docCountPart = ` documents: ${totalCount}`;
} catch {
// Same graceful degradation as above
}
return `Delete knowledge base ${indexId}${namePart}${docCountPart}\nThis permanently removes the knowledge base with all documents and chunks. It cannot be undone.`;
}
export default defineCommand({
description: "Delete a knowledge base with all its documents and chunks",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: KB_DELETE_FLAGS,
notes: [
"Irreversible — the knowledge base and all indexed content are permanently removed.",
"Files in the data center are not affected; only the knowledge base index is deleted.",
],
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx", "--index-id idx-xxx --yes"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// This endpoint is back to snake_case: body { index_id }
const body = { index_id: flags.indexId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDelete);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const summary = flags.yes
? "" // --yes bypasses the prompt, so skip the summary lookups
: await buildDeleteSummary(ctx, workspaceId, flags.indexId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`deleted: ${flags.indexId}`);
return;
}
emitResult(response, format);
},
});
@@ -0,0 +1,123 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type Client,
type FlagsDef,
type RagIndexListResponse,
type RagIndexRow,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const KB_INFO_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
function indexDetailUrl(workspaceId: string, indexId: string): string {
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexList));
url.searchParams.set("pipeline_id", indexId);
url.searchParams.set("page_number", "1");
url.searchParams.set("page_size", "1");
return url.toString();
}
/**
* The index/list API now supports pipeline_id filtering, so a single
* request suffices instead of paginating. Reused by kb delete for its
* confirmation summary.
*/
export async function fetchIndexDetail(
client: Client,
workspaceId: string,
indexId: string,
): Promise<RagIndexRow> {
const response = await client.requestJson<RagIndexListResponse>({
path: indexDetailUrl(workspaceId, indexId),
method: "GET",
});
const row = response.data?.rows?.[0];
if (!row) {
throw new BailianError(
`Knowledge base not found: ${indexId}`,
ExitCode.GENERAL,
"Check the id — list knowledge bases in this workspace to verify it.",
);
}
return row;
}
function formatField(label: string, value: string | number | boolean | null | undefined): string {
return ` ${label}: ${value ?? "-"}`;
}
export default defineCommand({
description: "Show knowledge base configuration details",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: KB_INFO_FLAGS,
notes: ["Indexing settings are immutable; changing them requires recreating the knowledge base."],
exampleArgs: ["--index-id idx-xxx --workspace-id ws-xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
{
endpoint: indexDetailUrl(workspaceId, flags.indexId),
request: null,
},
format,
);
return;
}
const row = await fetchIndexDetail(ctx.client, workspaceId, flags.indexId);
if (settings.quiet) {
emitBare(row.id ?? "");
return;
}
if (format !== "text") {
emitResult(row, format);
return;
}
// Grouped by diagnostic concern; the immutable annotation on Indexing tells
// users which settings require recreating the knowledge base
emitBare("Basic:");
emitBare(formatField("id", row.id));
emitBare(formatField("name", row.name));
emitBare(formatField("description", row.description));
emitBare(formatField("dataType", row.dataType));
emitBare("Indexing: [immutable — recreate required to change]");
emitBare(formatField("embeddingModelName", row.embeddingModelName));
emitBare(formatField("embeddingDimension", row.embeddingDimension));
emitBare(formatField("chunkSize", row.chunkSize));
emitBare(formatField("overlapSize", row.overlapSize));
emitBare(formatField("chunkMode", row.chunkMode));
emitBare(formatField("separator", row.separator));
emitBare("Retrieval:");
emitBare(formatField("rerankModelName", row.rerankModelName));
emitBare(formatField("rerankMinScore", row.rerankMinScore));
emitBare(formatField("rerankTopN", row.rerankTopN));
emitBare(formatField("rerankMode", row.rerankMode));
emitBare(formatField("enableRewrite", row.enableRewrite));
emitBare(formatField("denseSimilarityTopK", row.denseSimilarityTopK));
emitBare(formatField("sparseSimilarityTopK", row.sparseSimilarityTopK));
emitBare("Data:");
emitBare(formatField("sourceType", row.sourceType));
emitBare(formatField("connectorId", row.connectorId));
},
});
@@ -0,0 +1,92 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagIndexListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts";
const KB_LIST_FLAGS = {
name: {
type: "string",
valueHint: "<text>",
description: "Filter by knowledge base name (fuzzy match, 1-20 chars)",
},
...PAGE_FLAGS,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "List knowledge bases in the workspace",
auth: "apiKey",
usageArgs: "[flags]",
flags: KB_LIST_FLAGS,
notes: [
"Auth: uses DashScope API Key (Bearer token).",
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.",
"Use the returned id as --index-id in knowledge base / document management commands.",
],
exampleArgs: ["--workspace-id ws-xxx", "--name demo --page-number 2 --page-size 50"],
validate(flags) {
if (flags.name !== undefined && (flags.name.length < 1 || flags.name.length > 20)) {
return "--name must be 1-20 characters";
}
if (flags.pageSize !== undefined && (flags.pageSize < 1 || flags.pageSize > 100)) {
return "--page-size must be between 1 and 100";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Pagination and filter parameters must go in the query string — the server ignores them in the body
const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexList));
if (flags.name) url.searchParams.set("pipeline_name", flags.name);
url.searchParams.set("page_number", String(flags.pageNumber ?? 1));
url.searchParams.set("page_size", String(flags.pageSize ?? 20));
const endpoint = url.toString();
if (settings.dryRun) {
emitResult({ endpoint, request: null }, format);
return;
}
const response = await ctx.client.requestJson<RagIndexListResponse>({
path: endpoint,
method: "GET",
});
const rows = response.data?.rows ?? [];
if (settings.quiet) {
for (const row of rows) emitBare(row.id);
return;
}
if (format === "text") {
if (rows.length === 0) {
emitBare("No knowledge bases found.");
} else {
for (const row of rows) {
emitBare(
truncateLine(
[
row.id,
row.name,
row.embeddingModelName ?? "-",
row.chunkSize ?? "-",
row.description ?? "",
].join(" "),
),
);
}
}
emitBare(`total: ${response.data?.total ?? rows.length}`);
} else {
emitResult(response, format);
}
},
});
@@ -0,0 +1,125 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type RagMonitorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const KB_STATS_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
start: {
type: "string",
valueHint: "<time>",
description:
"Range start: Unix seconds or ISO date, must be in the past (default: 24 hours ago)",
},
end: {
type: "string",
valueHint: "<time>",
description: "Range end: Unix seconds or ISO date, must be in the past (default: now)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Normalize time input to a second-precision string (the API requires seconds as a string). Accepts Unix seconds or an ISO date. */
export function toEpochSecondsString(input: string): string {
if (/^\d+$/.test(input)) {
// Digits-only input is treated as Unix seconds; 13-digit millisecond timestamps are reduced to seconds
return input.length >= 13 ? String(Math.floor(Number(input) / 1000)) : input;
}
// Non-numeric input must be a full ISO date (YYYY-MM-DD, optionally with a time
// part). Date.parse alone is too lenient — V8 silently reads truncated input
// like "2026-" as Jan 1st, which would query a misleading range.
const parsedMs = /^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(input) ? Date.parse(input) : Number.NaN;
if (Number.isNaN(parsedMs)) {
throw new BailianError(
`Invalid time value: ${input}`,
ExitCode.USAGE,
"Pass Unix seconds (e.g. 1780900000) or an ISO date (e.g. 2026-07-30T00:00:00Z).",
);
}
return String(Math.floor(parsedMs / 1000));
}
export default defineCommand({
description: "Show knowledge base storage and QPS monitoring data",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: KB_STATS_FLAGS,
notes: [
"Defaults to the last 24 hours when --start/--end are omitted.",
"Timestamps are normalized to epoch seconds as required by the server.",
"Future timestamps are rejected for --start and clamped to now for --end, since the monitor API only returns past data.",
],
exampleArgs: [
"--index-id idx-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --start 2026-07-30 --end 2026-07-31",
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const nowSeconds = Math.floor(Date.now() / 1000);
const startTimestamp = flags.start
? toEpochSecondsString(flags.start)
: String(nowSeconds - 24 * 3600);
let endTimestamp = flags.end ? toEpochSecondsString(flags.end) : String(nowSeconds);
// The monitor API rejects future timestamps with a misleading
// "missing or invalid" error — validate here with a clear message.
if (Number(startTimestamp) > nowSeconds) {
throw new BailianError(
`Start time is in the future; the monitor API only accepts past or current timestamps.`,
ExitCode.USAGE,
"Use a start date/time at or before now, or omit --start to default to 24 hours ago.",
);
}
const clampedEnd = Number(endTimestamp) > nowSeconds;
if (clampedEnd) {
endTimestamp = String(nowSeconds);
}
const body = { indexId: flags.indexId, startTimestamp, endTimestamp };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexMonitor);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagMonitorResponse>({
path: endpoint,
method: "POST",
body,
});
if (format !== "text") {
emitResult(response, format);
return;
}
if (clampedEnd) {
emitBare("note: end time was in the future, clamped to now.");
}
// Shape verified against the live API: the monitor fields are objects, not arrays
const storage = response.data?.storageMonitorData;
const qps = response.data?.qpsMonitorData;
emitBare(`plan: ${response.data?.pipelineCommercialType ?? "-"}`);
emitBare(
`storage: ${storage?.indexStorageUsage ?? "-"} / ${storage?.indexStorageLimit ?? "-"}`,
);
emitBare(`peak qps: ${qps?.peakQps ?? "-"}`);
emitBare(`qps windows: ${qps?.monitorData?.length ?? 0} data point(s)`);
},
});
@@ -0,0 +1,100 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const KB_UPDATE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base ID",
required: true,
},
name: {
type: "string",
valueHint: "<text>",
description: "New knowledge base name (1-20 chars)",
},
description: {
type: "string",
valueHint: "<text>",
description: "New knowledge base description",
},
rerankMinScore: {
type: "number",
valueHint: "<score>",
description: "Rerank minimum score threshold, range 0-1 (chunks below are filtered)",
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Update knowledge base name, description or rerank threshold",
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: KB_UPDATE_FLAGS,
notes: [
"Indexing settings (embedding model, chunk size, etc.) are immutable — recreate the knowledge base to change them.",
],
exampleArgs: [
"--index-id idx-xxx --description 'product docs v2' --workspace-id ws-xxx",
"--index-id idx-xxx --rerank-min-score 0.3",
],
validate(flags) {
if (
flags.name === undefined &&
flags.description === undefined &&
flags.rerankMinScore === undefined
) {
return "Nothing to update — pass --name, --description or --rerank-min-score";
}
if (flags.name !== undefined && (flags.name.length < 1 || flags.name.length > 20)) {
return "--name must be 1-20 characters";
}
if (
flags.rerankMinScore !== undefined &&
(flags.rerankMinScore < 0 || flags.rerankMinScore > 1)
) {
return "--rerank-min-score must be between 0 and 1";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
// Gotcha: this endpoint names the knowledge base ID parameter `id` (not index_id/indexId)
const body = {
id: flags.indexId,
...(flags.name !== undefined ? { name: flags.name } : {}),
...(flags.description !== undefined ? { description: flags.description } : {}),
...(flags.rerankMinScore !== undefined ? { rerankMinScore: flags.rerankMinScore } : {}),
};
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexUpdate);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
body,
});
if (settings.quiet) return;
if (format === "text") {
emitBare(`updated: ${flags.indexId}`);
return;
}
emitResult(response, format);
},
});
@@ -2,13 +2,12 @@ import {
defineCommand,
knowledgeSearchEndpoint,
detectOutputFormat,
BailianError,
ExitCode,
type FlagsDef,
type KnowledgeSearchRequest,
type KnowledgeSearchResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SEARCH_FLAGS = {
query: {
@@ -23,23 +22,21 @@ const SEARCH_FLAGS = {
description: "Retrieval service ID (find in console knowledge retrieval page)",
required: true,
},
// 知识库走 workspace 专属域名,--workspace-id 属命令自有 flag(console 凭证域不适用)。
workspaceId: {
// Knowledge APIs use a workspace-specific host, so --workspace-id is a per-command
// flag here (the console credential scope does not apply).
...WORKSPACE_FLAG,
// Named to avoid the runtime-reserved global --version flag
agentVersion: {
type: "string",
valueHint: "<id>",
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
valueHint: "<version>",
description:
"Service version to call: beta (draft for debugging) or a published number; default is the latest published version",
},
image: {
type: "array",
valueHint: "<url>",
description: "Image URL for multimodal retrieval (repeatable)",
},
queryHistory: {
type: "string",
valueHint: "<json>",
description:
'User conversation history JSON for context understanding and query rewriting. Format: \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]\'',
},
} satisfies FlagsDef;
export default defineCommand({
@@ -51,24 +48,16 @@ export default defineCommand({
"Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.",
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
"`--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.",
"`--agent-version beta` calls the draft config for debugging before it is deployed.",
],
exampleArgs: [
'--query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
'--api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg',
'--query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]\'',
],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = flags.workspaceId || settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
);
}
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
@@ -77,23 +66,14 @@ export default defineCommand({
agent_id: flags.agentId,
};
if (flags.image && flags.image.length > 0) {
body.images = flags.image;
// Omitted flag → field not sent (default behavior unchanged: latest published
// version); the value is not validated — the set of versions is server-side state
if (flags.agentVersion) {
body.agent_version = flags.agentVersion;
}
// Parse query_history JSON for multi-turn context
if (flags.queryHistory) {
try {
body.query_history = JSON.parse(flags.queryHistory) as Array<{
role: "user" | "assistant";
content: string;
}>;
} catch {
throw new BailianError(
'--query-history must be valid JSON. Example: --query-history \'[{"role":"user","content":"What is RAG"}]\'',
ExitCode.USAGE,
);
}
if (flags.image && flags.image.length > 0) {
body.images = flags.image;
}
const url = knowledgeSearchEndpoint(workspaceId);
@@ -0,0 +1,67 @@
import {
defineCommand,
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagAgentMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SERVICE_COPY_FLAGS = {
agentId: {
type: "string",
valueHint: "<id>",
description: "Source service (agent) ID to copy",
required: true,
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: "Copy a service into a new draft (name gets a copy_ prefix)",
auth: "apiKey",
usageArgs: "--agent-id <id> [flags]",
flags: SERVICE_COPY_FLAGS,
notes: [
"The copy starts as a beta draft; test it with --agent-version beta, then deploy to publish.",
"Requires the knowledge-base create permission in the workspace.",
],
exampleArgs: ["--agent-id aid-xxx --workspace-id ws-xxx"],
async run(ctx) {
const { settings, flags } = ctx;
const workspaceId = resolveWorkspaceId(ctx);
const format = detectOutputFormat(settings.output);
const body = { agent_id: flags.agentId };
const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentCopy);
if (settings.dryRun) {
emitResult({ endpoint, request: body }, format);
return;
}
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
path: endpoint,
method: "POST",
body,
});
const newAgentId = agentMutationField(response, "agent_id");
if (settings.quiet) {
emitBare(newAgentId ?? "");
return;
}
if (format === "text") {
emitBare(
`new agent_id: ${newAgentId ?? "-"} (name: ${agentMutationField(response, "agent_name") ?? "-"}, status: ${agentMutationField(response, "agent_status") ?? "draft"})`,
);
emitBare(
"Test the draft with --agent-version beta on search/chat, then deploy it to publish.",
);
return;
}
emitResult(response, format);
},
});

Some files were not shown because too many files have changed in this diff Show More