Compare commits

..

83 Commits

Author SHA1 Message Date
clark-fc e76ebee681 Merge pull request #172 from modelstudioai/feat/iteration1-w1-foundation
Feat/iteration1 w1 foundation
2026-08-22 13:23:54 +08:00
zeyu.fz 78a1c547d3 fix(cli): 修复知识库创建描述必填及配置更新误报警告
- `knowledge create` 命令新增 `--description` 参数,设置为必填并且在本地校验长度限制
- 修正 `knowledge service update` 命令更新配置时,避免对服务返回的已知字段误报未知字段警告
- 更新命令帮助文案,明确描述类参数的具体用途
- `knowledge retrieve --rerank-model` 参数帮助说明补充前置条件,提示需预先配置重排序模型
- 新增 `bailian-web-search` 路由技能,支持智能选择搜索入口
- 添加完整的 Knowledge Studio CLI 命令手册,包含详细示例及覆盖范围
2026-08-22 13:09:53 +08:00
zeyu.fz a95ad7242b docs(cli): 添加完整的 KSCLI 命令手册文档
- 新增知识库管理(kb)命令详解,包含创建、查询、更新、删除和状态监控
- 补充文档(doc)管理命令,包括文件上传、导入状态查询、标签管理等
- 增加数据中心文件(file)管理说明,覆盖文件列表、详情、删除操作
- 完善集合与分类管理(collection/category)命令手册,支持创建、查询、删除等功能
- 详细描述 chunk 管理命令,包括添加、更新、查询和删除操作
- 统一说明通用约定,涵盖鉴权、全局参数、输出格式、确认机制及干跑模式
- 提供丰富参数说明、输出格式及使用示例,提升 CLI 使用体验和易用性
2026-08-22 12:19:55 +08:00
zeyu.fz b830a14e11 docs(knowledge): 扩展知识库描述长度限制到 500 字符
- 修改命令行参数文档,将 --description 长度限制由 200 字符增加到 500 字符
- 更新代码校验逻辑,支持描述长度最大 500 字符
- 调整相关提示信息,反映新的长度限制
- 修改测试用例,支持 501 字符的描述参数触发用法错误
- 更新 CLI 参考文档中描述字段的长度说明
2026-08-22 11:52:39 +08:00
zeyu.fz a6291e00e1 docs(knowledge): 强制添加知识库描述参数
- 升级知识库创建命令,`--description` 参数变为必填,描述知识库内容和用途
- 更新所有相关文档示例,统一加入 `--description` 参数和示例文本
- CLI 校验增强,缺失或超长的描述参数本地报错,避免服务端拒绝
- 优化服务创建命令,推荐填写描述以帮助 agent 选择合适服务
- 多个测试用例添加对描述参数的验证和断言
- 知识库和集合列表中描述信息作为区分同类项目的辅助信息显式展示
- 其他细节调整包括命令帮助及参数说明内容的更新
2026-08-22 11:21:22 +08:00
Gong Shiqi 0a63115aed Merge pull request #170 from modelstudioai/feat/add-websearch-skill
Add bailian-web-search routing skill
2026-08-20 19:07:52 +08:00
clh02467605 abad3a6643 feat(skills): add bailian-web-search routing skill 2026-08-19 17:12:16 +08:00
Gong Shiqi d69f73f1bc Merge pull request #169 from modelstudioai/release/1.17.0
chore(release): prepare 1.17.0
2026-08-18 19:10:54 +08:00
若麒 28fc1b6056 chore(release): prepare 1.17.0 2026-08-18 18:57:33 +08:00
Gong Shiqi a8774cc143 Merge pull request #168 from modelstudioai/feat/bilingual-quick-start-examples
feat(i18n): support bilingual CLI help and quick start
2026-08-18 17:07:45 +08:00
若麒 fea86dc5aa Merge branch 'main' into feat/bilingual-quick-start-examples 2026-08-18 16:28:23 +08:00
若麒 06210a4e33 feat(i18n): localize newly added CLI help content 2026-08-18 15:58:39 +08:00
若麒 e31addf0d6 Merge branch 'main' into feat/bilingual-quick-start-examples 2026-08-18 14:33:34 +08:00
gujieye 6e3fdeafc0 Merge pull request #167 from modelstudioai/feat/usage_auto_stop
fix(usage): allow disabling auto-stop regardless of quota and fix Aut…
2026-08-17 20:54:33 +08:00
Gong Shiqi 0d28a35e26 Merge pull request #158 from modelstudioai/feat/cma-deployment
feat(managed-agent): add OpenAgentPack deployment support
2026-08-17 20:43:22 +08:00
故璃 af95a9ec67 fix(usage): allow disabling auto-stop regardless of quota and fix Auto-Stop column display
- Remove client-side check that blocked 'freetier --off' when quota
  remains; align with web behavior (switch is always operable)
- Prioritize stopMap ON/OFF over quotaStatus UNKNOWN in rendering
- Query auto-stop status only for filtered models to avoid server-side
  batch limit error (TRAIN_INTERNAL_ERROR_EXP with 494 models)
2026-08-17 20:25:56 +08:00
chenanran555 7aa6aab7d7 chore(release): prepare 1.17.0 2026-08-17 17:28:01 +08:00
chenanran555 6811ec619d Merge remote-tracking branch 'origin/main' into feat/cma-deployment
# Conflicts:
#	packages/cli/package.json
#	packages/commands/package.json
#	packages/core/package.json
#	packages/kscli/package.json
#	packages/runtime/package.json
#	skills/bailian-cli/SKILL.md
#	skills/bailian-finetune/SKILL.md
#	skills/bailian-gen/SKILL.md
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-protocol/SKILL.md
2026-08-17 17:18:53 +08:00
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
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
故璃 7461189007 Merge branch 'main' into feat/cli-skill-sync 2026-08-15 10:22:09 +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
chenanran555 8b7956d547 Merge remote-tracking branch 'origin/main' into feat/cma-deployment
# Conflicts:
#	CHANGELOG.md
#	CHANGELOG.zh.md
#	packages/cli/package.json
#	packages/commands/package.json
#	packages/core/package.json
#	packages/kscli/package.json
#	packages/runtime/package.json
#	skills/bailian-cli/SKILL.md
#	skills/bailian-finetune/SKILL.md
#	skills/bailian-gen/SKILL.md
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-protocol/SKILL.md
2026-08-14 19:15:13 +08:00
chenanran555 7e21573793 feat(managed-agent): add OpenAgentPack deployment support 2026-08-14 19:01:47 +08:00
若麒 f6cf2b999a feat(config): add bilingual UI with in-place language switching 2026-08-14 18:55:09 +08:00
若麒 2fe50f59a4 feat(commands): localize command help examples 2026-08-14 17:15:46 +08:00
clh02467605 418dcffc53 docs(install,skills): prefer bl skill init and clarify post-install guidance 2026-08-14 16:57:24 +08:00
若麒 9ddb8dab53 refactor(runtime): remove unused i18next dependency 2026-08-14 15:56:29 +08:00
若麒 23f1ab7fd4 feat(commands): localize remaining command help 2026-08-14 15:54:38 +08:00
zeyu.fz d5c4bd3572 docs(knowledge): 优化知识库文档内容及CLI说明
- 修改表格/图片知识库必须提供`--doc-id`的描述,更准确表达要求
- 调整CLI命令文档中过期或不准确信息,说明集合删除暂不支持
- 更新chunk添加命令中`--doc-id`的说明,强调对所有知识库类型均必需
- 精简chunk删除命令备注,明确批量操作自动分批处理
- 优化文档删除命令描述,强调删除异步传播及输出行为
- 修正文档状态命令中错误提示用词,更清晰表达
- 文件列表命令修改说明,明确默认分类ID不解析
- 知识库信息命令删除过时备注,突出索引设置不可变
- 服务删除命令简洁描述幂等性和权限要求
- 服务列表命令调整对场景参数的描述,明确必传要求
2026-08-14 13:40:08 +08:00
若麒 f67ca55ec6 feat(commands): localize multimodal command help 2026-08-14 11:42:16 +08:00
若麒 6b2f49de71 feat(commands): localize help for core commands 2026-08-14 11:28:25 +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
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
若麒 29ce8990b9 fix(runtime): accept localized Command Pack descriptions 2026-08-13 17:54:35 +08:00
若麒 a770cbe787 feat(cli): adapt Quick Start to the configured language 2026-08-13 17:54:10 +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
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
若麒 6b685964f3 feat(runtime): support colocated localized CLI help text 2026-08-13 16:12:09 +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
若麒 ea7b0f016f feat(cli): add bilingual quick-start examples 2026-08-11 15:49:05 +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
故璃 5007b9b574 feat: change output to json 2026-08-07 16:35:47 +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
故璃 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
故璃 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
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
303 changed files with 28879 additions and 2474 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 }}"
+2 -1
View File
@@ -14,6 +14,7 @@ git add \
skills/bailian-finetune/SKILL.md \
skills/bailian-finetune/reference \
skills/bailian-managed-agent/SKILL.md \
skills/bailian-managed-agent/reference
skills/bailian-managed-agent/reference \
skills/bailian-web-search/SKILL.md
vp staged
+5 -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-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts``packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
Skill / 命令手册随 `skills/bailian-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts``packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
约定:
@@ -122,6 +122,10 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的
例外: 仅当作用域极小(≤3 行)且语义从上下文完全明确时,可使用 `k`/`v`(Object.entries 的 key/value)。
### 6. 用户可见 CLI 文案必须支持中英文
新增或修改用户可见的 CLI 文案时必须同时提供 `en-US` / `zh-CN`;runtime 公共文案遵循同一规则,服务端错误仍按第 3 节原样透传。命令文案的具体检查项见 [command-add-remove.md](docs/agents/command-add-remove.md)。
## 完成改动后的快速验证
```sh
+51
View File
@@ -6,6 +6,57 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.17.1] - 2026-08-22
### Fixed
- **`knowledge create` now requires `--description`** — aligns with the server's required-description validation: the new `--description` flag is mandatory and its 1-500 character limit is checked locally before the request goes out. `bl knowledge create` / `kscli kb create` calls need to pass it.
- **`knowledge service update` warned about config fields the server itself returned** — updating the draft config through scalar flags such as `--policy` reads the full draft and merges before writing back; the draft's `user_system_prompt`, `anti_leak_prompt`, `refusal_prompt`, `credibility_prompt`, `session_file_parse_mode`, and `enable_thinking` / `enable_temperature` / `enable_credibility` / `enable_max_completion_tokens` were not recognized by the CLI, so every update printed a run of `unknown agent_config field passed through` warnings. The config itself was always written correctly; the spurious warnings are gone.
### Added
- **`bailian-web-search` routing skill** — `bl skill init` now also installs a dedicated web-search routing skill, so agents pick the right search entry point instead of guessing.
- **Knowledge Studio CLI command manual** — full `kscli` reference docs covering knowledge bases, documents, chunks, collections/categories, files, retrieval/Q&A services, and search/chat, with runnable examples for every command.
### Changed
- **Description flags explain what to write** — help text for the collection and service `--description` flags now states what the field is for (telling similar items apart in lists; for services, agents read it to pick the right one) rather than just repeating "required".
- **`knowledge retrieve --rerank-model` documents its precondition** — help now states that the target knowledge base must already have a rerank model configured, otherwise every value is rejected.
## [1.17.0] - 2026-08-18
### Added
- **Native Bailian Managed Agent Deployments** — `deployments` declared in `agents.yaml` now materialize as native AgentStudio resources, with server-side cron schedules, local file resource uploads, archival through `destroy`, and migration of legacy emulated state on the next `apply`.
- **Bilingual CLI experience** — Set `language` to `en-US` or `zh-CN` through `bl config set` or Config UI to switch CLI Help, Quick Start, command examples, and Config UI between English and Chinese. The selected language follows the active config.
### Fixed
- **Free Tier Auto-Stop controls** — `bl usage freetier --off` can now disable Auto-Stop even when free quota remains; status rendering reflects the actual switch state, and filtered model queries avoid server-side batch-limit failures.
## [1.16.0] - 2026-08-17
> Full knowledge-base lifecycle management arrives in the CLI: create and configure knowledge bases, upload documents, tune chunks, and deploy retrieval/Q&A services — all from `bl knowledge` and `kscli`.
### Added
- **Knowledge base management** — `bl knowledge create` / `list` / `info` / `update` / `delete` manage knowledge bases end to end; `bl knowledge stats` reports document counts and usage over a past time range.
- **Document management** — `bl knowledge doc upload` uploads local files or whole directories (recursive scan, skips unsupported formats and tool directories like `node_modules`); `doc list` / `status` / `tag` / `delete` cover the rest of the document lifecycle, and `doc import-oss` imports documents from OSS.
- **Retrieval / Q&A service management** — `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` manage retrieval and Q&A service configurations, including deploying a draft to a published version.
- **Chunk management** — `bl knowledge chunk add` / `list` / `update` / `delete` inspect and fine-tune document chunks.
- **Data-center management** — `bl knowledge category list` / `add` / `delete`, `bl knowledge file list` / `get` / `delete`, and `bl knowledge collection create` / `get` manage categories, raw files, and data collections.
- **Service version selection for retrieval and chat** — `bl knowledge search` and `bl knowledge chat` accept `--agent-version` to call the beta (draft) config for debugging or a specific published version.
- **`kscli` parity** — all new knowledge commands are also available in Knowledge Studio CLI under shorter paths, e.g. `kscli kb list`, `kscli doc upload`, `kscli service deploy`.
### Removed
- **`bl knowledge search --query-history` removed** — the parameter never took effect; use `bl knowledge chat` with `--message` history for multi-turn scenarios.
### Internal
- Requests now carry a static OpenAPI source identification header for backend channel attribution.
- Added knowledge-base E2E suites, including five user-journey scenarios covering cold start, content ops, chunk tuning, service tuning, and the data plane.
## [1.15.1] - 2026-08-17
### Added
+51
View File
@@ -6,6 +6,57 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.17.1] - 2026-08-22
### 修复
- **`knowledge create``--description` 更新为必填** —— 对齐服务端对知识库描述的必填校验:新增 `--description` 参数并设为必填,在发出请求前于本地校验 1500 个字符的长度限制。`bl knowledge create` / `kscli kb create` 调用需带上该参数。
- **`knowledge service update` 对服务端自己返回的配置字段误报警告** —— 通过 `--policy` 等标量参数更新草稿配置时CLI 会先读取完整草稿再合并回写;草稿中的 `user_system_prompt``anti_leak_prompt``refusal_prompt``credibility_prompt``session_file_parse_mode` 以及 `enable_thinking` / `enable_temperature` / `enable_credibility` / `enable_max_completion_tokens` 此前不被 CLI 识别,导致每次更新都刷出一串 `unknown agent_config field passed through` 警告。配置本身始终被正确写入,现在不再误报。
### 新增
- **`bailian-web-search` 路由技能** —— `bl skill init` 现在会一并安装专门的联网搜索路由技能,让 agent 直接选中正确的搜索入口,不再靠猜。
- **Knowledge Studio CLI 命令手册** —— 完整的 `kscli` 参考文档,覆盖知识库、文档、切片、集合/类目、文件、检索/问答服务以及 search/chat每条命令均附可运行示例。
### 变更
- **描述类参数说明写清该填什么** —— 数据集合与服务的 `--description` 帮助文案现在会说明该字段的用途(在列表中区分同类项;服务描述供 agent 判断该调用哪个服务),不再只是重复「必填」。
- **`knowledge retrieve --rerank-model` 补充前置条件说明** —— 帮助文案现在会说明目标知识库必须已配置重排序模型,否则任何取值都会被拒绝。
## [1.17.0] - 2026-08-18
### 新增
- **百炼原生 Managed Agent Deployment** —— `agents.yaml` 中声明的 `deployments` 现在会创建原生 AgentStudio 资源,支持服务端 Cron 调度、本地文件资源上传、通过 `destroy` 归档,以及在下次 `apply` 时迁移旧版模拟 Deployment state。
- **CLI 中英文体验** —— 可通过 `bl config set` 或 Config UI 将 `language` 设置为 `en-US``zh-CN`,在英文和中文的 CLI Help、Quick Start、命令示例及 Config UI 之间切换;所选语言跟随当前激活的配置。
### 修复
- **Free Tier Auto-Stop 控制** —— `bl usage freetier --off` 现在可在免费额度尚有剩余时关闭 Auto-Stop状态展示会反映实际开关状态并仅查询筛选后的模型避免触发服务端批量查询上限。
## [1.16.0] - 2026-08-17
> CLI 迎来知识库全生命周期管理:从创建配置知识库、上传文档、调优切片,到部署检索/问答服务,均可通过 `bl knowledge` 与 `kscli` 完成。
### 新增
- **知识库管理** —— `bl knowledge create` / `list` / `info` / `update` / `delete` 覆盖知识库的完整生命周期;`bl knowledge stats` 查询指定过去时间段内的文档数量与用量统计。
- **文档管理** —— `bl knowledge doc upload` 支持上传本地文件或整个目录(递归扫描,自动跳过不支持的格式及 `node_modules` 等工具目录);`doc list` / `status` / `tag` / `delete` 覆盖文档生命周期其余环节,`doc import-oss` 支持从 OSS 导入文档。
- **检索 / 问答服务管理** —— `bl knowledge service list` / `get` / `create` / `update` / `deploy` / `delete` / `copy` 管理检索与问答服务配置,支持将草稿部署为正式版本。
- **切片管理** —— `bl knowledge chunk add` / `list` / `update` / `delete` 查看并精调文档切片。
- **数据中心管理** —— `bl knowledge category list` / `add` / `delete``bl knowledge file list` / `get` / `delete``bl knowledge collection create` / `get` 管理类目、原始文件与数据集。
- **检索与问答支持指定服务版本** —— `bl knowledge search``bl knowledge chat` 新增 `--agent-version`,可调用 beta草稿配置进行调试或指定已发布的版本号。
- **`kscli` 同步支持** —— 全部新知识库命令在 Knowledge Studio CLI 中以更短路径提供,如 `kscli kb list``kscli doc upload``kscli service deploy`
### 移除
- **移除 `bl knowledge search --query-history`** —— 该参数此前并未实际生效;多轮场景请改用 `bl knowledge chat` 并通过 `--message` 传入对话历史。
### 内部
- 请求现在携带静态的 OpenAPI 来源标识请求头,用于后端渠道归因。
- 新增知识库 E2E 测试套件,含冷启动、内容运营、切片调优、服务调优、数据面五条用户旅程场景。
## [1.15.1] - 2026-08-17
### 新增
+3
View File
@@ -166,6 +166,9 @@ bl config list
# Switch config profile
bl config use --name token-plan
# Switch the CLI interface to Chinese
bl config set --key language --value zh-CN
```
Config file location: `~/.bailian/config.json`
+3
View File
@@ -165,6 +165,9 @@ bl config list
# 切换配置档
bl config use --name token-plan
# 将 CLI 界面切换为中文
bl config set --key language --value zh-CN
```
配置文件位置:`~/.bailian/config.json`
+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`
+1
View File
@@ -74,6 +74,7 @@ packages/commands/src/index.ts
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
- [ ] 用户可见 Help 文案在命令文件中就近提供 `en-US` / `zh-CN`:命令 `description`、flag `description``notes` 和包含自然语言的 `exampleArgs`;纯命令语法示例可保留为字符串,服务端错误不翻译
- [ ] `packages/commands/src/index.ts`:新增或移除对应 export
- [ ] 如果命令调用 Console Gateway,设置 `auth: "console"`;不要重复声明 console 凭证域 flags
- [ ] 如果命令不需要网络或自己管理配置/登录,设置 `auth: "none"`;不要绕过 runtime auth stage
+5 -5
View File
@@ -23,11 +23,11 @@
bailian-protocol ← 共享协议consent / 鉴权 / 版本 / 错误上报)
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
┌───────┴────────┬────────────────┬──────────────────┐
bailian-gen bailian-finetune bailian-managed-agent
(领域路由表) (领域工作流) IaC 安全闸)
│ │ │
└────────────────┼──────────────────┘
┌───────┴────────┬────────────────┬──────────────────┬───────────────────
bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
(领域路由表) (领域工作流) IaC 安全闸) (搜索路由+兜底)
│ │ │
└────────────────┼──────────────────┴─────────────────────
▼ 软 hand-off按 skill 名)
bailian-clihub
hub 路由表:本职命令 + 领域 hand-off 行
+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)
+342
View File
@@ -0,0 +1,342 @@
# 知识库管理命令手册
知识库Knowledge Base / pipeline / index是 RAG 的核心载体,存储文档解析后的向量索引。本组命令覆盖知识库的创建、查看、更新、删除和监控。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](../knowledge-cli-guide.md#通用约定)。
---
#### `bl knowledge list`
列出工作区中的知识库。
**用法**
```bash
bl knowledge list [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------- | ------ | ---- | --------------------------------- |
| `--name <text>` | string | 否 | 按知识库名称模糊过滤1-20 字符) |
| `--page-number <n>` | number | 否 | 页码默认1 |
| `--page-size <n>` | number | 否 | 每页条数默认20最大 100 |
**参数约束**
- `--name` 长度 1-20 字符
- `--page-size` 范围 1-100
**输出**
text 模式:每行一个知识库,字段以双空格分隔,末尾显示总数。
```
idx-xxx my-kb text-embedding-v4 600 product docs
total: 1
```
quiet 模式:每行一个知识库 ID。
json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。
**注意事项**
- 返回的 `id` 字段作为后续命令的 `--index-id` 使用。
**示例**
```bash
# 列出所有知识库
bl knowledge list --workspace-id ws-xxx
# 按名称过滤,第二页
bl knowledge list --name demo --page-number 2 --page-size 50
```
---
#### `bl knowledge info`
查看知识库配置详情。
**用法**
```bash
bl knowledge info --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
**输出**
text 模式:按诊断维度分组展示。
```
Basic:
id: idx-xxx
name: my-kb
description: product docs
dataType: ...
Indexing: [immutable — recreate required to change]
embeddingModelName: text-embedding-v4
embeddingDimension: 1024
chunkSize: 600
overlapSize: ...
chunkMode: ...
separator: ...
Retrieval:
rerankModelName: ...
rerankMinScore: ...
rerankTopN: ...
rerankMode: ...
enableRewrite: ...
denseSimilarityTopK: ...
sparseSimilarityTopK: ...
Data:
sourceType: ...
connectorId: ...
```
quiet 模式:输出知识库 ID。
json 模式:返回知识库完整配置 JSON。
**注意事项**
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
**示例**
```bash
# 查看知识库详情
bl knowledge info --index-id idx-xxx --workspace-id ws-xxx
```
---
#### `bl knowledge create`
创建知识库并导入数据中心文件或分类。
**用法**
```bash
bl knowledge create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
| `--name <text>` | string | 是 | 知识库名称1-20 字符,工作区内唯一) |
| `--description <text>` | string | 是 | 知识库装了什么内容、给谁用1-500 字符) |
| `--doc-id <id>` | array | 否¹ | 数据中心文件 ID可重复`--category-id` 互斥 |
| `--category-id <id>` | array | 否¹ | 按分类导入该分类下所有文件(可重复);与 `--doc-id` 互斥 |
| `--embedding-model <name>` | string | 否 | 向量模型名称(默认:`text-embedding-v4` |
| `--chunk-size <n>` | number | 否 | 切片大小字符数默认600建议 300-800 |
| `--wait` | switch | 否 | 轮询初始导入任务直到终态 |
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数默认5 |
> ¹ `--doc-id` 和 `--category-id` 二选一,必须提供其一。
**参数约束**
- `--name` 长度 1-20 字符
- `--description` 长度 1-500 字符,缺失或超长会在本地被拦截
- `--doc-id``--category-id` 互斥,必须提供其一
**输出**
text 模式:
```
index_id: idx-xxx
ingestion_id: job-xxx
status: COMPLETED
Next: check the import job status, then search against this knowledge base.
```
quiet 模式:只输出知识库 ID。
json 模式:返回 API 原始响应,包含 `pipelineId`(知识库 ID`ingestionId`(导入任务 ID`--wait` 时追加 `final_status` 字段。
**注意事项**
- 结构/存储类型固定为默认文档知识库非结构化BUILT_IN 存储)。
- 返回知识库 ID`pipelineId`)和初始导入任务 ID`ingestionId`)。
- 使用 `doc status``--wait` 跟踪导入进度。
- 如果 `--wait` 后部分文档解析失败CLI 以非零退出码报错,知识库已创建成功的事实会在 hint 中提示。
**示例**
```bash
# 从指定文件创建知识库
bl knowledge create --name demo --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx
# 从分类导入并等待导入完成
bl knowledge create --name demo --description '产品文档' --category-id cate-xxx --wait
# 指定向量模型和切片大小
bl knowledge create --name my-kb --description '产品文档 v2' --doc-id file-a --doc-id file-b --embedding-model text-embedding-v4 --chunk-size 400 --workspace-id ws-xxx
```
---
#### `bl knowledge update`
更新知识库名称、描述或 rerank 阈值。
**用法**
```bash
bl knowledge update --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------------- | ------ | ---- | -------------------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--name <text>` | string | 否 | 新名称1-20 字符) |
| `--description <text>` | string | 否 | 新描述 |
| `--rerank-min-score <score>` | number | 否 | rerank 最低分数阈值,范围 0-1低于此分的 chunk 被过滤) |
**参数约束**
- 至少提供 `--name``--description``--rerank-min-score` 之一,否则报错 "Nothing to update"
- `--name` 长度 1-20 字符
- `--rerank-min-score` 范围 0-1
**输出**
text 模式:
```
updated: idx-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
**示例**
```bash
# 更新描述
bl knowledge update --index-id idx-xxx --description "product docs v2" --workspace-id ws-xxx
# 调整 rerank 阈值
bl knowledge update --index-id idx-xxx --rerank-min-score 0.3
```
---
#### `bl knowledge delete`
删除知识库及其所有文档和 chunk。
**用法**
```bash
bl knowledge delete --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: idx-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes`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 --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx --wait
# → 返回 index-id (pipelineId) 和导入任务状态
# 3. 创建检索服务search 场景)
bl knowledge service create --name my-search --scene search --index-id idx-xxx --workspace-id ws-xxx
# → 返回 agent-id
# 4. 部署服务
bl knowledge service deploy --agent-id aid-xxx --workspace-id ws-xxx --yes
# 5. 执行检索
bl knowledge search --query "什么是RAG" --agent-id aid-xxx --workspace-id ws-xxx
```
### 场景 B上传目录并导入到已有知识库
```bash
# 1. 上传整个目录到数据中心并直接导入到知识库(一步到位)
bl knowledge doc upload --file ./docs/ --index-id idx-xxx --workspace-id ws-xxx --wait
# → 文件逐个上传到 OSS → 注册到数据中心 → 创建合并导入任务 → 轮询到完成
# 2. 检查文档状态
bl knowledge doc list --index-id idx-xxx --workspace-id ws-xxx
# → 查看 doc_id 和解析状态
# 3. 如果有文档解析失败,查看导入任务详情
bl knowledge doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
```
### 场景 C创建并部署 Q&A 服务
```bash
# 1. 创建 chat 场景的检索服务
bl knowledge service create --name my-qa --scene chat --index-id idx-xxx --workspace-id ws-xxx
# → 初始状态: draft, 版本: beta
# 2. 调整配置(如修改模型、温度)
bl knowledge service update --agent-id aid-xxx --model qwen-max --temperature 0.7 --workspace-id ws-xxx
# 3. 用 beta 版本测试
bl knowledge chat --message "什么是RAG?" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
# 4. 测试通过后发布
bl knowledge service deploy --agent-id aid-xxx --version-desc "首版" --workspace-id ws-xxx --yes
```
### 场景 D知识库内容运维
```bash
# 1. 查看 chunk 列表
bl knowledge chunk list --index-id idx-xxx --workspace-id ws-xxx
# → 返回 metadata._id (chunk id) 和 metadata.doc_id (document id)
# 2. 修改 chunk 内容
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --content "修正后的内容" --workspace-id ws-xxx
# 3. 排除某个 chunk 不参与检索(不删除内容)
bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --exclude --workspace-id ws-xxx
# 4. 手动添加新 chunk
bl knowledge chunk add --index-id idx-xxx --content "新增的知识片段" --title "补充说明" --workspace-id ws-xxx
# 5. 删除 chunk批量自动分批每 10 个一组)
bl knowledge chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --yes --workspace-id ws-xxx
```
### 场景 E服务迁移/复用
```bash
# 1. 复制现有服务为新草稿
bl knowledge service copy --agent-id aid-source --workspace-id ws-xxx
# → 返回新的 agent-id名称加 copy_ 前缀
# 2. 修改新服务配置
bl knowledge service update --agent-id aid-new --name "改进版" --temperature 0.5 --workspace-id ws-xxx
# 3. 测试并发布
bl knowledge chat --message "测试" --agent-id aid-new --agent-version beta --workspace-id ws-xxx
bl knowledge service deploy --agent-id aid-new --workspace-id ws-xxx --yes
```
### 场景 F从 OSS 批量导入文件
```bash
# 1. 从已授权的 OSS bucket 批量导入文件到数据中心
bl knowledge doc import-oss \
--bucket my-bucket --region cn-beijing \
--oss-key docs/a.pdf --oss-key docs/b.docx \
--workspace-id ws-xxx
# → 返回各文件的 fileId
# 2. 创建知识库并导入这些文件
bl knowledge create --name oss-kb --description 'OSS 导入文档' --doc-id file-a --doc-id file-b --workspace-id ws-xxx --wait
# 3. 检索
bl knowledge search --query "相关内容" --agent-id aid-xxx --workspace-id ws-xxx
```
---
## 命令手册
以下按功能域分组,覆盖全部 34 个子命令。每个条目包含功能说明、用法签名kscli 前缀)和详细手册链接。
> 完整参数表、参数约束、输出说明、注意事项与示例请参阅各子域手册。子域手册中的用法签名使用 `bl knowledge` 前缀。
---
### 知识库管理
> 📖 [完整手册](knowledge/kb.md) — 6 个命令
#### `kscli kb list`
列出工作区中的知识库。
```bash
kscli kb list [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-list)
---
#### `kscli kb info`
查看知识库配置详情。
```bash
kscli kb info --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-info)
---
#### `kscli kb create`
创建知识库并导入数据中心文件或分类。
```bash
kscli kb create --name <text> (--doc-id <id> | --category-id <id>) [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-create)
---
#### `kscli kb update`
更新知识库名称、描述或 rerank 阈值。
```bash
kscli kb update --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-update)
---
#### `kscli kb delete`
删除知识库及其所有文档和 chunk。
```bash
kscli kb delete --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-delete)
---
#### `kscli kb stats`
查看知识库存储和 QPS 监控数据。
```bash
kscli kb stats --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/kb.md#bl-knowledge-stats)
---
### 文档管理
> 📖 [完整手册](knowledge/doc.md) — 6 个命令
#### `kscli doc list`
列出知识库中的文档及其解析/索引状态。
```bash
kscli doc list --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-list)
---
#### `kscli doc status`
查看知识库导入任务状态。
```bash
kscli doc status --index-id <id> --job-id <id> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-status)
---
#### `kscli doc upload`
上传本地文件或目录到数据中心,可选导入到知识库。
```bash
kscli doc upload --file <path> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-upload)
---
#### `kscli doc delete`
从知识库中删除文档及其 chunk。
```bash
kscli doc delete --index-id <id> --doc-id <id> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-delete)
---
#### `kscli doc tag`
批量更新数据中心文件的标签。
```bash
kscli doc tag --doc-id <id> --tag <text> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-tag)
---
#### `kscli doc import-oss`
从已授权的 OSS bucket 批量导入文件到数据中心。
```bash
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
```
→ [完整参数与示例](knowledge/doc.md#bl-knowledge-doc-import-oss)
---
### 检索服务管理
> 📖 [完整手册](knowledge/service.md) — 7 个命令
#### `kscli service list`
列出工作区中的检索/Q&A 服务。
```bash
kscli service list --scene <chat|search> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-list)
---
#### `kscli service get`
查看服务详情,含各版本配置。
```bash
kscli service get --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-get)
---
#### `kscli service create`
创建检索/Q&A 服务,初始状态为 draft版本为 beta。
```bash
kscli service create --name <text> --scene <chat|search> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-create)
---
#### `kscli service update`
更新服务名称、描述或草稿配置。
```bash
kscli service update --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-update)
---
#### `kscli service deploy`
发布 beta 草稿为新版本。
```bash
kscli service deploy --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-deploy)
---
#### `kscli service delete`
删除检索/Q&A 服务(软删除,幂等)。
```bash
kscli service delete --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-delete)
---
#### `kscli service copy`
复制服务为新草稿(名称自动加 `copy_` 前缀)。
```bash
kscli service copy --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/service.md#bl-knowledge-service-copy)
---
### Chunk 管理
> 📖 [完整手册](knowledge/chunk.md) — 4 个命令
#### `kscli chunk add`
直接向知识库添加 chunk。
```bash
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
```
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-add)
---
#### `kscli chunk list`
列出知识库中的 chunk含内容和状态。
```bash
kscli chunk list --index-id <id> [flags]
```
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-list)
---
#### `kscli chunk update`
更新 chunk 内容或切换其检索可见性。
```bash
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
```
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-update)
---
#### `kscli chunk delete`
从知识库中删除 chunk不可逆
```bash
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
```
→ [完整参数与示例](knowledge/chunk.md#bl-knowledge-chunk-delete)
---
### 数据中心文件管理
> 📖 [完整手册](knowledge/file.md) — 3 个命令
#### `kscli file list`
列出数据中心分类下的文件。
```bash
kscli file list --category-id <id> [flags]
```
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-list)
---
#### `kscli file get`
查看数据中心文件详情。
```bash
kscli file get --file-id <id> [flags]
```
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-get)
---
#### `kscli file delete`
从数据中心永久删除文件。
```bash
kscli file delete --file-id <id> [flags]
```
→ [完整参数与示例](knowledge/file.md#bl-knowledge-file-delete)
---
### 数据中心集合与分类
> 📖 [完整手册](knowledge/collection-category.md) — 5 个命令
#### `kscli collection create`
创建 FILE 数据集合。
```bash
kscli collection create --name <text> --description <text> [flags]
```
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-create)
---
#### `kscli collection get`
查看数据集合详情。
```bash
kscli collection get (--collection-id <id> | --name <text>) [flags]
```
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-collection-get)
---
#### `kscli category list`
列出数据中心分类。
```bash
kscli category list [flags]
```
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-list)
---
#### `kscli category add`
创建数据中心分类。
```bash
kscli category add --name <text> [flags]
```
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-add)
---
#### `kscli category delete`
删除数据中心分类。
```bash
kscli category delete --category-id <id> [flags]
```
→ [完整参数与示例](knowledge/collection-category.md#bl-knowledge-category-delete)
---
### 检索与对话
> 📖 [完整手册](knowledge/search-chat.md) — 3 个命令
#### `kscli retrieve`
从知识库检索(已废弃,请用 `search` 替代)。
```bash
kscli retrieve --index-id <id> --query <text> [flags]
```
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-retrieve)
---
#### `kscli search`
对知识库执行语义检索RAG 检索)。
```bash
kscli search --query <text> --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-search)
---
#### `kscli chat`
与知识库进行 RAG 对话(流式输出)。
```bash
kscli chat --message <text> --agent-id <id> [flags]
```
→ [完整参数与示例](knowledge/search-chat.md#bl-knowledge-chat)
---
## 常见错误与排查
### Workspace ID 缺失
**报错**`Workspace ID is required.`
**原因**:所有 knowledge 管理命令都需要 workspace ID 来构造 API 端点(`{workspaceId}.cn-beijing.maas.aliyuncs.com`)。
**解决**
```bash
# 方式1命令行参数
bl knowledge list --workspace-id ws-xxx
# 方式2环境变量
export BAILIAN_WORKSPACE_ID=ws-xxx
# 方式3配置文件
bl config set workspace_id ws-xxx
```
### 知识库 ID 不存在
**报错**`Knowledge base not found: idx-xxx`
**原因**`--index-id` 指定的知识库在当前 workspace 中不存在。
**解决**:先 `bl knowledge list` 确认知识库 ID。
### 导入任务 SystemError
**报错**:服务端返回 `SystemError`
**原因**`doc status` 传入了不存在的 job ID或知识库空闲无任务。
**解决**:检查 `doc list` 输出中的 `ingestionId`,或从 `doc upload`/`knowledge create` 的返回值获取。
### doc_id 与 fileId 混淆
**问题**`doc delete` 时用了 `doc upload` 返回的 `fileId` 而非 `doc list` 返回的 `doc_id`
**原因**:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。
**解决**:始终用 `doc list --quiet` 获取 `doc_id`
### retrieve 已废弃
**问题**`retrieve` 命令输出废弃警告。
**解决**:改用 `search` 命令。`search` 通过 `--agent-id` 驱动检索策略支持多知识库、路由、rerank 等高级特性。`retrieve` 直接操作 `--index-id`,功能受限且不再迭代。
### OSS 导入权限错误
**报错**:服务端返回权限相关错误。
**原因**OSS bucket 未授权给平台服务角色。
**解决**:检查 RAM 控制台中的 `AliyunServiceRoleForBailian` 角色是否已正确授权。
### Chat SSE error
**报错**`Chat API error` + API error code。
**原因**:流式对话过程中服务端返回 error 事件。
**解决**:检查 `--agent-id` 是否存在、服务是否已部署、API Key 是否有效。错误消息和 code 原样透传,不二次包装。
### file list 返回空
**问题**`file list --category-id default` 返回空列表。
**原因**:与上传 API 不同,`file list` 不解析字面量 `default`,需要真实分类 ID。
**解决**:通过 `file get` 的 category 字段或 `category list` 获取真实分类 ID。
### 集合无法删除
**问题**:没有 `collection delete` 命令。
**原因**:暂不支持通过 CLI 删除。
**解决**:创建集合需谨慎。如需隔离,创建新集合并迁移文件。
---
## 附录:命令速查表
| 命令 | 功能 | 关键参数 |
| ------------------------- | ------------ | ----------------------------------------------------------- |
| `kscli kb list` | 列出知识库 | `--name` |
| `kscli kb info` | 知识库详情 | `--index-id` |
| `kscli kb create` | 创建知识库 | `--name`, `--doc-id`/`--category-id` |
| `kscli kb update` | 更新知识库 | `--index-id`, `--name`/`--description`/`--rerank-min-score` |
| `kscli kb delete` | 删除知识库 | `--index-id`, `--yes` |
| `kscli kb stats` | 监控数据 | `--index-id`, `--start`/`--end` |
| `kscli doc list` | 文档列表 | `--index-id` |
| `kscli doc status` | 导入任务状态 | `--index-id`, `--job-id`, `--wait` |
| `kscli doc upload` | 上传文件 | `--file`, `--index-id`, `--wait` |
| `kscli doc delete` | 删除文档 | `--index-id`, `--doc-id` |
| `kscli doc tag` | 文件打标签 | `--doc-id`, `--tag`, `--mode` |
| `kscli doc import-oss` | OSS 导入 | `--bucket`, `--region`, `--oss-key` |
| `kscli service list` | 服务列表 | `--scene` |
| `kscli service get` | 服务详情 | `--agent-id` |
| `kscli service create` | 创建服务 | `--name`, `--scene`, `--index-id` |
| `kscli service update` | 更新服务 | `--agent-id`, 配置参数 |
| `kscli service deploy` | 发布服务 | `--agent-id`, `--yes` |
| `kscli service delete` | 删除服务 | `--agent-id`, `--yes` |
| `kscli service copy` | 复制服务 | `--agent-id` |
| `kscli chunk add` | 添加 chunk | `--index-id`, `--content`/`--field` |
| `kscli chunk list` | chunk 列表 | `--index-id`, `--doc-id` |
| `kscli chunk update` | 更新 chunk | `--index-id`, `--chunk-id`, `--doc-id` |
| `kscli chunk delete` | 删除 chunk | `--index-id`, `--chunk-id`, `--yes` |
| `kscli file list` | 文件列表 | `--category-id` |
| `kscli file get` | 文件详情 | `--file-id` |
| `kscli file delete` | 删除文件 | `--file-id`, `--yes` |
| `kscli collection create` | 创建集合 | `--name`, `--description` |
| `kscli collection get` | 集合详情 | `--collection-id`/`--name` |
| `kscli category list` | 分类列表 | `--collection-id`, `--parent-id` |
| `kscli category add` | 创建分类 | `--name`, `--parent-id` |
| `kscli category delete` | 删除分类 | `--category-id`, `--yes` |
| `kscli retrieve` | 检索(废弃) | `--index-id`, `--query` |
| `kscli search` | 语义检索 | `--query`, `--agent-id` |
| `kscli chat` | RAG 对话 | `--message`, `--agent-id` |
+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 字符;建议填写 —— agent 靠它判断该调用哪个服务
**输出**
text 模式:
```
created: aid-xxx (status: draft, version: beta)
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
```
quiet 模式:输出 agent ID。
json 模式:返回 API 原始响应。
**注意事项**
- 不指定 `--index-id` 时,服务端使用默认 agent 配置。
- beta 草稿可通过 search/chat 的 `--agent-version beta` 测试,部署后才生效。
- 需要工作区的知识库创建权限。
**示例**
```bash
# 创建 Q&A 服务
bl knowledge service create --name my-qa --scene chat --workspace-id ws-xxx
# 创建检索服务并绑定知识库
bl knowledge service create --name my-search --scene search --index-id idx-xxx
```
---
#### `bl knowledge service update`
更新服务名称、描述或草稿配置。
**用法**
```bash
bl knowledge service update --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------------------ | ------ | ---- | ---------------------------------------------------------------------------------------- |
| `--agent-id <id>` | string | 是 | 服务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)
+248
View File
@@ -0,0 +1,248 @@
# Chunk 管理命令手册
Chunk 是知识库中最小的检索单元。文档导入后自动切分为 chunk也可以手动添加。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli chunk add`
直接向知识库添加 chunk。
**用法**
```bash
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ------------------------------------------------------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | string | 否² | 所属文档 ID表格/图片知识库必填,文档型可选 |
| `--content <text>` | string | 否¹ | Chunk 正文,最多 6000 字符(文档型);与 `--content-file` 互斥 |
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取正文(`.md`/`.txt` 等);与 `--content` 互斥 |
| `--title <text>` | string | 否 | Chunk 标题,最多 50 字符(文档型) |
| `--image-url <url>` | array | 否 | Chunk 图片 URL可重复最多 10 个;文档型) |
| `--field <key=value>` | array | 否¹ | 任意字段键值对(可重复),用于表格/图片知识库,键为 Excel 列名;与 content/title/image 互斥 |
> ¹ `--content`/`--content-file`/`--title`/`--image-url` 与 `--field` 互斥,必须提供其一。
> ² 表格/图片知识库必须提供 `--doc-id`。文档型知识库可选。
**参数约束**
- `--field``--content`/`--content-file`/`--title`/`--image-url` 互斥
- `--content``--content-file` 互斥
- `--content` 最多 6000 字符
- `--title` 最多 50 字符
- `--image-url` 最多 10 个
**输出**
text 模式:
```
chunk created (pipeline: idx-xxx)
List chunks to find the new chunk id.
```
quiet 模式:无输出(成功退出码 0
json 模式:返回 API 原始响应(不含 chunk ID
**注意事项**
- 支持文档/表格/图片知识库;音视频知识库不支持。
- API 响应不含 chunk ID需用 `chunk list` 查找新 chunk。
- API 幂等但限流 10 次/秒,批量脚本需自行节流。
- 表格/图片知识库用 `--field`,键为 Excel 列名,值为字符串。
**示例**
```bash
# 添加文本 chunk
kscli chunk add --index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx
# 添加表格行(字段方式)
kscli chunk add --index-id idx-xxx --field 列A=v1 --field 列B=v2
# 从文件读取内容
kscli chunk add --index-id idx-xxx --content-file ./chunk.md --doc-id doc-xxx
```
---
#### `kscli chunk list`
列出知识库中的 chunk含内容和状态。
**用法**
```bash
kscli chunk list --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------- | ------ | ---- | ------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | string | 否 | 只显示属于此文档的 chunk |
| `--page-number <n>` | number | 否 | 页码默认1 |
| `--page-size <n>` | number | 否 | 每页条数默认20最大 100 |
**参数约束**
- `--page-size` 范围 1-100
**输出**
text 模式:
```
[chunk] chunk-xxx (doc: intro.md, doc_id: file-xxx) status: COMPLETED
chunk content preview (truncated at 200 chars)…
total: 1
```
> 如果 chunk 被排除检索,行尾会显示 `[excluded from retrieval]`。
quiet 模式:每行一个 `metadata._id`chunk ID用于管道传给 update/delete。
json 模式:返回 API 原始响应,`data.nodes[]` 含完整 chunk 数据。
**注意事项**
-`metadata._id` 作为 chunk ID`metadata.doc_id` 作为文档 ID在 chunk update/delete 中使用。
- 页大小默认 20最大 100。
**示例**
```bash
# 列出所有 chunk
kscli chunk list --index-id idx-xxx --workspace-id ws-xxx
# 只看某文档的 chunk
kscli chunk list --index-id idx-xxx --doc-id file-xxx --page-size 50
```
---
#### `kscli chunk update`
更新 chunk 内容或切换其检索可见性。
**用法**
```bash
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ------------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | string | 是 | Chunk ID`metadata._id`,来自 chunk list 输出) |
| `--doc-id <id>` | string | 是 | 所属文档 ID`metadata.doc_id`,来自 chunk list 输出) |
| `--content <text>` | string | 否¹ | 新内容10-6000 字符;与 `--content-file` 互斥 |
| `--content-file <path>` | string | 否¹ | 从 UTF-8 文本文件读取新内容 |
| `--title <text>` | string | 否 | Chunk 标题0-50 字符(空字符串清除标题;不传则不变) |
| `--exclude` | switch | 否² | 将此 chunk 排除出检索 |
| `--include` | switch | 否² | 将此 chunk 恢复检索(默认行为) |
> ¹ `--content` 与 `--content-file` 互斥。
> ² `--exclude` 与 `--include` 互斥。
**参数约束**
- `--content``--content-file` 互斥
- `--exclude``--include` 互斥
- 至少提供一个更新项(`--content`/`--content-file`/`--title`/`--exclude`/`--include`
- `--content` 长度 10-6000 字符
- `--title` 最多 50 字符
**输出**
text 模式:
```
updated: chunk-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 内容必须 10-6000 字符,且不超过知识库的 max chunk size。
- `--content-file` 期望 UTF-8 纯文本文件,不解析 `.docx`/`.pdf` 等文档格式。
- 仅切换 `--exclude`/`--include` 而不提供新内容时CLI 自动读回当前内容并重新提交API 要求 content 字段必填CLI 隐藏了此限制)。
**示例**
```bash
# 修改内容
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text" --workspace-id ws-xxx
# 排除 chunk 不参与检索
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --exclude
# 恢复检索
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --include
```
---
#### `kscli chunk delete`
从知识库中删除 chunk不可逆
**用法**
```bash
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | array | 是 | Chunk ID可重复每批最多 10 个,超出自动分批) |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: 2 chunk(s) in 1 batch(es)
```
quiet 模式:无输出。
json 模式:返回 `{ deleted_count, batches }`
**注意事项**
- 服务端每次最多接受 10 个 chunk IDCLI 自动分批。
- 如果某批失败,操作停止,已删除的批次会在错误 hint 中列出。
- Chunk 被永久移除,不可恢复。
**示例**
```bash
# 删除多个 chunk
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --workspace-id ws-xxx
# 跳过确认
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --yes
```
---
← [返回总览](./kscli-cli-guide.md)
+268
View File
@@ -0,0 +1,268 @@
# 数据中心集合与分类命令手册
集合collection是数据中心的顶层容器对应服务端的 connector。分类category用于组织集合内的文件支持多级嵌套。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli collection create`
创建 FILE 数据集合。
**用法**
```bash
kscli collection create --name <text> --description <text> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------- | ------ | ---- | ---------------------------------------------------------------- |
| `--name <text>` | string | 是 | 集合名称1-20 字符) |
| `--description <text>` | string | 是 | 集合描述 |
| `--store-type <type>` | string | 否 | 存储类型:`platform`(托管,默认)或 `custom`(自有 OSS bucket |
| `--oss-region <id>` | string | 否 | OSS region ID`--store-type custom` 时必填) |
| `--oss-bucket <name>` | string | 否 | OSS bucket 名称(`--store-type custom` 时必填) |
**参数约束**
- `--name` 长度 1-20 字符
- `--store-type` 只能是 `platform``custom`
- `--store-type custom``--oss-region``--oss-bucket` 必填
**输出**
text 模式:
```
created: conn-xxx (my-collection, PLATFORM)
```
quiet 模式:输出集合 ID。
json 模式:返回 API 原始响应。
**注意事项**
- `platform` 使用平台托管存储;`custom` 使用已授权的 OSS bucket。
- 自定义 bucket 必须携带标签 `bailian-connector-access=ReadAndWrite`(百炼的标签访问控制),否则服务端报 `setBucketCORS failed` 误导性错误。
- **无集合删除 API**,创建需谨慎。
**示例**
```bash
# 创建平台托管的集合
kscli collection create --name my-collection --description "team docs" --workspace-id ws-xxx
# 创建使用自有 OSS bucket 的集合
kscli collection create --name oss-coll --description "own bucket" --store-type custom --oss-region cn-beijing --oss-bucket my-bucket
```
---
#### `kscli collection get`
查看数据集合详情。
**用法**
```bash
kscli collection get (--collection-id <id> | --name <text>) [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------- | ------ | ---- | -------- |
| `--collection-id <id>` | string | 否¹ | 集合 ID |
| `--name <text>` | string | 否¹ | 集合名称 |
> ¹ `--collection-id` 和 `--name` 二选一,必须提供其一。
**参数约束**
- `--collection-id``--name` 互斥,必须提供其一
**输出**
text 模式:
```
id: conn-xxx
name: my-collection
description: team docs
```
quiet 模式:输出集合 ID。
json 模式:返回 API 原始响应。
**注意事项**
- getConnector 不返回 `fileConnectorConfig``storeType`/`regionId`/`bucketName`),这些字段仅在创建时通过请求体传入,查询时不可读回。
**示例**
```bash
# 按 ID 查询
kscli collection get --collection-id conn-xxx --workspace-id ws-xxx
# 按名称查询
kscli collection get --name my-collection
```
---
#### `kscli category list`
列出数据中心分类。
**用法**
```bash
kscli category list [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------- | ------ | ---- | ------------------------------------------------------ |
| `--collection-id <id>` | string | 否 | 按集合 ID 过滤 |
| `--parent-id <id>` | string | 否 | 列出此分类的子分类 |
| `--name <text>` | string | 否 | 按分类名称过滤(精确匹配,与知识库列表的模糊匹配不同) |
| `--next-token <token>` | string | 否 | 游标分页令牌 |
| `--max-result <n>` | number | 否 | 每页条数默认20 |
**输出**
text 模式:
```
cate-xxx product-docs
cate-yyy system-docs [default]
next: --next-token eyJ...
```
> 标记 `[default]` 的是文件未指定分类时的默认归属。
quiet 模式:每行一个 `categoryId`
json 模式:返回 API 原始响应。
**注意事项**
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
**示例**
```bash
# 列出所有分类
kscli category list --workspace-id ws-xxx
# 按名称过滤
kscli category list --name my-category
# 翻页
kscli category list --next-token eyJ...
```
---
#### `kscli category add`
创建数据中心分类。
**用法**
```bash
kscli category add --name <text> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------- | ------ | ---- | -------------------------------- |
| `--name <text>` | string | 是 | 分类名称1-20 字符) |
| `--parent-id <id>` | string | 否 | 创建为指定分类的子分类 |
| `--collection-id <id>` | string | 否 | 创建在此集合下(默认:平台集合) |
**参数约束**
- `--name` 长度 1-20 字符
**输出**
text 模式:
```
created: cate-xxx (product-docs)
```
quiet 模式:输出分类 ID。
json 模式:返回 API 原始响应。
**注意事项**
- 用分类按业务域组织数据中心文件。
**示例**
```bash
# 创建分类
kscli category add --name product-docs --workspace-id ws-xxx
# 创建子分类
kscli category add --name sub --parent-id cate-xxx
```
---
#### `kscli category delete`
删除数据中心分类。
**用法**
```bash
kscli category delete --category-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------ |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: cate-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 含文件或子分类的分类的删除行为由服务端定义——服务端错误原样透传。
**示例**
```bash
# 删除分类(交互确认)
kscli category delete --category-id cate-xxx --workspace-id ws-xxx
# 跳过确认
kscli category delete --category-id cate-xxx --yes
```
---
← [返回总览](./kscli-cli-guide.md)
+344
View File
@@ -0,0 +1,344 @@
# 文档管理命令手册
文档管理覆盖文件上传、OSS 导入、解析状态跟踪、文档删除和标签管理。文档导入知识库后自动解析为 chunk。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli doc list`
列出知识库中的文档及其解析/索引状态。
**用法**
```bash
kscli doc list --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------- | ------ | ---- | ------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--page-number <n>` | number | 否 | 页码默认1 |
| `--page-size <n>` | number | 否 | 每页条数默认10最大 100 |
**参数约束**
- `--page-size` 范围 1-100
**输出**
text 模式:每行一个文档,`FAILED` 状态的文档红色高亮。
```
doc-xxx COMPLETED intro.md md 1024
total: 1
```
quiet 模式:每行一个 `doc_id`
json 模式:返回 API 原始响应。
**注意事项**
- `doc_id``file_id` 的关系:通过 `kb create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;通过 `doc upload --index-id` 导入的,`doc_id` 可能包含 workspace 后缀。
- 页大小默认 10服务端默认最大 100。
**示例**
```bash
# 列出文档
kscli doc list --index-id idx-xxx --workspace-id ws-xxx
# 每页 100 条
kscli doc list --index-id idx-xxx --page-size 100
```
---
#### `kscli doc status`
查看知识库导入任务状态。
**用法**
```bash
kscli doc status --index-id <id> --job-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | --------------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--job-id <id>` | string | 是 | 导入任务 ID`ingestionId`,由 create/upload 返回) |
| `--page-number <n>` | number | 否 | 页码 |
| `--page-size <n>` | number | 否 | 每页条数 |
| `--wait` | switch | 否 | 轮询直到任务到达终态 |
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数默认5 |
**输出**
text 模式:
```
status: COMPLETED
doc-xxx COMPLETED intro.md
```
quiet 模式:输出任务状态(`PENDING`/`RUNNING`/`COMPLETED`)。
json 模式:返回 API 原始响应,`data.rows[]` 包含每个文档的状态。
**注意事项**
- `--index-id``--job-id` 服务端均要求必传,只传一个会返回 `SystemError`
- 整体任务状态为 `PENDING` / `RUNNING` / `COMPLETED`(无 `FAILED` 值)。
- 单个文档可能解析失败(如 `PARSE_FAILED`),此时 CLI 以非零退出码报错,服务端消息原样透传。
- 如果服务端对空闲知识库返回 `SystemError`,说明该 job 可能不存在。
**示例**
```bash
# 查看任务状态
kscli doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
# 轮询等待完成10 秒间隔
kscli doc status --index-id idx-xxx --job-id job-xxx --wait --poll-interval 10
```
---
#### `kscli doc upload`
上传本地文件或目录到数据中心,可选导入到知识库。
**用法**
```bash
kscli doc upload --file <path> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | ---------------------------------------------------------------- |
| `--file <path>` | array | 是 | 本地文件或目录路径(可重复)。目录递归扫描,不支持的格式自动跳过 |
| `--index-id <id>` | string | 否 | 上传后导入到此知识库(所有文件合并为一个导入任务) |
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:工作区默认分类) |
| `--tag <text>` | array | 否 | 文件标签(可重复),应用到每个上传的文件 |
| `--wait` | switch | 否 | 轮询导入任务直到终态(需要 `--index-id` |
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数默认5 |
**参数约束**
- `--wait` 要求同时指定 `--index-id`
**输出**
text 模式:
```
intro.md file-xxx registered
job: job-xxx
status: COMPLETED
Uploaded 1 file.
```
quiet 模式:每行一个 `fileId`
json 模式:返回自定义结构,包含 `files`(路径和 fileId`skipped``index_id``ingestion_id``final_status`
**注意事项**
- 上传管道:申请 lease → PUT 到 OSS → 注册文件 →(可选)创建导入任务。
- 目录递归扫描,`node_modules``.git` 等自动跳过。
- 多文件按顺序处理(无并发),避免 OSS 限流。
- 支持的文件格式:`.pdf .doc .docx .ppt .pptx .xls .xlsx .csv .md .txt .html .png .jpg .jpeg .bmp .gif`
- 部分文件上传失败时,已注册的 fileId 会在错误 hint 中列出。
**示例**
```bash
# 上传单个文件
kscli doc upload --file ./a.md --workspace-id ws-xxx
# 上传多个文件并导入到知识库,等待完成
kscli doc upload --file ./a.md --file ./b.pdf --index-id idx-xxx --wait
# 上传整个目录
kscli doc upload --file ./docs/ --workspace-id ws-xxx
# 干跑预览(查看将上传和跳过的文件)
kscli doc upload --file ./docs/ --dry-run --verbose
```
---
#### `kscli doc delete`
从知识库中删除文档及其 chunk。
**用法**
```bash
kscli doc delete --index-id <id> --doc-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID可重复 |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: 2 document(s)
doc-a
doc-b
```
quiet 模式:每行一个已删除的 `doc_id`
json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID 列表。
**注意事项**
- 只从知识库索引中移除文档,数据中心源文件不受影响(用 `file delete` 删除源文件)。
- `doc_id` 应从 `doc list --quiet` 获取,而非 `doc upload` 返回的 `fileId`
- 删除是异步的:服务端立即返回 Success`doc list` 中可能仍显示该文档(约 30 秒后传播完成)。
- 输出的是服务端实际删除的 ID 列表,可能与请求的数量不一致(会在 stderr 警告)。
**示例**
```bash
# 删除单个文档
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
# 批量删除,跳过确认
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
```
---
#### `kscli doc tag`
批量更新数据中心文件的标签。
**用法**
```bash
kscli doc tag --doc-id <id> --tag <text> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | ------------------------------------------------------ |
| `--doc-id <id>` | array | 是 | 数据中心文件 ID可重复最多 20 个/次) |
| `--tag <text>` | array | 是 | 标签(可重复),应用到每个 `--doc-id` |
| `--mode <mode>` | string | 否 | 更新模式:`append`(默认,追加)或 `overwrite`(覆盖) |
**参数约束**
- `--doc-id` 最多 20 个/次
- `--tag` 最多 100 个
- 每个标签最多 32 字符
- 标签总长度最多 700 字符
- `--mode` 只能是 `append``overwrite`
**输出**
text 模式:
```
tagged: 2 file(s) with [project-a, draft]
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 同一组标签应用到所有 `--doc-id`;不同标签集需多次执行。
**示例**
```bash
# 追加标签
kscli doc tag --doc-id file-xxx --tag project-a --tag draft --workspace-id ws-xxx
# 覆盖标签
kscli doc tag --doc-id file-a --doc-id file-b --tag final --mode overwrite
```
---
#### `kscli doc import-oss`
从已授权的 OSS bucket 批量导入文件到数据中心。
**用法**
```bash
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------------------------------- |
| `--bucket <name>` | string | 是 | 已授权的 OSS bucket 名称 |
| `--region <id>` | string | 是 | OSS region ID`cn-beijing` |
| `--oss-key <key>` | array | 是 | OSS 对象 key可重复最多 10 个/次) |
| `--category-id <id>` | string | 否 | 目标数据中心分类(默认:默认分类) |
| `--tag <text>` | array | 否 | 文件标签(可重复,最多 10 个) |
| `--overwrite` | switch | 否 | 覆盖之前从相同 OSS key 导入的文件 |
**参数约束**
- `--oss-key` 最多 10 个/次
- `--tag` 最多 10 个
**输出**
text 模式:
```
imported: 2 file(s)
file-a SUCCESS docs/a.pdf
file-b SUCCESS docs/b.docx
```
quiet 模式:每行一个 `fileId`
json 模式:返回 API 原始响应,`data.addFileResultList[]` 包含每个文件的 fileId、status 和 ossKey。
**注意事项**
- bucket 必须事先授权给平台服务角色RAM 中的 `AliyunServiceRoleForBailian`)。
- 文件名取自 OSS key 的 basename。
- `--overwrite` 会替换之前导入的文件并生成**新的 fileId**(旧 fileId 失效)。
**示例**
```bash
# 导入单个文件
kscli doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx
# 导入多个文件并覆盖
kscli doc import-oss --bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --oss-key docs/b.docx --overwrite
```
---
← [返回总览](./kscli-cli-guide.md)
+157
View File
@@ -0,0 +1,157 @@
# 数据中心文件管理命令手册
数据中心是知识库文件的存储层。文件通过 `doc upload``doc import-oss` 进入数据中心,再导入到知识库。数据中心文件可被多个知识库引用。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli file list`
列出数据中心分类下的文件。
**用法**
```bash
kscli file list --category-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------- | ------ | ---- | -------------------------------------------------- |
| `--category-id <id>` | string | 是 | 分类 ID通过 `category list``file get` 获取) |
| `--name <text>` | string | 否 | 按文件名过滤 |
| `--file-id <id>` | array | 否 | 按文件 ID 过滤(可重复) |
| `--next-token <token>` | string | 否 | 游标分页令牌(从上次输出获取) |
| `--max-result <n>` | number | 否 | 每页条数 |
**输出**
text 模式:
```
file-xxx SUCCESS intro.md 1024
next: --next-token eyJ...
```
quiet 模式:每行一个 `fileId`
json 模式:返回 API 原始响应。
**注意事项**
- `--category-id` 必须是真实的分类 ID。与上传 API 不同,字面量 `default` 在此不被解析,传入会返回空列表。通过 `file get` 的 category 字段或 `category list` 获取真实 ID。
- 分页是游标方式:使用输出的 `next: --next-token <token>` 继续翻页。
**示例**
```bash
# 列出分类下文件
kscli file list --category-id cate-xxx --workspace-id ws-xxx
# 按名称过滤
kscli file list --category-id cate-xxx --name report
# 翻页
kscli file list --category-id cate-xxx --next-token eyJ...
```
---
#### `kscli file get`
查看数据中心文件详情。
**用法**
```bash
kscli file get --file-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
**输出**
text 模式:
```
id: file-xxx
name: intro.md
type: md
size: 1024
status: SUCCESS
parser: AUTO_SELECT
category: cate-xxx
uploaded: 2026-01-01T00:00:00Z
tags: project-a, draft
```
quiet 模式:输出 JSON 格式。
json 模式:返回 API 原始响应。
**注意事项**
- 无特殊注意事项。
**示例**
```bash
# 查看文件详情
kscli file get --file-id file-xxx --workspace-id ws-xxx
```
---
#### `kscli file delete`
从数据中心永久删除文件。
**用法**
```bash
kscli file delete --file-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: file-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- **不可逆操作**:如果知识库引用了此文件,相关文档索引会失效。
-`doc delete` 的区别:`doc delete` 只从单个知识库索引中移除文档,数据中心源文件保留;`file delete` 删除源文件本身,影响所有引用它的知识库。
**示例**
```bash
# 删除文件(交互确认)
kscli file delete --file-id file-xxx --workspace-id ws-xxx
# 跳过确认
kscli file delete --file-id file-xxx --yes
```
---
← [返回总览](./kscli-cli-guide.md)
+342
View File
@@ -0,0 +1,342 @@
# 知识库管理命令手册
知识库Knowledge Base / pipeline / index是 RAG 的核心载体,存储文档解析后的向量索引。本组命令覆盖知识库的创建、查看、更新、删除和监控。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli kb list`
列出工作区中的知识库。
**用法**
```bash
kscli kb list [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------- | ------ | ---- | --------------------------------- |
| `--name <text>` | string | 否 | 按知识库名称模糊过滤1-20 字符) |
| `--page-number <n>` | number | 否 | 页码默认1 |
| `--page-size <n>` | number | 否 | 每页条数默认20最大 100 |
**参数约束**
- `--name` 长度 1-20 字符
- `--page-size` 范围 1-100
**输出**
text 模式:每行一个知识库,字段以双空格分隔,末尾显示总数。
```
idx-xxx my-kb text-embedding-v4 600 product docs
total: 1
```
quiet 模式:每行一个知识库 ID。
json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。
**注意事项**
- 返回的 `id` 字段作为后续命令的 `--index-id` 使用。
**示例**
```bash
# 列出所有知识库
kscli kb list --workspace-id ws-xxx
# 按名称过滤,第二页
kscli kb list --name demo --page-number 2 --page-size 50
```
---
#### `kscli kb info`
查看知识库配置详情。
**用法**
```bash
kscli kb info --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
**输出**
text 模式:按诊断维度分组展示。
```
Basic:
id: idx-xxx
name: my-kb
description: product docs
dataType: ...
Indexing: [immutable — recreate required to change]
embeddingModelName: text-embedding-v4
embeddingDimension: 1024
chunkSize: 600
overlapSize: ...
chunkMode: ...
separator: ...
Retrieval:
rerankModelName: ...
rerankMinScore: ...
rerankTopN: ...
rerankMode: ...
enableRewrite: ...
denseSimilarityTopK: ...
sparseSimilarityTopK: ...
Data:
sourceType: ...
connectorId: ...
```
quiet 模式:输出知识库 ID。
json 模式:返回知识库完整配置 JSON。
**注意事项**
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
**示例**
```bash
# 查看知识库详情
kscli kb info --index-id idx-xxx --workspace-id ws-xxx
```
---
#### `kscli kb create`
创建知识库并导入数据中心文件或分类。
**用法**
```bash
kscli kb create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | -------------------------------------------------------- |
| `--name <text>` | string | 是 | 知识库名称1-20 字符,工作区内唯一) |
| `--description <text>` | string | 是 | 知识库装了什么内容、给谁用1-500 字符) |
| `--doc-id <id>` | array | 否¹ | 数据中心文件 ID可重复`--category-id` 互斥 |
| `--category-id <id>` | array | 否¹ | 按分类导入该分类下所有文件(可重复);与 `--doc-id` 互斥 |
| `--embedding-model <name>` | string | 否 | 向量模型名称(默认:`text-embedding-v4` |
| `--chunk-size <n>` | number | 否 | 切片大小字符数默认600建议 300-800 |
| `--wait` | switch | 否 | 轮询初始导入任务直到终态 |
| `--poll-interval <seconds>` | number | 否 | 轮询间隔秒数默认5 |
> ¹ `--doc-id` 和 `--category-id` 二选一,必须提供其一。
**参数约束**
- `--name` 长度 1-20 字符
- `--description` 长度 1-500 字符,缺失或超长会在本地被拦截
- `--doc-id``--category-id` 互斥,必须提供其一
**输出**
text 模式:
```
index_id: idx-xxx
ingestion_id: job-xxx
status: COMPLETED
Next: check the import job status, then search against this knowledge base.
```
quiet 模式:只输出知识库 ID。
json 模式:返回 API 原始响应,包含 `pipelineId`(知识库 ID`ingestionId`(导入任务 ID`--wait` 时追加 `final_status` 字段。
**注意事项**
- 结构/存储类型固定为默认文档知识库非结构化BUILT_IN 存储)。
- 返回知识库 ID`pipelineId`)和初始导入任务 ID`ingestionId`)。
- 使用 `doc status``--wait` 跟踪导入进度。
- 如果 `--wait` 后部分文档解析失败CLI 以非零退出码报错,知识库已创建成功的事实会在 hint 中提示。
**示例**
```bash
# 从指定文件创建知识库
kscli kb create --name demo --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx
# 从分类导入并等待导入完成
kscli kb create --name demo --description '产品文档' --category-id cate-xxx --wait
# 指定向量模型和切片大小
kscli kb create --name my-kb --description '产品文档 v2' --doc-id file-a --doc-id file-b --embedding-model text-embedding-v4 --chunk-size 400 --workspace-id ws-xxx
```
---
#### `kscli kb update`
更新知识库名称、描述或 rerank 阈值。
**用法**
```bash
kscli kb update --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------------------- | ------ | ---- | -------------------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--name <text>` | string | 否 | 新名称1-20 字符) |
| `--description <text>` | string | 否 | 新描述 |
| `--rerank-min-score <score>` | number | 否 | rerank 最低分数阈值,范围 0-1低于此分的 chunk 被过滤) |
**参数约束**
- 至少提供 `--name``--description``--rerank-min-score` 之一,否则报错 "Nothing to update"
- `--name` 长度 1-20 字符
- `--rerank-min-score` 范围 0-1
**输出**
text 模式:
```
updated: idx-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 索引设置(向量模型、切片大小等)不可变,修改需重建知识库。
**示例**
```bash
# 更新描述
kscli kb update --index-id idx-xxx --description "product docs v2" --workspace-id ws-xxx
# 调整 rerank 阈值
kscli kb update --index-id idx-xxx --rerank-min-score 0.3
```
---
#### `kscli kb delete`
删除知识库及其所有文档和 chunk。
**用法**
```bash
kscli kb delete --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: idx-xxx
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes`CLI 会先查询知识库名称和文档数量作为确认摘要。
**示例**
```bash
# 删除(交互确认)
kscli kb delete --index-id idx-xxx --workspace-id ws-xxx
# 跳过确认
kscli kb delete --index-id idx-xxx --yes
```
---
#### `kscli kb stats`
查看知识库存储和 QPS 监控数据。
**用法**
```bash
kscli kb stats --index-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--start <time>` | string | 否 | 范围起始Unix 秒或 ISO 日期默认24 小时前) |
| `--end <time>` | string | 否 | 范围结束Unix 秒或 ISO 日期(默认:当前时间) |
**输出**
text 模式:
```
plan: ...
storage: 100 / 1000
peak qps: 5
qps windows: 24 data point(s)
```
quiet 模式:输出 json 格式。
json 模式:返回 API 原始响应,包含 `storageMonitorData``qpsMonitorData`
**注意事项**
- 默认查询最近 24 小时数据。
- 时间戳自动转换为 epoch 秒API 要求秒级字符串。13 位毫秒时间戳会自动降为秒。
**示例**
```bash
# 查看最近 24 小时监控
kscli kb stats --index-id idx-xxx --workspace-id ws-xxx
# 指定日期范围
kscli kb stats --index-id idx-xxx --start 2026-07-30 --end 2026-07-31
```
---
← [返回总览](./kscli-cli-guide.md)
+929
View File
@@ -0,0 +1,929 @@
# `kscli` 命令完整用法指南
> Knowledge Studio CLI`kscli`)命令总览,覆盖全部 37 个命令34 个知识库命令 + 3 个配置/维护命令。完整参数与示例请参阅各子域手册。
---
## 目录
1. [概述](#概述)
2. [核心概念与实体关系](#核心概念与实体关系)
3. [通用约定](#通用约定)
4. [典型工作流](#典型工作流)
5. [命令手册](#命令手册)
- [知识库管理](#知识库管理) → [完整手册](kb.md)
- [文档管理](#文档管理) → [完整手册](doc.md)
- [检索服务管理](#检索服务管理) → [完整手册](service.md)
- [Chunk 管理](#chunk-管理) → [完整手册](chunk.md)
- [数据中心文件管理](#数据中心文件管理) → [完整手册](file.md)
- [数据中心集合与分类](#数据中心集合与分类) → [完整手册](collection-category.md)
- [检索与对话](#检索与对话) → [完整手册](search-chat.md)
- [配置与维护](#配置与维护)
6. [常见错误与排查](#常见错误与排查)
7. [附录:命令速查表](#附录命令速查表)
---
## 概述
`kscli``knowledge-studio-cli`)是面向 RAG 开发者的知识库专用 CLI把知识库能力铺平成一级命令组覆盖 RAG检索增强生成全链路
- **知识库全生命周期管理**:创建、查看、更新、删除、监控
- **文档管理**:上传本地文件或目录、从 OSS 批量导入、查看解析状态、删除、打标签
- **Chunk 级运维**:直接增删改查知识库中的内容切片
- **检索服务管理**:创建/部署/复制/删除 Q&A 和检索服务agent管理 draft 与发布版本
- **数据中心管理**文件、集合connector、分类的增删查
- **检索与对话**语义检索search、多轮对话chat、兼容旧检索retrieve
- **配置与维护**:查看/修改本地配置、自更新 CLI
共 37 个命令34 个知识库命令(按功能域分为 7 组)+ `config show` / `config set` / `update`。所有知识库命令均使用 DashScope API Key 鉴权。
> **与 `bl` 的关系**`kscli` 与 `bl knowledge` 复用同一套命令实现flag 名、行为逻辑、校验规则完全一致,只有命令路径不同 —— `kscli` 把知识库能力铺平(`kscli kb list`、`kscli file list``bl` 则把它们收在 `bl knowledge` 之下。用 `bl` 的读者请参阅 [`bl knowledge` 指南](../knowledge/knowledge-cli-guide.md)。
安装与运行:
```bash
# 免安装执行(推荐,版本可控)
npx knowledge-studio-cli@latest --help
# 全局安装后使用 kscli
npm install -g knowledge-studio-cli
kscli --help
```
> 后文示例统一写作 `kscli <command>`;若未全局安装,把 `kscli` 换成 `npx knowledge-studio-cli@latest` 即可。
---
## 核心概念与实体关系
```
┌─────────────────────────────────────────────────────────────┐
│ 数据中心 (Data Center) │
│ │
│ 集合 (Collection) ──┬── 分类 (Category) ── 文件 (File) │
│ │ "connector" 可多级嵌套 │
│ └── 默认分类 │
│ │
│ 文件来源doc upload(本地上传) / doc import-oss(OSS导入) │
└──────────────────────────┬──────────────────────────────────┘
│ 导入 (import job)
┌─────────────────────────────────────────────────────────────┐
│ 知识库 (Knowledge Base) │
│ │
│ 知识库 (KB / pipeline / index) │
│ ├── 文档 (Doc) ── 解析状态: PENDING/RUNNING/COMPLETED │
│ │ └── Chunk ── 内容切片,可增删改查、排除/恢复检索 │
│ └── 索引设置 (immutable): 向量模型、切片大小等 │
│ │
│ 知识库管理命令: kb create / list / info / update / delete / stats │
└──────────────────────────┬──────────────────────────────────┘
│ 绑定 (agent_config.kb_search_configs)
┌─────────────────────────────────────────────────────────────┐
│ 检索服务 (Service / Agent) │
│ │
│ Service (agent) │
│ ├── scene: chat (Q&A) 或 search (检索) │
│ ├── 版本: beta (草稿) → 1, 2, 3... (已发布) │
│ ├── 状态: draft → deployed → edited → deleted │
│ └── 配置: 模型、温度、策略、rerank 等 │
│ │
│ 消费方式: search (语义检索) / chat (多轮对话) │
│ 管理命令: create / update / deploy / copy / delete / list / get │
└─────────────────────────────────────────────────────────────┘
```
**关键关系**
- **数据中心文件 → 知识库**:通过 `kscli kb create --doc-id``kscli doc upload --index-id` 导入,文件解析后自动生成 chunk
- **知识库 → 检索服务**:一个服务可绑定多个知识库,服务配置中 `kb_search_configs` 指定关联的知识库 ID
- **检索服务 → 检索/对话**`kscli search``kscli chat` 通过 `--agent-id` 指定服务来执行检索或对话
---
## 通用约定
### 鉴权
所有知识库命令均使用 **DashScope API Key**Bearer token鉴权。获取方式百炼控制台 API Key 页面。
优先级(高 → 低):
1. `--api-key <key>` 命令行参数
2. `DASHSCOPE_API_KEY` 环境变量
3. 配置文件中的 `api_key``kscli config set --key api_key --value <key>`
### Workspace ID
知识库 API 使用 workspace 级域名(`{workspaceId}.cn-beijing.maas.aliyuncs.com`),因此 **几乎所有知识库命令都需要 workspace ID**
优先级(高 → 低):
1. `--workspace-id <id>` 命令行参数
2. `BAILIAN_WORKSPACE_ID` 环境变量
3. 配置文件中的 `workspace_id``kscli config set --key workspace_id --value <id>`
缺失时报错:`Workspace ID is required.`
### 全局通用参数
以下参数在所有知识库命令中通用,后续命令手册中不再逐条列出:
| 参数 | 类型 | 说明 |
| --------------------- | ------ | ----------------------------------------------------------- |
| `--output <format>` | string | 输出格式:`text`(默认,人类友好)或 `json`API 原始响应) |
| `--api-key <key>` | string | DashScope API Key |
| `--base-url <url>` | string | API 基地址(一般不需要指定) |
| `--timeout <seconds>` | number | 请求超时秒数 |
| `--quiet` | switch | 静默模式,只输出关键结果(如 ID 列表) |
| `--verbose` | switch | 详细模式,打印 HTTP 请求/响应详情到 stderr |
| `--dry-run` | switch | 干跑模式,预览将发送的请求结构,不实际调用 API |
| `--config <name>` | string | 使用指定配置 profile 执行命令 |
> **注意**:命令手册中每个命令的参数表只列出该命令**特有**的参数。上述全局参数对所有命令有效。
### 输出格式约定
- **text 模式**(默认):人类友好的表格/结构化文本,适合终端查看。不同命令的输出格式见各命令的「输出」部分。
- **json 模式**`--output json`):返回 API 原始 JSON 响应,适合程序化处理和 agent 解析。
- **quiet 模式**`--quiet`):只输出最精简的结果(通常只有 ID适合管道串联。
### 危险操作确认
涉及删除的命令(`kb delete``doc delete``chunk delete``file delete``category delete``service delete`)以及 `service deploy` 在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
### Dry-run 模式
`--dry-run` 模式下,命令会输出将发送的 endpoint 和 request body但**不实际发起网络请求**。部分命令在 dry-run 下仍会执行本地校验(如文件扩展名检查、参数约束检查)。
---
## 典型工作流
### 场景 A从零搭建知识库并检索
```bash
# 1. 上传本地文件到数据中心
kscli doc upload --file ./docs/intro.md --workspace-id ws-xxx
# → 返回 file-id
# 2. 用文件创建知识库
kscli kb create --name my-kb --description '产品文档' --doc-id file-xxx --workspace-id ws-xxx --wait
# → 返回 index-id (pipelineId) 和导入任务状态
# 3. 创建检索服务search 场景)
kscli service create --name my-search --scene search --index-id idx-xxx --workspace-id ws-xxx
# → 返回 agent-id
# 4. 部署服务
kscli service deploy --agent-id aid-xxx --workspace-id ws-xxx --yes
# 5. 执行检索
kscli search --query "什么是RAG" --agent-id aid-xxx --workspace-id ws-xxx
```
### 场景 B上传目录并导入到已有知识库
```bash
# 1. 上传整个目录到数据中心并直接导入到知识库(一步到位)
kscli doc upload --file ./docs/ --index-id idx-xxx --workspace-id ws-xxx --wait
# → 文件逐个上传到 OSS → 注册到数据中心 → 创建合并导入任务 → 轮询到完成
# 2. 检查文档状态
kscli doc list --index-id idx-xxx --workspace-id ws-xxx
# → 查看 doc_id 和解析状态
# 3. 如果有文档解析失败,查看导入任务详情
kscli doc status --index-id idx-xxx --job-id job-xxx --workspace-id ws-xxx
```
### 场景 C创建并部署 Q&A 服务
```bash
# 1. 创建 chat 场景的检索服务
kscli service create --name my-qa --scene chat --index-id idx-xxx --workspace-id ws-xxx
# → 初始状态: draft, 版本: beta
# 2. 调整配置(如修改模型、温度)
kscli service update --agent-id aid-xxx --model qwen-max --temperature 0.7 --workspace-id ws-xxx
# 3. 用 beta 版本测试
kscli chat --message "什么是RAG?" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
# 4. 测试通过后发布
kscli service deploy --agent-id aid-xxx --version-desc "首版" --workspace-id ws-xxx --yes
```
### 场景 D知识库内容运维
```bash
# 1. 查看 chunk 列表
kscli chunk list --index-id idx-xxx --workspace-id ws-xxx
# → 返回 metadata._id (chunk id) 和 metadata.doc_id (document id)
# 2. 修改 chunk 内容
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --content "修正后的内容" --workspace-id ws-xxx
# 3. 排除某个 chunk 不参与检索(不删除内容)
kscli chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id doc-xxx --exclude --workspace-id ws-xxx
# 4. 手动添加新 chunk
kscli chunk add --index-id idx-xxx --content "新增的知识片段" --title "补充说明" --workspace-id ws-xxx
# 5. 删除 chunk批量自动分批每 10 个一组)
kscli chunk delete --index-id idx-xxx --chunk-id chunk-a --chunk-id chunk-b --yes --workspace-id ws-xxx
```
### 场景 E服务迁移/复用
```bash
# 1. 复制现有服务为新草稿
kscli service copy --agent-id aid-source --workspace-id ws-xxx
# → 返回新的 agent-id名称加 copy_ 前缀
# 2. 修改新服务配置
kscli service update --agent-id aid-new --name "改进版" --temperature 0.5 --workspace-id ws-xxx
# 3. 测试并发布
kscli chat --message "测试" --agent-id aid-new --agent-version beta --workspace-id ws-xxx
kscli service deploy --agent-id aid-new --workspace-id ws-xxx --yes
```
### 场景 F从 OSS 批量导入文件
```bash
# 1. 从已授权的 OSS bucket 批量导入文件到数据中心
kscli doc import-oss \
--bucket my-bucket --region cn-beijing \
--oss-key docs/a.pdf --oss-key docs/b.docx \
--workspace-id ws-xxx
# → 返回各文件的 fileId
# 2. 创建知识库并导入这些文件
kscli kb create --name oss-kb --description 'OSS 导入文档' --doc-id file-a --doc-id file-b --workspace-id ws-xxx --wait
# 3. 检索
kscli search --query "相关内容" --agent-id aid-xxx --workspace-id ws-xxx
```
---
## 命令手册
以下按功能域分组,覆盖全部 37 个命令。每个条目包含功能说明、用法签名和详细手册链接。
> 完整参数表、参数约束、输出说明、注意事项与示例请参阅各子域手册。
---
### 知识库管理
> 📖 [完整手册](kb.md) — 6 个命令
#### `kscli kb list`
列出工作区中的知识库。
```bash
kscli kb list [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-list)
---
#### `kscli kb info`
查看知识库配置详情。
```bash
kscli kb info --index-id <id> [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-info)
---
#### `kscli kb create`
创建知识库并导入数据中心文件或分类。
```bash
kscli kb create --name <text> --description <text> (--doc-id <id> | --category-id <id>) [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-create)
---
#### `kscli kb update`
更新知识库名称、描述或 rerank 阈值。
```bash
kscli kb update --index-id <id> [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-update)
---
#### `kscli kb delete`
删除知识库及其所有文档和 chunk。
```bash
kscli kb delete --index-id <id> [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-delete)
---
#### `kscli kb stats`
查看知识库存储和 QPS 监控数据。
```bash
kscli kb stats --index-id <id> [flags]
```
→ [完整参数与示例](kb.md#kscli-kb-stats)
---
### 文档管理
> 📖 [完整手册](doc.md) — 6 个命令
#### `kscli doc list`
列出知识库中的文档及其解析/索引状态。
```bash
kscli doc list --index-id <id> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-list)
---
#### `kscli doc status`
查看知识库导入任务状态。
```bash
kscli doc status --index-id <id> --job-id <id> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-status)
---
#### `kscli doc upload`
上传本地文件或目录到数据中心,可选导入到知识库。
```bash
kscli doc upload --file <path> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-upload)
---
#### `kscli doc delete`
从知识库中删除文档及其 chunk。
```bash
kscli doc delete --index-id <id> --doc-id <id> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-delete)
---
#### `kscli doc tag`
批量更新数据中心文件的标签。
```bash
kscli doc tag --doc-id <id> --tag <text> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-tag)
---
#### `kscli doc import-oss`
从已授权的 OSS bucket 批量导入文件到数据中心。
```bash
kscli doc import-oss --bucket <name> --region <id> --oss-key <key> [flags]
```
→ [完整参数与示例](doc.md#kscli-doc-import-oss)
---
### 检索服务管理
> 📖 [完整手册](service.md) — 7 个命令
#### `kscli service list`
列出工作区中的检索/Q&A 服务。
```bash
kscli service list --scene <chat|search> [flags]
```
→ [完整参数与示例](service.md#kscli-service-list)
---
#### `kscli service get`
查看服务详情,含各版本配置。
```bash
kscli service get --agent-id <id> [flags]
```
→ [完整参数与示例](service.md#kscli-service-get)
---
#### `kscli service create`
创建检索/Q&A 服务,初始状态为 draft版本为 beta。
```bash
kscli service create --name <text> --scene <chat|search> [flags]
```
→ [完整参数与示例](service.md#kscli-service-create)
---
#### `kscli service update`
更新服务名称、描述或草稿配置。
```bash
kscli service update --agent-id <id> [flags]
```
→ [完整参数与示例](service.md#kscli-service-update)
---
#### `kscli service deploy`
发布 beta 草稿为新版本。
```bash
kscli service deploy --agent-id <id> [flags]
```
→ [完整参数与示例](service.md#kscli-service-deploy)
---
#### `kscli service delete`
删除检索/Q&A 服务(软删除,幂等)。
```bash
kscli service delete --agent-id <id> [flags]
```
→ [完整参数与示例](service.md#kscli-service-delete)
---
#### `kscli service copy`
复制服务为新草稿(名称自动加 `copy_` 前缀)。
```bash
kscli service copy --agent-id <id> [flags]
```
→ [完整参数与示例](service.md#kscli-service-copy)
---
### Chunk 管理
> 📖 [完整手册](chunk.md) — 4 个命令
#### `kscli chunk add`
直接向知识库添加 chunk。
```bash
kscli chunk add --index-id <id> (--content <text> | --field <k=v>) [flags]
```
→ [完整参数与示例](chunk.md#kscli-chunk-add)
---
#### `kscli chunk list`
列出知识库中的 chunk含内容和状态。
```bash
kscli chunk list --index-id <id> [flags]
```
→ [完整参数与示例](chunk.md#kscli-chunk-list)
---
#### `kscli chunk update`
更新 chunk 内容或切换其检索可见性。
```bash
kscli chunk update --index-id <id> --chunk-id <id> --doc-id <id> [flags]
```
→ [完整参数与示例](chunk.md#kscli-chunk-update)
---
#### `kscli chunk delete`
从知识库中删除 chunk不可逆
```bash
kscli chunk delete --index-id <id> --chunk-id <id> [flags]
```
→ [完整参数与示例](chunk.md#kscli-chunk-delete)
---
### 数据中心文件管理
> 📖 [完整手册](file.md) — 3 个命令
#### `kscli file list`
列出数据中心分类下的文件。
```bash
kscli file list --category-id <id> [flags]
```
→ [完整参数与示例](file.md#kscli-file-list)
---
#### `kscli file get`
查看数据中心文件详情。
```bash
kscli file get --file-id <id> [flags]
```
→ [完整参数与示例](file.md#kscli-file-get)
---
#### `kscli file delete`
从数据中心永久删除文件。
```bash
kscli file delete --file-id <id> [flags]
```
→ [完整参数与示例](file.md#kscli-file-delete)
---
### 数据中心集合与分类
> 📖 [完整手册](collection-category.md) — 5 个命令
#### `kscli collection create`
创建 FILE 数据集合。
```bash
kscli collection create --name <text> --description <text> [flags]
```
→ [完整参数与示例](collection-category.md#kscli-collection-create)
---
#### `kscli collection get`
查看数据集合详情。
```bash
kscli collection get (--collection-id <id> | --name <text>) [flags]
```
→ [完整参数与示例](collection-category.md#kscli-collection-get)
---
#### `kscli category list`
列出数据中心分类。
```bash
kscli category list [flags]
```
→ [完整参数与示例](collection-category.md#kscli-category-list)
---
#### `kscli category add`
创建数据中心分类。
```bash
kscli category add --name <text> [flags]
```
→ [完整参数与示例](collection-category.md#kscli-category-add)
---
#### `kscli category delete`
删除数据中心分类。
```bash
kscli category delete --category-id <id> [flags]
```
→ [完整参数与示例](collection-category.md#kscli-category-delete)
---
### 检索与对话
> 📖 [完整手册](search-chat.md) — 3 个命令
#### `kscli retrieve`
从知识库检索(已废弃,请用 `search` 替代)。
```bash
kscli retrieve --index-id <id> --query <text> [flags]
```
→ [完整参数与示例](search-chat.md#kscli-retrieve)
---
#### `kscli search`
对知识库执行语义检索RAG 检索)。
```bash
kscli search --query <text> --agent-id <id> [flags]
```
→ [完整参数与示例](search-chat.md#kscli-search)
---
#### `kscli chat`
与知识库进行 RAG 对话(流式输出)。
```bash
kscli chat --message <text> --agent-id <id> [flags]
```
→ [完整参数与示例](search-chat.md#kscli-chat)
---
### 配置与维护
这 3 个命令不调用知识库 API用于管理本地配置与 CLI 自身版本。配置文件默认位于 `~/.bailian/config.json`(可用 `BAILIAN_CONFIG_DIR` 改写目录)。
#### `kscli config show`
显示当前生效配置(含 base_url、output、timeout、profile 名和配置文件路径;密钥类字段自动脱敏)。
```bash
kscli config show [--output json]
```
示例:
```bash
# 查看当前配置
kscli config show
# JSON 输出,便于脚本解析
kscli config show --output json
```
---
#### `kscli config set`
写入一个配置项到配置文件。
```bash
kscli config set --key <key> --value <value>
```
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--key <key>` | string | 是 | 配置项名称:`language``base_url``output``output_dir``timeout``api_key``access_token``access_key_id``access_key_secret``security_token``default_*_model``workspace_id` |
| `--value <value>` | string | 是 | 要写入的值(按 key 类型校验并转换) |
示例:
```bash
# 持久化 API Key
kscli config set --key api_key --value sk-xxx
# 持久化 workspace省去每次传 --workspace-id
kscli config set --key workspace_id --value ws-xxx
# 默认输出 JSON
kscli config set --key output --value json
```
**注意事项**
- `--dry-run` 只打印将写入的键值和配置文件路径,不落盘。
- 密钥类字段(`api_key``access_token` 等)在回显时被掩码。
- 配合 `--config <name>` 可写入指定 profile。
---
#### `kscli update`
将 CLI 自更新到最新版本,或用 `--to` 指定目标版本。
```bash
kscli update [--to <version>]
```
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ------------------------------------------------------------------------- |
| `--to <version>` | string | 否 | 目标版本semver`1.13.0` / `v1.13.0` / `0.0.0-beta-<sha>-<时间戳>` |
示例:
```bash
# 更新到最新版
kscli update
# 回滚/固定到指定版本
kscli update --to 1.13.0
```
**注意事项**
- 更新方式按安装来源自动选择npm 全局安装或二进制安装)。
- `--to` 传入非法 semver 会在本地被拦截并报错。
---
## 常见错误与排查
### Workspace ID 缺失
**报错**`Workspace ID is required.`
**原因**:所有知识库管理命令都需要 workspace ID 来构造 API 端点(`{workspaceId}.cn-beijing.maas.aliyuncs.com`)。
**解决**
```bash
# 方式1命令行参数
kscli kb list --workspace-id ws-xxx
# 方式2环境变量
export BAILIAN_WORKSPACE_ID=ws-xxx
# 方式3配置文件
kscli config set --key workspace_id --value ws-xxx
```
### 知识库 ID 不存在
**报错**`Knowledge base not found: idx-xxx`
**原因**`--index-id` 指定的知识库在当前 workspace 中不存在。
**解决**:先 `kscli kb list` 确认知识库 ID。
### 导入任务 SystemError
**报错**:服务端返回 `SystemError`
**原因**`doc status` 传入了不存在的 job ID或知识库空闲无任务。
**解决**:检查 `doc list` 输出中的 `ingestionId`,或从 `doc upload` / `kb create` 的返回值获取。
### doc_id 与 fileId 混淆
**问题**`doc delete` 时用了 `doc upload` 返回的 `fileId` 而非 `doc list` 返回的 `doc_id`
**原因**:通过 `kb create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。
**解决**:始终用 `kscli doc list --quiet` 获取 `doc_id`
### retrieve 已废弃
**问题**`retrieve` 命令输出废弃警告。
**解决**:改用 `search` 命令。`search` 通过 `--agent-id` 驱动检索策略支持多知识库、路由、rerank 等高级特性。`retrieve` 直接操作 `--index-id`,功能受限且不再迭代。
### OSS 导入权限错误
**报错**:服务端返回权限相关错误。
**原因**OSS bucket 未授权给平台服务角色。
**解决**:检查 RAM 控制台中的 `AliyunServiceRoleForBailian` 角色是否已正确授权。
### Chat SSE error
**报错**`Chat API error` + API error code。
**原因**:流式对话过程中服务端返回 error 事件。
**解决**:检查 `--agent-id` 是否存在、服务是否已部署、API Key 是否有效。错误消息和 code 原样透传,不二次包装。
### file list 返回空
**问题**`file list --category-id default` 返回空列表。
**原因**:与上传 API 不同,`file list` 不解析字面量 `default`,需要真实分类 ID。
**解决**:通过 `file get` 的 category 字段或 `category list` 获取真实分类 ID。
### 集合无法删除
**问题**:没有 `collection delete` 命令。
**原因**:暂不支持通过 CLI 删除。
**解决**:创建集合需谨慎。如需隔离,创建新集合并迁移文件。
---
## 附录:命令速查表
| 命令 | 功能 | 关键参数 |
| ------------------------- | ------------ | ----------------------------------------------------------- |
| `kscli kb list` | 列出知识库 | `--name` |
| `kscli kb info` | 知识库详情 | `--index-id` |
| `kscli kb create` | 创建知识库 | `--name`, `--description`, `--doc-id`/`--category-id` |
| `kscli kb update` | 更新知识库 | `--index-id`, `--name`/`--description`/`--rerank-min-score` |
| `kscli kb delete` | 删除知识库 | `--index-id`, `--yes` |
| `kscli kb stats` | 监控数据 | `--index-id`, `--start`/`--end` |
| `kscli doc list` | 文档列表 | `--index-id` |
| `kscli doc status` | 导入任务状态 | `--index-id`, `--job-id`, `--wait` |
| `kscli doc upload` | 上传文件 | `--file`, `--index-id`, `--wait` |
| `kscli doc delete` | 删除文档 | `--index-id`, `--doc-id` |
| `kscli doc tag` | 文件打标签 | `--doc-id`, `--tag`, `--mode` |
| `kscli doc import-oss` | OSS 导入 | `--bucket`, `--region`, `--oss-key` |
| `kscli service list` | 服务列表 | `--scene` |
| `kscli service get` | 服务详情 | `--agent-id` |
| `kscli service create` | 创建服务 | `--name`, `--scene`, `--index-id` |
| `kscli service update` | 更新服务 | `--agent-id`, 配置参数 |
| `kscli service deploy` | 发布服务 | `--agent-id`, `--yes` |
| `kscli service delete` | 删除服务 | `--agent-id`, `--yes` |
| `kscli service copy` | 复制服务 | `--agent-id` |
| `kscli chunk add` | 添加 chunk | `--index-id`, `--content`/`--field` |
| `kscli chunk list` | chunk 列表 | `--index-id`, `--doc-id` |
| `kscli chunk update` | 更新 chunk | `--index-id`, `--chunk-id`, `--doc-id` |
| `kscli chunk delete` | 删除 chunk | `--index-id`, `--chunk-id`, `--yes` |
| `kscli file list` | 文件列表 | `--category-id` |
| `kscli file get` | 文件详情 | `--file-id` |
| `kscli file delete` | 删除文件 | `--file-id`, `--yes` |
| `kscli collection create` | 创建集合 | `--name`, `--description` |
| `kscli collection get` | 集合详情 | `--collection-id`/`--name` |
| `kscli category list` | 分类列表 | `--collection-id`, `--parent-id` |
| `kscli category add` | 创建分类 | `--name`, `--parent-id` |
| `kscli category delete` | 删除分类 | `--category-id`, `--yes` |
| `kscli retrieve` | 检索(废弃) | `--index-id`, `--query` |
| `kscli search` | 语义检索 | `--query`, `--agent-id` |
| `kscli chat` | RAG 对话 | `--message`, `--agent-id` |
| `kscli config show` | 查看配置 | `--output` |
| `kscli config set` | 写入配置 | `--key`, `--value` |
| `kscli update` | 自更新 CLI | `--to` |
+218
View File
@@ -0,0 +1,218 @@
# 检索与对话命令手册
以下命令通过检索服务agent消费知识库。`search` 用于语义检索,`chat` 用于多轮对话。`retrieve` 已废弃。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli retrieve`
从知识库检索(已废弃,请用 `search` 替代)。
**用法**
```bash
kscli retrieve --index-id <id> --query <text> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------------------- | ------ | ---- | --------------------------------------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--query <text>` | string | 是 | 检索查询文本 |
| `--dense-similarity-top-k <n>` | number | 否 | 稠密检索 top K |
| `--sparse-similarity-top-k <n>` | number | 否 | 稀疏检索 top K |
| `--rerank` | switch | 否 | 启用 rerank |
| `--rerank-top-n <n>` | number | 否 | rerank 返回 top N 结果 |
| `--rerank-model <name>` | string | 否 | rerank 模型名,如 `qwen3-rerank-hybrid` |
| `--rerank-mode <mode>` | string | 否 | rerank 模式:`qa``similar``custom` |
| `--rerank-instruct <text>` | string | 否 | 自定义 rerank 指令(`--rerank-mode custom` 时使用) |
| `--top-k <n>` | number | 否 | 返回结果数(已废弃,用 `--rerank-top-n` 替代) |
**输出**
text/quiet 模式:
```
[1] (score: 0.9512)
检索到的文本内容...
[2] (score: 0.8734)
另一段文本内容...
```
> 无结果时输出 `No results found.`
json 模式:返回 API 原始响应。
**注意事项**
- **已废弃**,推荐使用 `search` 命令。`search` 通过 agent_id 驱动检索策略,支持更多高级特性。
- `--top-k` 已废弃,使用 `--rerank-top-n` 替代,传入 `--top-k` 会输出 stderr 警告。
- 此命令直接用 `--index-id` 检索,不需要创建检索服务。
**示例**
```bash
# 基础检索
kscli retrieve --index-id idx-xxx --query "How to use Alibaba Cloud Bailian" --workspace-id ws-xxx
# 启用 rerank
kscli retrieve --index-id idx-xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid
```
---
#### `kscli search`
对知识库执行语义检索RAG 检索)。
**用法**
```bash
kscli search --query <text> --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | ------------------------------------------------------------------- |
| `--query <text>` | string | 是 | 检索查询文本(不可为空) |
| `--agent-id <id>` | string | 是 | 检索服务 ID在控制台知识检索页面获取或通过 `service list` 查看) |
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
| `--image <url>` | array | 否 | 图片 URL可重复用于多模态检索 |
**参数约束**
- `--query` 不可为空API 要求 `minLength: 1`
**输出**
text/quiet 模式:
```
[1] (score: 0.9512)
检索到的文本内容...
[2] (score: 0.8734)
另一段文本内容...
```
> 无结果时输出 `No results found.`
json 模式:返回 API 原始响应,`data.nodes[]` 包含检索结果。
**注意事项**
- 检索范围和策略多知识库加权、路由、rerank 等)由 `--agent-id` 对应的服务配置驱动。只需 `--query``--agent-id` 即可调用。
- `--agent-version beta` 调试草稿配置进行调试,部署前验证效果。
-`retrieve` 的区别:`search` 通过 agent_id 间接驱动检索策略支持多知识库、路由、rerank 等),`retrieve` 直接操作 index_id 且功能较少。
**示例**
```bash
# 基础检索
kscli search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
# 多模态检索(带图片)
kscli search --query "describe this image" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg
# 调试草稿版本
kscli search --query "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
```
---
#### `kscli chat`
与知识库进行 RAG 对话(流式输出)。
**用法**
```bash
kscli chat --message <text> --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------ |
| `--message <text>` | array | 是¹ | 消息文本(可重复)。支持 `role:content` 前缀设置角色(如 `user:hello`),默认角色为 `user`。也支持完整 JSON 对象传递结构化消息 |
| `--agent-id <id>` | string | 是 | Q&A 服务 ID在控制台知识问答页面获取或通过 `service list --scene chat` 查看) |
| `--agent-version <version>` | string | 否 | 服务版本:`beta`(调试草稿)或已发布版本号;默认调用最新已发布版本 |
| `--image <url>` | array | 否 | 图片 URL可重复。附加到最后一条 user 消息作为多模态内容 |
> ¹ `--message` 或 `--image` 至少提供其一。纯图片查询可以只传 `--image`CLI 会自动创建空 user 消息承载图片)。
**参数约束**
- `--message``--image` 至少提供一个
- `--image` 不能与已包含 `image_url` 内容部分的消息同时使用
**输出**
**TTY text 模式**(实时流式):
```
🔍 Retrieving...
✍️ Generating...
这是AI生成的回答内容逐字流式输出...
```
> 进度标签由 SSE `step_change` 事件驱动:`tool_calling`(检索中)→ `plan_start`(规划中)→ `generation_start`(生成中)。
**非 TTY text 模式**(缓冲输出):
```
完整的回答文本...
```
**json 模式**`--output json`
```json
{
"answer": "完整的回答文本...",
"request_id": "xxx"
}
```
quiet 模式:输出完整的回答文本。
**注意事项**
- API 仅支持 SSE 流式响应。TTY 环境下实时打印 token非 TTY 环境缓冲后输出完整文本。
- SSE 事件生命周期:`tool_calling``tool_return``plan_start``planning``plan_end``generation_start``generating``generation_end``tool_calling``tool_return` 可能循环多次。
- 多轮对话:用 `--message "user:..."``--message "assistant:..."` 传递对话历史。
- `--agent-version beta` 调用草稿配置进行调试。
- `--image` 附加到最后一条 user 消息上。如果消息中已包含 `image_url` 内容部分,则不能再用 `--image`
- `--verbose` 模式下,所有 SSE 事件详情会输出到 stderr。
**示例**
```bash
# 单轮对话
kscli chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
# 多轮对话(带历史)
kscli chat \
--message "user:What is RAG?" \
--message "assistant:RAG is retrieval-augmented generation..." \
--message "How does it work?" \
--agent-id aid-xxx --workspace-id ws-xxx
# 多模态对话(带图片)
kscli chat \
--message "Describe these images" \
--image https://example.com/a.png \
--image https://example.com/b.png \
--agent-id aid-xxx --workspace-id ws-xxx
# 调试草稿版本
kscli chat --message "test" --agent-id aid-xxx --agent-version beta --workspace-id ws-xxx
```
---
← [返回总览](./kscli-cli-guide.md)
+401
View File
@@ -0,0 +1,401 @@
# 检索服务管理命令手册
检索服务(也称 agent是知识库的检索入口。通过 `--agent-id` 在 search/chat 命令中使用。服务有 `chat`(问答)和 `search`(检索)两种场景。
> **通用约定**鉴权、Workspace ID、全局参数、输出格式、危险操作确认、Dry-run 模式)请参阅 [总览文档](./kscli-cli-guide.md#通用约定)。
---
#### `kscli service list`
列出工作区中的检索/Q&A 服务。
**用法**
```bash
kscli service list --scene <chat|search> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------------ | ------ | ---- | ------------------------------------------------------- |
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`Q&A`search`(检索) |
| `--status <status>` | string | 否 | 按状态过滤:`draft``deployed`(含 edited`deleted` |
| `--name <text>` | string | 否 | 按服务名称模糊过滤 |
| `--agent-id <id>` | string | 否 | 按精确 agent ID 过滤 |
| `--index-id <id>` | string | 否 | 按关联知识库 ID 过滤 |
| `--page-number <n>` | number | 否 | 页码默认1 |
| `--page-size <n>` | number | 否 | 每页条数默认10最大 100 |
**参数约束**
- `--scene` 只能是 `chat``search`
- `--status` 只能是 `draft``deployed``deleted`
- `--page-size` 范围 1-100
**输出**
text 模式:
```
aid-xxx deployed 2 my-qa (kb: my-kb)
total: 1
Use an agent_id above with the chat command.
```
> 最后一行根据 scene 自动提示用 `search` 还是 `chat` 命令消费。
quiet 模式:每行一个 `agent_id`
json 模式:返回 API 原始响应。
**注意事项**
- 服务端要求 `--scene` 必填,要查看两种场景的服务需分别执行。
**示例**
```bash
# 列出 chat 服务
kscli service list --scene chat --workspace-id ws-xxx
# 只看已部署的检索服务
kscli service list --scene search --status deployed
```
---
#### `kscli service get`
查看服务详情,含各版本配置。
**用法**
```bash
kscli service get --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| --------------------------- | ------ | ---- | --------------------------------------------------------- |
| `--agent-id <id>` | string | 是 | 服务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
# 查看服务完整详情
kscli service get --agent-id aid-xxx --workspace-id ws-xxx
# 只看 beta 草稿配置
kscli service get --agent-id aid-xxx --agent-version beta
```
---
#### `kscli service create`
创建检索/Q&A 服务,初始状态为 draft版本为 beta。
**用法**
```bash
kscli service create --name <text> --scene <chat|search> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------------ | ------ | ---- | ------------------------------------------------- |
| `--name <text>` | string | 是 | 服务名称(最多 200 字符,同一场景下工作区内唯一) |
| `--scene <chat\|search>` | string | 是 | 服务场景:`chat`Q&A`search`(检索) |
| `--description <text>` | string | 建议 | 这个服务能回答什么、给谁用(最多 1000 字符) |
| `--index-id <id>` | string | 否 | 绑定此知识库;其他配置使用服务端默认值 |
**参数约束**
- `--name` 最多 200 字符
- `--scene` 只能是 `chat``search`
- `--description` 最多 1000 字符;建议填写 —— agent 靠它判断该调用哪个服务
**输出**
text 模式:
```
created: aid-xxx (status: draft, version: beta)
Test the draft with --agent-version beta on search/chat, then deploy it to publish.
```
quiet 模式:输出 agent ID。
json 模式:返回 API 原始响应。
**注意事项**
- 不指定 `--index-id` 时,服务端使用默认 agent 配置。
- beta 草稿可通过 search/chat 的 `--agent-version beta` 测试,部署后才生效。
- 需要工作区的知识库创建权限。
**示例**
```bash
# 创建 Q&A 服务
kscli service create --name my-qa --scene chat --workspace-id ws-xxx
# 创建检索服务并绑定知识库
kscli service create --name my-search --scene search --index-id idx-xxx
```
---
#### `kscli service update`
更新服务名称、描述或草稿配置。
**用法**
```bash
kscli service update --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ------------------------------ | ------ | ---- | ---------------------------------------------------------------------------------------- |
| `--agent-id <id>` | string | 是 | 服务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
# 调整温度
kscli service update --agent-id aid-xxx --temperature 0.7 --workspace-id ws-xxx
# 用 JSON 文件替换整个配置
kscli service update --agent-id aid-xxx --config-file ./agent-config.json
# 给已发布版本 1 加描述
kscli service update --agent-id aid-xxx --agent-version 1 --version-desc "first stable release"
```
---
#### `kscli service deploy`
发布 beta 草稿为新版本。
**用法**
```bash
kscli service deploy --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ---------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deployed: aid-xxx version 2
```
quiet 模式:输出新版本号。
json 模式:返回 API 原始响应。
**注意事项**
- 版本号自动递增,状态变为 `deployed`
- 发布影响线上调用方,确认提示会警告。
- 如果当前状态为 `edited`(已发布后又改了草稿),确认提示会额外警告「发布会覆盖线上行为」。
- 需要工作区的知识库修改权限。
**示例**
```bash
# 发布(交互确认)
kscli service deploy --agent-id aid-xxx --workspace-id ws-xxx
# 带描述并跳过确认
kscli service deploy --agent-id aid-xxx --version-desc "tuned rerank params" --yes
```
---
#### `kscli service delete`
删除检索/Q&A 服务(软删除,幂等)。
**用法**
```bash
kscli service delete --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
text 模式:
```
deleted: aid-xxx (status: deleted)
```
quiet 模式:无输出。
json 模式:返回 API 原始响应。
**注意事项**
- 删除不可撤销,`agent_id` 不再可用于 search/chat 调用。
- API 是幂等的:删除已删除的服务不会报错。
- 如果服务状态为 `deployed``edited`,确认提示会额外警告「此服务正在线上运行」。
- 需要工作区的知识库删除权限。
**示例**
```bash
# 删除(交互确认)
kscli service delete --agent-id aid-xxx --workspace-id ws-xxx
# 跳过确认
kscli service delete --agent-id aid-xxx --yes
```
---
#### `kscli service copy`
复制服务为新草稿(名称自动加 `copy_` 前缀)。
**用法**
```bash
kscli service copy --agent-id <id> [flags]
```
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--agent-id <id>` | string | 是 | 源服务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
# 复制服务
kscli service copy --agent-id aid-source --workspace-id ws-xxx
```
---
← [返回总览](./kscli-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"
+3
View File
@@ -166,6 +166,9 @@ bl config list
# Switch config profile
bl config use --name token-plan
# Switch the CLI interface to Chinese
bl config set --key language --value zh-CN
```
Config file location: `~/.bailian/config.json`
+3
View File
@@ -165,6 +165,9 @@ bl config list
# 切换配置档
bl config use --name token-plan
# 将 CLI 界面切换为中文
bl config set --key language --value zh-CN
```
配置文件位置:`~/.bailian/config.json`
+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.15.1",
"version": "1.17.1",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+72
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,
@@ -67,6 +98,7 @@ import {
finetuneTextCreate,
finetuneAudioCreate,
finetuneImageCreate,
finetuneVideoCreate,
finetuneList,
finetuneGet,
finetuneCancel,
@@ -76,6 +108,7 @@ import {
finetuneExport,
finetuneWatch,
finetuneCapability,
finetunePrice,
deployTextCreate,
deployAudioCreate,
deployImageCreate,
@@ -85,6 +118,8 @@ import {
deployScale,
deployUpdate,
deployDelete,
deployPause,
deployResume,
tokenPlanListSeats,
tokenPlanCreateKey,
tokenPlanAssignSeats,
@@ -157,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,
@@ -191,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,
@@ -200,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,
@@ -209,6 +279,8 @@ export const commands: Record<string, AnyCommand> = {
"deploy scale": deployScale,
"deploy update": deployUpdate,
"deploy delete": deployDelete,
"deploy pause": deployPause,
"deploy resume": deployResume,
"token-plan list-seats": tokenPlanListSeats,
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
+5 -4
View File
@@ -4,10 +4,11 @@ import { commandPackPolicy } from "./command-pack-policy.ts";
import pkg from "../package.json" with { type: "json" };
const quickStartTasks = [
"Help me generate a set of Amazon e-commerce main images for baseball caps (white background + lifestyle shots + model wear shots)",
"Help me generate a 3-minute humorous crosstalk audio clip",
"Help me generate a Little Red Riding Hood picture-book PDF (with illustrations)",
"Help me analyze this video and write a Xiaohongshu-style post",
"帮我创建一个能够生成短片分镜和视频的 Managed Agent。\n Help me create a Managed Agent that can generate short-film storyboards and videos.",
"生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。\n Generate an image of a cat in a spacesuit standing on Mars, then turn it into a video.",
"查看最近的模型用量、免费额度和限流情况。\n Check my recent model usage, free quota, and rate limits.",
"推荐一个适合图片理解和智能客服的模型。\n Recommend a model suitable for image understanding and intelligent customer service.",
"介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。\n Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.",
] as const;
void createCli(
+4 -1
View File
@@ -1,5 +1,8 @@
const ping = {
description: "Ping the Command Pack fixture",
description: {
"en-US": "Ping the Command Pack fixture",
"zh-CN": "调用 Command Pack 测试命令",
},
auth: "none",
flags: {
message: {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.15.1",
"version": "1.17.1",
"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": {
@@ -40,7 +40,7 @@
"check": "vp check"
},
"dependencies": {
"@openagentpack/sdk": "0.3.1",
"@openagentpack/sdk": "0.3.2",
"bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*",
"boxen": "catalog:",
@@ -226,24 +226,42 @@ function isEmptyResult(result: RecommendResult): boolean {
}
export default defineCommand({
description:
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
description: {
"en-US":
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
"zh-CN": "为你的使用场景推荐最佳模型(意图分析 → 候选召回 → LLM 排序)",
},
auth: "apiKey",
usageArgs: "--message <text> [flags]",
flags: {
message: {
type: "string",
valueHint: "<text>",
description: "Describe your requirements",
description: { "en-US": "Describe your requirements", "zh-CN": "描述你的需求" },
required: true,
},
},
exampleArgs: [
'--message "I need a visual-understanding chatbot"',
'--message "Build an Agent that auto-generates animations"',
'--message "Legal contract review, high precision required"',
'--message "Low-cost high-concurrency online customer service" --output text',
'--message "Long document summarization" --dry-run',
{
"en-US": '--message "I need a visual-understanding chatbot"',
"zh-CN": '--message "我需要一个能够理解图片的聊天机器人"',
},
{
"en-US": '--message "Build an Agent that auto-generates animations"',
"zh-CN": '--message "构建一个可以自动生成动画的智能体"',
},
{
"en-US": '--message "Legal contract review, high precision required"',
"zh-CN": '--message "审查法律合同,要求高准确率"',
},
{
"en-US": '--message "Low-cost high-concurrency online customer service" --output text',
"zh-CN": '--message "低成本、高并发的在线客服" --output text',
},
{
"en-US": '--message "Long document summarization" --dry-run',
"zh-CN": '--message "长文档摘要" --dry-run',
},
],
async run(ctx) {
const { settings, flags } = ctx;
+70 -17
View File
@@ -11,58 +11,111 @@ import {
import { ansi, emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Call a Bailian application (agent or workflow)",
description: {
"en-US": "Call a Bailian application (agent or workflow)",
"zh-CN": "调用百炼应用(智能体或工作流)",
},
auth: "apiKey",
usageArgs: "--app-id <id> --prompt <text> [flags]",
flags: {
appId: {
type: "string",
valueHint: "<id>",
description: "Application ID (required)",
description: { "en-US": "Application ID (required)", "zh-CN": "应用 ID必填" },
required: true,
},
prompt: {
type: "string",
valueHint: "<text>",
description: "Input prompt text",
description: { "en-US": "Input prompt text", "zh-CN": "输入提示词文本" },
required: true,
},
image: {
type: "array",
valueHint: "<url>",
description: "Image URL(s) to pass to the app (repeatable)",
description: {
"en-US": "Image URL(s) to pass to the app (repeatable)",
"zh-CN": "传给应用的图片 URL可重复",
},
},
fileId: {
type: "array",
valueHint: "<id>",
description: "Pre-uploaded file ID(s) (repeatable)",
description: {
"en-US": "Pre-uploaded file ID(s) (repeatable)",
"zh-CN": "已上传的文件 ID可重复",
},
},
sessionId: {
type: "string",
valueHint: "<id>",
description: "Session ID for multi-turn conversation",
description: {
"en-US": "Session ID for multi-turn conversation",
"zh-CN": "多轮对话的 Session ID",
},
},
stream: {
type: "switch",
description: {
"en-US": "Stream response (default: on in TTY)",
"zh-CN": "流式输出响应TTY 中默认开启)",
},
},
stream: { type: "switch", description: "Stream response (default: on in TTY)" },
pipelineIds: {
type: "string",
valueHint: "<ids>",
description: "Knowledge base pipeline IDs (comma-separated)",
description: {
"en-US": "Knowledge base pipeline IDs (comma-separated)",
"zh-CN": "知识库 Pipeline ID以逗号分隔",
},
},
memoryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Memory ID for long-term memory",
"zh-CN": "长期记忆使用的 Memory ID",
},
},
memoryId: { type: "string", valueHint: "<id>", description: "Memory ID for long-term memory" },
bizParams: {
type: "string",
valueHint: "<json>",
description: "Business parameters JSON (workflow variables)",
description: {
"en-US": "Business parameters JSON (workflow variables)",
"zh-CN": "业务参数 JSON工作流变量",
},
},
hasThoughts: {
type: "switch",
description: { "en-US": "Show agent thinking process", "zh-CN": "显示智能体思考过程" },
},
hasThoughts: { type: "switch", description: "Show agent thinking process" },
},
exampleArgs: [
'--app-id abc123 --prompt "Hello"',
'--app-id abc123 --prompt "Describe this image" --image https://example.com/photo.jpg',
'--app-id abc123 --prompt "Analyze the image" --image img1.jpg --image img2.jpg',
'--app-id abc123 --prompt "Continue" --session-id sess_xxx --stream',
'--app-id abc123 --prompt "Search for materials" --pipeline-ids pipe1,pipe2',
'--app-id abc123 --prompt "Start" --biz-params \'{"key":"value"}\'',
{
"en-US": '--app-id abc123 --prompt "Hello"',
"zh-CN": '--app-id abc123 --prompt "你好"',
},
{
"en-US":
'--app-id abc123 --prompt "Describe this image" --image https://example.com/photo.jpg',
"zh-CN": '--app-id abc123 --prompt "描述这张图片" --image https://example.com/photo.jpg',
},
{
"en-US": '--app-id abc123 --prompt "Analyze the image" --image img1.jpg --image img2.jpg',
"zh-CN": '--app-id abc123 --prompt "分析这些图片" --image img1.jpg --image img2.jpg',
},
{
"en-US": '--app-id abc123 --prompt "Continue" --session-id sess_xxx --stream',
"zh-CN": '--app-id abc123 --prompt "继续" --session-id sess_xxx --stream',
},
{
"en-US": '--app-id abc123 --prompt "Search for materials" --pipeline-ids pipe1,pipe2',
"zh-CN": '--app-id abc123 --prompt "搜索资料" --pipeline-ids pipe1,pipe2',
},
{
"en-US": '--app-id abc123 --prompt "Start" --biz-params \'{"key":"value"}\'',
"zh-CN": '--app-id abc123 --prompt "开始" --biz-params \'{"key":"value"}\'',
},
],
async run(ctx) {
const { settings, flags } = ctx;
+16 -5
View File
@@ -4,27 +4,38 @@ import { emitResult } from "bailian-cli-runtime";
const APP_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list";
export default defineCommand({
description: "List Bailian applications",
description: { "en-US": "List Bailian applications", "zh-CN": "列出百炼应用" },
auth: "console",
usageArgs: "[flags]",
flags: {
name: {
type: "string",
valueHint: "<name>",
description: "Filter by app name (keyword search)",
description: {
"en-US": "Filter by app name (keyword search)",
"zh-CN": "按应用名称筛选(关键词搜索)",
},
},
page: {
type: "number",
valueHint: "<n>",
description: "Page number (default: 1)",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 30)",
description: { "en-US": "Results per page (default: 30)", "zh-CN": "每页结果数默认30" },
},
},
exampleArgs: ["", "--name customer service", "--page 2 --page-size 10", "--output json"],
exampleArgs: [
"",
{
"en-US": "--name customer service",
"zh-CN": "--name 客户服务",
},
"--page 2 --page-size 10",
"--output json",
],
async run(ctx) {
const { settings, flags } = ctx;
const name = flags.name || "";
@@ -10,24 +10,33 @@ const FLAGS = {
accessKeyId: {
type: "string",
valueHint: "<id>",
description: "Alibaba Cloud Access Key ID",
description: { "en-US": "Alibaba Cloud Access Key ID", "zh-CN": "阿里云 Access Key ID" },
required: true,
},
accessKeySecret: {
type: "string",
valueHint: "<secret>",
description: "Alibaba Cloud Access Key Secret",
description: {
"en-US": "Alibaba Cloud Access Key Secret",
"zh-CN": "阿里云 Access Key Secret",
},
required: true,
},
securityToken: {
type: "string",
valueHint: "<token>",
description: "Alibaba Cloud STS Security Token to store (optional)",
description: {
"en-US": "Alibaba Cloud STS Security Token to store (optional)",
"zh-CN": "要保存的阿里云 STS Security Token可选",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "Generate a CLI access token using OpenAPI AK/SK",
description: {
"en-US": "Generate a CLI access token using OpenAPI AK/SK",
"zh-CN": "使用 OpenAPI AK/SK 生成 CLI Access Token",
},
auth: "none",
usageArgs: "--access-key-id <id> --access-key-secret <secret> --security-token <token>",
flags: FLAGS,
+31 -10
View File
@@ -15,8 +15,11 @@ function hasValue(value: unknown): value is string {
}
export default defineCommand({
description:
"Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist)",
description: {
"en-US":
"Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist)",
"zh-CN": "使用 API Key、控制台浏览器登录或 OpenAPI AK/SK 进行认证(多种凭证可共存)",
},
auth: "none",
usageArgs:
"--api-key <key> | --console | --open-api --access-key-id <id> --access-key-secret <secret>",
@@ -24,36 +27,54 @@ export default defineCommand({
apiKey: {
type: "string",
valueHint: "<key>",
description: "Model API key to store",
description: { "en-US": "Model API key to store", "zh-CN": "要保存的模型 API Key" },
},
baseUrl: {
type: "string",
valueHint: "<url>",
description: "Model API base URL (used with --api-key for validation)",
description: {
"en-US": "Model API base URL (used with --api-key for validation)",
"zh-CN": "模型 API Base URL用于配合 --api-key 进行验证)",
},
},
console: {
type: "switch",
description:
"Sign in via browser; use --console-site to choose domestic (default) or international",
description: {
"en-US":
"Sign in via browser; use --console-site to choose domestic (default) or international",
"zh-CN": "通过浏览器登录;使用 --console-site 选择国内站(默认)或国际站",
},
},
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
description: {
"en-US": "Console site: domestic, international",
"zh-CN": "控制台站点domestic、international",
},
},
openApi: {
type: "switch",
description: "Store Alibaba Cloud OpenAPI AK/SK credentials",
description: {
"en-US": "Store Alibaba Cloud OpenAPI AK/SK credentials",
"zh-CN": "保存阿里云 OpenAPI AK/SK 凭证",
},
},
accessKeyId: {
type: "string",
valueHint: "<id>",
description: "Alibaba Cloud Access Key ID to store",
description: {
"en-US": "Alibaba Cloud Access Key ID to store",
"zh-CN": "要保存的阿里云 Access Key ID",
},
},
accessKeySecret: {
type: "string",
valueHint: "<secret>",
description: "Alibaba Cloud Access Key Secret to store",
description: {
"en-US": "Alibaba Cloud Access Key Secret to store",
"zh-CN": "要保存的阿里云 Access Key Secret",
},
},
},
exampleArgs: [
+12 -3
View File
@@ -2,17 +2,26 @@ import { defineCommand } from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Clear stored credentials; full logout also clears the model Base URL",
description: {
"en-US": "Clear stored credentials; full logout also clears the model Base URL",
"zh-CN": "清除已保存的凭证;完整退出还会清除模型 Base URL",
},
auth: "none",
usageArgs: "[--console | --open-api] [--dry-run]",
flags: {
console: {
type: "switch",
description: "Only clear the console access_token, keep api_key intact",
description: {
"en-US": "Only clear the console access_token, keep api_key intact",
"zh-CN": "仅清除控制台 access_token保留 api_key",
},
},
openApi: {
type: "switch",
description: "Only clear OpenAPI AK/SK/STS credentials, keep other credentials intact",
description: {
"en-US": "Only clear OpenAPI AK/SK/STS credentials, keep other credentials intact",
"zh-CN": "仅清除 OpenAPI AK/SK/STS 凭证,保留其他凭证",
},
},
},
exampleArgs: ["", "--console", "--open-api", "--dry-run"],
@@ -3,7 +3,10 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
import { API_KEY_PAGE } from "bailian-cli-runtime";
export default defineCommand({
description: "Show current authentication state",
description: {
"en-US": "Show current authentication state",
"zh-CN": "显示当前认证状态",
},
auth: "none",
exampleArgs: ["", "--output json"],
async run(ctx) {
@@ -9,54 +9,73 @@ const FLAGS = {
agent: {
type: "string",
valueHint: "<name>",
description: `Target agent: ${VALID_AGENT_NAMES.join(", ")}`,
description: {
"en-US": `Target agent: ${VALID_AGENT_NAMES.join(", ")}`,
"zh-CN": `目标 Agent${VALID_AGENT_NAMES.join(", ")}`,
},
required: true,
choices: VALID_AGENT_NAMES,
},
baseUrl: {
type: "string",
valueHint: "<url>",
description: "API base URL",
description: { "en-US": "API base URL", "zh-CN": "API Base URL" },
},
region: {
type: "string",
valueHint: "<region>",
description:
"Model Studio region (e.g. cn-beijing, ap-southeast-1); converted into --base-url. Token Plan only",
description: {
"en-US":
"Model Studio region (e.g. cn-beijing, ap-southeast-1); converted into --base-url. Token Plan only",
"zh-CN":
"模型服务地域(例如 cn-beijing、ap-southeast-1将转换为 --base-url。仅用于 Token Plan",
},
},
apiKey: {
type: "string",
valueHint: "<key>",
description: "API key",
description: { "en-US": "API key", "zh-CN": "API Key" },
},
key: {
type: "string",
valueHint: "<encoded>",
description:
'Obfuscated API key from the web console (starts with "o1_"); decoded into --api-key',
description: {
"en-US":
'Obfuscated API key from the web console (starts with "o1_"); decoded into --api-key',
"zh-CN": '来自 Web 控制台的混淆 API Key以 "o1_" 开头);将解码为 --api-key',
},
},
model: {
type: "string",
valueHint: "<model>",
description: "Default model name",
description: { "en-US": "Default model name", "zh-CN": "默认模型名称" },
required: true,
},
contextWindow: {
type: "number",
valueHint: "<tokens>",
description: "OpenClaw only: model context window in tokens (default: 256000)",
description: {
"en-US": "OpenClaw only: model context window in tokens (default: 256000)",
"zh-CN": "仅 OpenClaw模型上下文窗口 Token 数默认256000",
},
},
wireApi: {
type: "string",
valueHint: "<api>",
description:
'Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0',
description: {
"en-US":
'Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0',
"zh-CN": '仅 Codex通信协议默认responses。"chat" 仅适用于旧版 Codex <= 0.80.0',
},
choices: ["chat", "responses"],
},
} satisfies FlagsDef;
export default defineCommand({
description: "Configure a coding agent to use DashScope API",
description: {
"en-US": "Configure a coding agent to use DashScope API",
"zh-CN": "配置编程 Agent 使用 DashScope API",
},
auth: "none",
usageArgs:
"--agent <name> (--base-url <url> | --region <region>) (--api-key <key> | --key <encoded>) --model <model>",
@@ -2,7 +2,10 @@ import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
export default defineCommand({
description: "List config profiles and show the active profile",
description: {
"en-US": "List config profiles and show the active profile",
"zh-CN": "列出配置 Profile 并显示当前激活项",
},
auth: "none",
exampleArgs: ["", "--output json"],
async run(ctx) {
@@ -1,3 +1,5 @@
import type { Language } from "bailian-cli-core";
/**
* Curated "Playground" scenarios surfaced in the config UI.
*
@@ -23,7 +25,7 @@ export interface Scenario {
inputs?: ScenarioInput[];
}
export const SCENARIOS: Scenario[] = [
export const SCENARIOS = [
// ---- 图像 ----
{
id: "image-generate",
@@ -150,17 +152,138 @@ export const SCENARIOS: Scenario[] = [
prompt:
"为当前工作目录的项目生成一个结构清晰的 README.md包含项目简介、安装步骤、使用示例、目录结构说明。请先阅读现有代码与配置再撰写内容必须与实际实现一致。",
},
];
] as const satisfies readonly Scenario[];
type ScenarioTranslation = Pick<Scenario, "title" | "description" | "category" | "prompt">;
type ScenarioId = (typeof SCENARIOS)[number]["id"];
const EN_US_SCENARIOS = {
"image-generate": {
title: "Text to image",
description: "Generate a sample image and save it to the output directory.",
category: "Image",
prompt:
"Use bl's image generation capability (such as `bl image generate`) to create a sample image: a corgi holding an umbrella in the rain, watercolor style, with soft lighting. Save it to the output directory and tell me the file path.",
},
"image-describe": {
title: "Image understanding",
description: "Pick an image from the output directory and describe its content and style.",
category: "Image",
prompt:
"Pick an image from the output directory (output/images by default). Describe its subject, composition, colors, and style in detail, then suggest suitable use cases. If the directory is empty, say so.",
},
"image-alt-batch": {
title: "Batch alt text",
description: "Generate accessible alt text for images in the output directory.",
category: "Image",
prompt:
"Scan all images in the output directory (output/images by default) and write concise, accurate accessibility alt text for each one. Summarize the results in a File name -> Alt text table. If the directory is empty, say so.",
},
"image-to-code": {
title: "Screenshot to code",
description: "Recreate a UI screenshot from the output directory with HTML and CSS.",
category: "Image",
prompt:
"Find a UI screenshot in the output directory (output/images by default) and recreate its layout, spacing, and colors as closely as possible with HTML and CSS. Save it as a single file that opens directly in a browser and briefly explain your approach. If no screenshot is available, say so.",
},
"speech-generate": {
title: "Text to speech",
description: "Turn a sample sentence into natural speech.",
category: "Audio",
prompt:
"Use bl's speech synthesis capability (such as a `bl speech` command) to turn this sentence into natural speech: Welcome to Alibaba Cloud Model Studio CLI, making multimodal creation easier. Save the audio to the output directory and tell me the file path.",
},
"audio-summarize": {
title: "Transcribe and summarize audio",
description: "Transcribe an audio file from the output directory and summarize its key points.",
category: "Audio",
prompt:
"Find an audio file in the output directory (output/speech by default), transcribe it, provide the full transcript, and then summarize the key points as a list. If the directory is empty or transcription is unavailable, explain that and try to complete the task with the capabilities available.",
},
"video-generate": {
title: "Text to video",
description: "Generate a sample short video.",
category: "Video",
prompt:
"Use bl's video generation capability (such as `bl video generate`) to create a sample short video: a teenager running along the beach at sunset, cinematic, in slow motion. Save it to the output directory and tell me the file path.",
},
"video-storyboard": {
title: "Video storyboard",
description: "Create a storyboard suitable for text-to-video generation.",
category: "Video",
prompt:
"Create a storyboard for a 15-30 second short video themed The first cup of coffee in the city at dawn. For each shot, provide the visual description, duration, subtitles or narration, and an English prompt ready for text-to-video generation.",
},
"media-prompt-craft": {
title: "Multimodal prompts",
description: "Expand a sample idea into image, video, and speech prompts.",
category: "Multimodal",
prompt:
"Expand the idea A night market in a futuristic cyber city into three high-quality generation prompts: 1) text to image, 2) text to video, and 3) speech style. Provide each prompt in both Chinese and English, with brief parameter recommendations.",
},
"image-story-narration": {
title: "Image narration",
description: "Write narration for an image in the output directory.",
category: "Multimodal",
prompt:
"Pick an image from the output directory (output/images by default) and write an engaging English narration of about 60 seconds. Then provide a plain-text version ready for speech synthesis. If the directory is empty, say so.",
},
"summarize-project": {
title: "Summarize this project",
description:
"Read the current directory and summarize its architecture, stack, and main modules.",
category: "Code",
prompt:
"Inspect the project structure and key source files in the current working directory, then concisely summarize: 1) what it does, 2) its technology stack, 3) its main modules and their responsibilities, and 4) notable design choices. Inspect the code before drawing conclusions; do not guess.",
},
"write-tests": {
title: "Add tests for a core module",
description: "Choose an under-tested core module and add unit tests.",
category: "Code",
prompt:
"Choose a core module in the current project that has no tests or weak coverage. Add comprehensive unit tests for its main branches and edge cases, following the project's existing test framework and style. Read the relevant files and dependencies before writing tests.",
},
"code-review": {
title: "Code review",
description: "Review core project code and identify concrete improvements.",
category: "Code",
prompt:
"Review the current project's core source code for potential bugs, security risks, performance issues, and maintainability problems. Give specific, actionable recommendations ordered by severity. Inspect the project structure and select the key files before reviewing them.",
},
"explain-code": {
title: "Explain core code",
description: "Choose an entry point or core module and explain how it works.",
category: "Code",
prompt:
"Choose the current project's entry point or a core module and explain its responsibilities, key execution flow, and dependencies. Use clear English and include the call relationships when useful.",
},
"generate-readme": {
title: "Generate README",
description: "Generate a clear README.md that matches the implementation.",
category: "Documentation",
prompt:
"Generate a clear README.md for the project in the current working directory. Include an overview, installation steps, usage examples, and a directory structure guide. Read the existing source code and configuration first; the content must match the actual implementation.",
},
} satisfies Record<ScenarioId, ScenarioTranslation>;
export function localizeScenarios(language: Language): Scenario[] {
if (language === "zh-CN") return SCENARIOS.map((scenario) => ({ ...scenario }));
return SCENARIOS.map((scenario) => ({
...scenario,
...EN_US_SCENARIOS[scenario.id],
}));
}
/** Look up a scenario by id, or undefined when unknown. */
export function getScenario(id: string): Scenario | undefined {
return SCENARIOS.find((s) => s.id === id);
export function getScenario(id: string, language: Language): Scenario | undefined {
return localizeScenarios(language).find((scenario) => scenario.id === id);
}
/** Fill a scenario's `{{placeholder}}` tokens from user-provided values. */
export function renderScenarioPrompt(scenario: Scenario, values: Record<string, string>): string {
return scenario.prompt.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => {
const v = values[key];
return typeof v === "string" ? v.trim() : "";
const value = values[key];
return typeof value === "string" ? value.trim() : "";
});
}
+9 -4
View File
@@ -3,25 +3,30 @@ import { emitResult } from "bailian-cli-runtime";
import { SECRET_KEYS, resolveKey, validateAndCoerce } from "./shared.ts";
export default defineCommand({
description: "Set a config value",
description: { "en-US": "Set a config value", "zh-CN": "设置配置项" },
auth: "none",
usageArgs: "--key <key> --value <value>",
flags: {
key: {
type: "string",
valueHint: "<key>",
description:
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
description: {
"en-US":
"Config key (language, base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
"zh-CN":
"配置项名称language、base_url、output、output_dir、timeout、api_key、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id",
},
required: true,
},
value: {
type: "string",
valueHint: "<value>",
description: "Value to set",
description: { "en-US": "Value to set", "zh-CN": "要设置的值" },
required: true,
},
},
exampleArgs: [
"--key language --value zh-CN",
"--key output --value json",
"--key timeout --value 600",
"--key base_url --value https://dashscope.aliyuncs.com",
@@ -1,7 +1,13 @@
import { BailianError, ExitCode, normalizeModelBaseUrl } from "bailian-cli-core";
import {
BailianError,
ExitCode,
normalizeModelBaseUrl,
SUPPORTED_LANGUAGES,
} from "bailian-cli-core";
/** Config keys that `config set` / `config ui` accept for read/write. */
export const VALID_KEYS = [
"language",
"base_url",
"output",
"output_dir",
@@ -47,6 +53,7 @@ export const UI_VALID_KEYS = [...VALID_KEYS, ...UI_EXTRA_KEYS] as const;
// Keys the UI renders as a fixed-choice dropdown instead of a free-text input.
export const UI_ENUM_KEYS: Record<string, string[]> = {
language: [...SUPPORTED_LANGUAGES],
output: ["text", "json"],
console_site: ["domestic", "international"],
};
@@ -144,6 +151,13 @@ export function validateAndCoerce(key: string, value: string): string | number {
);
}
if (resolvedKey === "language" && !(SUPPORTED_LANGUAGES as readonly string[]).includes(value)) {
throw new BailianError(
`Invalid language "${value}". Valid values: ${SUPPORTED_LANGUAGES.join(", ")}`,
ExitCode.USAGE,
);
}
if (resolvedKey === "output" && !["text", "json"].includes(value)) {
throw new BailianError(
`Invalid output format "${value}". Valid values: text, json`,
@@ -1,9 +1,9 @@
import { defineCommand, detectOutputFormat, maskToken } from "bailian-cli-core";
import { DEFAULT_LANGUAGE, defineCommand, detectOutputFormat, maskToken } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { SECRET_KEYS } from "./shared.ts";
export default defineCommand({
description: "Display current configuration",
description: { "en-US": "Display current configuration", "zh-CN": "显示当前配置" },
auth: "none",
exampleArgs: ["", "--output json"],
async run(ctx) {
@@ -14,6 +14,7 @@ export default defineCommand({
const result: Record<string, unknown> = {
...file,
language: file.language ?? DEFAULT_LANGUAGE,
base_url: client.baseUrl,
output: settings.output,
timeout: settings.timeout,
+192 -104
View File
@@ -4,7 +4,10 @@
// fetches carry the session token from the page URL. Visual language mirrors
// the bailian landing design system (Inter / Geist Mono, gradient accents,
// lift-on-hover cards).
export const PAGE_HTML = `<!doctype html>
import type { Language } from "bailian-cli-core";
import { renderConfigUiShell } from "./ui-i18n.ts";
const PAGE_HTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -30,6 +33,7 @@ export const PAGE_HTML = `<!doctype html>
--t-lift: .3s cubic-bezier(.2,.7,.2,1); --t-fast: .15s ease;
}
* { box-sizing: border-box; }
html:not(.i18n-ready) body { visibility: hidden; }
body { margin: 0; font-family: var(--font); font-size: 14px; line-height: 1.5;
color: var(--ink); background: #fafafc; -webkit-font-smoothing: antialiased; }
#app { display: flex; min-height: 100vh; }
@@ -694,6 +698,8 @@ export const PAGE_HTML = `<!doctype html>
var token = new URLSearchParams(location.search).get('token') || '';
var KEYS = [], SECRETS = [], ENUMS = {}, BOOLEANS = [], FIELD_DEFAULTS = {}, MODEL_CATALOG = {}, DATA = { default: {}, named: {} }, CURRENT = '', ACTIVE = 'default';
var loaded = { skills: false, mcp: false, agents: false, assets: false, playground: false, start: false };
var UI_LANGUAGE = '__BL_CONFIG_UI_LANGUAGE__';
var UI_TRANSLATIONS = __BL_CONFIG_UI_TRANSLATIONS__;
var SCENARIOS = [], BUILTIN_SCENARIOS = [], DISPATCH_AGENTS = [], SCN_Q = '', SCN_FILTER = 'all', SCN_PAGE = 1, DISPATCH_SCN = null;
var ASSETS = [], ASSET_FILTER = 'all', ASSET_SORT = 'new', ASSET_Q = '', ASSET_PAGE = 1;
var AGENTS = [], AGENT_FILTER = 'all', AGENT_Q = '', AGENT_PAGE = 1;
@@ -717,6 +723,67 @@ export const PAGE_HTML = `<!doctype html>
// Signed-in avatar photo. The generated colour + person icon stay as the
// background fallback while the image loads or if it fails.
var AVATAR_URL = 'https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg';
var UI_STATIC_COPY = [];
var UI_DOCUMENT_TITLE = document.title;
function translateUiText(value) {
var text = value == null ? '' : String(value);
if (UI_LANGUAGE !== 'zh-CN') return text;
for (var translationIndex = 0; translationIndex < UI_TRANSLATIONS.length; translationIndex++) {
var pair = UI_TRANSLATIONS[translationIndex];
text = text.split(pair[0]).join(pair[1]);
}
return text;
}
function captureStaticUiCopy() {
var root = document.getElementById('app');
function visit(node) {
if (node.nodeType === 3) {
if (node.nodeValue && node.nodeValue.trim()) UI_STATIC_COPY.push({ node: node, text: node.nodeValue });
return;
}
if (node.nodeType !== 1) return;
var tag = node.tagName;
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'CODE' || tag === 'PRE') return;
['title', 'placeholder', 'aria-label', 'alt'].forEach(function (name) {
if (node.hasAttribute(name)) UI_STATIC_COPY.push({ node: node, attr: name, text: node.getAttribute(name) });
});
Array.prototype.forEach.call(node.childNodes, visit);
}
visit(root);
}
function applyStaticUiCopy() {
document.documentElement.lang = UI_LANGUAGE;
document.title = translateUiText(UI_DOCUMENT_TITLE);
UI_STATIC_COPY.forEach(function (entry) {
var value = translateUiText(entry.text);
if (entry.attr) entry.node.setAttribute(entry.attr, value);
else entry.node.nodeValue = value;
});
document.documentElement.classList.add('i18n-ready');
}
function applyUiLanguage(language) {
if (language !== 'en-US' && language !== 'zh-CN') return;
UI_LANGUAGE = language;
SCN_FILTER = 'all';
SCN_PAGE = 1;
applyStaticUiCopy();
renderProfiles();
renderForm();
if (loaded.start) loadQuickStart();
if (loaded.skills) renderSkills();
if (loaded.mcp) renderMcp();
if (loaded.agents) { renderAgentFilters(); renderAgents(); }
if (loaded.assets) { renderAssetFilters(); renderAssets(); }
if (loaded.playground) { loaded.playground = false; loadPlayground(); }
if (AUTH) renderAccount(AUTH);
var sidebar = document.getElementById('sidebar');
var sidebarToggle = document.getElementById('sidebarToggle');
if (sidebar && sidebarToggle) {
var sidebarLabel = sidebar.classList.contains('is-collapsed') ? 'Expand sidebar' : 'Collapse sidebar';
sidebarToggle.setAttribute('aria-label', translateUiText(sidebarLabel));
sidebarToggle.title = translateUiText(sidebarLabel);
}
}
function setAvatar(elm, seed) {
elm.style.background = avatarStyle(seed);
elm.innerHTML = PERSON_SVG;
@@ -736,13 +803,16 @@ export const PAGE_HTML = `<!doctype html>
if (text !== undefined && text !== null) e.textContent = text;
return e;
}
function uiEl(tag, cls, text) {
return el(tag, cls, translateUiText(text));
}
function originBadge(origin) {
var o = origin === 'remote' ? 'remote' : 'local';
return el('span', 'origin ' + o, o === 'remote' ? 'Remote' : 'Local');
return el('span', 'origin ' + o, translateUiText(o === 'remote' ? 'Remote' : 'Local'));
}
function setStatus(msg, isErr) {
var s = document.getElementById('status');
s.textContent = msg || '';
s.textContent = translateUiText(msg || '');
s.className = isErr ? 'err' : 'muted';
}
function setCount(view, n) {
@@ -786,18 +856,18 @@ export const PAGE_HTML = `<!doctype html>
renderQuickStart(HEALTH, auth, agents);
}).catch(function (e) { renderError(body, e); });
}
function qsDesc(html) { var p = el('p', 'qs-desc'); p.innerHTML = html; return p; }
function qsDesc(html) { var p = el('p', 'qs-desc'); p.innerHTML = translateUiText(html); return p; }
function qsBtn(label, onclick, primary) {
var b = el('button', primary ? 'btn-primary' : 'btn-soft', label);
var b = el('button', primary ? 'btn-primary' : 'btn-soft', translateUiText(label));
b.type = 'button'; b.onclick = onclick; return b;
}
function qsStep(n, done, locked, title, descNode, actNode) {
var step = el('div', 'qs-step' + (done ? ' done' : '') + (locked ? ' locked' : ''));
var head = el('div', 'qs-head');
head.appendChild(el('span', 'qs-ic', done ? '\u2713' : String(n)));
if (done) head.appendChild(el('span', 'qs-done-tag', 'Done'));
if (done) head.appendChild(el('span', 'qs-done-tag', translateUiText('Done')));
step.appendChild(head);
step.appendChild(el('div', 'qs-title', title));
step.appendChild(el('div', 'qs-title', translateUiText(title)));
if (descNode) step.appendChild(descNode);
if (actNode && !done) { var wrap = el('div', 'qs-act'); wrap.appendChild(actNode); step.appendChild(wrap); }
return step;
@@ -884,7 +954,7 @@ export const PAGE_HTML = `<!doctype html>
t.onclick = function () { CURRENT = name; renderProfiles(); renderForm(); setStatus(''); openDrawer(); };
grid.appendChild(t);
});
var add = el('div', 'tile tile-add', '+ New profile');
var add = uiEl('div', 'tile tile-add', '+ New profile');
add.onclick = newProfile;
grid.appendChild(add);
}
@@ -913,7 +983,7 @@ export const PAGE_HTML = `<!doctype html>
}
function detailSection(label, node, action) {
var sec = el('div', 'detail-sec');
var head = el('div', 'detail-label', label);
var head = el('div', 'detail-label', translateUiText(label));
if (action) { head.classList.add('detail-label-row'); head.appendChild(action); }
sec.appendChild(head);
sec.appendChild(node);
@@ -1022,7 +1092,7 @@ export const PAGE_HTML = `<!doctype html>
return html;
}
function renderMarkdownInto(container, md) {
if (!md) { container.appendChild(el('div', 'loading', '(empty)')); return; }
if (!md) { container.appendChild(uiEl('div', 'loading', '(empty)')); return; }
var wrap = el('div', 'md-body');
wrap.innerHTML = renderMarkdown(md);
container.appendChild(wrap);
@@ -1039,10 +1109,10 @@ export const PAGE_HTML = `<!doctype html>
{ source: 'windsurf', label: 'Windsurf' },
{ source: 'gemini', label: 'Gemini' }
];
function setSkillErr(msg) { var e = document.getElementById('skillErr'); if (e) e.textContent = msg || ''; }
function setSkillErr(msg) { var e = document.getElementById('skillErr'); if (e) e.textContent = translateUiText(msg || ''); }
function openSkillInstall() {
var title = document.getElementById('infoTitle');
title.textContent = ''; title.appendChild(el('span', '', 'Add skill'));
title.textContent = ''; title.appendChild(uiEl('span', '', 'Add skill'));
var body = document.getElementById('infoBody'); body.innerHTML = '';
var sel = el('select', 'select'); sel.id = 'skillNewSource';
SKILL_TARGETS.forEach(function (s) { var o = el('option', '', s.label); o.value = s.source; sel.appendChild(o); });
@@ -1053,21 +1123,21 @@ export const PAGE_HTML = `<!doctype html>
var fileInp = el('input'); fileInp.id = 'skillFile'; fileInp.type = 'file'; fileInp.accept = '.zip,application/zip';
fileInp.style.display = 'none';
var picker = el('div', 'file-picker');
var pickBtn = el('button', 'file-pick-btn', 'Choose .zip file'); pickBtn.type = 'button';
var fileName = el('span', 'file-name', 'No file selected');
var pickBtn = uiEl('button', 'file-pick-btn', 'Choose .zip file'); pickBtn.type = 'button';
var fileName = uiEl('span', 'file-name', 'No file selected');
pickBtn.onclick = function () { fileInp.click(); };
fileInp.onchange = function () {
var f = fileInp.files && fileInp.files[0];
fileName.textContent = f ? f.name : 'No file selected';
fileName.textContent = f ? f.name : translateUiText('No file selected');
fileName.classList.toggle('has-file', !!f);
};
picker.appendChild(pickBtn); picker.appendChild(fileName); picker.appendChild(fileInp);
body.appendChild(detailSection('Skill package (.zip)', picker));
body.appendChild(el('p', 'mcp-note', 'The .zip must contain a SKILL.md at its root or inside a single top-level folder.'));
body.appendChild(uiEl('p', 'mcp-note', 'The .zip must contain a SKILL.md at its root or inside a single top-level folder.'));
var err = el('div', 'modal-err'); err.id = 'skillErr'; body.appendChild(err);
openInfoDrawer();
var foot = document.getElementById('infoFoot'); foot.innerHTML = ''; foot.hidden = false;
var install = el('button', 'btn-primary', 'Install'); install.type = 'button';
var install = uiEl('button', 'btn-primary', 'Install'); install.type = 'button';
install.onclick = function () { doInstallSkill(install); };
foot.appendChild(install);
}
@@ -1106,7 +1176,7 @@ export const PAGE_HTML = `<!doctype html>
chips.appendChild(el('span', 'chip', s.fileCount + ' files'));
body.appendChild(detailSection('Installed in', chips));
body.appendChild(detailSection('Path', el('div', 'detail-path', s.path)));
var codeSec = detailSection('SKILL.md', el('div', 'loading', 'Loading…'));
var codeSec = detailSection('SKILL.md', uiEl('div', 'loading', 'Loading…'));
body.appendChild(codeSec);
openInfoDrawer();
api('/api/skill?id=' + encodeURIComponent(s.id)).then(function (r) { return r.json(); }).then(function (j) {
@@ -1114,14 +1184,14 @@ export const PAGE_HTML = `<!doctype html>
renderMarkdownInto(codeSec, (j && j.content) || '');
}).catch(function (e) {
codeSec.removeChild(codeSec.lastChild);
codeSec.appendChild(el('div', 'err', 'Failed to load: ' + e));
codeSec.appendChild(uiEl('div', 'err', 'Failed to load: ' + e));
});
}
function makeSelect(key, options, current) {
var sel = document.createElement('select');
sel.id = 'f_' + key; sel.name = key; sel.className = 'select';
var blank = document.createElement('option'); blank.value = ''; blank.textContent = '(unset)';
var blank = document.createElement('option'); blank.value = ''; blank.textContent = translateUiText('(unset)');
sel.appendChild(blank);
options.forEach(function (opt) {
var o = document.createElement('option'); o.value = opt; o.textContent = opt;
@@ -1134,12 +1204,12 @@ export const PAGE_HTML = `<!doctype html>
function renderForm() {
var form = document.getElementById('form');
form.innerHTML = '';
document.getElementById('currentName').textContent = CURRENT === '' ? 'default (top-level)' : CURRENT;
document.getElementById('currentName').textContent = CURRENT === '' ? translateUiText('default (top-level)') : CURRENT;
document.getElementById('deleteBtn').style.display = CURRENT === '' ? 'none' : '';
var selectedName = CURRENT === '' ? 'default' : CURRENT;
var useBtn = document.getElementById('useBtn');
useBtn.disabled = selectedName === ACTIVE;
useBtn.textContent = selectedName === ACTIVE ? 'Active' : 'Save & Activate';
useBtn.textContent = translateUiText(selectedName === ACTIVE ? 'Active' : 'Save & Activate');
var data = profileData(CURRENT);
KEYS.forEach(function (key) {
var row = el('div', 'row');
@@ -1154,10 +1224,10 @@ export const PAGE_HTML = `<!doctype html>
// the key). 'new-password' reliably suppresses saved-credential autofill.
input.autocomplete = 'new-password';
input.setAttribute('autocorrect', 'off'); input.spellcheck = false;
var toggle = el('button', 'toggle', 'show'); toggle.type = 'button';
var toggle = el('button', 'toggle', translateUiText('show')); toggle.type = 'button';
toggle.onclick = function () {
if (input.type === 'password') { input.type = 'text'; toggle.textContent = 'hide'; }
else { input.type = 'password'; toggle.textContent = 'show'; }
if (input.type === 'password') { input.type = 'text'; toggle.textContent = translateUiText('hide'); }
else { input.type = 'password'; toggle.textContent = translateUiText('show'); }
};
var wrap = el('div', 'inputwrap'); wrap.appendChild(input); wrap.appendChild(toggle);
row.appendChild(label); row.appendChild(wrap);
@@ -1190,7 +1260,7 @@ export const PAGE_HTML = `<!doctype html>
function modelCatalogBlock(key, input) {
var opts = MODEL_CATALOG[key] || [];
var box = el('div', 'model-cat');
box.appendChild(el('div', 'model-cat-hint', 'Available ' + modelCatLabel(key) + ' models · click to use'));
box.appendChild(el('div', 'model-cat-hint', translateUiText('Available ' + modelCatLabel(key) + ' models · click to use')));
var chips = el('div', 'model-chips');
function mark() {
var cur = input.value.trim();
@@ -1211,7 +1281,7 @@ export const PAGE_HTML = `<!doctype html>
var note = modelCatNote(key);
if (note) {
var noteEl = el('div', 'model-cat-note');
noteEl.innerHTML = note;
noteEl.innerHTML = translateUiText(note);
box.appendChild(noteEl);
}
setTimeout(mark, 0);
@@ -1235,6 +1305,9 @@ export const PAGE_HTML = `<!doctype html>
if (!result.ok) throw new Error((result.json && result.json.error) || 'error');
var saved = result.json.saved || {};
if (name === '') DATA.default = saved; else DATA.named[name] = saved;
if (result.json.uiLanguage && result.json.uiLanguage !== UI_LANGUAGE) {
applyUiLanguage(result.json.uiLanguage);
}
return saved;
});
}
@@ -1259,17 +1332,17 @@ export const PAGE_HTML = `<!doctype html>
}
function setModalErr(msg) {
document.getElementById('modalErr').textContent = msg || '';
document.getElementById('modalErr').textContent = translateUiText(msg || '');
}
var _confirmOnOk = null;
function openConfirm(opts) {
opts = opts || {};
document.getElementById('confirmTitle').textContent = opts.title || 'Are you sure?';
document.getElementById('confirmMsg').textContent = opts.message || '';
document.getElementById('confirmTitle').textContent = translateUiText(opts.title || 'Are you sure?');
document.getElementById('confirmMsg').textContent = translateUiText(opts.message || '');
var ok = document.getElementById('confirmOk');
var cancel = document.getElementById('confirmCancel');
ok.textContent = opts.okLabel || 'Delete';
ok.textContent = translateUiText(opts.okLabel || 'Delete');
ok.className = opts.danger === false ? 'btn-primary' : 'btn-danger-solid';
cancel.hidden = !!opts.hideCancel;
_confirmOnOk = opts.onConfirm || null;
@@ -1322,6 +1395,9 @@ export const PAGE_HTML = `<!doctype html>
}).then(function (result) {
if (!result.ok) throw new Error('Saved, but activation failed: ' + ((result.json && result.json.error) || 'error'));
ACTIVE = result.json.activeProfile || 'default';
if (result.json.uiLanguage && result.json.uiLanguage !== UI_LANGUAGE) {
applyUiLanguage(result.json.uiLanguage);
}
renderProfiles(); renderForm(); setStatus('Saved and activated.');
}).catch(function (err) { setStatus(err.message || String(err), true); });
}
@@ -1350,8 +1426,8 @@ export const PAGE_HTML = `<!doctype html>
function renderEmpty(container, msg, hint) {
container.innerHTML = '';
var box = el('div', 'empty');
box.appendChild(el('div', '', msg));
if (hint) { var p = el('p', 'muted'); p.style.marginTop = '10px'; p.innerHTML = hint; box.appendChild(p); }
box.appendChild(el('div', '', translateUiText(msg)));
if (hint) { var p = el('p', 'muted'); p.style.marginTop = '10px'; p.innerHTML = translateUiText(hint); box.appendChild(p); }
container.appendChild(box);
}
function renderError(container, e) { renderEmpty(container, 'Failed to load: ' + e); }
@@ -1406,7 +1482,7 @@ export const PAGE_HTML = `<!doctype html>
}
});
bar.appendChild(navBtn('\u203a', info.page + 1, info.page >= info.pages));
var sel = el('select', 'pg-size'); sel.title = 'Items per page';
var sel = el('select', 'pg-size'); sel.title = translateUiText('Items per page');
PAGE_SIZES.forEach(function (s) {
var o = el('option', '', s + ' / page'); o.value = String(s);
if (s === getPageSize(view)) o.selected = true;
@@ -1461,7 +1537,7 @@ export const PAGE_HTML = `<!doctype html>
if (s.description) t.appendChild(el('p', 'tile-desc', s.description));
var srcRow = el('div', 'tile-foot');
(s.sources || []).forEach(function (src) { srcRow.appendChild(el('span', 'chip blue', src)); });
srcRow.appendChild(el('span', 'chip', s.fileCount + ' files'));
srcRow.appendChild(uiEl('span', 'chip', s.fileCount + ' files'));
t.appendChild(srcRow);
grid.appendChild(t);
});
@@ -1537,16 +1613,16 @@ export const PAGE_HTML = `<!doctype html>
body.appendChild(detailSection('Configuration' + (editable ? ' (editable)' : ''), block, copyButton(function () {
return editable ? document.getElementById('mcpEdit').value : json;
})));
if (!editable) body.appendChild(el('p', 'mcp-note', 'This source is read-only here (its config is TOML). Edit it directly in ' + (m.source || 'its config file') + '.'));
if (!editable) body.appendChild(uiEl('p', 'mcp-note', 'This source is read-only here (its config is TOML). Edit it directly in ' + (m.source || 'its config file') + '.'));
var err = el('div', 'modal-err'); err.id = 'mcpErr'; body.appendChild(err);
openInfoDrawer();
if (editable) {
var foot = document.getElementById('infoFoot'); foot.innerHTML = ''; foot.hidden = false;
var del = el('button', 'btn-danger', 'Delete'); del.type = 'button';
var del = uiEl('button', 'btn-danger', 'Delete'); del.type = 'button';
del.onclick = function () {
openConfirm({ title: 'Delete MCP server?', message: 'Remove "' + m.name + '" from ' + m.source + '? This rewrites the source config file.', onConfirm: function () { doDeleteMcp(m); } });
};
var save = el('button', 'btn-primary', 'Save'); save.type = 'button';
var save = uiEl('button', 'btn-primary', 'Save'); save.type = 'button';
save.onclick = function () { doSaveMcp(m, save); };
foot.appendChild(del); foot.appendChild(save);
}
@@ -1564,16 +1640,16 @@ export const PAGE_HTML = `<!doctype html>
{ id: 'qoderwork', label: 'QoderWork' }
];
function copyButton(getText) {
var b = el('button', 'copy-btn', 'Copy'); b.type = 'button';
var b = uiEl('button', 'copy-btn', 'Copy'); b.type = 'button';
b.onclick = function () {
var text = getText();
var done = function () { b.classList.add('copied'); b.textContent = 'Copied'; setTimeout(function () { b.classList.remove('copied'); b.textContent = 'Copy'; }, 1500); };
var done = function () { b.classList.add('copied'); b.textContent = translateUiText('Copied'); setTimeout(function () { b.classList.remove('copied'); b.textContent = translateUiText('Copy'); }, 1500); };
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done).catch(function () { fallbackCopy(text); done(); }); }
else { fallbackCopy(text); done(); }
};
return b;
}
function setMcpErr(msg) { var e = document.getElementById('mcpErr'); if (e) e.textContent = msg || ''; }
function setMcpErr(msg) { var e = document.getElementById('mcpErr'); if (e) e.textContent = translateUiText(msg || ''); }
function parseMcpEditor() {
var parsed = JSON.parse(document.getElementById('mcpEdit').value);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Config must be a JSON object.');
@@ -1605,7 +1681,7 @@ export const PAGE_HTML = `<!doctype html>
}
function openMcpCreate() {
var title = document.getElementById('infoTitle');
title.textContent = ''; title.appendChild(el('span', '', 'New MCP server'));
title.textContent = ''; title.appendChild(uiEl('span', '', 'New MCP server'));
var body = document.getElementById('infoBody'); body.innerHTML = '';
var sel = el('select', 'select'); sel.id = 'mcpNewSource';
MCP_SOURCES.forEach(function (s) { var o = el('option', '', s.label); o.value = s.id; sel.appendChild(o); });
@@ -1618,7 +1694,7 @@ export const PAGE_HTML = `<!doctype html>
var err = el('div', 'modal-err'); err.id = 'mcpErr'; body.appendChild(err);
openInfoDrawer();
var foot = document.getElementById('infoFoot'); foot.innerHTML = ''; foot.hidden = false;
var create = el('button', 'btn-primary', 'Create'); create.type = 'button';
var create = uiEl('button', 'btn-primary', 'Create'); create.type = 'button';
create.onclick = function () { doCreateMcp(create); };
foot.appendChild(create);
}
@@ -1664,7 +1740,7 @@ export const PAGE_HTML = `<!doctype html>
var labels = { all: 'All', local: 'Local', remote: 'Remote' };
['all', 'local', 'remote'].forEach(function (cat) {
var b = el('button', 'filter' + (cat === AGENT_FILTER ? ' is-active' : ''));
b.appendChild(el('span', '', labels[cat]));
b.appendChild(uiEl('span', '', labels[cat]));
b.appendChild(el('span', 'n', String(counts[cat] || 0)));
b.onclick = function () { AGENT_FILTER = cat; AGENT_PAGE = 1; renderAgentFilters(); renderAgents(); };
bar.appendChild(b);
@@ -1701,9 +1777,9 @@ export const PAGE_HTML = `<!doctype html>
var meta = el('div', 'tile-meta');
meta.appendChild(originBadge(a.origin));
var pill;
if (a.installed && a.configured) pill = el('span', 'pill ok', 'Connected');
else if (a.installed) pill = el('span', 'pill neutral', 'Installed');
else pill = el('span', 'pill off', 'Not installed');
if (a.installed && a.configured) pill = uiEl('span', 'pill ok', 'Connected');
else if (a.installed) pill = uiEl('span', 'pill neutral', 'Installed');
else pill = uiEl('span', 'pill off', 'Not installed');
meta.appendChild(pill);
t.appendChild(meta);
var foot = el('div', 'tile-foot');
@@ -1718,17 +1794,17 @@ export const PAGE_HTML = `<!doctype html>
var actions = el('div', 'tile-actions');
var launch = el('button', 'icon-run');
launch.innerHTML = PLAY_SVG;
launch.setAttribute('aria-label', 'Quick launch');
launch.setAttribute('aria-label', translateUiText('Quick launch'));
var connected = a.installed && a.configured;
var st = el('span', 'launch-status');
if (connected && a.launchable) {
launch.title = 'Open a new terminal and start this agent';
launch.title = translateUiText('Open a new terminal and start this agent');
launch.onclick = function (e) { e.stopPropagation(); launchAgentCli(a, launch, st); };
} else {
launch.disabled = true;
if (!a.installed) launch.title = 'Install this agent before launching';
else if (!connected) launch.title = 'Connect this agent to bailian-cli before launching';
else launch.title = 'The CLI for this agent was not found on your PATH — install it before launching';
if (!a.installed) launch.title = translateUiText('Install this agent before launching');
else if (!connected) launch.title = translateUiText('Connect this agent to bailian-cli before launching');
else launch.title = translateUiText('The CLI for this agent was not found on your PATH — install it before launching');
}
actions.appendChild(st);
actions.appendChild(launch);
@@ -1741,15 +1817,15 @@ export const PAGE_HTML = `<!doctype html>
function launchAgentCli(a, btn, st) {
btn.disabled = true;
st.textContent = 'Launching…'; st.className = 'launch-status';
st.textContent = translateUiText('Launching…'); st.className = 'launch-status';
api('/api/agent/launch?id=' + encodeURIComponent(a.id), { method: 'POST' })
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
.then(function (res) {
btn.disabled = false;
if (!res.ok) { st.textContent = (res.j && res.j.error) || 'Launch failed'; st.className = 'launch-status err'; return; }
st.textContent = 'Launched → ' + (res.j.command || a.id); st.className = 'launch-status ok';
if (!res.ok) { st.textContent = (res.j && res.j.error) || translateUiText('Launch failed'); st.className = 'launch-status err'; return; }
st.textContent = translateUiText('Launched → ') + (res.j.command || a.id); st.className = 'launch-status ok';
})
.catch(function (e) { btn.disabled = false; st.textContent = 'Launch failed: ' + e; st.className = 'launch-status err'; });
.catch(function (e) { btn.disabled = false; st.textContent = translateUiText('Launch failed: ') + e; st.className = 'launch-status err'; });
}
function openAgentDetail(a) {
@@ -1770,9 +1846,9 @@ export const PAGE_HTML = `<!doctype html>
var chips = el('div', 'detail-chips');
chips.appendChild(el('span', 'chip', d.id));
var pill;
if (d.installed && d.configured) pill = el('span', 'pill ok', 'Connected');
else if (d.installed) pill = el('span', 'pill neutral', 'Installed');
else pill = el('span', 'pill off', 'Not installed');
if (d.installed && d.configured) pill = uiEl('span', 'pill ok', 'Connected');
else if (d.installed) pill = uiEl('span', 'pill neutral', 'Installed');
else pill = uiEl('span', 'pill off', 'Not installed');
chips.appendChild(pill);
body.appendChild(detailSection('Status', chips));
if (d.fields && d.fields.length) {
@@ -1784,13 +1860,13 @@ export const PAGE_HTML = `<!doctype html>
row.appendChild(val);
if (f.secret && f.raw) {
var shown = false;
var btn = el('button', 'kv-reveal', 'Show');
var btn = uiEl('button', 'kv-reveal', 'Show');
btn.type = 'button';
btn.onclick = function () {
shown = !shown;
val.textContent = shown ? f.raw : f.value;
if (shown) { val.classList.remove('secret'); } else { val.classList.add('secret'); }
btn.textContent = shown ? 'Hide' : 'Show';
btn.textContent = translateUiText(shown ? 'Hide' : 'Show');
};
row.appendChild(btn);
}
@@ -1798,9 +1874,9 @@ export const PAGE_HTML = `<!doctype html>
});
body.appendChild(detailSection('Configuration', kv));
} else if (d.installed) {
body.appendChild(detailSection('Configuration', el('div', 'detail-path', 'bailian-cli is not wired into this agent yet.')));
body.appendChild(detailSection('Configuration', uiEl('div', 'detail-path', 'bailian-cli is not wired into this agent yet.')));
} else {
body.appendChild(detailSection('Configuration', el('div', 'detail-path', 'This agent is not installed yet.')));
body.appendChild(detailSection('Configuration', uiEl('div', 'detail-path', 'This agent is not installed yet.')));
}
if (d.files && d.files.length) {
var wrap = el('div', '');
@@ -1821,9 +1897,9 @@ export const PAGE_HTML = `<!doctype html>
item.appendChild(el('pre', 'detail-code', fl.text));
sw.appendChild(item);
});
var openBtn = el('button', 'kv-reveal', 'Open');
var openBtn = uiEl('button', 'kv-reveal', 'Open');
openBtn.type = 'button';
openBtn.title = 'Open the config file with the system default app';
openBtn.title = translateUiText('Open the config file with the system default app');
openBtn.onclick = function () { openAgentSettings(d); };
body.appendChild(detailSection('Settings', sw, openBtn));
}
@@ -1848,18 +1924,23 @@ export const PAGE_HTML = `<!doctype html>
renderScenarios();
}).catch(function (e) { renderError(body, e); });
}
var SCN_ORDER = ['图像', '音频', '视频', '多模态', '代码', '文档'];
function scenarioOrder() {
return UI_LANGUAGE === 'zh-CN'
? ['图像', '音频', '视频', '多模态', '代码', '文档']
: ['Image', 'Audio', 'Video', 'Multimodal', 'Code', 'Documentation'];
}
function scenarioCategories() {
var present = {};
var order = scenarioOrder();
SCENARIOS.forEach(function (s) { if (s.category) present[s.category] = true; });
var cats = SCN_ORDER.filter(function (c) { return present[c]; });
Object.keys(present).forEach(function (c) { if (SCN_ORDER.indexOf(c) < 0) cats.push(c); });
var cats = order.filter(function (c) { return present[c]; });
Object.keys(present).forEach(function (c) { if (order.indexOf(c) < 0) cats.push(c); });
return cats;
}
function renderScenarioFilters() {
var bar = document.getElementById('scenarioFilters');
bar.innerHTML = '';
var defs = [{ key: 'all', label: 'All', n: SCENARIOS.length }];
var defs = [{ key: 'all', label: translateUiText('All'), n: SCENARIOS.length }];
scenarioCategories().forEach(function (c) {
defs.push({ key: c, label: c, n: SCENARIOS.filter(function (s) { return s.category === c; }).length });
});
@@ -1875,7 +1956,7 @@ export const PAGE_HTML = `<!doctype html>
return matchQ(s.title, q) || matchQ(s.description, q) || matchQ(s.category, q);
}
function pgWarnNode() {
return el('div', 'pg-warn', 'No connected agent can accept tasks yet. Install and connect qwen-code (or another supported agent) first, and make sure its CLI is on your PATH.');
return uiEl('div', 'pg-warn', 'No connected agent can accept tasks yet. Install and connect qwen-code (or another supported agent) first, and make sure its CLI is on your PATH.');
}
function renderScenarios() {
var body = document.getElementById('playgroundBody');
@@ -1896,7 +1977,7 @@ export const PAGE_HTML = `<!doctype html>
var grid = el('div', 'grid');
info.items.forEach(function (s) {
var t = el('div', 'tile clickable');
t.title = 'Click to edit this scenario';
t.title = translateUiText('Click to edit this scenario');
t.onclick = function () { openScnDrawer(s); };
var top = el('div', 'tile-top');
top.appendChild(el('span', 'tile-name', s.title));
@@ -1908,12 +1989,12 @@ export const PAGE_HTML = `<!doctype html>
var actions = el('div', 'tile-actions');
var run = el('button', 'icon-run');
run.innerHTML = PLAY_SVG;
run.setAttribute('aria-label', 'Run it');
run.setAttribute('aria-label', translateUiText('Run it'));
if (DISPATCH_AGENTS.length) {
run.title = 'Dispatch this task to a local agent';
run.title = translateUiText('Dispatch this task to a local agent');
run.onclick = function (e) { e.stopPropagation(); openDispatch(s); };
} else {
run.disabled = true; run.title = 'No connected agent available';
run.disabled = true; run.title = translateUiText('No connected agent available');
}
actions.appendChild(run);
t.appendChild(actions);
@@ -1943,7 +2024,7 @@ export const PAGE_HTML = `<!doctype html>
if (!DISPATCH_SCN) return;
document.getElementById('dispatchPreview').textContent = fillPrompt(DISPATCH_SCN, dispatchValues());
}
function setDispatchErr(m) { document.getElementById('dispatchErr').textContent = m || ''; }
function setDispatchErr(m) { document.getElementById('dispatchErr').textContent = translateUiText(m || ''); }
var LAST_AGENT_KEY = 'bl.lastDispatchAgent';
function openDispatch(s) {
DISPATCH_SCN = s;
@@ -2052,7 +2133,7 @@ export const PAGE_HTML = `<!doctype html>
return arr.filter(function (s) { return s && s.id && s.title && s.prompt; }).map(function (s) {
return {
id: s.id, title: s.title, description: s.description || '',
category: s.category || 'Custom', prompt: s.prompt,
category: s.category || translateUiText('Custom'), prompt: s.prompt,
inputs: Array.isArray(s.inputs) ? s.inputs : [], custom: true
};
});
@@ -2072,12 +2153,12 @@ export const PAGE_HTML = `<!doctype html>
var l = el('input'); l.className = 'scn-l'; l.placeholder = 'label'; l.autocomplete = 'off'; l.spellcheck = false;
var p = el('input'); p.className = 'scn-p'; p.placeholder = 'placeholder (optional)'; p.autocomplete = 'off'; p.spellcheck = false;
if (inp) { k.value = inp.key || ''; l.value = inp.label || ''; p.value = inp.placeholder || ''; }
var del = el('button', 'btn-mini', '\u00d7'); del.type = 'button'; del.title = 'Remove'; del.onclick = function () { row.remove(); };
var del = el('button', 'btn-mini', '\u00d7'); del.type = 'button'; del.title = translateUiText('Remove'); del.onclick = function () { row.remove(); };
row.appendChild(k); row.appendChild(l); row.appendChild(p); row.appendChild(del);
return row;
}
function addScnInputRow(inp) { document.getElementById('scnInputRows').appendChild(scnInputRow(inp)); }
function setScnErr(m) { document.getElementById('scnDrawerErr').textContent = m || ''; }
function setScnErr(m) { document.getElementById('scnDrawerErr').textContent = translateUiText(m || ''); }
function fillScnCatList() {
var dl = document.getElementById('scnCatList'); dl.innerHTML = '';
scenarioCategories().forEach(function (c) { var o = document.createElement('option'); o.value = c; dl.appendChild(o); });
@@ -2086,9 +2167,9 @@ export const PAGE_HTML = `<!doctype html>
SCN_EDIT_ID = existing ? existing.id : null;
SCN_EDIT_KIND = existing ? (existing.custom ? 'custom' : 'builtin') : 'new';
var isEdit = !!existing;
document.getElementById('scnDrawerTitle').textContent = isEdit ? (existing.custom ? 'Edit scenario' : 'Edit preset scenario') : 'Custom scenario';
document.getElementById('scnDrawerTitle').textContent = translateUiText(isEdit ? (existing.custom ? 'Edit scenario' : 'Edit preset scenario') : 'Custom scenario');
document.getElementById('scnTitle').value = existing ? existing.title : '';
document.getElementById('scnCategory').value = existing ? (existing.category || '') : 'Custom';
document.getElementById('scnCategory').value = existing ? (existing.category || '') : translateUiText('Custom');
document.getElementById('scnDesc').value = existing ? (existing.description || '') : '';
document.getElementById('scnPrompt').value = existing ? existing.prompt : '';
var rows = document.getElementById('scnInputRows'); rows.innerHTML = '';
@@ -2096,7 +2177,7 @@ export const PAGE_HTML = `<!doctype html>
fillScnCatList();
var del = document.getElementById('scnDelete');
del.hidden = !isEdit;
del.textContent = 'Delete';
del.textContent = translateUiText('Delete');
setScnErr('');
document.getElementById('scnDrawer').hidden = false;
document.body.style.overflow = 'hidden';
@@ -2131,7 +2212,7 @@ export const PAGE_HTML = `<!doctype html>
if (!prompt) { setScnErr('Prompt template is required.'); return; }
var res = collectScnInputs();
if (res.error) { setScnErr(res.error); return; }
var cat = document.getElementById('scnCategory').value.trim() || 'Custom';
var cat = document.getElementById('scnCategory').value.trim() || translateUiText('Custom');
var desc = document.getElementById('scnDesc').value.trim();
if (SCN_EDIT_KIND === 'builtin' && SCN_EDIT_ID && isBuiltin(SCN_EDIT_ID)) {
var ov = loadOverrides();
@@ -2187,10 +2268,10 @@ export const PAGE_HTML = `<!doctype html>
return '\uD83D\uDCC4';
}
function assetKindLabel(kind) {
if (kind === 'image') return 'Image';
if (kind === 'video') return 'Video';
if (kind === 'audio') return 'Audio';
return 'File';
if (kind === 'image') return translateUiText('Image');
if (kind === 'video') return translateUiText('Video');
if (kind === 'audio') return translateUiText('Audio');
return translateUiText('File');
}
function fmtDim(w, h) { return w && h ? w + ' \u00d7 ' + h : ''; }
@@ -2243,14 +2324,14 @@ export const PAGE_HTML = `<!doctype html>
tabs.forEach(function (t) {
var cat = t[0];
var b = el('button', 'filter' + (cat === ASSET_FILTER ? ' is-active' : ''));
b.appendChild(el('span', '', t[1]));
b.appendChild(uiEl('span', '', t[1]));
b.appendChild(el('span', 'n', String(counts[cat] || 0)));
b.onclick = function () { ASSET_FILTER = cat; ASSET_PAGE = 1; renderAssetFilters(); renderAssets(); };
bar.appendChild(b);
});
var sort = el('button', 'filter sort-toggle');
sort.title = 'Toggle sort by generation time';
sort.textContent = ASSET_SORT === 'new' ? '↓ Newest first' : '↑ Oldest first';
sort.title = translateUiText('Toggle sort by generation time');
sort.textContent = translateUiText(ASSET_SORT === 'new' ? '↓ Newest first' : '↑ Oldest first');
sort.onclick = function () { ASSET_SORT = ASSET_SORT === 'new' ? 'old' : 'new'; ASSET_PAGE = 1; renderAssetFilters(); renderAssets(); };
bar.appendChild(sort);
}
@@ -2285,7 +2366,7 @@ export const PAGE_HTML = `<!doctype html>
var mediaEl = null;
if (a.kind === 'image') {
var img = el('img'); img.src = src; img.loading = 'lazy'; img.alt = a.name;
img.title = 'View details';
img.title = translateUiText('View details');
media.appendChild(img); mediaEl = img;
} else if (a.kind === 'video') {
var vid = el('video'); vid.src = src; vid.controls = true; vid.preload = 'metadata';
@@ -2297,16 +2378,16 @@ export const PAGE_HTML = `<!doctype html>
media.appendChild(au);
} else {
var icon = el('span', 'asset-icon', assetIcon(a.kind));
icon.title = 'View details';
icon.title = translateUiText('View details');
media.appendChild(icon);
}
card.appendChild(media);
card.appendChild(el('span', 'asset-cat', assetKindLabel(a.kind)));
var del = el('button', 'asset-del', '×'); del.title = 'Delete';
var del = el('button', 'asset-del', '×'); del.title = translateUiText('Delete');
del.onclick = function (e) { e.stopPropagation(); deleteAsset(a); };
card.appendChild(del);
var b = el('div', 'asset-body');
var nm = el('div', 'asset-name link', a.name); nm.title = 'View details — ' + a.relPath;
var nm = el('div', 'asset-name link', a.name); nm.title = translateUiText('View details — ') + a.relPath;
b.appendChild(nm);
var folder = assetFolder(a.relPath);
if (folder) {
@@ -2363,9 +2444,9 @@ export const PAGE_HTML = `<!doctype html>
body.appendChild(detailSection('Details', chips));
body.appendChild(detailSection('Path', el('div', 'detail-path', a.relPath)));
openInfoDrawer();
var openBtn = el('button', 'btn-primary', 'Open locally');
var openBtn = uiEl('button', 'btn-primary', 'Open locally');
openBtn.onclick = function () { openAsset(a); };
var delBtn = el('button', 'btn-danger', 'Delete');
var delBtn = uiEl('button', 'btn-danger', 'Delete');
delBtn.onclick = function () { deleteAsset(a, true); };
var foot = document.getElementById('infoFoot');
foot.appendChild(openBtn); foot.appendChild(delBtn);
@@ -2393,6 +2474,8 @@ export const PAGE_HTML = `<!doctype html>
}
/* ---------- wiring ---------- */
captureStaticUiCopy();
applyStaticUiCopy();
var navItems = document.querySelectorAll('.nav-item');
for (var n = 0; n < navItems.length; n++) {
navItems[n].onclick = function () { showView(this.getAttribute('data-view')); };
@@ -2468,8 +2551,8 @@ export const PAGE_HTML = `<!doctype html>
var btn = document.getElementById('copyUrl');
btn.onclick = function () {
var done = function () {
btn.textContent = 'Copied'; btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1400);
btn.textContent = translateUiText('Copied'); btn.classList.add('copied');
setTimeout(function () { btn.textContent = translateUiText('Copy'); btn.classList.remove('copied'); }, 1400);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(done).catch(function () { fallbackCopy(url); done(); });
@@ -2485,10 +2568,10 @@ export const PAGE_HTML = `<!doctype html>
}
function authMethodLabel(p) {
if (p === 'console') return 'Console gateway';
if (p === 'apiKey') return 'API key';
if (p === 'console') return translateUiText('Console gateway');
if (p === 'apiKey') return translateUiText('API key');
if (p === 'openapi') return 'OpenAPI (AK/SK)';
return 'Account';
return translateUiText('Account');
}
function acctHue(seed) {
var h = 0, s = seed || 'bl';
@@ -2533,7 +2616,7 @@ export const PAGE_HTML = `<!doctype html>
var meta = [];
if (st.region) meta.push(st.region);
if (st.site) meta.push(st.site);
document.getElementById('acctMeta').textContent = meta.join(' · ') || 'Authenticated';
document.getElementById('acctMeta').textContent = meta.join(' · ') || translateUiText('Authenticated');
var tok = document.getElementById('acctToken');
tok.textContent = st.masked || '';
tok.hidden = !st.masked;
@@ -2542,7 +2625,7 @@ export const PAGE_HTML = `<!doctype html>
closeAcctMenu();
loginBtn.hidden = false;
loginBtn.disabled = false;
loginBtn.querySelector('span').textContent = 'Log in';
loginBtn.querySelector('span').textContent = translateUiText('Log in');
}
}
function toggleAcctMenu() {
@@ -2560,8 +2643,9 @@ export const PAGE_HTML = `<!doctype html>
}
function setLoginUi(busy, label) {
var b = document.getElementById('loginBtn');
if (b) { b.disabled = busy; b.querySelector('span').textContent = label; }
if (QS_LOGIN_BTN && document.body.contains(QS_LOGIN_BTN)) { QS_LOGIN_BTN.disabled = busy; QS_LOGIN_BTN.textContent = label; }
var translatedLabel = translateUiText(label);
if (b) { b.disabled = busy; b.querySelector('span').textContent = translatedLabel; }
if (QS_LOGIN_BTN && document.body.contains(QS_LOGIN_BTN)) { QS_LOGIN_BTN.disabled = busy; QS_LOGIN_BTN.textContent = translatedLabel; }
}
function startLogin() {
setLoginUi(true, 'Opening browser…');
@@ -2607,3 +2691,7 @@ export const PAGE_HTML = `<!doctype html>
</body>
</html>
`;
export function renderConfigUiHtml(language: Language): string {
return renderConfigUiShell(PAGE_HTML, language);
}
@@ -0,0 +1,369 @@
import type { Language } from "bailian-cli-core";
/**
* Config UI is a self-contained HTML document, so this catalog is embedded in
* the page and applied only to UI-owned text and attributes in the browser.
* Replacements are longest-first to keep a short label such as "Save" from
* changing a longer sentence before it is matched.
*/
const ZH_CN_REPLACEMENTS: ReadonlyArray<readonly [string, string]> = [
["Config - Alibaba Cloud Model Studio CLI", "配置 - 阿里云百炼 CLI"],
[
'The login was not completed in time. Click "Log in" to try again.',
"登录未能及时完成。请点击“登录”重试。",
],
[
"No connected agent can accept tasks yet. Install and connect qwen-code (or another supported agent) first, and make sure its CLI is on your PATH.",
"当前没有可接收任务的已连接 Agent。请先安装并连接 qwen-code或其他受支持的 Agent并确保其 CLI 位于 PATH 中。",
],
[
"Complete a few steps to check your local environment and finish first-time setup: sign in to Bailian, connect a local coding agent, and run your first task. Each circle turns from gray to green as you go.",
"完成以下步骤以检查本地环境并完成首次配置:登录百炼、连接本地编程 Agent并运行第一个任务。完成后每个圆点会由灰色变为绿色。",
],
[
'Pick a scenario to dispatch a preset task to a connected local coding agent, running in a new terminal. Tasks run in the directory where <code style="font-family:var(--mono)">bl config ui</code> was started.',
'选择一个场景,将预设任务发送给已连接的本地编程 Agent并在新终端中运行。任务会在启动 <code style="font-family:var(--mono)">bl config ui</code> 的目录中执行。',
],
[
"Pick a scenario to dispatch a preset task to a connected local coding agent, running in a new terminal. Tasks run in the directory where ",
"选择一个场景,将预设任务发送给已连接的本地编程 Agent并在新终端中运行。任务会在启动 ",
],
[" was started.", " 的目录中执行。"],
[
"Credentials and default models. The active profile (marked with a star) is used by every bl command. Click a profile to edit its settings.",
"管理凭证和默认模型。所有 bl 命令都会使用带星标的已激活 Profile。点击 Profile 可编辑其设置。",
],
[
'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>.',
'展示从本地各 Agent 模块中发现的 Skill~/.agents/skills 以及各 Agent 的 skills 目录),可通过 <code style="font-family:var(--mono)">npx skills add</code> 安装。',
],
[
"Agent skills discovered across every local agent module (~/.agents/skills plus each agent's skills folder). Installed via ",
"展示从本地各 Agent 模块中发现的 Skill~/.agents/skills 以及各 Agent 的 skills 目录),可通过 ",
],
[
"Media that bl writes into the output directory, grouped by type (images, videos, audio, files) and generation time.",
"bl 写入输出目录的媒体文件,按类型(图片、视频、音频、文件)和生成时间展示。",
],
[
'Frameworks bl can configure. "Connected" means the bailian-cli provider is wired into that agent.',
'bl 可以配置的编程 Agent。"已连接"表示该 Agent 已接入 bailian-cli provider。',
],
[
"Model Context Protocol servers declared in your local coding-agent configs.",
"本地编程 Agent 配置中声明的 Model Context Protocol 服务器。",
],
[
"Create a named profile with its own credentials and default models.",
"创建一个拥有独立凭证和默认模型的命名 Profile。",
],
[
"Inputs become fillable fields when dispatching. Reference each one in the prompt as {{key}}.",
"任务发送时,输入项会显示为可填写字段。请在提示词中使用 {{key}} 引用对应输入。",
],
[
"The .zip must contain a SKILL.md at its root or inside a single top-level folder.",
".zip 根目录或唯一的顶层目录中必须包含 SKILL.md。",
],
["The active profile is used by every bl command.", "所有 bl 命令都会使用已激活的 Profile。"],
['Delete profile "', "删除 Profile“"],
[
'"? This permanently removes its credentials and default models.',
"”?这会永久删除其中的凭证和默认模型。",
],
['Delete asset "', "删除资产“"],
['"? This removes the file from disk.', "”?这会从磁盘中删除该文件。"],
[
"MCP servers configured in Claude Code, Codex, Qwen Code or OpenCode will appear here.",
"在 Claude Code、Codex、Qwen Code 或 OpenCode 中配置的 MCP 服务器会显示在这里。",
],
[
"Assets from <code>bl image</code>, <code>bl video</code>, <code>bl speech</code> and <code>bl omni</code> will appear here.",
"通过 <code>bl image</code>、<code>bl video</code>、<code>bl speech</code> 和 <code>bl omni</code> 生成的资产会显示在这里。",
],
["Remote agents loaded from a URL will appear here.", "通过 URL 加载的远程 Agent 会显示在这里。"],
[
"The CLI for this agent was not found on your PATH — install it before launching",
"未在 PATH 中找到该 Agent 的 CLI请先安装再启动",
],
[
"Connect this agent to bailian-cli before launching",
"请先将该 Agent 连接到 bailian-cli 再启动",
],
["Install this agent before launching", "请先安装该 Agent 再启动"],
["Open a new terminal and start this agent", "在新终端中启动该 Agent"],
["Open the config file with the system default app", "使用系统默认应用打开配置文件"],
["Task sent to ", "任务已发送至 "],
[". Check the newly opened terminal window.", "。请查看新打开的终端窗口。"],
["Please enter a profile name.", "请输入 Profile 名称。"],
["Only letters, numbers, - and _ are allowed.", "仅允许使用字母、数字、- 和 _。"],
['"default" is reserved for the top-level profile.', "“default”保留用于顶层 Profile。"],
['A profile named "', "名为“"],
['" already exists.', "”的 Profile 已存在。"],
["Saved, but activation failed: ", "已保存,但激活失败:"],
["Could not read runtime environment info.", "无法读取运行环境信息。"],
["Runtime Node <code>", "运行时 Node <code>"],
["</code> · platform <code>", "</code> · 平台 <code>"],
["Signed in: ", "已登录:"],
["Connected: ", "已连接:"],
["All agents (~/.agents/skills)", "所有 Agent~/.agents/skills"],
[
"Optional — folder name (defaults to the archive folder)",
"可选 — 文件夹名称(默认使用压缩包中的文件夹名称)",
],
["Skill name (optional)", "Skill 名称(可选)"],
["Skill package (.zip)", "Skill 包(.zip"],
["Please choose a .zip file.", "请选择 .zip 文件。"],
[
"Install with <code>npx skills add modelstudioai/cli --all -g</code>",
"使用 <code>npx skills add modelstudioai/cli --all -g</code> 安装",
],
['Skill "', "Skill“"],
['MCP server "', "MCP 服务器“"],
['" installed (', "”已安装("],
[" files) to ", " 个文件),位置:"],
['" was updated.', "”已更新。"],
['" was added to ', "”已添加至 "],
['Remove "', "移除“"],
['" from ', "”(来源:"],
["? This rewrites the source config file.", ")?此操作会重写来源配置文件。"],
["Could not delete this server.", "无法删除该服务器。"],
["Delete MCP server?", "删除 MCP 服务器?"],
["Config must be a JSON object.", "配置必须是 JSON 对象。"],
["Invalid JSON: ", "无效的 JSON"],
["Please enter a server name.", "请输入服务器名称。"],
["No remote agents yet.", "暂无远程 Agent。"],
["No agents in this category.", "该分类下暂无 Agent。"],
['No agents match "', "没有匹配“"],
["bailian-cli is not wired into this agent yet.", "该 Agent 尚未连接 bailian-cli。"],
["This agent is not installed yet.", "该 Agent 尚未安装。"],
["No scenarios in this category.", "该分类下暂无场景。"],
['No scenarios match "', "没有匹配“"],
["Click to edit this scenario", "点击编辑此场景"],
["Dispatch this task to a local agent", "将此任务发送至本地 Agent"],
["No connected agent available", "没有可用的已连接 Agent"],
["Please fill in: ", "请填写:"],
['Input key "', "输入键“"],
['" may only use letters, digits, underscore.', "”只能使用字母、数字和下划线。"],
["Duplicate input key: ", "输入键重复:"],
["Title is required.", "标题为必填项。"],
["Prompt template is required.", "提示词模板为必填项。"],
['No assets match "', "没有匹配“"],
["Toggle sort by generation time", "切换生成时间排序"],
["↓ Newest first", "↓ 最新优先"],
["↑ Oldest first", "↑ 最早优先"],
["View details — ", "查看详情 — "],
["Available ", "可用的 "],
[" models · click to use", " 模型 · 点击使用"],
[
"Applies to <code>bl video generate</code> only (text/image-to-video). ",
"仅适用于 <code>bl video generate</code>(文生视频/图生视频)。",
],
[
"<code>bl video ref</code> (multi-image) and <code>bl video edit</code> keep their own ",
"<code>bl video ref</code>(多图)和 <code>bl video edit</code> 仍使用各自的",
],
[
"fixed models — pass <code>--model</code> to override those per run.",
"固定模型;可在每次运行时通过 <code>--model</code> 覆盖。",
],
['No skills match "', "没有匹配“"],
['No MCP servers match "', "没有匹配“"],
["Install to", "安装到"],
["Transport / Source", "传输方式 / 来源"],
["Target agent config", "目标 Agent 配置"],
["Server name", "服务器名称"],
["Quick launch", "快速启动"],
["Config files", "配置文件"],
["Environment check", "环境检查"],
["Edit preset scenario", "编辑预设场景"],
["Edit scenario", "编辑场景"],
["Save &amp; Activate", "保存并激活"],
["Are you sure?", "确认执行此操作吗?"],
["Save failed", "保存失败"],
["Create failed", "创建失败"],
["Install failed", "安装失败"],
["Launch failed", "启动失败"],
["Dispatch failed", "发送失败"],
["Delete failed", "删除失败"],
["Created", "已创建"],
["Dispatched", "已发送"],
["Saved", "已保存"],
["OK", "确定"],
["Configuration", "配置"],
["Settings", "设置"],
["Status", "状态"],
["Open", "打开"],
["Remove", "移除"],
["Show", "显示"],
["Hide", "隐藏"],
["All", "全部"],
["Images", "图片"],
["Videos", "视频"],
["Files", "文件"],
["Image", "图片"],
["Video", "视频"],
["Audio", "音频"],
["File", "文件"],
[" files", " 个文件"],
["MCPs", "MCP 服务"],
["Coding", "编程"],
["Generated", "生成的"],
["API key", "API 密钥"],
["Console gateway", "控制台网关"],
["Account", "账户"],
["(empty)", "(空)"],
[" (editable)", "(可编辑)"],
[" (missing)", "(缺失)"],
["default (top-level)", "default顶层"],
[
"This source is read-only here (its config is TOML). Edit it directly in ",
"该来源在此处为只读(配置格式为 TOML。请直接编辑",
],
[
"No connected local coding agent detected (e.g. qwen-code). Open the Agents page to install and connect one.",
"未检测到已连接的本地编程 Agent例如 qwen-code。请打开 Agent 页面进行安装和连接。",
],
["Installed but not wired into bl: ", "已安装但尚未接入 bl"],
[". Open the Agents page to finish connecting.", "。请打开 Agent 页面完成连接。"],
[
"Finish the previous step first (connect a dispatchable agent), then come back to Playground to run your first scenario.",
"请先完成上一步(连接可接收任务的 Agent再回到 Playground 运行第一个场景。",
],
[
"Go to Playground, pick a scenario and click Run it to complete your first dispatch.",
"前往 Playground选择一个场景并点击“运行”完成首次任务发送。",
],
[
"Sign in to the Bailian console to obtain credentials (opens a login page in your browser).",
"登录百炼控制台以获取凭证(将在浏览器中打开登录页面)。",
],
["You have dispatched at least one task.", "你已经成功发送过至少一个任务。"],
["Node 18 or newer is recommended.", "建议使用 Node.js 18 或更高版本。"],
["Save & Activate", "保存并激活"],
["Save scenario", "保存场景"],
["Custom scenario", "自定义场景"],
["+ Custom scenario", "+ 自定义场景"],
["New profile", "新建 Profile"],
["+ New profile", "+ 新建 Profile"],
["Profile name", "Profile 名称"],
["Target agent", "目标 Agent"],
["Task to dispatch", "要发送的任务"],
["Prompt template", "提示词模板"],
["One-line description (optional)", "一句话描述(可选)"],
["Use {{key}} placeholders for inputs", "使用 {{key}} 作为输入占位符"],
["e.g. Translate docs to English", "例如:将文档翻译成英文"],
["e.g. Image / Custom", "例如:图像 / 自定义"],
["e.g. work, intl, test", "例如work、intl、test"],
["Search scenarios…", "搜索场景…"],
["Search skills…", "搜索 Skill…"],
["Search MCP servers…", "搜索 MCP 服务器…"],
["Search agents…", "搜索 Agent…"],
["Search assets…", "搜索资产…"],
["Installed Skills", "已安装的 Skill"],
["Coding Agents", "编程 Agent"],
["Generated Assets", "生成资产"],
["Get Started", "开始使用"],
["Quick Start", "快速开始"],
["Playground", "Playground"],
["Extensions", "扩展"],
["Workspace", "工作区"],
["Skills", "Skill"],
["Agents", "Agent"],
["Assets", "资产"],
["Config", "配置"],
["Log in", "登录"],
["Log out", "退出登录"],
["Copy this URL", "复制此链接"],
["QR code for this session URL", "当前会话链接的二维码"],
["Collapse sidebar", "收起侧边栏"],
["Expand sidebar", "展开侧边栏"],
["Model Studio CLI home", "百炼 CLI 首页"],
["Loading…", "加载中…"],
["Delete profile", "删除 Profile"],
["Delete asset", "删除资产"],
["Open failed", "打开失败"],
["Open locally", "在本地打开"],
["View details", "查看详情"],
["Jump backward 5 pages", "向前跳转 5 页"],
["Jump forward 5 pages", "向后跳转 5 页"],
["Items per page", "每页数量"],
["No generated assets yet.", "暂无生成资产。"],
["No assets in this category.", "该分类下暂无资产。"],
["No skills installed.", "尚未安装 Skill。"],
["No local MCP servers found.", "未发现本地 MCP 服务器。"],
["No coding agents found.", "未发现编程 Agent。"],
["No scenarios found.", "未发现场景。"],
["Failed to load: ", "加载失败:"],
["Load failed: ", "加载失败:"],
["Save failed: ", "保存失败:"],
["Create failed: ", "创建失败:"],
["Install failed: ", "安装失败:"],
["Launch failed: ", "启动失败:"],
["Saving…", "正在保存…"],
["Saved and activated.", "已保存并激活。"],
["Saved.", "已保存。"],
["Profile created and saved.", "Profile 已创建并保存。"],
["Opening browser…", "正在打开浏览器…"],
["Waiting for login…", "等待登录…"],
["Signed in…", "已登录…"],
["Login timed out", "登录超时"],
["Authenticated", "已认证"],
["Connect a local agent", "连接本地 Agent"],
["Run your first task", "运行第一个任务"],
["Sign in to Bailian", "登录百炼"],
["Check local environment", "检查本地环境"],
["Go to Playground", "前往 Playground"],
["Go to Agents", "前往 Agent"],
["Run it", "运行"],
["Done", "已完成"],
["Connected", "已连接"],
["Not installed", "未安装"],
["Installed", "已安装"],
["Launching…", "正在启动…"],
["Launched → ", "已启动 → "],
["Remote", "远程"],
["Local", "本地"],
["Add skill", "添加 Skill"],
["+ Add skill", "+ 添加 Skill"],
["Choose .zip file", "选择 .zip 文件"],
["No file selected", "未选择文件"],
["New MCP server", "新建 MCP 服务器"],
["+ Add MCP", "+ 添加 MCP"],
["Description", "描述"],
["Installed in", "安装位置"],
["Details", "详情"],
["Type", "类型"],
["Path", "路径"],
["Scope", "范围"],
["Title", "标题"],
["Category", "分类"],
["Inputs", "输入项"],
["Add input", "添加输入"],
["Create", "创建"],
["Install", "安装"],
["Delete", "删除"],
["Cancel", "取消"],
["Close", "关闭"],
["Active", "已激活"],
["Save", "保存"],
["Copy", "复制"],
["Copied", "已复制"],
["show", "显示"],
["hide", "隐藏"],
["(unset)", "(未设置)"],
[" / page", " / 页"],
];
function serializeTranslations(): string {
return JSON.stringify(
[...ZH_CN_REPLACEMENTS].sort(([englishA], [englishB]) => englishB.length - englishA.length),
).replaceAll("<", "\\u003c");
}
export function renderConfigUiShell(html: string, language: Language): string {
return html
.replace('<html lang="en">', `<html lang="${language}">`)
.replace("__BL_CONFIG_UI_LANGUAGE__", language)
.replace("__BL_CONFIG_UI_TRANSLATIONS__", serializeTranslations());
}
+47 -10
View File
@@ -12,13 +12,15 @@ import {
readConfigFile,
writeConfigFile,
deleteConfigProfile,
DEFAULT_LANGUAGE,
REGIONS,
type ConfigStore,
type FlagsDef,
type Language,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { listenLocalServer, openInBrowser, openPath } from "../shared/local-server.ts";
import { PAGE_HTML } from "./ui-html.ts";
import { renderConfigUiHtml } from "./ui-html.ts";
import {
UI_VALID_KEYS,
UI_ENUM_KEYS,
@@ -40,7 +42,12 @@ import {
installSkillZip,
} from "./inventory.ts";
import { launchAgent, agentLaunchable, agentSupportsPrompt } from "./agent-launch.ts";
import { SCENARIOS, getScenario, renderScenarioPrompt, type Scenario } from "./scenarios.ts";
import {
getScenario,
localizeScenarios,
renderScenarioPrompt,
type Scenario,
} from "./scenarios.ts";
import { qrSvg } from "./qr.ts";
import { makeAuthUiBridge, type AuthUiBridge } from "../auth/console-ui.ts";
import { listAssets, resolveAssetPath, defaultOutputBase, contentType } from "./assets.ts";
@@ -49,9 +56,18 @@ const FLAGS = {
port: {
type: "number",
valueHint: "<port>",
description: "Port to listen on (default: random free port)",
description: {
"en-US": "Port to listen on (default: random free port)",
"zh-CN": "监听端口(默认:随机可用端口)",
},
},
noOpen: {
type: "switch",
description: {
"en-US": "Do not open the browser automatically",
"zh-CN": "不自动打开浏览器",
},
},
noOpen: { type: "switch", description: "Do not open the browser automatically" },
} satisfies FlagsDef;
const MAX_BODY = 1 << 20; // 1 MiB
@@ -69,6 +85,10 @@ function sendJson(res: http.ServerResponse, status: number, obj: unknown): void
res.end(JSON.stringify(obj));
}
function configUiLanguage(configStore: ConfigStore): Language {
return configStore.read().language ?? DEFAULT_LANGUAGE;
}
function readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let size = 0;
@@ -156,6 +176,13 @@ export function createConfigUiServer(
outputBase: string = defaultOutputBase(),
authBridge?: AuthUiBridge,
): http.Server {
let activatedUiProfile: string | null = null;
const uiLanguage = (): Language => {
if (activatedUiProfile === null) return configUiLanguage(configStore);
const configName = activatedUiProfile === "default" ? undefined : activatedUiProfile;
return readConfigFile(configName).language ?? DEFAULT_LANGUAGE;
};
return http.createServer(async (req, res) => {
try {
const host = (req.headers.host || "").split(":")[0];
@@ -186,7 +213,7 @@ export function createConfigUiServer(
"img-src 'self' data: https://img.alicdn.com https://oss.aliyuncs.com; " +
"media-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
});
res.end(PAGE_HTML);
res.end(renderConfigUiHtml(uiLanguage()));
return;
}
@@ -371,7 +398,10 @@ export function createConfigUiServer(
dispatchable: launchable[i] && agentSupportsPrompt(a.id),
}))
.filter((a) => a.dispatchable);
sendJson(res, 200, { scenarios: SCENARIOS, agents: targets });
sendJson(res, 200, {
scenarios: localizeScenarios(uiLanguage()),
agents: targets,
});
return;
}
@@ -534,7 +564,10 @@ export function createConfigUiServer(
inputs,
};
} else {
scenario = typeof body.scenario === "string" ? getScenario(body.scenario) : undefined;
scenario =
typeof body.scenario === "string"
? getScenario(body.scenario, uiLanguage())
: undefined;
}
if (!scenario) {
sendJson(res, 400, { error: "unknown scenario" });
@@ -579,7 +612,8 @@ export function createConfigUiServer(
const body = parsed as { name?: unknown };
try {
const activeProfile = await configStore.activate(body.name);
sendJson(res, 200, { activeProfile });
activatedUiProfile = activeProfile;
sendJson(res, 200, { activeProfile, uiLanguage: uiLanguage() });
} catch (err) {
sendJson(res, 400, { error: errMessage(err) });
}
@@ -612,7 +646,7 @@ export function createConfigUiServer(
const existing = readConfigFile(normalized) as Record<string, unknown>;
const saved = mergeUnmanagedProfileFields(existing, cleaned);
await writeConfigFile(saved, normalized);
sendJson(res, 200, { saved });
sendJson(res, 200, { saved, uiLanguage: uiLanguage() });
return;
}
@@ -642,7 +676,10 @@ export function createConfigUiServer(
}
export default defineCommand({
description: "Open a local web UI to manage config profiles",
description: {
"en-US": "Open a local web UI to manage config profiles",
"zh-CN": "打开用于管理配置 Profile 的本地 Web UI",
},
auth: "none",
usageArgs: "[--port <port>] [--no-open]",
flags: FLAGS,
+5 -2
View File
@@ -2,14 +2,17 @@ import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
export default defineCommand({
description: "Set the active config profile",
description: { "en-US": "Set the active config profile", "zh-CN": "设置当前激活的配置 Profile" },
auth: "none",
usageArgs: "--name <name>",
flags: {
name: {
type: "string",
valueHint: "<name>",
description: "Existing profile name, or default",
description: {
"en-US": "Existing profile name, or default",
"zh-CN": "已有 Profile 名称,或 default",
},
required: true,
},
},
@@ -7,20 +7,26 @@ import {
import { emitResult } from "bailian-cli-runtime";
export default defineCommand({
description: "Call a Bailian console API via the CLI gateway",
description: {
"en-US": "Call a Bailian console API via the CLI gateway",
"zh-CN": "通过 CLI Gateway 调用百炼控制台 API",
},
auth: "console",
usageArgs: "--api <api> --data <json> [flags]",
flags: {
api: {
type: "string",
valueHint: "<api>",
description: "API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries)",
description: {
"en-US": "API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries)",
"zh-CN": "API 名称(例如 zeldaEasy.broadscope-bailian.memory-library.getLibraries",
},
required: true,
},
data: {
type: "string",
valueHint: "<json>",
description: "Request data as JSON string",
description: { "en-US": "Request data as JSON string", "zh-CN": "JSON 字符串格式的请求数据" },
required: true,
},
},
@@ -1,17 +1,17 @@
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: {
type: "string",
valueHint: "<id>",
description: "Dataset file ID (required)",
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID必填" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Delete a dataset file by ID",
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
auth: "apiKey",
usageArgs: "--file-id <id>",
flags: DELETE_FLAGS,
@@ -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");
}
},
});
+12 -19
View File
@@ -1,17 +1,20 @@
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: {
type: "string",
valueHint: "<id>",
description: "Dataset file ID (required)",
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID必填" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Get details of a single dataset file",
description: {
"en-US": "Get details of a single dataset file",
"zh-CN": "获取单个数据集文件的详情",
},
auth: "apiKey",
usageArgs: "--file-id <id>",
flags: GET_FLAGS,
@@ -19,10 +22,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 +47,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);
},
});
+21 -23
View File
@@ -1,29 +1,38 @@
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)" },
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 10, max 100)",
description: {
"en-US": "Results per page (default: 10, max 100)",
"zh-CN": "每页结果数默认10最多100",
},
},
purpose: {
type: "string",
valueHint: "<name>",
description: 'Filter by purpose (e.g. "fine-tune", "evaluation"). Omit to list all.',
description: {
"en-US": 'Filter by purpose (e.g. "fine-tune", "evaluation"). Omit to list all.',
"zh-CN": '按用途筛选(例如 "fine-tune"、"evaluation")。省略时列出全部。',
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "List uploaded dataset files",
description: { "en-US": "List uploaded dataset files", "zh-CN": "列出已上传的数据集文件" },
auth: "apiKey",
usageArgs: "[--page <n>] [--page-size <n>] [--purpose <name>]",
flags: LIST_FLAGS,
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 +42,7 @@ export default defineCommand({
page_size: flags.pageSize,
purpose: flags.purpose,
},
format,
"json",
);
return;
}
@@ -46,7 +55,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 +62,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,51 +1,71 @@
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: {
"en-US": "Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT, ≤2GB media zip)",
"zh-CN":
"本地数据集文件(.jsonl 或 .zipSFT/DPO 不超过 200MBCPT 不超过 300MB媒体 ZIP 不超过 2GB",
},
required: true,
},
purpose: {
type: "string",
valueHint: "<name>",
description: 'Dataset purpose tag (default: "fine-tune"; e.g. "evaluation")',
description: {
"en-US": 'Dataset purpose tag (default: "fine-tune"; e.g. "evaluation")',
"zh-CN": '数据集用途标签(默认:"fine-tune";例如 "evaluation"',
},
},
schema: {
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.',
description: {
"en-US":
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
"zh-CN":
'记录 Schema"chatml"SFT、"dpo"chosen/rejected、"cpt"(原始文本)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。',
},
},
noValidate: {
type: "switch",
description: "Skip the local JSONL pre-flight check (not recommended)",
description: {
"en-US": "Skip the local JSONL pre-flight check (not recommended)",
"zh-CN": "跳过本地 JSONL 预检查(不推荐)",
},
},
fullValidate: {
type: "switch",
description: "JSON.parse every line instead of sampling (slower)",
description: {
"en-US": "JSON.parse every line instead of sampling (slower)",
"zh-CN": "使用 JSON.parse 检查每一行,而不是抽样检查(速度较慢)",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "Upload a dataset file (.jsonl or .zip) to Bailian",
description: {
"en-US": "Upload a dataset file (.jsonl or .zip) to Bailian",
"zh-CN": "将数据集文件(.jsonl 或 .zip上传到百炼",
},
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",
@@ -57,35 +77,39 @@ export default defineCommand({
"--file train.jsonl --no-validate",
],
notes: [
"Supports .jsonl (text) and .zip (audio/image archives with a data.jsonl",
"manifest). Five 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",
"OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is",
"persisted (the DashScope-native /api/v1/files drops it).",
{
"en-US":
'Supports .jsonl (text) and .zip (audio/image/video archives with a data.jsonl 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); video = {first_frame_path:...} (video generation).',
"zh-CN":
'支持 .jsonl文本和 .zip包含 data.jsonl 清单的音频、图片或视频归档)。可识别六种记录 Schemachatml = {messages:[...]}SFTdpo = {messages:[...], chosen, rejected}cpt = {text:"..."}持续预训练原始文本tts = {wav_fn:"train/xxx.wav", text:"..."}音频微调image = {img_path:"..."}图片生成video = {first_frame_path:...}(视频生成)。',
},
{
"en-US":
"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.",
"zh-CN":
"未指定 --schema 时,包含 wav_fn 的记录按 TTS 验证,包含 img_path 的按 image 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text无 messages的按 CPT 验证,其他记录按 ChatML 验证。",
},
{
"en-US":
"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).",
"zh-CN":
"上传上限SFT/DPO 文本 200MB、CPT 300MB、媒体 ZIP 2GB。上传使用 OpenAI 兼容的 /compatible-mode/v1/files Endpoint以便保留 purpose 标签DashScope 原生 /api/v1/files 会丢弃该标签。",
},
],
async run(ctx) {
const { identity, settings, flags } = ctx;
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 +149,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 +166,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,86 +1,90 @@
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",
valueHint: "<path>",
description: "Local dataset file (.jsonl or .zip)",
description: {
"en-US": "Local dataset file (.jsonl or .zip)",
"zh-CN": "本地数据集文件(.jsonl 或 .zip",
},
required: true,
},
fullValidate: {
type: "switch",
description: "JSON.parse every line instead of sampling (slower)",
description: {
"en-US": "JSON.parse every line instead of sampling (slower)",
"zh-CN": "使用 JSON.parse 检查每一行,而不是抽样检查(速度较慢)",
},
},
schema: {
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.',
description: {
"en-US":
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
"zh-CN":
'记录 Schema"chatml"SFT、"dpo"chosen/rejected、"cpt"(原始文本)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。',
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "Locally validate a dataset file (.jsonl or .zip) without uploading",
description: {
"en-US": "Locally validate a dataset file (.jsonl or .zip) without uploading",
"zh-CN": "在本地验证数据集文件(.jsonl 或 .zip不执行上传",
},
// 纯本地校验,不触网、不需 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",
],
notes: [
"Default scan: every line gets a structural check, then ~160 lines (front 50,",
"evenly spaced 100, last 10) are JSON.parsed against the active schema.",
"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.",
{
"en-US":
"Default scan: every line gets a structural check, then ~160 lines (front 50, evenly spaced 100, last 10) are JSON.parsed against the active schema.",
"zh-CN":
"默认扫描:先对每一行进行结构检查,再抽取约 160 行(前 50 行、均匀抽取 100 行、最后 10 行),使用 JSON.parse 按当前 Schema 验证。",
},
{
"en-US":
'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); video = {first_frame_path:"...", video_path:"..."} (video generation, i2v first-frame or kf2v first+last-frame with last_frame_path).',
"zh-CN":
'Schemachatml = {messages:[...]}SFTdpo = {messages:[...], chosen, rejected}cpt = {text:"..."}持续预训练原始文本tts = {wav_fn:"train/xxx.wav", text:"..."}音频微调image = {img_path:"..."}图片生成video = {first_frame_path:"...", video_path:"..."}(视频生成,支持 i2v 首帧或通过 last_frame_path 指定 kf2v 首尾帧)。',
},
{
"en-US":
"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.",
"zh-CN":
"未指定 --schema 时,包含 wav_fn 的记录按 TTS 验证,包含 img_path 的按 image 验证,包含 first_frame_path/video_path 的按 video 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text无 messages的按 CPT 验证,其他记录按 ChatML 验证。使用 --schema 要求每条记录符合指定结构。",
},
{
"en-US":
"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.",
"zh-CN":
"ZIP 归档(.zip除逐条检查记录内容外还会执行结构验证存在 data.jsonl、媒体引用可解析。使用 --full-validate 对每一行执行 JSON.parse。",
},
],
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 +93,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) {
+98 -61
View File
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
createDeployment,
pickPlanStrategy,
STRATEGIES,
@@ -11,81 +10,123 @@ 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: {
"en-US": "Model to deploy — fine-tuned output name or catalog model (required)",
"zh-CN": "要部署的模型:微调输出模型名称或模型目录中的模型(必填)",
},
required: true,
},
name: {
displayName: {
type: "string",
valueHint: "<display_name>",
description: "Console display name for the deployment (required)",
description: {
"en-US": "Console display name for the deployment (required)",
"zh-CN": "部署在控制台中的显示名称(必填)",
},
required: true,
},
plan: {
type: "string",
valueHint: "<plan>",
description: "Billing plan: lora (default, Token-billed) | ptu (Token-billed) | mu",
description: {
"en-US": "Billing plan: lora (default, Token-billed) | ptu (Token-billed) | mu",
"zh-CN": "计费方案lora默认按 Token 计费)| ptu按 Token 计费)| mu",
},
},
deploySpec: {
type: "string",
valueHint: "<id>",
description: "Deploy spec (only used by plan=mu; auto-picked if omitted)",
description: {
"en-US": "Deploy spec (only used by plan=mu; auto-picked if omitted)",
"zh-CN": "部署规格(仅 plan=mu 使用;省略时自动选择)",
},
},
capacity: {
type: "number",
valueHint: "<n>",
description: "Resource units (plan=mu only; required by API; defaults to the template's unit)",
description: {
"en-US": "Resource units (plan=mu only; required by API; defaults to the template's unit)",
"zh-CN": "资源单元数(仅 plan=muAPI 必填;默认为模板的单元数)",
},
},
billingMethod: {
type: "string",
valueHint: "<m>",
description: 'Billing method (plan=mu only; default "POST_PAY", the only supported value)',
description: {
"en-US": 'Billing method (plan=mu only; default "POST_PAY", the only supported value)',
"zh-CN": '计费方式(仅 plan=mu默认且仅支持 "POST_PAY"',
},
},
inputTpm: {
type: "number",
valueHint: "<n>",
description: "PTU max input tokens/min (required for plan=ptu)",
description: {
"en-US": "PTU max input tokens/min (required for plan=ptu)",
"zh-CN": "PTU 每分钟最大输入 Token 数plan=ptu 时必填)",
},
},
outputTpm: {
type: "number",
valueHint: "<n>",
description: "PTU max output tokens/min (required for plan=ptu)",
description: {
"en-US": "PTU max output tokens/min (required for plan=ptu)",
"zh-CN": "PTU 每分钟最大输出 Token 数plan=ptu 时必填)",
},
},
thinkingOutputTpm: {
type: "number",
valueHint: "<n>",
description: "PTU max thinking-output tokens/min (optional, some models)",
description: {
"en-US": "PTU max thinking-output tokens/min (optional, some models)",
"zh-CN": "PTU 每分钟最大思考输出 Token 数(部分模型可选)",
},
},
} 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-",
"billed) for audio (CosyVoice TTS). Pass --plan to override.",
"For plan=ptu (Token-billed, provisioned throughput), --input-tpm and",
"--output-tpm are required (the platform rejects creation without an",
"explicit ptu_capacity despite the doc listing defaults).",
"For plan=mu, `capacity`, `billing_method` and `deploy_spec` are required.",
"billing_method defaults to POST_PAY (only supported value); deploy_spec",
"and capacity are auto-picked from GET /deployments/models when omitted.",
"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.",
{
"en-US":
"Plan defaults to `lora` (Token-billed) for text/image and `mu` (model-unit-billed) for audio (CosyVoice TTS). Pass --plan to override.",
"zh-CN":
"文本和图片部署默认使用 `lora`(按 Token 计费音频CosyVoice TTS默认使用 `mu`(按模型单元计费)。可通过 --plan 覆盖。",
},
{
"en-US":
"For plan=ptu (Token-billed, provisioned throughput), --input-tpm and --output-tpm are required (the platform rejects creation without an explicit ptu_capacity despite the doc listing defaults).",
"zh-CN":
"plan=ptu按 Token 计费的预置吞吐)时,--input-tpm 和 --output-tpm 必填;即使文档列出了默认值,未显式传入 ptu_capacity 时平台也会拒绝创建。",
},
{
"en-US":
"For plan=mu, `capacity`, `billing_method` and `deploy_spec` are required. billing_method defaults to POST_PAY (only supported value); deploy_spec and capacity are auto-picked from GET /deployments/models when omitted.",
"zh-CN":
"plan=mu 时,`capacity`、`billing_method` 和 `deploy_spec` 必填。billing_method 默认且仅支持 POST_PAY省略 deploy_spec 和 capacity 时,会从 GET /deployments/models 自动选择。",
},
{
"en-US": "Use `bl deploy models --source base` to inspect available templates.",
"zh-CN": "使用 `bl deploy models --source base` 查看可用模板。",
},
{
"en-US":
"After creation, status starts at PENDING and transitions to RUNNING. Invoke the deployed model with: bl text chat --model <deployed_model>",
"zh-CN":
"创建后状态从 PENDING 开始,随后转为 RUNNING。调用已部署模型bl text chat --model <deployed_model>",
},
{
"en-US":
"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>`).",
"zh-CN":
"注意:--model-name 是要部署的模型(例如 `qwen3-8b-ft-...`)。创建响应中的 `deployed_model` 是部署实例 ID例如 `qwen3-8b-5ecb5f068d79`),用于推理(`bl text chat --model <deployed_model>`)及生命周期命令(`deploy get/scale/pause/resume/delete --deployed-model <id>`)。",
},
];
/**
@@ -119,10 +160,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 +186,7 @@ async function runCreate(
};
if (settings.dryRun) {
emitResult({ action: "deploy.create", body }, format);
emitResult({ action: "deploy.create", body }, "json");
return;
}
@@ -155,31 +195,22 @@ 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");
}
}
/** `bl deploy text create` — deploy a text model. */
export const deployTextCreate = defineCommand({
description: "Create a text model deployment",
description: { "en-US": "Create a text model deployment", "zh-CN": "创建文本模型部署" },
auth: "apiKey",
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),
@@ -188,14 +219,17 @@ export const deployTextCreate = defineCommand({
/** `bl deploy audio create` — deploy an audio (TTS) model. Defaults to plan=mu. */
export const deployAudioCreate = defineCommand({
description: "Create an audio (TTS) model deployment",
description: {
"en-US": "Create an audio (TTS) model deployment",
"zh-CN": "创建音频TTS模型部署",
},
auth: "apiKey",
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),
@@ -204,14 +238,17 @@ export const deployAudioCreate = defineCommand({
/** `bl deploy image create` — deploy an image generation model. */
export const deployImageCreate = defineCommand({
description: "Create an image generation model deployment",
description: {
"en-US": "Create an image generation model deployment",
"zh-CN": "创建图片生成模型部署",
},
auth: "apiKey",
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),
+17 -12
View File
@@ -1,24 +1,29 @@
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: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
description: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
skipPrecheck: {
type: "switch",
description: "Skip the local STOPPED/FAILED status precheck",
description: {
"en-US": "Skip the local STOPPED/FAILED status precheck",
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
},
},
} satisfies FlagsDef;
@@ -30,7 +35,10 @@ const DELETE_FLAGS = {
* DELETE call.
*/
export default defineCommand({
description: "Delete a model deployment (must be STOPPED or FAILED)",
description: {
"en-US": "Delete a model deployment (must be STOPPED or FAILED)",
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED",
},
auth: "apiKey",
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: DELETE_FLAGS,
@@ -38,10 +46,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 +62,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 +77,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");
}
},
});
+13 -20
View File
@@ -1,17 +1,23 @@
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: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
description: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Get details of a single model deployment",
description: {
"en-US": "Get details of a single model deployment",
"zh-CN": "获取单个模型部署的详情",
},
auth: "apiKey",
usageArgs: "--deployed-model <id>",
flags: GET_FLAGS,
@@ -22,10 +28,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 +38,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 +62,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");
},
});
+18 -35
View File
@@ -1,40 +1,44 @@
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)" },
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 10, max 100)",
description: {
"en-US": "Results per page (default: 10, max 100)",
"zh-CN": "每页结果数默认10最多100",
},
},
status: {
type: "string",
valueHint: "<s>",
description: "Filter by status (PENDING / RUNNING / STOPPED / FAILED)",
description: {
"en-US": "Filter by status (PENDING / RUNNING / STOPPED / FAILED)",
"zh-CN": "按状态筛选PENDING / RUNNING / STOPPED / FAILED",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "List model deployments",
description: { "en-US": "List model deployments", "zh-CN": "列出模型部署" },
auth: "apiKey",
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>]",
flags: LIST_FLAGS,
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 +61,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");
},
});
+64 -107
View File
@@ -1,33 +1,38 @@
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)" },
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 100)",
description: { "en-US": "Results per page (default: 100)", "zh-CN": "每页结果数默认100" },
},
// 全局 --version 是保留 flag,目录版本过滤改名 --catalog-version。
catalogVersion: {
type: "string",
valueHint: "<v>",
description: "Catalog version filter (default: v1.0; required for new catalog models)",
description: {
"en-US": "Catalog version filter (default: v1.0; required for new catalog models)",
"zh-CN": "模型目录版本筛选默认v1.0;新目录模型必填)",
},
},
source: {
type: "string",
valueHint: "<s>",
description: "Model source filter: custom (fine-tuned) | base (catalog) | public",
description: {
"en-US": "Model source filter: custom (fine-tuned) | base (catalog) | public",
"zh-CN": "模型来源筛选custom微调模型| base模型目录| public",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "List models available for deployment",
description: { "en-US": "List models available for deployment", "zh-CN": "列出可部署的模型" },
auth: "apiKey",
usageArgs: "[--page <n>] [--page-size <n>] [--catalog-version <v>] [--source <custom|public>]",
flags: MODELS_FLAGS,
@@ -39,7 +44,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 +58,7 @@ export default defineCommand({
version,
model_source: modelSource,
},
format,
"json",
);
return;
}
@@ -72,102 +76,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,104 @@
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: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
skipPrecheck: {
type: "switch",
description: {
"en-US": "Skip the local RUNNING/PENDING status precheck",
"zh-CN": "跳过本地 RUNNING/PENDING 状态预检查",
},
},
} 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: {
"en-US": "Pause a running model deployment (stops billing for mu/ptu)",
"zh-CN": "暂停运行中的模型部署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: [
{
"en-US":
"While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.",
"zh-CN":
"暂停期间mu/ptu 方案将停止计费。使用 `deploy resume` 恢复服务,或使用 `deploy delete` 删除部署。",
},
{
"en-US":
"Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.",
"zh-CN":
"发起暂停前会预检查部署状态是否为 RUNNING/PENDING可传入 --skip-precheck 跳过检查。",
},
],
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,100 @@
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: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
skipPrecheck: {
type: "switch",
description: {
"en-US": "Skip the local STOPPED status precheck",
"zh-CN": "跳过本地 STOPPED 状态预检查",
},
},
} 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: {
"en-US": "Resume a paused model deployment (brings service back online)",
"zh-CN": "恢复已暂停的模型部署(使服务重新上线)",
},
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: [
{
"en-US":
"Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.",
"zh-CN": "发起恢复前会预检查部署状态是否为 STOPPED可传入 --skip-precheck 跳过检查。",
},
{
"en-US": "For mu/ptu plans, billing resumes once the service is back online.",
"zh-CN": "对于 mu/ptu 方案,服务重新上线后将恢复计费。",
},
],
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");
}
},
});
+21 -20
View File
@@ -1,32 +1,39 @@
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: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
description: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
capacity: {
type: "number",
valueHint: "<n>",
description: "New capacity in plan units (must be a multiple of base_capacity)",
description: {
"en-US": "New capacity in plan units (must be a multiple of base_capacity)",
"zh-CN": "以方案单元表示的新容量(必须是 base_capacity 的整数倍)",
},
},
inputTpm: {
type: "number",
valueHint: "<n>",
description: "PTU only — input tokens per minute",
description: {
"en-US": "PTU only — input tokens per minute",
"zh-CN": "仅 PTU每分钟输入 Token 数",
},
},
outputTpm: {
type: "number",
valueHint: "<n>",
description: "PTU only — output tokens per minute",
description: {
"en-US": "PTU only — output tokens per minute",
"zh-CN": "仅 PTU每分钟输出 Token 数",
},
},
} satisfies FlagsDef;
@@ -37,7 +44,7 @@ const SCALE_FLAGS = {
* integer multiple of `base_capacity` (visible via `bl deploy get`).
*/
export default defineCommand({
description: "Scale a deployment's capacity",
description: { "en-US": "Scale a deployment's capacity", "zh-CN": "调整部署容量" },
auth: "apiKey",
usageArgs: "--deployed-model <id> --capacity <n> [--input-tpm <n>] [--output-tpm <n>]",
flags: SCALE_FLAGS,
@@ -52,7 +59,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 +66,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");
}
},
});
+20 -23
View File
@@ -1,27 +1,25 @@
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: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
description: {
"en-US": "Deployed model identifier (required)",
"zh-CN": "已部署模型标识(必填)",
},
required: true,
},
rpmLimit: {
type: "number",
valueHint: "<n>",
description: "Requests per minute",
description: { "en-US": "Requests per minute", "zh-CN": "每分钟请求数" },
},
tpmLimit: {
type: "number",
valueHint: "<n>",
description: "Tokens per minute",
description: { "en-US": "Tokens per minute", "zh-CN": "每分钟 Token 数" },
},
} satisfies FlagsDef;
@@ -32,7 +30,10 @@ const UPDATE_FLAGS = {
* Body: at least one of `rpm_limit` (requests/min) or `tpm_limit` (tokens/min).
*/
export default defineCommand({
description: "Update a deployment's rate limits (rpm_limit / tpm_limit)",
description: {
"en-US": "Update a deployment's rate limits (rpm_limit / tpm_limit)",
"zh-CN": "更新部署的限流配置rpm_limit / tpm_limit",
},
auth: "apiKey",
usageArgs: "--deployed-model <id> [--rpm-limit <n>] [--tpm-limit <n>]",
flags: UPDATE_FLAGS,
@@ -40,7 +41,12 @@ export default defineCommand({
"--deployed-model dep-... --rpm-limit 1000",
"--deployed-model dep-... --rpm-limit 1000 --tpm-limit 200000",
],
notes: ["At least one of --rpm-limit / --tpm-limit must be provided."],
notes: [
{
"en-US": "At least one of --rpm-limit / --tpm-limit must be provided.",
"zh-CN": "--rpm-limit / --tpm-limit 至少需要提供一个。",
},
],
validate: (flags) =>
flags.rpmLimit === undefined && flags.tpmLimit === undefined
? "Provide at least one of --rpm-limit / --tpm-limit."
@@ -48,31 +54,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");
}
},
});
+12 -3
View File
@@ -2,20 +2,29 @@ import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Upload a local file to DashScope temporary storage (48h)",
description: {
"en-US": "Upload a local file to DashScope temporary storage (48h)",
"zh-CN": "将本地文件上传到 DashScope 临时存储(保留 48 小时)",
},
auth: "apiKey",
usageArgs: "--file <path> --model <model>",
flags: {
file: {
type: "string",
valueHint: "<path>",
description: "Local file to upload (image, video, audio)",
description: {
"en-US": "Local file to upload (image, video, audio)",
"zh-CN": "要上传的本地文件(图片、视频或音频)",
},
required: true,
},
model: {
type: "string",
valueHint: "<model>",
description: "Target model name (file is bound to this model)",
description: {
"en-US": "Target model name (file is bound to this model)",
"zh-CN": "目标模型名称(文件将与该模型绑定)",
},
required: true,
},
},
@@ -1,46 +1,44 @@
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: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Cancel a running fine-tune job",
description: { "en-US": "Cancel a running fine-tune job", "zh-CN": "取消正在运行的微调任务" },
auth: "apiKey",
usageArgs: "--job-id <id>",
flags: CANCEL_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
notes: [
"Only PENDING / RUNNING jobs can be cancelled. Completed / failed / already-",
"cancelled jobs return a server-side error (passed through verbatim).",
{
"en-US":
"Only PENDING / RUNNING jobs can be cancelled. Completed / failed / already-cancelled jobs return a server-side error (passed through verbatim).",
"zh-CN":
"只有 PENDING / RUNNING 状态的任务可以取消。已完成、失败或已取消的任务会返回服务端错误(原样透传)。",
},
],
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,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
fetchModelListAll,
fetchModelCapability,
listSupportedTrainingTypes,
@@ -27,59 +26,67 @@ async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapabi
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.",
description: {
"en-US": "List training types supported by this base model.",
"zh-CN": "列出该基础模型支持的训练类型。",
},
},
trainingType: {
type: "string",
valueHint: "<t>",
description: `List models supporting this training type: ${TRAINING_TYPES_CLI.join(" | ")}.`,
description: {
"en-US": `List models supporting this training type: ${TRAINING_TYPES_CLI.join(" | ")}.`,
"zh-CN": `列出支持该训练类型的模型:${TRAINING_TYPES_CLI.join(" | ")}`,
},
},
} satisfies FlagsDef;
export default defineCommand({
description:
"Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)",
description: {
"en-US":
"Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)",
"zh-CN": "查询微调训练能力:按模型查询其支持的训练类型,或按训练类型查询支持它的模型",
},
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.",
"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.",
{
"en-US": "Exactly one of --base-model / --training-type is required.",
"zh-CN": "--base-model 和 --training-type 必须且只能指定一个。",
},
{
"en-US":
"Training-type values use the `<method>` / `<method>-lora` convention: sft | sft-lora | dpo | dpo-lora | cpt. (cpt has no -lora variant server-side.)",
"zh-CN":
"训练类型遵循 `<method>` / `<method>-lora` 命名约定sft | sft-lora | dpo | dpo-lora | cpt。服务端没有 cpt-lora 变体。)",
},
{
"en-US": "Queries listFoundationModels, a public API — no console login needed.",
"zh-CN": "查询公开 API listFoundationModels无需登录控制台。",
},
],
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(
@@ -88,7 +95,7 @@ export default defineCommand({
model,
training_type: trainingType,
},
format,
"json",
);
return;
}
@@ -97,7 +104,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);
@@ -105,23 +112,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;
}
@@ -146,20 +145,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,37 +1,45 @@
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: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
} 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",
description: {
"en-US": "List checkpoints produced by a fine-tune job",
"zh-CN": "列出微调任务生成的 Checkpoint",
},
auth: "apiKey",
usageArgs: "--job-id <id>",
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.",
{
"en-US":
"`model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model-name`.",
"zh-CN":
"SUCCEEDED Checkpoint 中显示的 `model_name` 可直接作为 `deploy create --model-name` 的输入。",
},
{
"en-US":
"Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.",
"zh-CN": "Checkpoint 创建后约 15 天过期,`expire_time` 显示截止时间。请在过期前导出或部署。",
},
],
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 +52,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);
},
});
+215 -83
View File
@@ -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,34 +215,50 @@ 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: {
"en-US": "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
"zh-CN": "要微调的基础模型(例如 qwen3-8b不是输出模型名称",
},
required: true,
},
datasets: {
type: "string",
valueHint: "<ids|paths>",
description:
"Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image). Local paths are uploaded (validated) first, then their file-ids are used.",
description: {
"en-US":
"Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image/video). Local paths are uploaded (validated) first, then their file-ids are used.",
"zh-CN":
"数据集文件 ID 或本地路径,以逗号分隔(文本使用 .jsonl音频、图片和视频使用 .zip。本地路径会先验证并上传再使用对应的 file-id。",
},
required: true,
},
validations: {
type: "string",
valueHint: "<ids|paths>",
description:
"Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets).",
description: {
"en-US":
"Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets).",
"zh-CN": "验证数据集文件 ID 或本地路径,以逗号分隔(与 --datasets 一样自动上传)。",
},
},
modelName: {
type: "string",
valueHint: "<name>",
description: "Output model name (after training)",
description: {
"en-US": "Output model name (after training)",
"zh-CN": "训练完成后的输出模型名称",
},
},
suffix: {
type: "string",
valueHint: "<text>",
description: "Output suffix appended by the platform (finetuned_output_suffix)",
description: {
"en-US": "Output suffix appended by the platform (finetuned_output_suffix)",
"zh-CN": "平台追加的输出后缀finetuned_output_suffix",
},
},
} satisfies FlagsDef;
@@ -258,28 +273,37 @@ const TEXT_FLAGS = {
trainingType: {
type: "string",
valueHint: "<t>",
description: `Training type: ${TRAINING_TYPES_CLI.join(" | ")} (default: ${DEFAULT_TRAINING_TYPE}). Mapping to the server happens at the interface boundary (e.g. sft-lora -> efficient_sft, dpo -> dpo_full).`,
description: {
"en-US": `Training type: ${TRAINING_TYPES_CLI.join(" | ")} (default: ${DEFAULT_TRAINING_TYPE}). Mapping to the server happens at the interface boundary (e.g. sft-lora -> efficient_sft, dpo -> dpo_full).`,
"zh-CN": `训练类型:${TRAINING_TYPES_CLI.join(" | ")}(默认:${DEFAULT_TRAINING_TYPE})。在接口边界转换为服务端值(例如 sft-lora -> efficient_sft、dpo -> dpo_full`,
},
},
nEpochs: {
type: "number",
valueHint: "<n>",
description: "Number of epochs (default: 3)",
description: { "en-US": "Number of epochs (default: 3)", "zh-CN": "训练轮数默认3" },
},
batchSize: {
type: "number",
valueHint: "<n>",
description:
"Per-device batch size (clamped to [8, 1024]). Auto-set to 8 for small datasets (<100KB)",
description: {
"en-US":
"Per-device batch size (clamped to [8, 1024]). Auto-set to 8 for small datasets (<100KB)",
"zh-CN": "单设备 Batch Size限制在 [8, 1024])。小数据集(<100KB自动设为 8",
},
},
learningRate: {
type: "string",
valueHint: "<str>",
description: 'Learning rate as a string to preserve precision (e.g. "1.6e-5")',
description: {
"en-US": 'Learning rate as a string to preserve precision (e.g. "1.6e-5")',
"zh-CN": '以字符串形式指定学习率以保留精度(例如 "1.6e-5"',
},
},
maxLength: {
type: "number",
valueHint: "<n>",
description: "Max sequence length",
description: { "en-US": "Max sequence length", "zh-CN": "最大序列长度" },
},
} satisfies FlagsDef;
@@ -306,63 +330,131 @@ const IMAGE_FLAGS = {
type: "string",
choices: ["t2i", "i2i"] as const,
valueHint: "<t2i|i2i>",
description:
"Generation type: t2i (default) | i2i. Sets generation_type/max_pixels. Required to train I2I from a file-id or with --dry-run (local data auto-detects input_img).",
description: {
"en-US":
"Generation type: t2i (default) | i2i. Sets generation_type/max_pixels. Required to train I2I from a file-id or with --dry-run (local data auto-detects input_img).",
"zh-CN":
"生成类型t2i默认| i2i。用于设置 generation_type/max_pixels。通过 file-id 训练 I2I 或使用 --dry-run 时必填(本地数据会自动识别 input_img。",
},
},
learningRate: {
type: "string",
valueHint: "<str>",
description: 'Learning rate as a string to preserve precision (e.g. "3e-5")',
description: {
"en-US": 'Learning rate as a string to preserve precision (e.g. "3e-5")',
"zh-CN": '以字符串形式指定学习率以保留精度(例如 "3e-5"',
},
},
} 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: { "en-US": "Training epochs (default: 50)", "zh-CN": "训练轮数默认50" },
},
batchSize: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Batch size (default: model-specific, 1 for wan2.7, 4 for wan2.5/2.2)",
"zh-CN": "Batch Size默认值因模型而异wan2.7 为 1wan2.5/2.2 为 4",
},
},
learningRate: {
type: "string",
valueHint: "<str>",
description: {
"en-US": 'Learning rate as a string to preserve precision (default: "2e-5")',
"zh-CN": '以字符串形式指定学习率以保留精度(默认:"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.",
"Use --dry-run to preview the request body without submitting.",
"--datasets / --validations accept either file-ids (from `dataset upload`)",
"or local paths. Local paths are validated and uploaded first, then their",
"file-ids are submitted — a one-step upload-and-train.",
{
"en-US": "Creating a job uploads any local datasets and consumes training quota.",
"zh-CN": "创建任务会上传所有本地数据集并消耗训练额度。",
},
{
"en-US": "Use --dry-run to preview the request body without submitting.",
"zh-CN": "使用 --dry-run 预览请求体,不实际提交。",
},
{
"en-US":
"--datasets / --validations accept either file-ids (from `dataset upload`) or local paths. Local paths are validated and uploaded first, then their file-ids are submitted — a one-step upload-and-train.",
"zh-CN":
"--datasets / --validations 可接受 file-id来自 `dataset upload`)或本地路径。本地路径会先验证并上传,再提交对应的 file-id实现一步上传并训练。",
},
];
const TEXT_NOTES = [
...COMMON_NOTES,
"Training-type values use the `<method>` / `<method>-lora` convention:",
"sft (full) | sft-lora (LoRA) | dpo (full) | dpo-lora (LoRA) | cpt. These map",
"to the server's training_type at the interface boundary, so the rest of the",
"CLI never sees the raw server strings.",
"Before submitting (non dry-run) the job, the model's training capability is",
"checked via listFoundationModels (no console login required); an unsupported",
"training type fails fast with the list the model actually supports.",
"n_epochs defaults to 3. Other hyper-parameters are platform defaults unless set.",
"Learning rate is forwarded as a string to avoid JSON-number precision loss.",
"Pre-submit gate: if the training dataset's sample count is not greater",
"than batch_size, the job is rejected before upload or quota consumption",
"(the platform would otherwise fail ~10 min in, after data processing).",
{
"en-US":
"Training-type values use the `<method>` / `<method>-lora` convention: sft (full) | sft-lora (LoRA) | dpo (full) | dpo-lora (LoRA) | cpt. These map to the server's training_type at the interface boundary, so the rest of the CLI never sees the raw server strings.",
"zh-CN":
"训练类型遵循 `<method>` / `<method>-lora` 命名约定sft全量| sft-loraLoRA| dpo全量| dpo-loraLoRA| cpt。这些值会在接口边界映射为服务端 training_type因此 CLI 的其他部分不会接触服务端原始字符串。",
},
{
"en-US":
"Before submitting (non dry-run) the job, the model's training capability is checked via listFoundationModels (no console login required); an unsupported training type fails fast with the list the model actually supports.",
"zh-CN":
"提交任务前(非 dry-run会通过 listFoundationModels 检查模型训练能力(无需登录控制台);如果训练类型不受支持,会立即失败并列出该模型实际支持的训练类型。",
},
{
"en-US": "n_epochs defaults to 3. Other hyper-parameters are platform defaults unless set.",
"zh-CN": "n_epochs 默认为 3。其他超参数未设置时使用平台默认值。",
},
{
"en-US": "Learning rate is forwarded as a string to avoid JSON-number precision loss.",
"zh-CN": "学习率以字符串形式传递,避免 JSON 数字精度损失。",
},
{
"en-US":
"Pre-submit gate: if the training dataset's sample count is not greater than batch_size, the job is rejected before upload or quota consumption (the platform would otherwise fail ~10 min in, after data processing).",
"zh-CN":
"提交前检查:如果训练数据集的样本数不大于 batch_size会在上传或消耗额度前拒绝任务否则平台会在数据处理约 10 分钟后才失败)。",
},
];
const AUDIO_NOTES = [
...COMMON_NOTES,
"Audio TTS training runs sft-lora (efficient_sft) with fixed CosyVoice",
"hyper-parameter defaults; there are no training-type or hyper-parameter",
"knobs to set.",
{
"en-US":
"Audio TTS training runs sft-lora (efficient_sft) with fixed CosyVoice hyper-parameter defaults; there are no training-type or hyper-parameter knobs to set.",
"zh-CN":
"音频 TTS 训练使用 sft-loraefficient_sft和固定的 CosyVoice 超参数默认值;没有可设置的训练类型或超参数选项。",
},
];
const IMAGE_NOTES = [
...COMMON_NOTES,
"Image generation training runs sft-lora (efficient_sft) with fixed defaults;",
"only --learning-rate is overridable. T2I vs I2I is declared with",
"--generation-type (default t2i), which sets generation_type/max_pixels. For",
"local data the type is auto-detected (records with input_img train I2I);",
"pass --generation-type explicitly to train I2I from a file-id or in --dry-run.",
{
"en-US":
"Image generation training runs sft-lora (efficient_sft) with fixed defaults; only --learning-rate is overridable. T2I vs I2I is declared with --generation-type (default t2i), which sets generation_type/max_pixels. For local data the type is auto-detected (records with input_img train I2I); pass --generation-type explicitly to train I2I from a file-id or in --dry-run.",
"zh-CN":
"图片生成训练使用 sft-loraefficient_sft和固定默认值仅 --learning-rate 可覆盖。通过 --generation-type默认 t2i声明 T2I 或 I2I并设置 generation_type/max_pixels。本地数据会自动识别类型包含 input_img 的记录训练 I2I通过 file-id 训练 I2I 或使用 --dry-run 时,请显式传入 --generation-type。",
},
];
/**
@@ -383,7 +475,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 +533,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 +702,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 +711,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,34 +721,29 @@ 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");
}
}
/** `bl finetune text create` — fine-tune a text model. Datasets are `.jsonl`. */
export const finetuneTextCreate = defineCommand({
description: "Create a text model fine-tune job (sft | sft-lora | dpo | dpo-lora | cpt)",
description: {
"en-US": "Create a text model fine-tune job (sft | sft-lora | dpo | dpo-lora | cpt)",
"zh-CN": "创建文本模型微调任务sft | sft-lora | dpo | dpo-lora | cpt",
},
auth: "apiKey",
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),
@@ -662,16 +751,19 @@ export const finetuneTextCreate = defineCommand({
/** `bl finetune audio create` — fine-tune an audio TTS model. Datasets are `.zip`. */
export const finetuneAudioCreate = defineCommand({
description: "Create an audio TTS model fine-tune job (sft-lora)",
description: {
"en-US": "Create an audio TTS model fine-tune job (sft-lora)",
"zh-CN": "创建音频 TTS 模型微调任务sft-lora",
},
auth: "apiKey",
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),
@@ -679,18 +771,58 @@ export const finetuneAudioCreate = defineCommand({
/** `bl finetune image create` — fine-tune an image generation model. Datasets are `.zip`. */
export const finetuneImageCreate = defineCommand({
description: "Create an image generation model fine-tune job (sft-lora)",
description: {
"en-US": "Create an image generation model fine-tune job (sft-lora)",
"zh-CN": "创建图片生成模型微调任务sft-lora",
},
auth: "apiKey",
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,
{
"en-US":
"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.",
"zh-CN":
"视频生成训练Wan i2v/kf2v使用 efficient_sft 和模型专属默认值wan2.7batch_size=1、max_pixels=102400wan2.5/2.2batch_size=4max_pixels 因模型而异)。可通过 --batch-size/--n-epochs 覆盖。",
},
{
"en-US": "Datasets are .zip archives with data.jsonl + frame images + videos.",
"zh-CN": "数据集为包含 data.jsonl、帧图片和视频的 .zip 归档。",
},
{
"en-US": "Recommended: ≥10 training samples, 20-100 for stable results.",
"zh-CN": "建议至少准备 10 个训练样本20100 个样本可获得更稳定的效果。",
},
];
/** `bl finetune video create` — fine-tune a video generation model. Datasets are `.zip`. */
export const finetuneVideoCreate = defineCommand({
description: {
"en-US": "Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft)",
"zh-CN": "创建视频生成模型微调任务Wan i2v/kf2vefficient_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,32 +1,34 @@
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: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Delete a fine-tune job record",
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
auth: "apiKey",
usageArgs: "--job-id <id>",
flags: DELETE_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
notes: [
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses",
"to delete jobs that are still in flight.",
{
"en-US":
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
"zh-CN": "请先通过 `finetune cancel` 取消 RUNNING 任务,平台拒绝删除仍在运行的任务。",
},
],
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 +36,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,49 +1,52 @@
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: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
checkpoint: {
type: "string",
valueHint: "<name>",
description: "Checkpoint identifier from `finetune checkpoints` (required)",
description: {
"en-US": "Checkpoint identifier from `finetune checkpoints` (required)",
"zh-CN": "来自 `finetune checkpoints` 的 Checkpoint 标识(必填)",
},
required: true,
},
modelName: {
type: "string",
valueHint: "<name>",
description: "Deployable model name (required)",
description: { "en-US": "Deployable model name (required)", "zh-CN": "可部署模型名称(必填)" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Publish a checkpoint as a deployable model",
description: {
"en-US": "Publish a checkpoint as a deployable model",
"zh-CN": "将 Checkpoint 发布为可部署模型",
},
auth: "apiKey",
usageArgs: "--job-id <id> --checkpoint <name> --model-name <name>",
flags: EXPORT_FLAGS,
exampleArgs: ["--job-id ft-xxx --checkpoint ckpt-3 --model-name my-qwen-sft"],
notes: [
"Required before `deploy <modality> create` can target a checkpoint. The",
"platform may auto-export the best checkpoint when a job reaches SUCCEEDED —",
"explicit export is the canonical path for non-best checkpoints.",
{
"en-US":
"Required before `deploy <modality> create` can target a checkpoint. The platform may auto-export the best checkpoint when a job reaches SUCCEEDED — explicit export is the canonical path for non-best checkpoints.",
"zh-CN":
"必须先执行此操作,`deploy <modality> create` 才能使用 Checkpoint。任务达到 SUCCEEDED 后,平台可能自动导出最佳 Checkpoint对于非最佳 Checkpoint显式导出是标准方式。",
},
],
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 +56,7 @@ export default defineCommand({
checkpoint,
model_name: modelName,
},
format,
"json",
);
return;
}
@@ -64,14 +67,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;
}
}
+34 -34
View File
@@ -1,28 +1,31 @@
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: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
} satisfies FlagsDef;
export default defineCommand({
description: "Get details of a single fine-tune job",
description: {
"en-US": "Get details of a single fine-tune job",
"zh-CN": "获取单个微调任务的详情",
},
auth: "apiKey",
usageArgs: "--job-id <id>",
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 +33,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 +62,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");
},
});
+41 -51
View File
@@ -1,83 +1,73 @@
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)" },
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 10, max 100)",
description: {
"en-US": "Results per page (default: 10, max 100)",
"zh-CN": "每页结果数默认10最多100",
},
},
status: {
type: "string",
valueHint: "<s>",
description: "Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED)",
description: {
"en-US": "Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED)",
"zh-CN": "按状态筛选PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED",
},
},
baseModel: {
type: "string",
valueHint: "<model>",
description: {
"en-US": "Filter by base model ID (server-side)",
"zh-CN": "按基础模型 ID 筛选(服务端筛选)",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "List fine-tune jobs",
description: { "en-US": "List fine-tune jobs", "zh-CN": "列出微调任务" },
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");
},
});
+39 -52
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 };
@@ -67,31 +66,47 @@ const LOGS_FLAGS = {
jobId: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码默认1" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Lines per page (default: server-defined)",
description: {
"en-US": "Lines per page (default: server-defined)",
"zh-CN": "每页行数(默认:由服务端决定)",
},
},
search: {
type: "string",
valueHint: "<keyword>",
description:
"Case-insensitive substring filter. When set, all log pages are fetched and filtered client-side (--page is ignored).",
description: {
"en-US":
"Case-insensitive substring filter. When set, all log pages are fetched and filtered client-side (--page is ignored).",
"zh-CN": "不区分大小写的子字符串筛选。设置后会获取全部日志页并在客户端筛选(忽略 --page。",
},
},
tail: {
type: "number",
valueHint: "<n>",
description:
"Keep only the last N entries. When set, all log pages are fetched and the trailing N are kept (--page is ignored).",
description: {
"en-US":
"Keep only the last N entries. When set, all log pages are fetched and the trailing N are kept (--page is ignored).",
"zh-CN": "仅保留最后 N 条记录。设置后会获取全部日志页并保留末尾 N 条(忽略 --page。",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: "Fetch training logs for a fine-tune job",
description: {
"en-US": "Fetch training logs for a fine-tune job",
"zh-CN": "获取微调任务的训练日志",
},
auth: "apiKey",
usageArgs: "--job-id <id> [--page <n>] [--page-size <n>] [--search <keyword>] [--tail <n>]",
flags: LOGS_FLAGS,
@@ -110,7 +125,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 +136,7 @@ export default defineCommand({
search,
tail,
},
format,
"json",
);
return;
}
@@ -147,18 +161,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 +170,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,166 @@
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: {
"en-US": "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
"zh-CN": "待微调的基座模型(例如 qwen3-8b不是输出模型名称",
},
required: true,
},
datasets: {
type: "string",
valueHint: "<ids>",
description: {
"en-US": "Training dataset file IDs, comma-separated (required)",
"zh-CN": "训练数据集文件 ID多个以逗号分隔必填",
},
required: true,
},
trainingType: {
type: "string",
valueHint: "<type>",
description: {
"en-US": "Training type: sft | dpo | cpt (default: sft)",
"zh-CN": "训练类型sft | dpo | cpt默认sft",
},
},
nEpochs: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Number of training epochs (default: 3)",
"zh-CN": "训练轮数默认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: {
"en-US": "Estimate the training cost for a fine-tune job (token billing)",
"zh-CN": "估算微调任务的训练费用(按 Token 计费)",
},
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: [
{
"en-US":
"Estimate only — the server computes token usage from the datasets; final cost is subject to the bill.",
"zh-CN": "该结果仅为估算值——服务端根据数据集计算 Token 用量,最终费用以账单为准。",
},
{
"en-US":
"Covers token billing for sft / dpo / cpt. Training-unit (MTU) billing is not supported by this command.",
"zh-CN": "支持估算 sft / dpo / cpt 的 Token 计费此命令不支持训练单元MTU计费估算。",
},
{
"en-US":
"Hyper-parameters other than --n-epochs are fixed at representative defaults for estimation.",
"zh-CN": "除 --n-epochs 外,其他超参数会使用具有代表性的固定默认值进行估算。",
},
],
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;
@@ -52,50 +52,84 @@ const WATCH_FLAGS = {
jobId: {
type: "string",
valueHint: "<id>",
description: "Fine-tune job ID (required)",
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
follow: {
type: "switch",
description:
"Block and poll until a terminal state (the legacy behavior). Without it, a single status probe is performed and the command returns immediately.",
description: {
"en-US":
"Block and poll until a terminal state (the legacy behavior). Without it, a single status probe is performed and the command returns immediately.",
"zh-CN": "阻塞并轮询至终态(旧版行为)。不使用时仅查询一次状态并立即返回。",
},
},
interval: {
type: "number",
valueHint: "<sec>",
description: `Seconds between polls with --follow (default: ${DEFAULT_INTERVAL_SEC}, min: ${MIN_INTERVAL_SEC}). Ignored without --follow.`,
description: {
"en-US": `Seconds between polls with --follow (default: ${DEFAULT_INTERVAL_SEC}, min: ${MIN_INTERVAL_SEC}). Ignored without --follow.`,
"zh-CN": `使用 --follow 时的轮询间隔秒数(默认:${DEFAULT_INTERVAL_SEC},最小:${MIN_INTERVAL_SEC})。未使用 --follow 时忽略。`,
},
},
pollTimeout: {
type: "number",
valueHint: "<sec>",
description:
"With --follow, stop polling after this many seconds (default: no limit). Ignored without --follow.",
description: {
"en-US":
"With --follow, stop polling after this many seconds (default: no limit). Ignored without --follow.",
"zh-CN": "使用 --follow 时,在指定秒数后停止轮询(默认:无限制)。未使用 --follow 时忽略。",
},
},
} satisfies FlagsDef;
export default defineCommand({
description:
"Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal.",
description: {
"en-US":
"Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal.",
"zh-CN": "查询微调任务状态(默认:单次非阻塞获取)。使用 --follow 持续轮询至终态。",
},
auth: "apiKey",
usageArgs: "--job-id <id> [--follow] [--interval <sec>] [--poll-timeout <sec>]",
flags: WATCH_FLAGS,
exampleArgs: [
"--job-id ft-xxx # single probe, returns immediately",
"--job-id ft-xxx --output json # status probe for agents",
"--job-id ft-xxx --follow # block until terminal",
{
"en-US": "--job-id ft-xxx # single probe, returns immediately",
"zh-CN": "--job-id ft-xxx # 单次查询,立即返回",
},
{
"en-US": "--job-id ft-xxx --output json # status probe for agents",
"zh-CN": "--job-id ft-xxx --output json # 供智能体查询状态",
},
{
"en-US": "--job-id ft-xxx --follow # block until terminal",
"zh-CN": "--job-id ft-xxx --follow # 阻塞等待至终态",
},
"--job-id ft-xxx --follow --interval 5",
"--job-id ft-xxx --follow --poll-timeout 3600",
],
notes: [
"Default (no --follow) is a NON-BLOCKING single status probe: one fetch, then",
"return immediately. This is the mode meant for agents / scripts — the caller",
"owns the polling cadence, so the CLI never holds the terminal.",
"A terminal FAILED/CANCELED status raises a normal CLI error (non-zero exit);",
"a SUCCEEDED or still-running status returns 0. With --follow, exceeding",
"--poll-timeout raises a timeout error.",
"Use --follow for the blocking, human-terminal-follow experience; use the",
"default mode when driving the loop yourself (e.g. from an agent).",
"For per-step training output (not status), use `finetune logs`.",
{
"en-US":
"Default (no --follow) is a NON-BLOCKING single status probe: one fetch, then return immediately. This is the mode meant for agents / scripts — the caller owns the polling cadence, so the CLI never holds the terminal.",
"zh-CN":
"默认不使用 --follow执行非阻塞的单次状态查询获取一次后立即返回。该模式适用于 Agent / 脚本由调用方控制轮询节奏CLI 不会持续占用终端。",
},
{
"en-US":
"A terminal FAILED/CANCELED status raises a normal CLI error (non-zero exit); a SUCCEEDED or still-running status returns 0. With --follow, exceeding --poll-timeout raises a timeout error.",
"zh-CN":
"终态 FAILED/CANCELED 会触发普通 CLI 错误非零退出码SUCCEEDED 或仍在运行时返回 0。使用 --follow 时,超过 --poll-timeout 会触发超时错误。",
},
{
"en-US":
"Use --follow for the blocking, human-terminal-follow experience; use the default mode when driving the loop yourself (e.g. from an agent).",
"zh-CN":
"需要在人工终端中阻塞跟踪时使用 --follow自行驱动轮询例如通过 Agent时使用默认模式。",
},
{
"en-US": "For per-step training output (not status), use `finetune logs`.",
"zh-CN": "要查看逐步骤训练输出(而非状态),请使用 `finetune logs`。",
},
],
async run(ctx) {
const { settings, flags } = ctx;
@@ -103,7 +137,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 +147,7 @@ export default defineCommand({
interval: intervalSec,
timeout: pollTimeoutSec,
},
format,
"json",
);
return;
}
@@ -132,16 +165,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 +209,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 +256,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;
+90 -21
View File
@@ -35,41 +35,67 @@ const EDIT_FLAGS = {
image: {
type: "array",
valueHint: "<url>",
description: "Source image URL or local file path (repeatable for multi-image merge)",
description: {
"en-US": "Source image URL or local file path (repeatable for multi-image merge)",
"zh-CN": "源图片 URL 或本地文件路径(多图融合时可重复)",
},
required: true,
},
prompt: {
type: "string",
valueHint: "<text>",
description: "Edit instruction text",
description: { "en-US": "Edit instruction text", "zh-CN": "编辑指令文本" },
required: true,
},
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-3.0)",
description: {
"en-US": "Model ID (default: qwen-image-3.0)",
"zh-CN": "模型 ID默认qwen-image-3.0",
},
},
size: {
type: "string",
valueHint: "<W*H>",
description: "Output image size: ratio (3:4, 16:9) or pixels (2048*2048)",
description: {
"en-US": "Output image size: ratio (3:4, 16:9) or pixels (2048*2048)",
"zh-CN": "输出图片尺寸比例3:4、16:9或像素2048*2048",
},
},
n: {
type: "number",
valueHint: "<count>",
description: "Number of images (default: 1, max: 6)",
description: {
"en-US": "Number of images (default: 1, max: 6)",
"zh-CN": "图片数量默认1最多6",
},
},
seed: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Random seed for reproducible results",
"zh-CN": "用于复现结果的随机种子",
},
},
seed: { type: "number", valueHint: "<n>", description: "Random seed for reproducible results" },
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
description: {
"en-US": "Negative prompt to exclude unwanted content",
"zh-CN": "负向提示词,用于排除不需要的内容",
},
},
function: {
type: "string",
valueHint: "<name>",
description:
"wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit",
description: {
"en-US":
"wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit",
"zh-CN":
"wanx*-imageedit 功能默认description_edit。例如stylization_all、description_edit",
},
},
promptExtend: {
type: "boolean",
@@ -81,36 +107,79 @@ const EDIT_FLAGS = {
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
},
outDir: { type: "string", valueHint: "<dir>", description: "Download images to directory" },
outDir: {
type: "string",
valueHint: "<dir>",
description: { "en-US": "Download images to directory", "zh-CN": "将图片下载到指定目录" },
},
outPrefix: {
type: "string",
valueHint: "<prefix>",
description: "Filename prefix (default: edited)",
description: {
"en-US": "Filename prefix (default: edited)",
"zh-CN": "文件名前缀默认edited",
},
},
...ASYNC_FLAG,
...CONCURRENT_FLAG,
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 3)",
description: {
"en-US": "Polling interval when waiting (default: 3)",
"zh-CN": "等待任务时的轮询间隔默认3 秒)",
},
},
} satisfies FlagsDef;
type EditFlags = ParsedFlags<typeof EDIT_FLAGS>;
export default defineCommand({
description: "Edit an existing image with text instructions (Qwen-Image / Wan 2.7)",
description: {
"en-US": "Edit an existing image with text instructions (Qwen-Image / Wan 2.7)",
"zh-CN": "使用文本指令编辑现有图片Qwen-Image / Wan 2.7",
},
auth: "apiKey",
usageArgs: "--image <url> --prompt <text> [flags]",
flags: EDIT_FLAGS,
exampleArgs: [
'--image ./photo.png --prompt "Replace the background with a beach"',
'--image https://example.com/logo.png --prompt "Change color to blue" --n 3',
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
'--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all',
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
{
"en-US": '--image ./photo.png --prompt "Replace the background with a beach"',
"zh-CN": '--image ./photo.png --prompt "将背景替换为海滩"',
},
{
"en-US": '--image https://example.com/logo.png --prompt "Change color to blue" --n 3',
"zh-CN": '--image https://example.com/logo.png --prompt "将颜色改为蓝色" --n 3',
},
{
"en-US": '--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
"zh-CN": '--image ./a.png --image ./b.png --prompt "将两张图片合并成一张拼贴图"',
},
{
"en-US":
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
"zh-CN":
'--image https://example.com/photo.png --prompt "移除人物" --model qwen-image-2.0-pro',
},
{
"en-US": '--image ./photo.png --prompt "Change the style" --model wan2.7-image',
"zh-CN": '--image ./photo.png --prompt "更改图片风格" --model wan2.7-image',
},
{
"en-US":
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
"zh-CN": '--image ./photo.png --prompt "将主体放在桌面上" --model wan2.5-i2i-preview',
},
{
"en-US":
'--image ./photo.png --prompt "Convert to a picture-book style" --model wanx2.1-imageedit --function stylization_all',
"zh-CN":
'--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all',
},
{
"en-US":
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
"zh-CN": '--image ./photo.png --prompt "将背景替换为海滩" --watermark false',
},
],
async run(ctx) {
const { settings, flags } = ctx;
@@ -31,31 +31,51 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai
import { join } from "path";
const GENERATE_FLAGS = {
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
prompt: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Image description", "zh-CN": "图片描述" },
required: true,
},
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-3.0)",
description: {
"en-US": "Model ID (default: qwen-image-3.0)",
"zh-CN": "模型 ID默认qwen-image-3.0",
},
},
size: {
type: "string",
valueHint: "<W*H>",
description: "Image size: ratio (3:4, 16:9, 1:1) or pixels (2048*2048)",
description: {
"en-US": "Image size: ratio (3:4, 16:9, 1:1) or pixels (2048*2048)",
"zh-CN": "图片尺寸比例3:4、16:9、1:1或像素2048*2048",
},
},
n: {
type: "number",
valueHint: "<count>",
description: "Number of images per request (default: 1, max: 6)",
description: {
"en-US": "Number of images per request (default: 1, max: 6)",
"zh-CN": "每次请求生成的图片数量默认1最多6",
},
},
seed: {
type: "number",
valueHint: "<n>",
description: "Random seed for reproducible generation",
description: {
"en-US": "Random seed for reproducible generation",
"zh-CN": "用于复现生成结果的随机种子",
},
},
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
description: {
"en-US": "Negative prompt to exclude unwanted content",
"zh-CN": "负向提示词,用于排除不需要的内容",
},
},
promptExtend: {
type: "boolean",
@@ -69,37 +89,83 @@ const GENERATE_FLAGS = {
},
...ASYNC_FLAG,
...CONCURRENT_FLAG,
outDir: { type: "string", valueHint: "<dir>", description: "Download images to directory" },
outDir: {
type: "string",
valueHint: "<dir>",
description: { "en-US": "Download images to directory", "zh-CN": "将图片下载到指定目录" },
},
outPrefix: {
type: "string",
valueHint: "<prefix>",
description: "Filename prefix (default: image)",
description: {
"en-US": "Filename prefix (default: image)",
"zh-CN": "文件名前缀默认image",
},
},
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 3)",
description: {
"en-US": "Polling interval when waiting (default: 3)",
"zh-CN": "等待任务时的轮询间隔默认3 秒)",
},
},
} satisfies FlagsDef;
type GenerateFlags = ParsedFlags<typeof GENERATE_FLAGS>;
export default defineCommand({
description: "Generate images (Qwen-Image / wan2.x)",
description: {
"en-US": "Generate images (Qwen-Image / wan2.x)",
"zh-CN": "生成图片Qwen-Image / wan2.x",
},
auth: "apiKey",
usageArgs: "--prompt <text> [flags]",
flags: GENERATE_FLAGS,
exampleArgs: [
'--prompt "A cat in a spacesuit on Mars"',
'--prompt "Logo design" --n 3 --out-dir ./generated/',
'--prompt "Mountain landscape" --size 2688*1536',
'--prompt "A castle" --seed 42 --prompt-extend false',
'--prompt "Logo" --watermark false',
'--prompt "An alien in the space" --watermark false',
'--prompt "sunset" --model wan2.6-t2i --async --quiet',
'--prompt "plush doll" --model z-image-turbo --size 1024*1024',
'--prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024',
'--prompt "Pro quality" --model qwen-image-2.0-pro',
'--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
{
"en-US": '--prompt "A cat in a spacesuit on Mars"',
"zh-CN": '--prompt "一只穿着宇航服的猫站在火星上"',
},
{
"en-US": '--prompt "Logo design" --n 3 --out-dir ./generated/',
"zh-CN": '--prompt "Logo 设计" --n 3 --out-dir ./generated/',
},
{
"en-US": '--prompt "Mountain landscape" --size 2688*1536',
"zh-CN": '--prompt "山地景观" --size 2688*1536',
},
{
"en-US": '--prompt "A castle" --seed 42 --prompt-extend false',
"zh-CN": '--prompt "一座城堡" --seed 42 --prompt-extend false',
},
{
"en-US": '--prompt "Logo" --watermark false',
"zh-CN": '--prompt "Logo" --watermark false',
},
{
"en-US": '--prompt "An alien in the space" --watermark false',
"zh-CN": '--prompt "太空中的外星人" --watermark false',
},
{
"en-US": '--prompt "sunset" --model wan2.6-t2i --async --quiet',
"zh-CN": '--prompt "日落" --model wan2.6-t2i --async --quiet',
},
{
"en-US": '--prompt "plush doll" --model z-image-turbo --size 1024*1024',
"zh-CN": '--prompt "毛绒玩偶" --model z-image-turbo --size 1024*1024',
},
{
"en-US": '--prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024',
"zh-CN": '--prompt "日落" --model wanx2.0-t2i-turbo --size 1024*1024',
},
{
"en-US": '--prompt "Pro quality" --model qwen-image-2.0-pro',
"zh-CN": '--prompt "专业品质" --model qwen-image-2.0-pro',
},
{
"en-US": '--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
"zh-CN": '--prompt "产品摄影" --n 2 --concurrent 3 # 并行生成 6 张图片',
},
],
async run(ctx) {
const { settings, flags } = ctx;
@@ -0,0 +1,90 @@
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: { "en-US": "Category name (1-20 chars)", "zh-CN": "类目名称120 个字符)" },
required: true,
},
parentId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Create as a sub-category of this category",
"zh-CN": "创建为该类目的子类目",
},
},
collectionId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Create under this collection (defaults to the platform collection)",
"zh-CN": "在该数据集合下创建(默认为平台数据集合)",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Create a data-center category", "zh-CN": "创建数据中心类目" },
auth: "apiKey",
usageArgs: "--name <text> [flags]",
flags: CATEGORY_ADD_FLAGS,
notes: [
{
"en-US": "Use categories to organize data-center files by business domain.",
"zh-CN": "使用类目按业务领域组织数据中心文件。",
},
],
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,72 @@
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: { "en-US": "Category ID to delete", "zh-CN": "要删除的类目 ID" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a data-center category", "zh-CN": "删除数据中心类目" },
auth: "apiKey",
usageArgs: "--category-id <id> [flags]",
flags: CATEGORY_DELETE_FLAGS,
notes: [
{
"en-US":
"Behavior for categories containing files or sub-categories is server-defined — the server error is passed through as-is.",
"zh-CN": "包含文件或子类目时的处理行为由服务端定义——服务端返回的错误将原样透传。",
},
],
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,113 @@
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: { "en-US": "Filter by exact collection ID", "zh-CN": "按数据集合 ID 精确筛选" },
},
parentId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "List sub-categories of this exact parent category",
"zh-CN": "列出该父类目下的子类目",
},
},
name: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "Filter by category name (exact match, unlike the knowledge base list)",
"zh-CN": "按类目名称筛选(精确匹配,与知识库列表不同)",
},
},
nextToken: {
type: "string",
valueHint: "<token>",
description: {
"en-US": "Cursor for the next page (from previous output)",
"zh-CN": "下一页游标(来自上一次输出)",
},
},
maxResult: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Items per page (default: 20)", "zh-CN": "每页条目数默认20" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "List data-center categories", "zh-CN": "列出数据中心类目" },
auth: "apiKey",
usageArgs: "[flags]",
flags: CATEGORY_LIST_FLAGS,
notes: [
{
"en-US": "Categories marked [default] are where files land when no category is specified.",
"zh-CN": "未指定类目时,文件会进入标记为 [default] 的类目。",
},
{
"en-US": "Pagination is cursor-based: reuse the printed next token to continue.",
"zh-CN": "分页使用游标:复用输出中的 next token 继续查询。",
},
],
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,30 +13,48 @@ 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: {
type: "array",
valueHint: "<text>",
description:
"Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format",
description: {
"en-US":
"Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format",
"zh-CN":
"消息文本(可重复)。支持使用 role:content 前缀指定角色(例如 user:hello默认为 user。遵循 OpenAI 消息格式",
},
},
agentId: {
type: "string",
valueHint: "<id>",
description: "Q&A service ID (find in console knowledge Q&A page)",
description: {
"en-US": "Q&A service ID (find in console knowledge Q&A page)",
"zh-CN": "问答服务 ID可在控制台知识库问答页面查看",
},
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: {
"en-US":
"Service version to call: beta (draft for debugging) or a published number; default is the latest published version",
"zh-CN": "要调用的服务版本beta用于调试的草稿或已发布版本号默认使用最新发布版本",
},
},
image: {
type: "array",
valueHint: "<url>",
description: "Image URL (repeatable). Attached to the last user message as multimodal content",
description: {
"en-US": "Image URL (repeatable). Attached to the last user message as multimodal content",
"zh-CN": "图片 URL可重复。将作为多模态内容附加到最后一条用户消息",
},
},
} satisfies FlagsDef;
type ChatFlags = ParsedFlags<typeof CHAT_FLAGS>;
@@ -137,20 +155,58 @@ const STEP_LABELS: Record<string, string> = {
};
export default defineCommand({
description: "Chat with a Bailian knowledge base (RAG Q&A with streaming)",
description: {
"en-US": "Chat with a Bailian knowledge base (RAG Q&A with streaming)",
"zh-CN": "与百炼知识库对话(支持流式输出的 RAG 问答)",
},
auth: "apiKey",
usageArgs: "--message <text> --agent-id <id> [flags]",
flags: CHAT_FLAGS,
notes: [
"Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.",
"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.',
{
"en-US":
"Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.",
"zh-CN":
"响应以 SSE 流事件返回。事件生命周期tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end。tool_calling → tool_return 可能循环多次。",
},
{
"en-US":
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
"zh-CN": "鉴权:使用 DashScope API KeyBearer Token。可在控制台 API Key 页面获取。",
},
{
"en-US":
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
"zh-CN":
"`--workspace-id` 可通过 BAILIAN_WORKSPACE_ID 环境变量或 `kscli config set workspace_id <id>` 设置。",
},
{
"en-US":
'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.',
"zh-CN": '多轮对话:使用 --message "user:..." 和 --message "assistant:..." 传入对话历史。',
},
{
"en-US": "`--agent-version beta` calls the draft config for debugging before it is deployed.",
"zh-CN": "`--agent-version beta` 会调用尚未部署的草稿配置,便于发布前调试。",
},
],
exampleArgs: [
'--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
'--message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx',
'--message "Describe these images" --image https://example.com/a.png --image https://example.com/b.png --agent-id aid-xxx --workspace-id ws-xxx',
{
"en-US": '--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
"zh-CN": '--message "什么是 RAG" --agent-id aid-xxx --workspace-id ws-xxx',
},
{
"en-US":
'--message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx',
"zh-CN":
'--message "user:什么是 RAG" --message "assistant:RAG 是……" --message "它是如何工作的?" --agent-id aid-xxx --workspace-id ws-xxx',
},
{
"en-US":
'--message "Describe these images" --image https://example.com/a.png --image https://example.com/b.png --agent-id aid-xxx --workspace-id ws-xxx',
"zh-CN":
'--message "描述这些图片" --image https://example.com/a.png --image https://example.com/b.png --agent-id aid-xxx --workspace-id ws-xxx',
},
],
validate: (f) =>
(f.message && f.message.length > 0) || (f.image && f.image.length > 0)
@@ -168,14 +224,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 +248,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,216 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description: {
"en-US":
"Owning document ID from the doc list command; required in practice for all knowledge base types",
"zh-CN": "来自文档列表命令的所属文档 ID实际使用时所有知识库类型都需要提供",
},
},
content: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "Chunk body text, up to 6000 chars (document-type); alternative to --content-file",
"zh-CN": "Chunk 正文,最多 6000 个字符(文档型);与 --content-file 二选一",
},
},
contentFile: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Read chunk body from a UTF-8 plain text file (.md/.txt etc.)",
"zh-CN": "从 UTF-8 纯文本文件(.md/.txt 等)读取 Chunk 正文",
},
},
title: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "Chunk title, up to 50 chars (document-type)",
"zh-CN": "Chunk 标题,最多 50 个字符(文档型)",
},
},
imageUrl: {
type: "array",
valueHint: "<url>",
description: {
"en-US": "Chunk image URL (repeatable, up to 10; document-type)",
"zh-CN": "Chunk 图片 URL可重复最多 10 个;文档型)",
},
},
field: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US":
"Arbitrary field entry (repeatable) for table/image knowledge bases where keys are Excel column headers; mutually exclusive with content/title/image flags",
"zh-CN":
"表格/图片知识库的自定义字段(可重复),键为 Excel 列标题;不能与 content/title/image 相关选项同时使用",
},
},
...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: {
"en-US": "Add a chunk directly to a knowledge base",
"zh-CN": "直接向知识库添加 Chunk",
},
auth: "apiKey",
usageArgs: "--index-id <id> (--content <text> | --field <k=v>) [flags]",
flags: CHUNK_ADD_FLAGS,
notes: [
{
"en-US": "Document / table / image knowledge bases are supported; audio-video ones are not.",
"zh-CN": "支持文档、表格和图片知识库;不支持音视频知识库。",
},
{
"en-US":
"--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.",
"zh-CN":
"实际使用时,所有知识库类型都需要 --doc-id。请使用文档列表命令返回的文档级 IDChunk 列表每行返回的 doc_id 不被接受。",
},
{
"en-US":
"Image-type documents do not support text chunks. Target a text-type document (docx/pdf/txt) instead.",
"zh-CN": "图片型文档不支持文本 Chunk请改为操作文本型文档docx/pdf/txt。",
},
{
"en-US":
"The API is idempotent but rate-limited to 10 calls per second — throttle batch scripts.",
"zh-CN": "该 API 具有幂等性,但限流为每秒 10 次调用——批处理脚本需要控制速率。",
},
{
"en-US": "The response carries no chunk id; list chunks afterwards to find the new one.",
"zh-CN": "响应不包含 Chunk ID添加后请列出 Chunk 以查找新条目。",
},
{
"en-US":
"For table/image knowledge bases use --field with Excel column headers as keys; values are passed through as strings.",
"zh-CN": "表格/图片知识库请使用 --field并以 Excel 列标题作为键;值将按字符串原样传递。",
},
],
exampleArgs: [
{
"en-US": '--index-id idx-xxx --content "chunk text" --title intro --workspace-id ws-xxx',
"zh-CN": '--index-id idx-xxx --content "Chunk 文本" --title 简介 --workspace-id ws-xxx',
},
{
"en-US": "--index-id idx-xxx --field columnA=v1 --field columnB=v2",
"zh-CN": "--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,119 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
chunkId: {
type: "array",
valueHint: "<id>",
description: {
"en-US": "Chunk ID to delete (repeatable; batches of 10 are sent automatically)",
"zh-CN": "要删除的 Chunk ID可重复每 10 个自动分批发送)",
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...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: {
"en-US": "Delete chunks from a knowledge base (irreversible)",
"zh-CN": "从知识库中删除 Chunk不可撤销",
},
auth: "apiKey",
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
flags: CHUNK_DELETE_FLAGS,
notes: [
{
"en-US": "Accepts at most 10 chunk ids per call; larger sets are batched automatically.",
"zh-CN": "每次调用最多接受 10 个 Chunk ID更多 ID 会自动分批处理。",
},
],
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,115 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Only show chunks belonging to this document",
"zh-CN": "仅显示属于该文档的 Chunk",
},
},
...PAGE_FLAGS,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "List chunks in a knowledge base with content and status",
"zh-CN": "列出知识库中的 Chunk 及其内容和状态",
},
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: CHUNK_LIST_FLAGS,
notes: [
{
"en-US":
"Use metadata._id as the chunk id and metadata.doc_id as the document id in chunk update/delete commands.",
"zh-CN":
"在 Chunk 更新/删除命令中,使用 metadata._id 作为 Chunk ID使用 metadata.doc_id 作为文档 ID。",
},
{
"en-US": "Page size defaults to 20 (server default), max 100.",
"zh-CN": "分页大小默认为 20服务端默认值最大为 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,220 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
chunkId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Chunk ID (metadata._id from the chunk list output)",
"zh-CN": "Chunk ID来自 Chunk 列表输出的 metadata._id",
},
required: true,
},
docId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Document ID owning the chunk (metadata.doc_id from the chunk list output)",
"zh-CN": "Chunk 所属文档 ID来自 Chunk 列表输出的 metadata.doc_id",
},
required: true,
},
content: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "New chunk content, 10-6000 chars; alternative to --content-file",
"zh-CN": "新的 Chunk 内容106000 个字符;与 --content-file 二选一",
},
},
contentFile: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Read new content from a UTF-8 plain text file (.md/.txt etc.)",
"zh-CN": "从 UTF-8 纯文本文件(.md/.txt 等)读取新内容",
},
},
title: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "Chunk title, 0-50 chars (empty string clears it; omit to keep unchanged)",
"zh-CN": "Chunk 标题050 个字符(空字符串表示清除;省略则保持不变)",
},
},
exclude: {
type: "switch",
description: {
"en-US": "Exclude this chunk from retrieval",
"zh-CN": "在检索中排除该 Chunk",
},
},
include: {
type: "switch",
description: {
"en-US": "Include this chunk in retrieval (default)",
"zh-CN": "在检索中包含该 Chunk默认",
},
},
...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: {
"en-US": "Update chunk content or toggle its retrieval visibility",
"zh-CN": "更新 Chunk 内容或切换其检索可见性",
},
auth: "apiKey",
usageArgs: "--index-id <id> --chunk-id <id> --doc-id <id> [flags]",
flags: CHUNK_UPDATE_FLAGS,
notes: [
{
"en-US": "Content must be 10-6000 characters and within the knowledge base's max chunk size.",
"zh-CN": "内容必须为 106000 个字符,且不能超过知识库的最大 Chunk 大小。",
},
{
"en-US":
"--content-file expects a UTF-8 plain text file; document formats (.docx/.pdf) are not parsed here.",
"zh-CN": "--content-file 需要 UTF-8 纯文本文件;此处不会解析 .docx/.pdf 等文档格式。",
},
{
"en-US":
"Toggling --exclude/--include without new content re-submits the existing content automatically.",
"zh-CN": "仅切换 --exclude/--include 而不提供新内容时,会自动重新提交现有内容。",
},
],
exampleArgs: [
{
"en-US":
'--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "corrected text"',
"zh-CN": '--index-id idx-xxx --chunk-id chunk-xxx --doc-id file-xxx --content "修正后的文本"',
},
"--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,146 @@
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: { "en-US": "Collection name", "zh-CN": "数据集合名称" },
required: true,
},
description: {
type: "string",
valueHint: "<text>",
description: {
"en-US":
"What this collection holds and what it is for — tells collections apart in the list",
"zh-CN": "数据集合装了什么内容、给谁用,用于在列表中区分同类集合",
},
required: true,
},
storeType: {
type: "string",
valueHint: "<type>",
description: {
"en-US": "Storage: platform (managed) or custom (your own OSS bucket)",
"zh-CN": "存储类型platform托管或 custom自有 OSS Bucket",
},
},
ossRegion: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "OSS region id (required with --store-type custom)",
"zh-CN": "OSS Region ID使用 --store-type custom 时必填)",
},
},
ossBucket: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "OSS bucket name (required with --store-type custom)",
"zh-CN": "OSS Bucket 名称(使用 --store-type custom 时必填)",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Create a FILE data collection", "zh-CN": "创建 FILE 数据集合" },
auth: "apiKey",
usageArgs: "--name <text> --description <text> [flags]",
flags: COLLECTION_CREATE_FLAGS,
notes: [
{
"en-US":
"Store type defaults to platform (managed storage); custom uses your authorized OSS bucket.",
"zh-CN": "存储类型默认为 platform托管存储custom 使用已授权的自有 OSS Bucket。",
},
{
"en-US":
"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.",
"zh-CN":
"自定义 Bucket 必须带有 bailian-connector-access=ReadAndWrite 标签(百炼基于标签的访问控制);缺少该标签时,服务端会以容易误解的 'setBucketCORS failed' 错误拒绝创建。",
},
{
"en-US": "There is no collection delete API — create collections deliberately.",
"zh-CN": "目前没有删除数据集合的 API——请谨慎创建。",
},
],
exampleArgs: [
{
"en-US": "--name my-collection --description 'team docs' --workspace-id ws-xxx",
"zh-CN": "--name my-collection --description '团队文档' --workspace-id ws-xxx",
},
{
"en-US":
"--name oss-coll --description 'own bucket' --store-type custom --oss-region cn-beijing --oss-bucket my-bucket",
"zh-CN":
"--name oss-coll --description '自有 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,81 @@
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: {
"en-US": "Collection ID; alternative to --name",
"zh-CN": "数据集合 ID与 --name 二选一",
},
},
name: {
type: "string",
valueHint: "<text>",
description: {
"en-US": "Collection name; alternative to --collection-id",
"zh-CN": "数据集合名称;与 --collection-id 二选一",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Show data collection details", "zh-CN": "查看数据集合详情" },
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,121 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
docId: {
type: "array",
valueHint: "<id>",
description: {
"en-US": "Document ID to delete (repeatable)",
"zh-CN": "要删除的文档 ID可重复",
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...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: {
"en-US": "Delete documents and their chunks from a knowledge base",
"zh-CN": "从知识库中删除文档及其 Chunk",
},
auth: "apiKey",
usageArgs: "--index-id <id> --doc-id <id> [flags]",
flags: DOC_DELETE_FLAGS,
notes: [
{
"en-US":
"Removes documents from the knowledge base index only; the source files remain in the data center.",
"zh-CN": "仅从知识库索引中移除文档;源文件仍保留在数据中心。",
},
{
"en-US":
"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.",
"zh-CN":
"请使用 `knowledge doc list --quiet` 返回的 doc_id而不是 `knowledge doc upload` 返回的 fileId。通过 `knowledge create --doc-id` 创建的文档,其 doc_id 等于 fileId通过 `knowledge doc upload --index-id` 导入的文档,其 doc_id 可能带有 Workspace 后缀。",
},
{
"en-US":
"Deletion may take up to ~30s to propagate — the document may still appear in the doc list briefly.",
"zh-CN": "删除结果最多可能需要约 30 秒才会生效——文档可能会短暂地继续出现在列表中。",
},
{
"en-US": "The output lists the ids actually deleted.",
"zh-CN": "输出会列出实际删除的 ID。",
},
],
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,148 @@
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: { "en-US": "Authorized OSS bucket name", "zh-CN": "已授权的 OSS Bucket 名称" },
required: true,
},
region: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "OSS region id (e.g. cn-beijing)",
"zh-CN": "OSS Region ID例如 cn-beijing",
},
required: true,
},
ossKey: {
type: "array",
valueHint: "<key>",
description: {
"en-US": "OSS object key to import (repeatable, 1-10 per call)",
"zh-CN": "要导入的 OSS Object Key可重复每次调用 110 个)",
},
required: true,
},
categoryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Target data-center category (default: the default category)",
"zh-CN": "目标数据中心类目(默认:默认类目)",
},
},
tag: {
type: "array",
valueHint: "<text>",
description: {
"en-US": "File tag applied to every imported file (repeatable, up to 10)",
"zh-CN": "应用于每个导入文件的标签(可重复,最多 10 个)",
},
},
overwrite: {
type: "switch",
description: {
"en-US": "Overwrite files previously imported from the same OSS keys",
"zh-CN": "覆盖此前从相同 OSS Key 导入的文件",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Batch import files from an authorized OSS bucket into the data center",
"zh-CN": "从已授权的 OSS Bucket 批量导入文件到数据中心",
},
auth: "apiKey",
usageArgs: "--bucket <name> --region <id> --oss-key <key> [flags]",
flags: DOC_IMPORT_OSS_FLAGS,
notes: [
{
"en-US":
"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.",
"zh-CN":
"必须预先将 Bucket 授权给平台服务角色;服务端权限错误将原样透传,并提示在 RAM 控制台检查 AliyunServiceRoleForBailian。",
},
{
"en-US": "File names are derived from the OSS key basename.",
"zh-CN": "文件名取自 OSS Key 的 basename。",
},
{
"en-US":
"--overwrite replaces the previously imported file and issues a NEW fileId (the old one becomes invalid) — verified live.",
"zh-CN":
"--overwrite 会替换此前导入的文件并生成新的 fileId旧 fileId 将失效)——已通过真实环境验证。",
},
],
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,93 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
...PAGE_FLAGS,
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "List documents in a knowledge base with parse/index status",
"zh-CN": "列出知识库文档及其解析/索引状态",
},
auth: "apiKey",
usageArgs: "--index-id <id> [flags]",
flags: DOC_LIST_FLAGS,
notes: [
{
"en-US":
"Documents with status FAILED are highlighted in text mode — use the import job status command to inspect failures.",
"zh-CN": "文本模式会突出显示状态为 FAILED 的文档——请使用导入任务状态命令检查失败详情。",
},
{
"en-US": "Page size defaults to 10 (server default), max 100.",
"zh-CN": "分页大小默认为 10服务端默认值最大为 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,151 @@
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: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
jobId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Import job ID (ingestionId returned by import commands)",
"zh-CN": "导入任务 ID导入命令返回的 ingestionId",
},
required: true,
},
...PAGE_FLAGS,
wait: {
type: "switch",
description: {
"en-US": "Poll until the job reaches a terminal state",
"zh-CN": "轮询直到任务进入终态",
},
},
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Polling interval when waiting (default: 5)",
"zh-CN": "等待时的轮询间隔默认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: {
"en-US": "Check knowledge base import job status",
"zh-CN": "检查知识库导入任务状态",
},
auth: "apiKey",
usageArgs: "--index-id <id> --job-id <id> [flags]",
flags: DOC_STATUS_FLAGS,
notes: [
{
"en-US": "Both --index-id and --job-id are required (passing only one returns SystemError).",
"zh-CN": "--index-id 和 --job-id 均为必填(只传其中一个会返回 SystemError。",
},
{
"en-US":
"If you see a SystemError, the job may not exist — check the ingestion id in the document list output.",
"zh-CN": "如果出现 SystemError任务可能不存在——请检查文档列表输出中的 ingestion ID。",
},
{
"en-US":
"Overall job states are PENDING / RUNNING / COMPLETED; per-document failures (for example PARSE_FAILED) exit non-zero with the error message passed through.",
"zh-CN":
"任务整体状态为 PENDING / RUNNING / COMPLETED单个文档失败例如 PARSE_FAILED时会以非零状态退出并原样透传错误信息。",
},
],
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,109 @@
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: {
"en-US": "Data-center file ID to tag (repeatable, 1-20 per call)",
"zh-CN": "要添加标签的数据中心文件 ID可重复每次调用 120 个)",
},
required: true,
},
tag: {
type: "array",
valueHint: "<text>",
description: {
"en-US": "Tag applied to every --doc-id (repeatable, each up to 32 chars)",
"zh-CN": "应用于每个 --doc-id 的标签(可重复,每个最多 32 个字符)",
},
required: true,
},
mode: {
type: "string",
valueHint: "<mode>",
description: {
"en-US": "Update mode: append (default) or overwrite",
"zh-CN": "更新模式append默认或 overwrite",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Batch update tags on data-center files",
"zh-CN": "批量更新数据中心文件标签",
},
auth: "apiKey",
usageArgs: "--doc-id <id> --tag <text> [flags]",
flags: DOC_TAG_FLAGS,
notes: [
{
"en-US":
"The same tag set is applied to every --doc-id; run the command multiple times for different tag sets.",
"zh-CN": "同一组标签会应用于每个 --doc-id若需应用不同标签组请多次运行该命令。",
},
{
"en-US":
"Server limits: up to 100 tags per file, total tag length up to 700 chars, tag up to 32 chars.",
"zh-CN":
"服务端限制:每个文件最多 100 个标签,标签总长度最多 700 个字符,单个标签最多 32 个字符。",
},
],
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,359 @@
// 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: {
"en-US":
"Local file or directory path (repeatable). Directories are scanned recursively; unsupported formats are skipped",
"zh-CN": "本地文件或目录路径(可重复)。目录会递归扫描,不支持的格式将被跳过",
},
required: true,
},
indexId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Import into this knowledge base after registration (one job for all files)",
"zh-CN": "文件注册后导入该知识库(所有文件共用一个任务)",
},
},
categoryId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Target data-center category; defaults to the workspace default category",
"zh-CN": "目标数据中心类目;默认为 Workspace 的默认类目",
},
},
tag: {
type: "array",
valueHint: "<text>",
description: {
"en-US": "File tag (repeatable), applied to every uploaded file",
"zh-CN": "文件标签(可重复),应用于每个上传文件",
},
},
wait: {
type: "switch",
description: {
"en-US": "Poll the import job to a terminal state (needs --index-id)",
"zh-CN": "轮询导入任务直到进入终态(需要 --index-id",
},
},
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Polling interval when waiting (default: 5)",
"zh-CN": "等待时的轮询间隔默认5",
},
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
interface UploadedFile {
path: string;
fileId: string;
}
export default defineCommand({
description: {
"en-US":
"Upload local files or directories to the data center and optionally import into a knowledge base",
"zh-CN": "上传本地文件或目录到数据中心,并可选择导入知识库",
},
auth: "apiKey",
usageArgs: "--file <path> [flags]",
flags: DOC_UPLOAD_FLAGS,
notes: [
{
"en-US":
"Pipeline: apply upload lease → PUT to OSS → register file → (with --index-id) create import job.",
"zh-CN":
"处理流程:申请上传凭证 → PUT 到 OSS → 注册文件 →(传入 --index-id 时)创建导入任务。",
},
{
"en-US": "Without --category-id the workspace default category is resolved automatically.",
"zh-CN": "未传入 --category-id 时,会自动解析 Workspace 的默认类目。",
},
{
"en-US":
"Directories are scanned recursively; node_modules, .git, and similar are skipped automatically.",
"zh-CN": "目录会递归扫描node_modules、.git 等目录会被自动跳过。",
},
{
"en-US":
"Multiple files are processed sequentially; on failure, already-registered file ids are listed in the error hint.",
"zh-CN": "多个文件会依次处理;失败时,错误提示会列出已经注册成功的文件 ID。",
},
],
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,105 @@
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: {
"en-US": "Data-center file ID to delete",
"zh-CN": "要删除的数据中心文件 ID",
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...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: {
"en-US": "Permanently delete a file from the data center",
"zh-CN": "从数据中心永久删除文件",
},
auth: "apiKey",
usageArgs: "--file-id <id> [flags]",
flags: FILE_DELETE_FLAGS,
notes: [
{
"en-US":
"Irreversible. If knowledge bases reference this file, their related document indexes become invalid.",
"zh-CN": "该操作不可撤销。如果知识库引用了此文件,其相关文档索引将失效。",
},
{
"en-US":
"To remove a document from a single knowledge base only, use the document delete command instead.",
"zh-CN": "如果只需从单个知识库中移除文档,请改用文档删除命令。",
},
],
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,70 @@
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: { "en-US": "Data-center file ID", "zh-CN": "数据中心文件 ID" },
required: true,
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Show data-center file details (size, MD5, tags, timestamps)",
"zh-CN": "查看数据中心文件详情大小、MD5、标签、时间戳",
},
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 || "-"}`);
},
});

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