Compare commits

...

487 Commits

Author SHA1 Message Date
故璃 640dd02bc5 chore(release): prepare 1.15.1 2026-08-17 11:55:36 +08:00
故璃 d0610a61dc docs(changelog): document 1.15.1 2026-08-17 11:25:17 +08:00
gujieye 57c2d98308 Merge pull request #160 from modelstudioai/feat/skill-init-simplify
feat: update skill init output
2026-08-17 11:01:35 +08:00
gujieye 78e6993475 Merge pull request #162 from modelstudioai/feat/model-command-update
feat: update model quota limit & add model permission command
2026-08-17 00:06:30 +08:00
故璃 79a0d2db9a fix(permission): drop explicit undefined return and make revoke --all e2e credential-independent 2026-08-16 23:48:19 +08:00
故璃 8e6af6c669 feat: update model quota limit & add model permission command 2026-08-16 19:49:23 +08:00
Gong Shiqi f7d32504ab Merge pull request #161 from modelstudioai/agent/release-1.15.0
chore(release): prepare 1.15.0
2026-08-15 14:51:34 +08:00
若麒 cdf94a8c89 chore(release): prepare 1.15.0 2026-08-15 14:33:55 +08:00
故璃 4ccda5f929 feat: update skill init output 2026-08-15 10:15:00 +08:00
Gong Shiqi ce4d66b736 Merge pull request #157 from modelstudioai/release/1.15.0
docs(changelog): document 1.14.2 through 1.15.0
2026-08-14 19:00:08 +08:00
若麒 196a0aa506 docs(changelog): document 1.14.2 through 1.15.0 2026-08-14 18:50:35 +08:00
gujieye a9b0a752a8 Merge pull request #155 from modelstudioai/feat/coding-plan-usage
feat: add coding plan usage
2026-08-14 17:36:47 +08:00
Gong Shiqi 2b7a0c742a Merge pull request #156 from modelstudioai/feat/text-chat-responses-api
feat(text): support Responses API
2026-08-14 17:21:40 +08:00
Gong Shiqi e5818e103c Merge pull request #145 from modelstudioai/feat/change-skills-install
Switch official skill install to bl skill init
2026-08-14 17:21:24 +08:00
若麒 7797940626 docs(skills): clarify update install channel 2026-08-14 17:14:23 +08:00
gujieye 5f1c97940d Merge branch 'main' into feat/coding-plan-usage 2026-08-14 17:05:53 +08:00
若麒 94ccab0898 feat(text): support Responses API 2026-08-14 17:02:29 +08:00
clh02467605 b895f88abb Merge remote-tracking branch 'origin/feat/change-skills-install' into feat/change-skills-install 2026-08-14 16:48:36 +08:00
clh02467605 7eedc05b99 docs: Modify the preferred installation method 2026-08-14 16:47:30 +08:00
若麒 f3c7b6fb10 docs(readme): add standalone installation options 2026-08-14 16:28:51 +08:00
若麒 eb196cb4a6 docs(readme): add standalone installation options 2026-08-14 16:26:47 +08:00
Gong Shiqi f1b6cacd7f Merge pull request #153 from modelstudioai/feat/mcp-support-sse
Add MCP classic SSE auto-fallback for Bailian and --url
2026-08-14 16:07:15 +08:00
故璃 9133b6bdd1 feat: add coding plan usage 2026-08-14 15:56:16 +08:00
clh02467605 98ba3279fa fix(runtime): expose errno in fetch-failed JSON cause.code 2026-08-14 15:27:26 +08:00
clh02467605 3b7c4cfabc Merge remote-tracking branch 'refs/remotes/origin/main' into feat/mcp-support-sse 2026-08-14 14:42:21 +08:00
clh02467605 d5d9fcb50f fix: fixed sse error 2026-08-14 14:41:37 +08:00
clh02467605 3ea2931152 fix(mcp): harden SSE parsing, abort, and fallback matching 2026-08-14 11:11:47 +08:00
gujieye b402f3eacd Merge pull request #141 from sonicg83/codex/usage-token-plan-reset-times
fix(usage): handle missing Token Plan quota fields
2026-08-14 11:10:56 +08:00
gujieye bedd59df27 Merge branch 'main' into codex/usage-token-plan-reset-times 2026-08-13 19:54:06 +08:00
故璃 39a488181e refactor(usage): align token-plan with --output convention and tolerant quota reading 2026-08-13 19:43:11 +08:00
若麒 4ec0f6828b fix(update): sync skills after binary upgrades 2026-08-13 19:14:49 +08:00
clh02467605 4dcec7d075 fix(mcp): fix SSE header timeout, 405 fallback matching, and parseSSE chunking 2026-08-13 18:32:54 +08:00
Gong Shiqi daefc094ec Merge pull request #149 from modelstudioai/fix/fixed_issue_146
fix: support sync-flash and qwen3-filetrans ASR models in speech recognize
2026-08-13 16:26:32 +08:00
clh02467605 ae0c2c1213 fix(speech): handle qwen3-filetrans singular result.transcription_url
Normalize async ASR transcription items so waiting mode downloads text and --out works without changing shared media task types.
2026-08-13 15:52:34 +08:00
clh02467605 01a62eb85b Merge remote-tracking branch 'refs/remotes/origin/main' into feat/mcp-support-sse 2026-08-13 15:40:04 +08:00
clh02467605 798ce596f6 fix(mcp): harden SSE fallback for Bailian and --url overrides 2026-08-13 15:37:32 +08:00
clh02467605 bd91e9d1c2 Merge remote-tracking branch 'refs/remotes/origin/main' into fix/fixed_issue_146
# Conflicts:
#	skills/bailian-gen/reference/index.md
#	skills/bailian-gen/reference/speech.md
2026-08-13 14:36:16 +08:00
clh02467605 e244771ee9 test(speech): harden flash ASR contract coverage and docs
Add SSE disable header, data-URI format inference, broader response text
parsing, HTTP contract e2e, pipeline routing tests, and ASR model selection
guidance in bailian-gen.
2026-08-13 14:25:47 +08:00
Gong Shiqi 94f9dbbe9e Merge pull request #151 from modelstudioai/feat/command-auth-help
feat(cli): show command authentication requirements in help
2026-08-13 13:38:28 +08:00
若麒 8a0dd70206 feat(cli): show command authentication requirements in help 2026-08-13 12:01:41 +08:00
clh02467605 9379da7a4c fix(speech): align flash vocabulary_id and qwen3-filetrans language params 2026-08-13 09:47:09 +08:00
clh02467605 ddcd564e61 test: dry-run realtime ASR usage-error e2e to skip auth in CI 2026-08-12 17:22:25 +08:00
gujieye 0e4dd4b824 Merge pull request #148 from modelstudioai/feat/usage_free_api
refactor(usage): consolidate shared poll logic; migrate freeTrial API…
2026-08-12 17:13:59 +08:00
clh02467605 241de61866 fix: support sync-flash and qwen3-filetrans ASR models in speech recognize
- Add asr-routes.ts with resolveAsrApi() to route models to the correct
  DashScope endpoint instead of always hitting asr/transcription
- Async filetrans: fun-asr / paraformer / *-filetrans → file_urls (plural)
- Async filetrans (qwen3): qwen3-asr-flash-filetrans* → file_url (singular)
- Sync flash (input-audio): fun-asr-flash* / qwen-audio-*-asr-flash → multimodal-generation
- Sync flash (qwen3): qwen3-asr-flash* → multimodal-generation + asr_options
- Realtime/streaming models now give a clear USAGE error instead of a
  confusing server-side "url error"
- Propagate same routing logic to pipeline speechRecognize step
- Add table-driven unit tests and dry-run e2e assertions
Fixes #146
2026-08-12 17:12:23 +08:00
故璃 61d9a74166 fix: 1.14.3 2026-08-12 17:05:18 +08:00
故璃 69eb759490 refactor(usage): consolidate shared poll logic; migrate freeTrial APIs to bailian-commerce
Dedup:
- shared.ts: extract generic pollConsoleUntilDone (request-builder callback
  absorbs each wrapper convention); pollTelemetryApi becomes a thin wrapper;
  add pollFreeTierBatch
- freetier.ts / stats.ts: drop inline duplicates of extractResponseData,
  polling, model-list paging, free-tier extractors and usage label maps;
  import from shared.ts (behaviour unchanged: freetier keeps its 20-poll
  budget, telemetry keeps 30)

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

Note: verified statically and via dry-run; live calls pending a fresh
console login (session expired).
2026-08-12 16:30:08 +08:00
clh02467605 313966d7a9 feat(mcp): add SSE support with fallback mechanism for MCP connections
- Add McpSseClient implementation for classic HTTP+SSE MCP protocol
- Implement connectBailianMcpWithFallback with Streamable HTTP to SSE fallback
- Add isStreamableHttpUnsupported helper to detect 405 streamableHttp errors
- Update activate-hint logic to handle WebSearch 405 streamableHttp cases
- Replace direct MCP client usage with connection manager in call/tools commands
- Add proper client cleanup with close() calls in finally blocks
- Export new MCP connection utilities and types from core client module
- Add comprehensive tests for SSE client and fallback behavior
2026-08-12 15:44:51 +08:00
sonicg83 4d84af614b Merge branch 'modelstudioai:main' into codex/usage-token-plan-reset-times 2026-08-07 23:29:01 +08:00
gujieye 2389681ad6 Merge pull request #144 from modelstudioai/feat/add-version-tag
feat: add version 1.14.2
2026-08-07 18:02:08 +08:00
clh02467605 9ae5dc924d docs(cli): update skill installation command from add --name all to init
- Replace `bl skill add --name all` with `bl skill init` across documentation
- Update installation instructions in README, INSTALL, and agent skill guides
- Modify code references in update checker and UI components
- Adjust documentation links and cross-references accordingly
- Revise command examples in protocol and asset files
- Update versioning and setup instructions to reflect new command
- Modify HTML UI rendering for skill installation guidance
- Change internal command constants and execution calls
2026-08-07 17:56:30 +08:00
故璃 946b7029c6 feat: add version 1.14.2 2026-08-07 17:42:31 +08:00
clh02467605 d6cb075629 Merge remote-tracking branch 'refs/remotes/origin/main' into feat/change-skills-install
# Conflicts:
#	README.md
#	README.zh.md
#	packages/cli/README.md
#	packages/cli/README.zh.md
2026-08-07 17:37:43 +08:00
gujieye b9ecd5c43b Merge pull request #143 from modelstudioai/feat/skill-init-commend
feat: add skill init & opt commend flags
2026-08-07 17:26:21 +08:00
Gong Shiqi 978f332fea Merge pull request #142 from modelstudioai/docs/update-readme-and-agent-guides
docs: refresh READMEs and auth maintenance guidance
2026-08-07 17:24:52 +08:00
故璃 4502424200 feat: add skill init & opt commend flags 2026-08-07 17:17:27 +08:00
若麒 03839766bc docs: update READMEs 2026-08-07 17:15:26 +08:00
若麒 1f8b9ace7e docs: refine auth maintenance guidance 2026-08-07 15:27:46 +08:00
clh02467605 0e33c70e65 docs: update skill installation instructions to use bl skill add
- Replace all instances of `npx skills add modelstudioai/cli --all -g` with `bl skill add --name all`
- Update installation documentation in INSTALL.md, README.md, and related files
- Modify code references in config/inventory.ts, generate-reference.ts, and other files
- Update HTML UI messages to reflect new installation command
- Correct setup.md to include binary installation option and update subset install instructions
- Adjust versioning documentation to use new skill installation command
- Update all SKILL.md files with consistent installation instructions
2026-08-07 14:09:23 +08:00
sonicg 24092b423c fix(usage): handle unavailable token plan quotas 2026-08-06 22:47:26 +08:00
sonicg 752a79e442 fix(usage): handle missing token plan reset times 2026-08-06 09:12:41 +08:00
sonicg 80bdcb83f6 feat(usage): add token plan usage view 2026-08-05 23:28:39 +08:00
Gong Shiqi 6338df36be Merge pull request #138 from modelstudioai/feat/update-defmodel
Update default image model to qwen-image-3.0
2026-08-05 19:43:48 +08:00
若麒 cb6740965f chore(release): prepare 1.14.1 2026-08-05 19:35:05 +08:00
Gong Shiqi 2dffee5b7a Merge pull request #139 from modelstudioai/feat/source-config-tags
feat: add CLI source config tags
2026-08-05 17:44:40 +08:00
若麒 01ec13aad8 feat: add CLI source config tags 2026-08-05 17:37:14 +08:00
clh02467605 b68ff45fb9 Merge remote-tracking branch 'refs/remotes/origin/main' into feat/update-defmodel 2026-08-05 17:08:44 +08:00
clh02467605 4990b27436 feat: update image default model 2026-08-05 16:58:29 +08:00
gujieye 262681484b Merge pull request #137 from modelstudioai/feat/deploy-update
feat: align agent registry with upstream and harden cross-platform install
2026-08-05 16:21:10 +08:00
故璃 8488b251f7 Merge branch 'main' into feat/deploy-update 2026-08-05 16:11:38 +08:00
Gong Shiqi b1908fa879 Merge pull request #134 from modelstudioai/chore/opti-skill
refactor(skills): split domain skills and introduce bailian-protocol companion
2026-08-05 11:16:08 +08:00
clh02467605 d64ba09bef merge: merged main to current branch 2026-08-05 10:59:26 +08:00
clh02467605 8cdd54cf7a docs(skills): remove companions claim; make --all -g the supported install path 2026-08-05 10:27:39 +08:00
故璃 121fa1317f feat(skills): align agent registry with upstream and harden cross-platform install 2026-08-05 10:17:06 +08:00
Gong Shiqi 564e21d9f1 Merge pull request #130 from modelstudioai/feat/multi-channel-install
Feat/multi channel install
2026-08-04 20:30:54 +08:00
若麒 081d09863b Merge branch 'main' into feat/multi-channel-install 2026-08-04 20:22:26 +08:00
clh02467605 17b13de162 merge: merged main to current branch 2026-08-04 18:43:50 +08:00
clh02467605 ca98d8a25d refactor(skills): introduce bailian-protocol companion and slim bailian-cli routing 2026-08-04 18:16:30 +08:00
若麒 1e1f5306b3 chore(release): prepare 1.14.0 2026-08-04 18:11:47 +08:00
clh02467605 13158856e8 feat: Refactor skills by granularity and optimize constraints 2026-08-04 15:31:07 +08:00
gujieye cf2592c07d Merge pull request #133 from modelstudioai/feat/bailian-wiki-doc-sync
feat: add skill commend & wiki sync
2026-08-03 20:07:35 +08:00
故璃 3766b6d7ca Merge branch 'main' into feat/bailian-wiki-doc-sync 2026-08-03 19:33:16 +08:00
故璃 1962758b0c feat: add request id 2026-08-03 19:32:27 +08:00
若麒 026e250cd3 Merge branch 'main' into feat/multi-channel-install 2026-08-03 17:16:56 +08:00
Gong Shiqi 6d61afc1d5 Merge pull request #132 from modelstudioai/feat/update-defmodel
feat: switch default text model to qwen3.8-max
2026-08-03 16:25:17 +08:00
若麒 7a870ec417 chore(release): prepare 1.13.1 2026-08-03 16:19:00 +08:00
clh02467605 1c38c381e5 feat: switch default text model to qwen3.8-max
Align text chat, pipeline, config UI, login validation, and Token Plan
text presets, and update README, skill reference, and related tests.
2026-08-03 15:34:18 +08:00
rendianmeng 658763af2c fix: ci test 2026-08-03 15:29:55 +08:00
rendianmeng da2ddb7a55 fix: ci test 2026-08-03 14:42:31 +08:00
rendianmeng be3033baf9 feat: win bl update exe file test 2026-07-31 19:40:53 +08:00
rendianmeng 8ad3e7b947 feat: win bl update exe file test 2026-07-31 19:05:53 +08:00
rendianmeng 525412f566 feat: win bl update exe file test 2026-07-31 18:53:37 +08:00
rendianmeng 75b056ba64 feat: win bl update exe file test 2026-07-31 18:16:53 +08:00
rendianmeng f5a36b1787 feat: win bl update exe file test 2026-07-31 17:57:28 +08:00
rendianmeng 9fb388b75d Merge branch 'main' of github.com:modelstudioai/cli into feat/multi-channel-install 2026-07-31 17:47:10 +08:00
rendianmeng 45d468838f feat: win bl update exe file test 2026-07-31 17:44:59 +08:00
ls ed81178ad7 Merge pull request #118 from modelstudioai/feat/config-ui-enhancements
Feat/config UI enhancements (本地配置管理面板能力增强)
2026-07-31 00:30:32 +08:00
lisheng.lisheng 7e23ba00fb chore(release): 发布 v1.13.0 版本
- 增加 `bl config ui` 功能,支持技能、MCP、代理和资产清单浏览与管理
- 新增模型目录建议芯片,方便配置 UI 中快速填充模型名
- 实现配置文件的 Profile 磁贴网格展示及新增弹窗
- 优化配置 UI 布局,增强响应式布局和编辑体验
- 修复软链接技能目录识别问题
- 支持基于环境变量的配置文件路径及旧版配置方案
- 同步更新相关包版本至 1.13.0
2026-07-31 00:21:51 +08:00
clh02467605 72955d66a7 refactor(skill): update bailian-cli metadata sync to handle multiple skills
Enhanced the sync script to update the `metadata.version` for all skills in the `skills` directory, rather than just `bailian-cli`. Improved error handling for missing frontmatter and ensured proper versioning across all skill files.
2026-07-30 15:50:29 +08:00
rendianmeng 389c932390 test(runtime): expect npm --version probe in command pack install
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 11:38:40 +08:00
rendianmeng 6870dc50a6 style: fix AGENTS.md table formatting for vp check
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 10:32:39 +08:00
rendianmeng 54b95ed122 Merge branch main of github.com:modelstudioai/cli into feat/multi-channel-install 2026-07-30 10:20:53 +08:00
rendianmeng 5e2833569a Merge branch main of github.com:modelstudioai/cli into feat/multi-channel-install 2026-07-30 10:12:42 +08:00
rendianmeng 434aac5b08 docs: install shell md 2026-07-30 10:05:31 +08:00
故璃 e46053b93e fix(tooling): stop interpolating filenames into staged check
Passing staged filenames per-file puts repository paths into the argv of
the vp check node process. When an endpoint security agent matches process
argv by substring, the whole process is SIGKILLed and pre-commit can never
finish. Use the function form so the command runs without filenames: one
whole-repo check, wider coverage than per-file, and independent of any path.
2026-07-29 17:54:54 +08:00
故璃 30fe8182f4 Merge branch 'main' into feat/bailian-wiki-doc-sync
# Conflicts:
#	packages/cli/src/commands.ts
#	packages/commands/tests/e2e/topic-routes.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
#	skills/bailian-cli/reference/index.md
2026-07-29 17:34:28 +08:00
故璃 65c0fe9604 feat: add skill commend & skill install 2026-07-29 17:04:17 +08:00
lisheng.lisheng 2c53b0692b refactor(inventory): 优化技能与代理配置代码格式和检测逻辑
- 统一代码格式,增加多处代码块的换行和缩进保持一致
- 调整技能安装目标列表的格式,提升可读性
- 修复解压缩逻辑中异常抛出格式,增强异常信息规范
- 优化归一化文件名过滤条件表达式格式
- 修改配置文件检测逻辑,兼容环境变量和旧版配置方案
- 增强对 Bailian 相关模型提供者的检测逻辑支持
- 规范代理详情字段生成方法的代码风格
- 调整 MCP 写回相关函数的格式,提升可维护性
- 改进技能和代理详情函数参数格式,统一参数拆分显示
- 修复单元测试中路径和 JSON 写入格式,增加不同配置场景测试覆盖
- 确保软链接技能目录被正确识别为安装来源
- 增加多代理配置文件和技能安装的检测测试用例,提升测试精准度
2026-07-28 20:51:07 +08:00
lisheng.lisheng adc89f635d Merge branch 'main' of github.com:modelstudioai/cli into feat/config-ui-enhancements
# Conflicts:
#	packages/commands/tests/config-ui.test.ts
2026-07-28 20:43:42 +08:00
ls afa43a42b9 Merge pull request #128 from modelstudioai/feat/config-agent-fix
feat(config-agent): align agent writers, add --key/--region, prepare 1.12.0
2026-07-28 20:39:56 +08:00
lisheng.lisheng bbf45a5961 Merge branch 'main' of github.com:modelstudioai/cli into feat/config-agent-fix
# 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
2026-07-28 20:33:16 +08:00
lisheng.lisheng 3988e701e1 chore(cli): 发布 1.11.0 版本,更新 agent 配置功能
- 新增 `bl config agent --key` / `--region`,支持控制台编码 API Key 本地解码和区域派生 Token Plan 地址
- 新增 `bl config agent --context-window`,设置 OpenClaw 配置的上下文窗口大小,默认 256000
- 新增 `bl config agent --wire-api`,支持选择 Codex 配置的通信协议,兼容旧版 chat 协议并提示警告
- 变更 Codex 默认写入通信协议为 `responses`,适配新版 Codex 不再支持旧 chat 模式
- 变更 Qwen Code 代理配置改用 `DASHSCOPE_API_KEY` 环境变量替代 `BAILIAN_CLI_API_KEY`
- 修复各 agent 配置格式不匹配问题,支持 JSONC 格式和官方结构,完善模型白名单与计费元数据
- 修复配置写入逻辑,合并保持用户自定义配置,避免覆盖及重复条目,优化显示名保留
- 更新所有相关包版本号至 1.11.0,包含 bailian-cli、commands、core、kscli、runtime
- 更新 bailian-cli 技能元数据版本号至 1.11.0
2026-07-28 20:27:28 +08:00
lisheng.lisheng 96744e3328 feat(config-agent): add --key and --region, default codex wire_api to responses
- --key: decode the web console's obfuscated API key (o1_ prefix) into
  the real key; mutually exclusive with --api-key, exactly one required
- --region: convert a Model Studio region into the Token Plan base URL
  (token-plan.<region>.maas.aliyuncs.com/compatible-mode/v1); mutually
  exclusive with --base-url, exactly one required
- codex: default wire_api to "responses" (current Codex rejects "chat");
  --wire-api chat kept for legacy Codex <= 0.80.0 with a warning
- regenerate skills reference for the new flags
2026-07-28 19:37:13 +08:00
Gong Shiqi 3b7993e854 Merge pull request #126 from modelstudioai/fix/web-search-and-thinking
fix(mcp,text): MCP activation hints and fix error caused by enable_thinking
2026-07-28 19:32:26 +08:00
若麒 be6ddb6126 chore(release): prepare 1.11.2 2026-07-28 19:24:40 +08:00
clh02467605 25ac5c9c84 fix: revert change about defaultTextModel 2026-07-28 18:05:18 +08:00
clh02467605 8ef91fe395 test(core): align token-plan preset expectation with qwen3.7-plus 2026-07-28 17:29:41 +08:00
clh02467605 5d9e22de8f Merge remote-tracking branch 'refs/remotes/origin/main' into fix/web-search-and-thinking 2026-07-28 17:20:46 +08:00
clh02467605 20e3555b84 fix(text,auth): drop enable_thinking retry and omit the field by default
Pass through model constraint errors instead of auto-retrying, switch token-plan default text model to qwen3.7-plus, and align related e2e expectations.
2026-07-28 17:19:05 +08:00
Gong Shiqi 81fa5b567c Merge pull request #123 from modelstudioai/fix/default-model
Fix/default model
2026-07-28 16:19:04 +08:00
若麒 df987ad536 docs(changelog): document image edit function option 2026-07-28 16:13:40 +08:00
若麒 4c4e7afb83 chore(release): prepare 1.11.1 2026-07-28 16:07:29 +08:00
若麒 17c52fb86f Merge branch 'main' into fix/default-model 2026-07-28 15:46:46 +08:00
rendianmeng fb0c4b81be docs: install shell md 2026-07-28 14:08:32 +08:00
Gong Shiqi 634d7045c6 Merge pull request #127 from modelstudioai/release/1.11.0
chore(release): prepare 1.11.0
2026-07-28 14:02:28 +08:00
rendianmeng 952f2277a4 docs: install shell md 2026-07-28 13:51:22 +08:00
rendianmeng 871c667e97 docs: install shell md 2026-07-28 13:49:51 +08:00
若麒 eadd92327f chore(release): prepare 1.11.0 2026-07-28 13:28:11 +08:00
clh02467605 4c494207d6 docs(skill): prefer bailian-cli for image/video/audio generation routing
Lead the skill description with a dedicated media-generation entry and
stronger class-3 priority so agents pick bl for gen/edit tasks, while
keeping host-first routing for ordinary text/search.
2026-07-28 10:25:54 +08:00
rendianmeng af3286dd00 Merge branch 'feat/multi-channel-install' of github.com:modelstudioai/cli into feat/multi-channel-install 2026-07-28 10:20:25 +08:00
rendianmeng 6465c4a78a feat: install shell test 2026-07-28 10:19:54 +08:00
故璃 467756b319 feat: update manifest.json 2026-07-28 10:17:16 +08:00
lisheng.lisheng 5a58f56b06 refactor(agent): 修改环境变量名并优化代码格式
- 将环境变量名从 BAILIAN_CLI_API_KEY 改为 DASHSCOPE_API_KEY
- 调整导入语句格式,提升代码可读性
- 优化 providers 条目查找的换行和缩进
- 标准化名称判断与赋值逻辑的格式与排列
2026-07-28 09:59:41 +08:00
clh02467605 7ad14a79b9 Merge branch 'main' of github.com:modelstudioai/cli into fix/web-search-and-thinking 2026-07-28 09:25:34 +08:00
clh02467605 8211268bd8 fix(text,mcp): keep thinking_budget on enable_thinking retry and hint MCP activation on 404 2026-07-28 09:25:02 +08:00
Gong Shiqi 7319f6d1ce Merge pull request #116 from modelstudioai/feat/cma
添加agent studio的cli能力
2026-07-27 22:46:50 +08:00
chenanran555 2dce9fe093 feat(agent): session and destroy failed error 2026-07-27 21:52:52 +08:00
chenanran555 9819eb6ddc feat(agent): 非bailian provider也走鉴权逻辑 2026-07-27 21:33:36 +08:00
qcq01083097 58252911a8 fix(image): correct wan2.5/2.6 size presets and wanx-v1 dated aliases 2026-07-27 21:09:32 +08:00
chenanran555 a03ee0c72c fix(agent): timeout error 2026-07-27 20:23:21 +08:00
chenanran555 05860b3bdd fix(agent): session output json with session_id 2026-07-27 20:10:47 +08:00
chenanran555 63ee5aaec3 fix(agent): plan command dry-run 2026-07-27 20:03:09 +08:00
chenanran555 93c9149e45 feat(agent): 鉴权分离线命令和在线命令,仅对bailian provider鉴权 2026-07-27 19:51:56 +08:00
chenanran555 6f9e006fef feat(agent): add skills for skill-list command 2026-07-27 18:17:39 +08:00
chenanran555 e22058b0f7 feat: update openagentpack sdk 2026-07-27 18:14:06 +08:00
lisheng.lisheng 0221e35803 fix(config-agent): 优化 Codex 配置写入与兼容性处理
- 调整 Codex 代理默认 wire_api 为 "responses",兼容新版 Codex
- 增加对 legacy Codex <= 0.80.0 使用 wire_api "chat" 的警告提示
- 修正 agent flags 描述,更准确说明 wire_api 默认与兼容范围
- 优化代码格式,统一 import 语句风格
- 增加测试用例覆盖不同 wire_api 配置及环境变量警告
- 修复写入过程中文件备份及合并逻辑,保留用户已有配置
- 修复多个 provider 写入时键名与内容匹配,避免重复添加
- 改善测试代码格式,提高可读性与一致性
2026-07-27 17:10:26 +08:00
故璃 7250de9228 feat: add changelog sync to oss 2026-07-27 16:56:46 +08:00
clh02467605 36ebd63716 fix(text): omit enable_thinking by default and retry when API requires false
Non-streaming chat no longer forces enable_thinking=false, which breaks
thinking-only models. Retry once with false only when the server demands it.
2026-07-27 16:51:11 +08:00
clh02467605 dac254af86 feat: add MCP WebSearch page URL and enhance error handling in web search command 2026-07-27 16:51:09 +08:00
chenanran555 8a0fb870f1 feat(agent): update openagentpack sdk version 2026-07-27 16:34:58 +08:00
故璃 51ed69596e feat: skill update REASON opt 2026-07-27 16:25:49 +08:00
故璃 67b7fa30a7 feat: opt bl skill update commend, keep it atom 2026-07-27 16:02:27 +08:00
故璃 bd17c27023 feat: index.json protocol adapter 2026-07-27 15:41:04 +08:00
故璃 87c37994f2 feat: update skill commend group 2026-07-27 12:30:20 +08:00
qcq01083097 ff469ce717 feat(install-docs): enhance installation documentation and validation processes 2026-07-27 11:24:22 +08:00
chenanran555 5f0966ec8d fix(agent): ci issues 2026-07-27 10:51:09 +08:00
qcq01083097 c4f5bb09c6 fix(image): resolve size and prompt_extend by model profile
Stop inferring size/prompt_extend from sync vs async; use per-family sizeProfile. wanx*-imageedit uses function+base_image_url; bare qwen-image uses the fixed resolution table.
2026-07-27 10:17:09 +08:00
qcq01083097 9a13700390 fix(image): route text-to-image and image-edit by model family
Fix wanx/wan2.x-t2i, wan2.5-i2i, z-image, and qwen-image-plus hitting the wrong endpoint, and add routing unit tests plus dry-run coverage.
2026-07-27 10:16:25 +08:00
chenanran555 32c497db63 feat(agent): add skill-list command and fix pr issues 2026-07-26 18:52:27 +08:00
故璃 ebbd173b79 feat: update manifest.json path 2026-07-25 08:43:38 +08:00
故璃 6bdc16597b feat: add secret 2026-07-25 08:07:29 +08:00
故璃 e736bab9c1 feat: add installer sync 2026-07-25 00:33:11 +08:00
故璃 8dd786287f feat: add skill commend 2026-07-24 19:56:53 +08:00
chenanran555 247bb82154 test(agent): cover config-write, profile, logout and error-mapping auth-chain scenarios 2026-07-24 18:44:18 +08:00
chenanran555 1e6165d7ff fix(agent): guarantee single valid JSON on stdout for --output json 2026-07-24 16:25:09 +08:00
chenanran555 1bf4fec9e6 feat(agent): support --dry-run for all local and remote mutations 2026-07-24 16:02:15 +08:00
rendianmeng d30fb2ae68 feat(release): distribute binaries as per-platform zips 2026-07-24 15:33:15 +08:00
chenanran555 1d589c5178 Merge remote-tracking branch 'origin/main' into feat/cma 2026-07-24 14:52:21 +08:00
chenanran555 9cad1994e7 feat(agent): validate by client apiKey auth type 2026-07-24 14:50:30 +08:00
Gong Shiqi fac2b2d18b Merge pull request #121 from modelstudioai/fix/install-doc-non-interactive
docs: fix installation guide flags
2026-07-24 11:19:40 +08:00
若麒 3e249279bc docs: fix installation guide flags 2026-07-24 11:08:06 +08:00
rendianmeng a1a448c5d2 fix(release): fix binary CI publish and clarify release modules
Stabilize Bun compile on 1.2.19, align manifests with OSS consumers,
and split gh / webhook / mode helpers out of binary-release.
2026-07-24 10:35:46 +08:00
rendianmeng 7b949d3d3c fix(release): fix binary CI publish and clarify release modules
Stabilize Bun compile on 1.2.19, align manifests with OSS consumers,
and split gh / webhook / mode helpers out of binary-release.
2026-07-24 10:34:39 +08:00
Gong Shiqi f9012a6330 Merge pull request #119 from modelstudioai/fix/base-url-origin-only
fix(core): normalize model base URL to origin
2026-07-23 19:35:24 +08:00
若麒 92ee845bdd fix(core): normalize model base URL to origin 2026-07-23 19:31:02 +08:00
lisheng.lisheng 4751145283 fix(config-agent): 修复 Qwen Code 凭证写入与模型名处理
- 凭证同时写入 env 和 security.auth,避免系统 OPENAI_API_KEY 干扰
- modelProviders 中按 id + baseUrl 作为键,保持 name 为模型显示名
- 修复旧的 bailian-cli 名称,防止其覆盖用户自定义显示名
- model 配置中新增 baseUrl 字段,用于消歧同 id 但不同地址的模型
- 调整测试用例验证上述行为,确保配置一致性和兼容性
2026-07-23 19:20:44 +08:00
rendianmeng 168e2b5ccb build: multi channel install test 2026-07-23 18:18:46 +08:00
rendianmeng 9fbd2e4ec6 build: multi channel install test 2026-07-23 18:12:42 +08:00
rendianmeng 4bd84e934c build: multi channel install test 2026-07-23 17:52:52 +08:00
rendianmeng 08bdc3be97 build: multi channel install test 2026-07-23 17:36:14 +08:00
rendianmeng 66a797203c multi channel install test 2026-07-23 17:34:30 +08:00
chenanran555 64335a6201 feat(agent): rename cli command to managed-agent 2026-07-23 16:46:06 +08:00
故璃 90a44d7140 feat: llm wiki sync 2026-07-23 15:31:57 +08:00
lisheng.lisheng 26a69a7c99 fix(config-agent): align agent writers with cc-switch and official Model Studio docs
- claude-code: honor CLAUDE_CONFIG_DIR; drop stale ANTHROPIC_API_KEY
- qwen-code: write $version:3; security.auth carries selectedType only
- opencode: tolerate JSONC (comments/trailing commas) via stripJsonc
- openclaw: add --context-window flag (default 256000), full cost fields,
  agents.defaults.models allowlist
- hermes: switch to official flat model.* block; api_mode only for
  anthropic endpoints
- codex: official env_key + auth.json fallback; add --wire-api flag
  (default chat, responses for supported models)
2026-07-23 11:12:34 +08:00
inhai e1caee99f2 feat(config-ui): MCP management, skill zip install, and UI polish
- MCP: editable JSON config in the detail drawer with secret masking and
  mask-preserving writes; create/update/delete across claude-code, qwen-code,
  opencode, cursor, windsurf, gemini, qoderwork, openclaw and Claude Desktop
- Skills: upload a .zip and install into any agent's skills root (self-contained
  ZIP reader, zip-slip safe); scan more roots (openclaw workspace, qoderwork,
  windsurf/codeium, gemini antigravity, workbuddy)
- Markdown: GFM table rendering in the skill detail drawer
- Layout: collapsible grouped sidebar with icons + persistent state, responsive
  breakpoint, wider main, single-line tile titles, 2-line description clamp,
  round icon run buttons, custom file picker, modal spacing
- Server: /api/mcp POST/DELETE, /api/skill/install, binary upload reader,
  constant-time token compare, CSP/no-store headers, error logging
2026-07-23 10:52:26 +08:00
chenanran555 1da3367de8 feat: fix ci 2026-07-22 17:44:50 +08:00
chenanran555 9e59b01326 feat: update openagentpack sdk 2026-07-22 17:01:14 +08:00
chenanran555 7cbd61dd5c Merge remote-tracking branch 'origin/main' into feat/cma
# Conflicts:
#	packages/commands/src/commands/auth/login.ts
#	packages/commands/src/commands/config/set.ts
#	packages/commands/src/index.ts
#	packages/core/src/client/index.ts
#	packages/core/src/config/schema.ts
#	skills/bailian-cli/reference/auth.md
#	skills/bailian-cli/reference/config.md
#	skills/bailian-cli/reference/index.md
2026-07-22 16:38:46 +08:00
chenanran555 1c9dac24e9 feat(cma): login时初始化agent相关的baseUrl 2026-07-22 14:20:24 +08:00
chenanran555 d6bd38a46a feat(agent): agent相关cli命令的client层功能,对齐cli client的基础能力 2026-07-22 13:40:28 +08:00
Gong Shiqi 678f60be75 Merge pull request #115 from modelstudioai/release/1.10.1
chore(release): prepare 1.10.1
2026-07-22 11:35:36 +08:00
若麒 d11b55b956 chore(release): prepare 1.10.1 2026-07-22 11:30:43 +08:00
Gong Shiqi e4e3f069e1 Merge pull request #112 from modelstudioai/chore/optimize-skill
docs: optimize bailian-cli skill routing and consent rules
2026-07-22 10:56:15 +08:00
Gong Shiqi 440cbfe6ae Merge pull request #114 from modelstudioai/feat/token-plan-default-models
feat: update Token Plan defaults and support local image inputs
2026-07-22 10:52:35 +08:00
若麒 1adfe797bd docs: simplify Bailian skill consent rules 2026-07-22 10:50:25 +08:00
Gong Shiqi d04012b0a4 Merge pull request #113 from modelstudioai/feat/node-engines-limit
Feat/node engines limit
2026-07-22 10:33:07 +08:00
若麒 81539005cc Merge branch 'main' into feat/token-plan-default-models 2026-07-22 09:55:39 +08:00
若麒 4c566fd60e feat(token-plan): support local images with base64 data URIs
- convert local images to Base64 for Token Plan image and video commands
- preserve the existing OSS upload flow for standard API Key profiles
- use wan2.7-image as the default image model with the sync endpoint
- hide full Base64 image content in dry-run output
- add Token Plan compatibility tests and update related docs
2026-07-22 09:54:54 +08:00
inhai 9ab5de8c2e feat(config-ui): enrich config UI with skills, MCP, agents, assets and model catalog
- Add Skills / MCP / Agents / Assets inventory views with click-to-open
  right-side detail drawers (reusable infoDrawer)
- Render SKILL.md as Markdown via a self-contained, XSS-safe inline renderer
  (HTML-escape first, strip YAML frontmatter, no external deps)
- Add local vs remote origin badges to Skills and MCP items
- Add quick-launch for coding agents (allowlisted id->binary, execFile, no
  shell); gate the button on Connected AND the CLI binary being on PATH
- Add per-category model catalog surfaced as click-to-fill suggestion chips
  under each default_*_model field, sourced from real bl pipeline model names
- Add assets browser (categorized, time-sorted) with preview, open-locally
  and delete, backed by path-traversal-guarded file serving
- Convert Profiles to a tile grid with an add-tile and design-consistent
  new-profile modal; make view headers sticky and use drawers for editing
- Tests for inventory, agent-launch, assets and config-ui endpoints
2026-07-21 21:54:27 +08:00
rendianmeng 853ce3caae docs: update README.zh.md 2026-07-21 14:10:49 +08:00
rendianmeng 3b779a708d feat: node engines limit change 2026-07-21 13:55:38 +08:00
chenanran555 6329427b4d feat(agent): add agent command group with session and state management 2026-07-21 10:43:22 +08:00
clh02467605 b4a2a1c42d docs: optimize bailian-cli skill 2026-07-20 18:26:08 +08:00
若麒 4ca3e2de80 feat(config): update token-plan default models
- switch the default text model to qwen3.8-max-preview
- add dedicated T2V, I2V, and R2V model defaults
- persist and consume per-mode video model settings
- enable thinking when validating the qwen3.8 preview model
2026-07-20 16:44:53 +08:00
Gong Shiqi 1f91fa42fa Merge pull request #109 from modelstudioai/feat/cli-access-token
chore(release): prepare 1.10.0
2026-07-19 16:55:33 +08:00
若麒 39f12e1a78 chore(release): prepare 1.10.0 2026-07-19 16:47:29 +08:00
若麒 a853319dd0 feat(onboarding): add Token Plan setup guidance
- add Token Plan subscription and login entry to CLI, README, INSTALL, and skill
- document built-in Base URL and automatic key validation
- remove the completed Token Plan integration design document
2026-07-19 16:05:35 +08:00
若麒 bc162f4678 Merge branch 'main' into feat/cli-access-token 2026-07-19 15:32:20 +08:00
Gong Shiqi 12ecac4d96 Merge pull request #107 from modelstudioai/feat/config-agent
feat: add `bl config agent` command for one-click coding agent configuration
2026-07-19 11:48:36 +08:00
Gong Shiqi 52f6e267e6 Merge pull request #108 from modelstudioai/fix/bailian-cli-skill
fix(skill): refine Bailian provider routing and consent
2026-07-19 11:09:43 +08:00
若麒 c9e5913034 Merge branch 'main' into fix/bailian-cli-skill 2026-07-19 11:03:25 +08:00
lisheng.lisheng ba062c1a71 feat: add bl config agent command for one-click coding agent configuration
Add `bl config agent` to configure a coding agent (Claude Code, Qwen Code,
OpenCode, OpenClaw, Hermes, Codex) to use a DashScope/ModelStudio endpoint
with a single command. Writers non-destructively merge into each agent's
local config with a timestamped backup and atomic writes.

- Field structures aligned to farion1231/cc-switch; qwen-code aligned to
  QwenLM/qwen-code source (modelProviders/security.auth keyed by protocol).
- provider id unified as `bailian-cli` (qwen-code brands via model entry
  name + BAILIAN_CLI_API_KEY, since it keys by protocol).
- Codex config.toml merged via smol-toml to preserve unrelated settings.
- Add writer unit tests + config e2e cases; regenerate skill reference.
2026-07-18 19:29:53 +08:00
故璃 d08edf0cd8 feat: sync wiki data from oss by fc 2026-07-17 16:43:06 +08:00
若麒 a03ba673be fix(auth): clear model base URL on full logout 2026-07-17 15:57:23 +08:00
Gong Shiqi 8196f67300 Merge pull request #95 from modelstudioai/feat/cli-access-token
feat: CLI access token 自动化 + bootstrap 一键开通 + 多命名配置 + config ui
2026-07-17 15:10:48 +08:00
若麒 4b504a1a52 docs(changelog): refine 1.9.0 release notes 2026-07-17 15:04:50 +08:00
lisheng.lisheng 00b4d095ed refactor(cli): rename bootstrap command to workspace init
- 将原有的 `bl bootstrap` 命令重命名为 `bl workspace init`
- 更新相关文档,包括中文和英文变更日志
- 修改命令导出和引用,调整文件与变量命名对应新命令
- 保持初始化 Bailian 工作空间及开通后付费服务功能不变
- 删除旧的 `bl bootstrap` 文档,新增 `bl workspace init` 命令帮助文档
- 更新 CLI 命令索引,替换旧命令为新命令
- 优化命令实现细节,增强代码规范和异常处理一致性
2026-07-17 10:59:46 +08:00
若麒 92719a1a21 Merge branch 'main' into feat/cli-access-token 2026-07-17 10:36:26 +08:00
若麒 310e6ead33 chore(release): prepare 1.9.0 2026-07-17 10:15:47 +08:00
若麒 ece0c8dd1c feat(config): activate explicit profile after successful login 2026-07-17 09:59:14 +08:00
Gong Shiqi 39f92567d3 Merge pull request #106 from modelstudioai/fix/windows-stdin
fix(text): support piped messages on Windows
2026-07-16 17:07:56 +08:00
若麒 8b91b9f35f feat: release 1.8.3 2026-07-16 16:43:22 +08:00
若麒 1fbafa8b1d fix(text): support piped messages on Windows
- use the standard input file descriptor instead of /dev/stdin
- reuse the shared file-or-stdin reader in text chat
2026-07-16 16:42:32 +08:00
若麒 052960e269 fix(config): mask all secret fields in config show 2026-07-16 15:27:18 +08:00
若麒 a8f45e93af fix(config): preserve unmanaged fields when saving profiles 2026-07-16 14:48:29 +08:00
若麒 84805f287c fix(core): normalize model base URLs across all sources
- preserve custom gateway path prefixes
- strip query, fragment, trailing slash, and known SDK suffixes
- normalize flag, environment, config, and fallback sources
- normalize auth and config writes before persistence
- add resolver, login, config, and UI coverage
2026-07-16 14:43:26 +08:00
若麒 196b2a1f51 fix(e2e): avoid live OpenAPI login with placeholder credentials 2026-07-16 11:53:09 +08:00
若麒 de9f1a3889 feat(config): add active profile selection
- persist the active profile in config.json
- resolve config with --config > active_config > default
- add config list and config use commands
- make auth and config writes target the selected profile
- reset activation to default when deleting the active profile
- update config UI with profile activation controls
- keep token refresh and pipeline execution profile-aware
- add loader, UI, auth, and CLI interaction coverage
2026-07-16 11:05:59 +08:00
若麒 75a45e1a2a fix(auth): clear STS credentials on logout 2026-07-16 10:02:00 +08:00
若麒 64ff057fe5 feat(auth): support token-plan model profile login
- add the built-in Token Plan profile preset
- validate and persist model API keys atomically
- materialize the default Base URL and models on login
- preserve flag > env > config precedence
2026-07-15 16:26:43 +08:00
gujieye 88fcda62aa Merge pull request #105 from modelstudioai/feat/change-log
feat: release 1.8.2
2026-07-15 11:42:06 +08:00
故璃 68336d41a2 feat: release 1.8.2 2026-07-15 11:29:06 +08:00
gujieye e246cb96d4 Merge pull request #104 from modelstudioai/feat/change-log
feat: add change log
2026-07-15 11:16:11 +08:00
故璃 569057f4d0 feat: add change log 2026-07-15 11:04:30 +08:00
gujieye f7dbafc6d2 Merge pull request #102 from modelstudioai/feat/model-info-query
feat: model info query & skill.md update
2026-07-15 10:29:31 +08:00
故璃 f45d0e3a71 Merge branch 'main' into feat/model-info-query 2026-07-15 10:22:44 +08:00
若麒 6d588a57b4 Merge branch 'main' into feat/cli-access-token 2026-07-14 19:23:47 +08:00
故璃 52aab33a3b feat: update SKILL.md 2026-07-14 16:14:06 +08:00
Gong Shiqi cc23b4be37 Merge pull request #101 from modelstudioai/fix/fail-stable-publish-on-existing-version
fix(release): fail when stable version is already published
2026-07-14 16:12:44 +08:00
若麒 91cd68b46e fix(release): fail when stable version is already published 2026-07-14 16:05:45 +08:00
故璃 054d4deb26 feat: update SKILL.md 2026-07-14 15:22:24 +08:00
ls cbf871c8d6 Merge pull request #100 from modelstudioai/feat/add-inner-console
chore(version): 发布1.8.1版本并更新白名单
2026-07-14 10:18:01 +08:00
lisheng.lisheng 20b67a6628 chore(version): 发布1.8.1版本并更新白名单
- 将所有相关包的版本号从1.8.0更新到1.8.1
- 在中英文CHANGELOG中添加1.8.1版本的变更记录
- 扩展Command Pack白名单,允许加载额外的内部命令扩展
2026-07-14 10:14:19 +08:00
ls 0956695f28 Merge pull request #99 from modelstudioai/feat/add-inner-console
feat(cli): 添加 @ali/bailian-plugin-inner-console-call 插件的命令前缀配置
2026-07-14 09:23:08 +08:00
lisheng.lisheng 96fcb1892c feat(cli): 添加 @ali/bailian-plugin-inner-console-call 插件的命令前缀配置
- 为插件 @ali/bailian-plugin-inner-console-call 添加命令前缀 inner-console
- 扩展了命令包策略以支持新的插件命令调用
- 确保命令前缀统一管理,提高插件识别准确性
2026-07-14 09:22:19 +08:00
lisheng.lisheng 84383f1c83 feat(bootstrap): 支持使用AK/SK生成CLI访问令牌并重构用户创建流程
- 新增命令行参数,支持通过--access-key-id和--access-key-secret传入阿里云凭证
- 集成generateCLIAccessToken接口实现AK/SK转CLI访问令牌
- 调用BailianControl OpenAPI完成用户创建,实现与控制台用户同步
- 新增获取工作空间列表接口,解析agentId以便权限管理
- 调用ResetPolicies4Agent接口完成用户权限授权
- 优化命令步骤日志输出,更详细展示执行流程
- 将轮询次数由120次减少至20次,缩短等待激活时长
- 移除旧有测试代码,适配新实现
- core包新增bailian-control客户端支持对应OpenAPI调用
- client模块添加openApiJson通用方法支持ROA风格接口调用
- console模块导出类型扩展,涵盖新的网关目标类型
- 修改acs请求签名类型支持number类型参数,增强签名兼容性
2026-07-14 08:47:16 +08:00
若麒 fc8351f136 fix(skill): refine provider routing and consent
- scope bl preference to matched Bailian and multimodal tasks
- ask once before provider-neutral remote or billable calls
- avoid routing ordinary text, generic search, and ambiguous usage requests to bl
2026-07-13 19:38:04 +08:00
Gong Shiqi de78a281c6 Merge pull request #98 from modelstudioai/feat/model-usage-and-detail
feat: model usage / recommend / model list / finetune / deploy
2026-07-13 17:07:17 +08:00
若麒 86aec0b723 chore: prepare 1.8.0 release 2026-07-13 17:01:24 +08:00
若麒 06f932c1bd Merge branch 'main' into feat/model-usage-and-detail 2026-07-13 16:23:19 +08:00
Gong Shiqi 1c5d1a8fcd Merge pull request #97 from modelstudioai/feat/command-pack
feat: add command pack protocol and plugin management
2026-07-13 16:10:33 +08:00
若麒 6388fd11e2 docs: sync command context capability usage 2026-07-13 16:05:08 +08:00
故璃 eca5403e71 feat: sync README 2026-07-13 16:02:51 +08:00
故璃 153e176c13 feat: sync README 2026-07-13 16:00:42 +08:00
故璃 16893b3f00 Merge branch 'main' into feat/model-usage-and-detail 2026-07-13 15:56:54 +08:00
故璃 78977dbe4e feat: update changelog 2026-07-13 15:55:36 +08:00
Gong Shiqi 7dd5431a22 Merge pull request #96 from modelstudioai/chore/commit-hooks
chore: speed up pre-commit skill asset sync
2026-07-13 15:55:10 +08:00
若麒 63edde3588 refactor: simplify command context capabilities
- replace lazy store and manager accessors with direct properties
- enforce command capability boundaries through lint rules
2026-07-13 15:47:57 +08:00
若麒 e932495560 Merge branch 'main' into feat/command-pack 2026-07-13 15:38:45 +08:00
clh02467605 2ae76e2013 fix: keep reference formatting in generate:reference for release check
release/check.mjs calls generate:reference directly then git diff --exit-code
on skills/bailian-cli/reference/. Formatting must stay on that script; root
sync:skill-assets should not duplicate it.
2026-07-13 15:34:47 +08:00
故璃 f3b85914ab Merge branch 'main' into feat/model-usage-and-detail 2026-07-13 15:34:04 +08:00
故璃 8b99617716 feat: fix code review issue 2026-07-13 15:23:35 +08:00
若麒 b2ca78512f feat: add command pack protocol and plugin management
- define the Command Pack API and integrate pack loading into the CLI runtime
- add product-specific package, command prefix, and credential access policies
- add plugin install, link, list, and remove commands
- add validation, tests, documentation, and generated command references
2026-07-13 15:02:35 +08:00
clh02467605 7004e58866 fix: format skill reference during sync:skill-assets for CI
CI runs sync:skill-assets before vp check without pre-commit's vp staged,
so reference markdown must be formatted in the shared sync script.
2026-07-13 14:45:25 +08:00
clh02467605 97e47f7054 refactor: update sync:skill-assets script to read from source directly
- Modified the `sync:skill-assets` script in `package.json` to remove the dependency on building `bailian-cli-core` before generating references and syncing skill versions.
2026-07-13 14:38:35 +08:00
lisheng.lisheng e0f3d450ae feat(config): add "config ui" local web UI to manage config profiles
启动绑定 127.0.0.1 的本地 HTTP server + 内嵌单页 WebUI,可视化查看/
新建/切换/删除全部命名 profile 并编辑键值与凭证。

- core: readConfigProfiles / deleteConfigProfile 全量配置读写 API
- commands/config/shared.ts: 抽出 VALID_KEYS/别名/校验,set.ts 复用
- commands/shared/local-server.ts: 抽出 listen/openInBrowser,login-console 复用
- config ui: token + Host 校验,--config 决定初始聚焦,密钥明文可编辑
2026-07-13 14:20:32 +08:00
故璃 d37f4c07eb feat: update monitor data & quato list 2026-07-13 13:43:03 +08:00
lisheng.lisheng ac4dbb9e88 Merge branch 'main' of github.com:modelstudioai/cli into feat/cli-access-token 2026-07-13 13:13:29 +08:00
lisheng.lisheng 155c9dc883 feat(config): 支持命名配置功能并隔离默认配置数据
- 新增 `--config <name>` 参数支持读取与写入命名的配置块
- 命名配置与默认配置完全隔离,互不影响
- 规范命名配置名称格式,禁止路径穿越及顶层字段冲突
- 配置文件读取写入逻辑改为维护原始完整对象,支持多配置块共存
- AuthStore 和 ConfigStore 均支持命名配置,登录登出只影响指定配置块
- CLI 命令增加对 `--config` 标志的支持,包括 config set/show/auth status 等
- 鉴权状态输出带上配置名和配置文件路径信息
- 提示和报错信息包含配置相关上下文,增强用户体验
- 完善相关单元测试覆盖命名配置行为
2026-07-13 13:04:50 +08:00
gujieye 0e857775fe Merge pull request #94 from modelstudioai/feat/recommend-new
feat: remove intent-detect-v3 model
2026-07-12 22:12:50 +08:00
故璃 1a63fcdb5c feat: add model list / opt usage 2026-07-12 22:11:32 +08:00
顾捷晔 58ab622e11 feat: remove intent-detect-v3 model 2026-07-12 13:44:00 +08:00
lisheng.lisheng e1532bf35c feat(bootstrap): 新增创建控制台用户步骤,完善服务激活流程
- 在 bootstrap 命令流程中添加第 3 步,调用 createUser 接口创建控制台账号用户
- 修改 callApi 函数支持传入请求体参数
- 增强错误日志输出,显示更完整的错误响应内容
- 在服务启动时校验登录信息包含 aliyun.uid,否则抛出错误
- 优化商品检查与激活逻辑,更清晰的状态输出及错误处理
- 在 CLI-core 控制台网关请求中添加日志,打印结构化请求负载,支持 verbose 模式
- BailianError 新增 rawResponse 字段,保存原始错误响应数据
- 增加 bootstrap 命令单元测试,覆盖用户创建及不同初始化场景
- 调整测试框架用例,增加调用控制台网关及错误处理的测试覆盖
2026-07-10 17:57:06 +08:00
Gong Shiqi 6e095a6ce5 Merge pull request #93 from modelstudioai/feat/migrate-e2e
refactor(e2e): decouple E2E tests into commands layer and shared e2e package
2026-07-10 17:55:45 +08:00
clh02467605 94adb919b1 refactor(e2e): enhance auth login tests and remove unused dependencies
- Removed the `bailian-cli-commands` dependency from `pnpm-lock.yaml` and `package.json` as it was no longer needed.
- Expanded the E2E tests in `auth.e2e.test.ts` to cover additional scenarios for the `auth login` command, ensuring proper error handling for conflicting flags and missing parameters.
- Improved test assertions for better clarity and coverage of authentication flows.
2026-07-10 17:46:17 +08:00
clh02467605 a0b4666940 refactor(e2e): remove outdated token-plan tests and add new route exports
- Deleted the `token-plan.e2e.test.ts` file as it contained outdated test cases.
- Introduced `TOKEN_PLAN_ROUTES` in `topic-routes.ts` to define new command routes for token-plan operations.
- This update streamlines the E2E testing structure and prepares for future enhancements.
2026-07-10 17:11:20 +08:00
clh02467605 e8666b33ae merge: merge main to feat/migtate-e2e 2026-07-10 16:54:14 +08:00
clh02467605 d646a4e780 refactor(e2e): update test routes and streamline command handling
- Replaced hardcoded command paths with dynamic route mappings in E2E tests.
- Enhanced documentation for command testing, specifying minimum route requirements.
- Consolidated command path management into `topic-routes.ts` for better maintainability.
- Updated various E2E test cases to utilize the new routing structure, ensuring consistency across tests.
- Removed redundant helper functions and improved test clarity by directly referencing route exports.
2026-07-10 15:55:53 +08:00
clh02467605 91a7106bf4 fix(e2e): update CLI version to match package.json 2026-07-10 14:04:21 +08:00
gujieye 03405d5cc1 Merge pull request #91 from modelstudioai/feat/model-deploy-update
feat: add tts/i2i/t2i model deploy
2026-07-10 13:55:48 +08:00
故璃 1870500f97 feat: code review fixture 2026-07-10 13:50:11 +08:00
lisheng.lisheng 3fb0c7211c feat(auth): add support for optional security token in CLI access token generation 2026-07-10 13:04:05 +08:00
clh02467605 7b77bf6a6d fix(e2e): run subprocess harness and proxy probe via tsx
Node's strip-only TypeScript mode cannot execute workspace source that
uses parameter properties. Use the monorepo tsx binary for runNodeMain and
the proxy e2e probe (.mts for top-level await).
2026-07-10 13:01:10 +08:00
clh02467605 6e5ecf8923 chore: drop redundant helpers.d.ts fmt/gitignore guards
Runtime tsconfig already limits pack to src, so tests/**/*.d.ts is never
emitted during build; the fmt ignore and gitignore entries are unnecessary.
2026-07-10 12:32:30 +08:00
clh02467605 301986b669 fix(ci): stop emitting and checking generated tests helpers.d.ts
Runtime pack build was pulling in e2e test imports and tsgo emitted
helpers.d.ts beside commands/tests; restrict runtime tsconfig to src and
ignore tests/**/*.d.ts in fmt/gitignore as a safety net.
2026-07-10 12:26:41 +08:00
clh02467605 0d420b6e5a chore: fix lint error 2026-07-10 12:10:30 +08:00
clh02467605 9602209113 feat(e2e): integrate e2e package and update test configurations
- Added e2e package as a workspace dependency in pnpm-lock.yaml.
- Updated globalSetup path in vite.config.ts to point to the new e2e package location.
- Enhanced documentation for CLI E2E testing, including architecture layers and triggering conditions.
- Refactored test paths and commands in the documentation to align with the new structure.
- Removed outdated dataset and auth E2E tests to streamline the test suite.
- Updated command references and validation checks in the documentation to reflect recent changes.
2026-07-10 11:46:05 +08:00
故璃 19b7aacb2e Merge branch 'main' into feat/model-deploy-update 2026-07-10 10:15:25 +08:00
lisheng.lisheng 049eecd991 feat(bootstrap): add command to initialize Bailian workspace and activate postpaid services 2026-07-10 10:06:32 +08:00
lisheng.lisheng 2907ad2625 feat(auth): add command to generate CLI access token 2026-07-10 00:09:10 +08:00
lisheng.lisheng 0f23527bfc feat(core): move generateCLIAccessToken to core and auto-refresh on NotLogined
Move the GenerateCLIAccessToken API call logic into core/auth/refresh-token.ts
so it can be reused across packages. Add refreshAccessToken() which reads
AK/SK from config, calls the API, and persists the new access_token.

Client.console() now catches NotLogined errors and automatically retries
with a refreshed token when AK/SK are available in config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-10 00:00:57 +08:00
lisheng.lisheng 8e3f8586b0 feat(auth): update access token retrieval in login command 2026-07-09 23:43:31 +08:00
lisheng.lisheng efb5243d0d feat(auth): call GenerateCLIAccessToken on open-api login
When logging in with --open-api, call the GenerateCLIAccessToken API
using the provided AK/SK to obtain an access token and persist it
alongside the credentials in config.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:40:47 +08:00
故璃 f66889c939 feat: update model deploy 2026-07-09 20:19:27 +08:00
Gong Shiqi 03f0e7c5c4 Merge pull request #90 from modelstudioai/feat/self-built-framework
feat: refactor command framework
2026-07-09 19:54:20 +08:00
若麒 0a301ee641 test(e2e): run proxy probe with tsx 2026-07-09 19:51:15 +08:00
若麒 66402d9868 chore(dev): run CLI workspace entries from source
- use tsx for bl/kscli dev and test runners
- point local library exports to src while keeping publishConfig on dist
- switch vendored telemetry modules to .cjs for source-run compatibility
- update stress/e2e helpers and agent docs for the new source execution path
2026-07-09 19:39:13 +08:00
若麒 4525d5df6c release: prepare 1.7.0 2026-07-09 17:47:41 +08:00
若麒 749549aa28 docs: align agent and skill docs with kscli split
- replace stale rag/package references with kscli/knowledge-studio-cli
- update release docs for core/runtime/commands/cli plus kscli publishing
- refresh skill setup notes and generated reference output defaults
2026-07-09 17:31:49 +08:00
若麒 7ce018cc53 fix(auth): stop printing quick start after login 2026-07-09 16:56:05 +08:00
若麒 13ade9181f fix(runtime): ignore unsupported lowercase proxy env vars 2026-07-09 16:55:03 +08:00
故璃 068ec0bfd3 Merge branch 'feat/self-built-framework' into feat/model-deploy-update 2026-07-09 15:37:21 +08:00
故璃 d8aa89dc6c feat: add image/audio finetune 2026-07-09 15:36:32 +08:00
若麒 bd4b0ad9a5 fix(runtime): tailor root help to product entrypoints
- render auth flag sections only for auth domains used by registered commands
- make quick-start prompts opt-in through createCli options
- keep the existing quick-start prompts wired only for bl
2026-07-09 15:28:08 +08:00
若麒 2debfdba6b feat(auth): add OpenAPI AK/SK auth domain for token plan
- add openapi auth requirement with command-scoped access key flags and paired credential resolution
- persist OpenAPI credentials through auth login/status/logout using access_key_* config fields
- route token-plan commands through the centralized ACS signing client
- keep legacy openapi_access_key_* config readable while rejecting it as a new config set key
- refresh docs, generated references, telemetry authMethod, and e2e coverage
2026-07-09 15:03:48 +08:00
故璃 8fd072bcd1 feat: merge self-built-framework 2026-07-09 10:02:56 +08:00
若麒 49095c3a8a refactor(commands): remove yes confirmation gates 2026-07-08 17:27:44 +08:00
若麒 c18844120a fix: preserve offline dry-run validation before auth 2026-07-08 15:19:31 +08:00
若麒 4dfe3ac907 Merge branch 'main' into feat/self-built-framework 2026-07-08 14:15:39 +08:00
若麒 b0c48bab6b Merge branch 'main' into feat/self-built-framework 2026-07-08 14:05:48 +08:00
若麒 d20e037dec fix(verbose): restore request-side log lines dropped in credential refactor 2026-07-07 23:16:28 +08:00
若麒 476dd3b841 refactor(output): centralize ANSI styling and remove no-color flag
- remove --no-color from GLOBAL_FLAGS and drop Settings.noColor
- move ANSI styling decisions into runtime color helpers with NO_COLOR support
- update command text renderers to use shared color helpers instead of local ANSI codes
- refresh e2e invocations, generated reference, and agent skill guidance
2026-07-07 00:07:21 +08:00
若麒 c35f2856e5 refactor(flags): align media command async and concurrent handling
- scope image/video task execution to --async and --concurrent
- add concurrent task fan-out for video edit and video ref
- extend image edit to the async image task path
- refresh e2e coverage and generated command references
2026-07-06 22:11:52 +08:00
若麒 b3b1a08baf docs(agents): align maintenance guides with split CLI architecture 2026-07-06 21:50:50 +08:00
若麒 468b4d710e refactor(flags): scope yes/async/concurrent to command-owned flags
- remove nonInteractive plus yes/async/concurrent from GLOBAL_FLAGS and Settings;
  command dispatch no longer resolves command-only switches into global settings
- add shared ASYNC_FLAG / CONCURRENT_FLAG definitions for commands that actually
  support task-only return or parallel requests
- keep quota downgrade protection by moving --yes onto quota request and reading
  flags.yes for confirmed downgrade submission
- update existing async/concurrent consumers to read own flags; no new capability
  matrix entries are added
- refresh generated command reference and remove stale --non-interactive usage
  from e2e/stress invocations
2026-07-06 20:43:29 +08:00
若麒 deab3b3841 refactor(flags): scope credential flags by command auth domain
- flags split into GLOBAL_FLAGS (all commands) plus MODEL_AUTH_FLAGS /
  CONSOLE_AUTH_FLAGS, parsed only for commands of the matching auth
  domain; cross-domain flags now fail with "Unknown flag" instead of
  being silently ignored
- all shadow redeclarations removed; the registry guard now rejects any
  own flag named after a reserved (global or visible-domain) flag
- --workspace-id joins the console domain (chain: flag > env > file);
  usage stats drops its private declaration and in-command priority
- auth login declares its credential args as own command parameters
  (--api-key / --base-url / --console-site, original behavior intact);
  auth status no longer accepts credential-domain overrides (use env or
  config set instead)
- command help and the generated reference both show Flags (own + auth
  domain) plus a full Global Flags section, replacing the footer hint
- breaking: pipeline run --timeout renamed to --step-timeout (collided
  with the global request timeout)
2026-07-06 17:42:11 +08:00
若麒 d31b7f83ca refactor(core): split god Config into Identity/Settings/Credential resolved at the dispatch boundary
- commands consume a narrowed context (identity/settings/own flags/client);
  config/auth commands additionally use configStore()/authStore() accessors
- resolution happens once at dispatch: buildSources/buildSettings plus
  per-domain credential resolvers; dry-run tolerates missing credentials
- transport takes structured deps; credentials are injected only by Client;
  console gateway takes a resolved target with optional token (anonymous
  catalog calls); pipeline steps and advisor run against client/settings
- telemetry receives authMethod as a value; global/command flags are split
  at dispatch with a same-type shadowing guard at registry build
- behavior change: base URL resolution now prefers DASHSCOPE_BASE_URL env
  over config file base_url (unified flag > env > file > default chain)
- priority chains, store semantics and command capability boundaries are
  locked by unit tests
2026-07-06 15:59:33 +08:00
gujieye 7b08b8863e Merge pull request #88 from modelstudioai/feat/update-recommend
feat: update model recommend by using intent model & using soft / hard score in model recall
2026-07-06 14:03:23 +08:00
故璃 d118875772 Merge branch 'main' into feat/update-recommend 2026-07-06 13:40:41 +08:00
故璃 d2312847eb feat: update model recommend 2026-07-06 13:40:00 +08:00
clark-fc b5f2b8b691 Merge pull request #87 from modelstudioai/feat/update-version
Feat/update version
2026-07-03 18:19:54 +08:00
qcq01083097 acfbc58516 feat: update skill version to 1.6.1 2026-07-03 18:15:53 +08:00
qcq01083097 e4849224c4 feat: update version to 1.6.1 2026-07-03 18:12:51 +08:00
clark-fc 3aa08e5d79 Merge pull request #86 from modelstudioai/feat/change-model
feat: change vision describe example model
2026-07-03 17:52:02 +08:00
qcq01083097 4914c5258b feat: change vision describe example model 2026-07-03 16:23:50 +08:00
clark-fc a20ab54406 Merge pull request #85 from modelstudioai/feat/knowledge-cli
Feat/knowledge cli
2026-07-02 16:45:13 +08:00
zeyu.fz e6a8bf09e7 test(knowledge): 添加条件跳过无法执行的e2e错误场景测试
- 根据isDashScopeE2EReady函数动态跳过错误场景测试集
- 修改测试注释明确标注环境变量可能泄露风险
- 将BAILIAN_CONFIG_DIR改为固定临时目录路径以稳定测试
- 在知识检索命令新增dry-run支持,绕过凭证直接使用API-KEY路径执行请求体输出
2026-07-02 16:42:52 +08:00
zeyu.fz c6426e9e94 test(cli): 更新测试用例以模拟空环境变量场景
- 在 knowledge chat 相关测试中加入 BAILIAN_WORKSPACE_ID 为空的环境变量模拟
- 在 knowledge search 相关测试中加入 BAILIAN_WORKSPACE_ID 为空的环境变量模拟
- 将 knowledge 相关测试中的部分环境变量由 undefined 改为空字符串以更准确模拟环境场景
- 保持测试逻辑不变,确保非零退出码及错误提示的正确性
2026-07-02 16:31:00 +08:00
zeyu.fz 03541b4fd1 test(e2e): 移除多处测试调试信息并优化 runCli 调用参数
- 从 file-upload.e2e.test.ts 中删除无用的调试日志代码
- global-setup.ts 中清理环境变量调试打印信息
- knowledge-chat.e2e.test.ts 和 knowledge-search.e2e.test.ts 中去除多余的环境变量传入
- knowledge.e2e.test.ts 中调整 runCli 调用,统一简化测试参数
- commands/knowledge 下 chat.ts 与 search.ts 增加 skipDefaultApiKeySetup 标记,避免默认 API Key 初始化
2026-07-02 16:28:42 +08:00
zeyu.fz 6dd206eda9 debug(cli): 增加文件上传测试对配置文件读取的调试日志
- 添加对用户主目录下配置文件路径的打印和存在性检查
- 打印环境变量 HOME 及 BAILIAN_CONFIG_DIR 的值
- 调用 readConfigFile 并打印返回内容及 api_key 相关信息
- 捕获并打印 readConfigFile 的异常信息
- 如果配置文件存在,读取并打印其原始内容
- 保留现有环境变量和功能状态的调试输出
2026-07-02 16:08:36 +08:00
zeyu.fz e2efcfda77 test(cli): 添加文件上传E2E测试的环境变量调试信息
- 引入 isBailianE2EEnabled 方法用于调试
- 在 worker 进程中打印关键环境变量 DASHSCOPE_API_KEY
- 打印 isDashScopeE2EReady 与 isBailianE2EEnabled 的返回结果
- 方便排查文件上传相关E2E测试环境状态问题
2026-07-02 15:58:14 +08:00
zeyu.fz a078670445 Merge remote-tracking branch 'origin/feat/composable-cli' into feat/knowledge-cli 2026-07-02 15:51:40 +08:00
zeyu.fz 8fc2fc54fb test(e2e): 添加全局设置调试日志,排查CI环境变量问题
- 增加日志输出,详细打印CI环境中的关键变量值
- 检查并打印本地配置文件内容及其API Key长度
- 引入新的辅助函数,支持更全面的环境就绪状态检测
- 提升对DashScope和Console等E2E测试环境的诊断能力
- 便于排查CI中DASHSCOPE_API_KEY及相关环境变量的来源和状态
2026-07-02 15:49:48 +08:00
zeyu.fz 9bf6c6d9af refactor(release): 移除未使用的导入以简化代码
- 从 publish-stable.mjs 中删除了未使用的 findPackage 导入
- 仅保留 ALL_PACKAGES 和 PACKAGES 的导入
- 提升代码的清晰度和维护性
2026-07-02 15:36:48 +08:00
zeyu.fz 892ae300ae feat(knowledge): 新增基于 workspace 的知识库语义检索与问答功能
- 新增 `bl knowledge search` 命令,支持语义检索及多模态检索参数
- 新增 `bl knowledge chat` 命令,支持知识库 SSE 流式问答及多轮历史对话
- 在 `bailian-cli-core` 中添加相应的知识 API 类型和端点支持
- `kscli` 新增 `search` 和 `chat` 两个命令,`retrieve` 标记为废弃
- 更新 `kscli` README,调整主推命令并标记 `retrieve` 废弃
- 补充完善 E2E 测试覆盖检索与问答功能的多种用例
- 修正若干缺少必要参数时的 CLI 行为,确保打印帮助并正常退出
- 升级各相关包版本至 1.6.0,更新 CHANGELOG 及相关文档说明
2026-07-02 15:19:38 +08:00
zeyu.fz 6c4f31ddb2 test(e2e): 删除kscli的chat和search端到端测试
- 移除chat命令的多种输出模式测试(JSON、文本、流模式)
- 删除多轮对话上下文感知回答的测试用例
- 删除chat命令无效agent_id时的容错测试
- 移除search命令的JSON和文本模式搜索测试
- 删除带查询历史的搜索功能测试
- 删除search命令无效agent_id时的错误处理测试
- 清理与测试相关的类型定义和辅助函数调用
2026-07-02 14:38:20 +08:00
zeyu.fz 9ff8c53d53 feat: merge 2026-07-02 14:22:50 +08:00
Gong Shiqi a8652f350d Merge pull request #84 from modelstudioai/chore/kscli-version-1.5.0
chore(release): align knowledge-studio-cli to 1.5.0
2026-07-01 16:08:40 +08:00
若麒 b1a0c0005d chore(release): align knowledge-studio-cli to 1.5.0
kscli joins the family lockstep version so the --knowledge stable publish
passes (validate.mjs asserts every package in ALL_PACKAGES matches
bailian-cli-core). Was 0.0.1, which blocked publish-stable --knowledge.
2026-07-01 16:06:20 +08:00
Gong Shiqi f597b94c46 Merge pull request #83 from modelstudioai/feat/composable-cli
Release 1.5.0: finetune / deploy / dataset / token-plan + composable CLI
2026-07-01 15:54:13 +08:00
若麒 750641dd0e chore(release): 1.5.0 2026-07-01 14:22:17 +08:00
若麒 11ed19723a fix(kscli): build entry key rag→kscli to fix publint path mismatch 2026-06-30 23:23:32 +08:00
若麒 848e44eb44 Merge branch 'main' into feat/composable-cli 2026-06-30 23:07:18 +08:00
gujieye 07875c309e Merge pull request #77 from modelstudioai/feat/auto-update
feat: auto-update CLI on major version gap
2026-06-30 14:15:45 +08:00
故璃 7d05649f8d Merge branch 'main' into feat/auto-update 2026-06-30 14:13:29 +08:00
gujieye fd9bba77a9 Merge pull request #80 from modelstudioai/feat/model-train
feat: add model finetune/deploy/dataset commend to support one step model training by Agent
2026-06-30 14:12:40 +08:00
故璃 0593c7eb28 Merge branch 'main' into feat/model-train 2026-06-30 14:10:47 +08:00
zeyu.fz 7c9ad7d6ce feat(packages): 添加 runtime 和 commands 包配置
- 在包列表中新增 runtime 包配置
- 在包列表中新增 commands 包配置
- 确保新包路径和名称正确设置
2026-06-29 18:18:44 +08:00
若麒 ae88f7a4ad refactor(runtime): group process-lifecycle setup into installProcessHandlers 2026-06-29 16:08:14 +08:00
若麒 bd431d769f feat(update-checker): surface update notice to non-TTY/agent runs
- drop the CI / non-TTY early-return so agents see "Update available" too
- widen check interval 4h→24h; stay silent inside the window (no per-command repeat)
- remove orphan isCI() helper (zero callers)
- versioning.md: drop the now-false "banner suppressed under agent" rationale
2026-06-29 15:45:34 +08:00
若麒 df89ededc2 test(e2e): align with UsageError validation contract
Missing required flags now throw UsageError (exit code 2, error on
stderr, JSON under --output json) instead of printing help and exiting
0. Update assertions and titles accordingly:

- missing-flag cases: exitCode 0/1 -> 2, retitle to "errors as usage error (2)"
- auth / video-task-get under --output json: assert the error JSON on stderr
- proxy probe: import setupProxyFromEnv from runtime/src/proxy.ts
- drop tests for the removed config export-schema command
- fix t2v dry-run: cliTimeoutPrefix misplaced between --model and its value
2026-06-29 14:53:24 +08:00
zeyu.fz 2ec2f34763 feat(cli): 支持多模态消息内容及图片URL数组
- 扩展聊天消息内容类型,支持文本和图片URL的数组形式
- 处理 --image 参数,将图片URL作为多模态内容附加到最后一条用户消息
- 若无用户消息且指定图片URL,自动创建空用户消息以承载图片内容
- 禁止同时使用内嵌图片内容和 --image 参数,避免冲突
- 将知识搜索接口请求的图片参数字段 image_list 重命名为 images
- 单元测试覆盖多模态内容及图片数组行为验证
- 优化消息解析,支持JSON结构化消息和 role:content 格式
- 更新API类型声明,明确多模态消息结构与字段类型
2026-06-29 13:36:32 +08:00
若麒 1f56feab24 refactor(commands): route all exits through the central error handler
Commands no longer call process.exit() directly. Every failure now throws
UsageError (bad input → exit 2) or BailianError (runtime failure, with
AUTH/TIMEOUT codes), so the runtime's handleError stays the single exit
point and telemetry always flushes.

- convert 27 process.exit() sites across 15 commands to throws
- move cross-flag/value checks into validate(); use flag `choices` for
  --events / --sort; drop dead --model required check
- keep pipeline's process.exitCode for lint-style soft failures
- enforce with unicorn/no-process-exit, allowed only in runtime/tools/tests
- fix incidental lint: void floating run(), narrow console errorCode,
  align generate-reference type imports to source
2026-06-29 09:07:12 +08:00
若麒 2f9558c161 refactor(auth): remove deprecated AK/SK auth for knowledge retrieve
AK/SK signing was used only by `knowledge retrieve`'s deprecated fallback,
which the api-key auth gate now makes unreachable. Drop it; the command is
pure api-key.

- knowledge/retrieve: remove the AK/SK path + --access-key-id/secret/workspace-id
  flags; api-key only
- delete client/ak-sign.ts and its signRequest/AkSignConfig exports
- drop access_key_id/access_key_secret from config schema, loader, and
  `config show` / `config set`
- remove the now-unused PascalCase KnowledgeRetrieve request/response types
2026-06-28 22:24:59 +08:00
若麒 95eb07d04a refactor(auth): centralize credential resolution into authStage + Client
Move all credential handling out of commands into one place. authStage
resolves the credential for the command's declared `auth` and bakes it into
`ctx.client`, gating (throw) when missing; commands reach the network only
through `ctx.client` and never touch tokens or baseUrl.

- add Client (request/requestJson/uploadFile/mcp/console/url) wrapping the
  token + baseUrl; commands call it instead of self-resolving
- run(config, flags) → run(ctx) + CommandContext; migrate all 45 commands
- split domains: model = pure api-key (drop access-token fallback), console =
  config.json only (drop DASHSCOPE_ACCESS_TOKEN env)
- consolidate env reads in loadConfig; CredentialSource = flag | env | config;
  priority flag > env > config
- endpoints return paths (xxxPath) instead of full URLs; baseUrl owned by Client
- auth status now uses describeAuth
- video/download: auth "none" → "apiKey" (it needs the model API)
- dedupe fetchModelList behind an injected console-call function
- remove ensureApiKey/ensure-key.ts, prompt.ts + isInteractive, and the old
  resolveCredential/resolveConsoleGatewayCredential resolvers
- tests: adapt auth.e2e to the new auth status shape; drop the
  DASHSCOPE_ACCESS_TOKEN branch from console-readiness gates
2026-06-28 20:42:33 +08:00
若麒 f9bf36c242 refactor(flags): keyed type-inferred flag schema; option→flag rename
Replace the positional `OptionDef[]` array (key/type regex-parsed from
"--x <v>" strings) with a keyed `FlagsDef` record whose `type` drives both
runtime parsing and compile-time flag-type inference (`Flags<typeof DEF>`).
GLOBAL_FLAGS becomes the single source; the hand-kept GlobalFlags interface
(types/flags.ts) is deleted.

- core: SwitchFlag|ValueFlag union, ParsedFlags/Flags inference, defineCommand
  infers F from spec.flags
- runtime: parseFlags dispatches on def.type (switch/string/number/boolean/
  array/choices) with declarative required-flag enforcement
- commands: migrate every flag declaration to the keyed form
- naming: option→flag throughout (OptionDef→FlagDef, OptionsDef→FlagsDef,
  GLOBAL_OPTIONS→GLOBAL_FLAGS, command field options→flags), plus user-facing
  "Options:"→"Flags:" in help and the regenerated skill reference docs

Behavior-preserving aside from the intentional Options→Flags wording:
vp check clean across all packages, 29 parser tests pass, reference regen
byte-identical before the terminology swap.
2026-06-28 11:19:21 +08:00
若麒 cbd3c1232c refactor(runtime): unify validation into UsageError; bare command shows help
- drop IncompleteCommandError: missing-required, failed validate, and bad/unknown
  flags are all UsageError now
- error boundary keys on bareness — bare command that fails → help (exit 0);
  non-bare invalid → error + message (exit 2)
- login: drop config.apiKey fallback, --api-key required-unless-console via validate
- speech: drop empty --text-file guard (empty content is the API's concern)
2026-06-27 10:56:28 +08:00
zeyu.fz d2aa8cac17 docs(cli): 统一所有参考文档表格格式及添加全局参数说明
- 统一调整所有命令参考文档中的表格格式,使用简洁markdown表格语法替换旧格式
- 规范所有命令详情中的字段表头格式,保持一致性
- 在索引中添加全局参数列表,列出所有命令通用的全局标志选项
- 修正配置键名称中的小错误(例如base_url写法统一)
- 优化目录索引部分格式,更加规范排列和对齐
- 未改变命令内容及描述,保证文档信息一致性
2026-06-26 19:15:26 +08:00
zeyu.fz ead1bc0f5f refactor(release): 重构发布流程并合并知识库发布逻辑
- 删除独立的 publish-knowledge.yml 工作流
- 在 publish.yml 中新增 package 选择,支持 bailian-cli 和 knowledge-studio-cli
- 发布脚本根据 package 参数传递 --knowledge 标志
- 修改发布任务并发组以包含 package 参数,避免冲突
- 调整发布稳定版与频道版任务名称显示 package 信息
- 精简发布依赖顺序注释,去除冗余部分
- 优化构建步骤,仅构建 bailian-cli-core 包
- 更新包管理代码,整合知识库相关包到统一发布流程
2026-06-26 18:12:35 +08:00
clark-fc c303b51b9e Merge pull request #82 from modelstudioai/feat/knowledge-publish-ci
refactor(ci): 合并知识库发布流程并支持多包发布
2026-06-26 18:04:07 +08:00
zeyu.fz 780ca6addb refactor(ci): 合并知识库发布流程并支持多包发布
- 删除了单独的 publish-knowledge.yml 工作流
- 在 publish.yml 中添加 package 输入以支持多包发布
- 根据 package 选择性传递 --knowledge 标志给发布脚本
- 更新并重命名发布任务以反映 package 区别
- 修改并扩展并发组以包含 package 维度
- 注释更新,说明 knowledge-studio-cli 通过主工作流发布并共享依赖
2026-06-26 18:01:58 +08:00
clark-fc 3375fca2f8 Merge pull request #81 from modelstudioai/feat/knowledge-publish-ci
feat(release): add --knowledge flag and publish-knowledge.yml workflow
2026-06-26 17:23:33 +08:00
zeyu.fz a966b4077f feat(release): add --knowledge flag and publish-knowledge.yml workflow
- packages.mjs: export KSCLI_PACKAGE and ALL_PACKAGES for knowledge-studio-cli
- check.mjs: support knowledge option to build/validate kscli
- validate.mjs: accept packages param, validate all packages in lockstep
- pack-scan.mjs: accept packages param
- publish-stable.mjs: refactor to iterate PACKAGES array; add --knowledge flag
- publish-channel.mjs: refactor to iterate PACKAGES array; add --knowledge flag
- New workflow publish-knowledge.yml: triggers publish with --knowledge flag

The original publish.yml (without --knowledge) publishes only core + cli.
The new publish-knowledge.yml publishes core + cli + knowledge-studio-cli.
2026-06-26 17:20:25 +08:00
zeyu.fz 4745d70587 feat(release): 支持 knowledge-studio-cli 的构建与发布流程
- 新增发布工作流 publish-knowledge.yml,支持 stable 和 channel 模式发布含 knowledge 的包
- runCheck 函数增加 knowledge 参数,支持同时构建和验证 knowledge-studio-cli 包
- publish-stable 和 publish-channel 脚本支持传入 knowledge 参数,调整发布的包列表
- packAndScan 函数支持指定发布包列表,增强灵活性
- 扩展 packages 模块,新增 ALL_PACKAGES 常量包含所有包(基础包加 knowledge-studio-cli)
- loadAndValidate
2026-06-26 16:53:59 +08:00
若麒 91e6c6f553 refactor(runtime): resolve/middleware kernel + declarative arg validation
把 main 从一堆 if + process.exit 重构为「argv 解析成数据 → 交给统一管线执行」。

内核
- resolve(argv) → Resolution(version/help/run/usageError):路由即数据,dispatch 只 switch
- compose 洋葱中间件 (versionCheck/telemetry/auth/runCommand),命令仍收 (config, flags)
- registry.locate() 统一 leaf/group/unknown,取代 isGroupPath + 抛异常的 resolve
- 删除 command-help.ts 全局可变单例:help 渲染收口到错误边界

错误模型
- 新增 UsageError(写错了 → exit 2) 与 IncompleteCommandError(没写完 → 打 help、exit 0)
- version / help / onboarding / 组帮助统一由 resolve 产出、dispatch 分派

参数与校验
- parseFlags 重写:无 positional、新增 switch 类型、值/类型/重复校验
- 无条件必填 → 解析器声明式强制 (OptionDef.required)
- 跨 flag / 条件约束 → 新增 command.validate(flags) 钩子
  (text-chat / search-web / speech / vision / video-ref)
- 移除全部交互式输入 (promptText/Select/Confirm),缺输入直接打 help

输出
- detectOutputFormat 默认 text,不再按 TTY 切 json

测试
- 删除 3 个 stale cli 测试,runtime 单测重写 (29 passed),e2e 适配新行为
2026-06-26 16:50:12 +08:00
zeyu.fz eaa6b07c7d build(kscli): 构建并发布 knowledge-studio-cli 包
- 将 knowledge-studio-cli 版本更新至 1.4.0
- 在发布脚本中新增 knowledge-studio-cli 构建步骤
- 更新包依赖顺序,加入 knowledge-studio-cli 包
- 确保知识库检索相关 CLI 正确构建发布
2026-06-26 16:24:17 +08:00
zeyu.fz ca69316446 feat(cli): 新增 knowledge search 和 knowledge chat 命令支持
- 在 CLI 命令中添加 knowledgeSearch 和 knowledgeChat 两个新命令
- 新增 knowledge 搜索命令,支持多模态图像检索及对话历史上下文传递
- 新增 knowledge 问答命令,支持多轮消息流式回答及多模态输入
- 在核心客户端库(core)中添加对应的 API 端点和类型定义
- 知识库检索接口 retrieve 标注为弃用,推荐使用 search 命令替代
- 更新 kscli 主程序入口,接入新命令并兼容旧命令
- 补充 e2e 测试覆盖 knowledge search 和 knowledge chat 的各类边界与流程
- 更新文档及命令示例,实现使用说明同步最新功能
- 增加测试配置,改善 E2E 测试环境与超时设置
2026-06-26 15:52:44 +08:00
若麒 9c8fe96a1f build(release): publish runtime + commands alongside core/cli; minify library builds 2026-06-26 14:17:27 +08:00
故璃 18d5c420df feat: add cpt dataset type 2026-06-25 19:59:17 +08:00
故璃 9ad85b6278 fix: test issue 2026-06-25 19:26:55 +08:00
故璃 82bdf9ed78 fix: resolve cr issue 2026-06-25 17:48:52 +08:00
故璃 4383eeb416 fix: fix variable name 2026-06-25 16:16:55 +08:00
zeyu.fz 46d8474ec1 feat(kscli): 新增 Knowledge Studio CLI 轻量级 RAG 命令行工具
- 用于阿里云 Model Studio 的知识库检索,支持 RAG(检索增强生成)场景
- 提供配置查看与设置、知识库检索、自更新功能
- 替换原 rag 子包,移除 rag 相关代码及配置
- 新增独立 package,包含完整的构建、启动和发布配置
- 添加详细的中英文 README 文档说明安装、使用与认证方式
- 配置 TypeScript 和 Vite 构建支持,确保开发体验和构建质量
- 更新根 package.json 脚本,将 rag dev 命令替换为 kscli dev
- 新增 Git 忽略文件,排除日志、构建输出等无关文件
2026-06-25 15:08:35 +08:00
故璃 1851ec85f0 feat: sync readme 2026-06-25 14:22:26 +08:00
故璃 0797b0767f Merge branch 'main' into feat/model-train 2026-06-25 14:02:23 +08:00
故璃 17f4454df4 feat: refact validator to support dpo dataset 2026-06-25 13:56:15 +08:00
故璃 e67615eabd feat: auto update 2026-06-24 20:50:30 +08:00
ls 39513200bc Merge pull request #79 from modelstudioai/token-plan-openapi
Token plan openapi
2026-06-24 17:39:00 +08:00
ls 7b5bb1c341 Merge pull request #76 from budiga/token-plan-openapi
Token plan openapi
2026-06-24 17:38:25 +08:00
故璃 e0a7c86f05 feat: update doc 2026-06-24 17:29:12 +08:00
ls 908439e3f9 Merge pull request #75 from modelstudioai/token-plan-openapi
feat(token-plan): add Token Plan organization & seats commands
2026-06-24 17:28:22 +08:00
若麒 7a0a083b2e refactor(core): replace skipDefaultApiKeySetup with required auth field
Commands now declare their credential requirement explicitly via a
required `auth: "apiKey" | "console" | "none"` field instead of the
boolean `skipDefaultApiKeySetup`. The runtime prepares credentials
based on this declaration and skips API-key setup under --dry-run.

- core: add AuthRequirement type and required `auth` field to
  Command/CommandSpec; drop skipDefaultApiKeySetup
- runtime: gate API-key setup on `auth === "apiKey" && !dryRun`
- commands: annotate all 45 commands (apiKey 25 / console 11 / none 9)
2026-06-24 16:41:09 +08:00
若麒 61689ec0da build: jump to source across packages via @bailian-cli/source export condition 2026-06-24 16:14:20 +08:00
雷骏 ba78d13a52 feat: add auto update cli 2026-06-24 15:31:24 +08:00
若麒 24abdbf450 refactor(commands): decouple command paths from binary name; drop path presets
Commands no longer hardcode "bl" or their path — the runtime renders the
`<bin> <path>` prefix from each product's registry key, so shared commands
show `bl knowledge retrieve` / `rag retrieve` from one codebase. commands
package now exports only individual commands (no groups/catalog); bl and rag
each spell out their own path map. Also removes the unused export-schema command.
2026-06-24 14:40:08 +08:00
故璃 cbffe6c541 Merge branch 'main' into feat/model-train 2026-06-24 14:19:58 +08:00
故璃 d567d6af0c feat: setup model train/deploy cli commend 2026-06-24 14:01:28 +08:00
wb-liuxuehuan 33b1df01cc Merge remote-tracking branch 'upstream/token-plan-openapi' into token-plan-openapi 2026-06-24 13:02:35 +08:00
lisheng.lisheng 0ba705f194 refactor(token-plan): rename top-level command tokenplan -> token-plan
Rename the public command group from `bl tokenplan` to `bl token-plan`
for kebab-case consistency. Source directory and reference doc renamed
accordingly; remote API paths (/tokenplan/...) and internal TS
identifiers are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-24 12:48:19 +08:00
wb-liuxuehuan 23d409f9ff Merge remote-tracking branch 'upstream/token-plan-openapi' into token-plan-openapi 2026-06-24 12:46:53 +08:00
wb-liuxuehuan 70ccc6a447 feat(tokenplan): 修改Token Plan 命令名称及相关优化
新增 `token-plan` 相关命令,包括 `add-member`、`assign-seats`、`create-key` 和 `list-seats`,支持管理 Token Plan 组织成员和 API 密钥。更新了命令的参数处理逻辑,确保对输入参数的验证更加严格,提升了代码的可读性和健壮性。同时,更新了相关文档,提供使用示例和参数说明。
2026-06-24 12:44:14 +08:00
lisheng.lisheng b7fba7679e chore: update Node.js version to 24 2026-06-24 12:34:43 +08:00
ls 88e5b903bc Merge pull request #74 from budiga/token-plan-openapi
Token plan openapi
2026-06-24 12:33:51 +08:00
wb-liuxuehuan ba1661356f feat(tokenplan): 重构 Token Plan 命令以支持新功能
对 `tokenplan` 相关命令进行了重构,新增了 `ak-sign` 模块以支持 ACS3-HMAC-SHA256 签名,优化了参数处理逻辑,简化了对凭证的处理。更新了 `add-member`、`assign-seats`、`create-key` 和 `seats` 命令,增强了对参数的验证和处理,确保代码的可读性和健壮性。同时,新增了类型定义和工具函数以支持更好的代码结构。
2026-06-24 11:14:57 +08:00
Gong Shiqi 60c49ec1ac Merge pull request #72 from modelstudioai/chore/list-voices
feat(omni,speech): add --list-voices and fix cosyvoice voice ID
2026-06-24 10:40:10 +08:00
若麒 07c71412cf chore(release): 1.4.2 2026-06-24 10:35:26 +08:00
若麒 e2c4935e84 Merge remote-tracking branch 'origin/main' into chore/list-voices 2026-06-24 10:29:15 +08:00
Gong Shiqi 4f10b7f50c Merge pull request #71 from modelstudioai/feat/console-login-site
feat: Add default login site selection for agent
2026-06-24 10:16:20 +08:00
wb-liuxuehuan dc5a535bf3 Merge remote-tracking branch 'upstream/main' into token-plan-openapi 2026-06-24 10:12:18 +08:00
若麒 ad236e9b11 test(runtime): move unit tests from cli to runtime package 2026-06-23 19:32:21 +08:00
wb-liuxuehuan 14105547e8 fix(tokenplan): 优化参数处理逻辑
更新 `assign-seats` 和 `seats` 命令中的参数处理逻辑,简化对 AccountIds 和 StatusList 的检查,确保在缺少必要参数时抛出相应错误。同时,增强对 `--query-assigned` 参数的验证,确保其值为 'true' 或 'false'。此更改提高了代码的可读性和健壮性。
2026-06-23 18:27:12 +08:00
若麒 d971a04fb8 refactor(cli): split into runtime / commands packages for composable CLIs
Decompose the monolithic `cli` package into three layers so multiple
products can be assembled from a shared base:

- bailian-cli-runtime: framework infra (createCli, registry, args,
  output, pipeline, utils) — product-agnostic
- bailian-cli-commands: command library, grouped (base/knowledge/text/
  media/memory/misc) so each product picks the sets it needs
- packages/cli (bl): full command set; packages/rag (rag): base +
  knowledge only

Product identity (binName / clientName / npmPackage) is injected at the
createCli boundary and required there, with no per-consumer defaults.
2026-06-23 17:51:14 +08:00
wb-liuxuehuan 1590e69d67 feat(tokenplan): 更新 AccountIds 参数处理逻辑
修改 `assign-seats` 命令中的 AccountIds 参数处理,将字符串类型的 AccountIds 转换为数组。同时,更新相关的测试用例以验证新逻辑的正确性。
2026-06-23 17:42:10 +08:00
clh02467605 fd36db5cab fix: remove unsupport voice 2026-06-23 17:08:37 +08:00
wb-liuxuehuan d74686f09f feat(tokenplan): 添加 Token Plan 相关命令
新增 `tokenplan add-member`、`tokenplan assign-seats` 和 `tokenplan create-key` 命令,支持管理 Token Plan 组织成员和 API 密钥。相关文档已更新,提供使用示例和参数说明。
2026-06-23 16:51:54 +08:00
clh02467605 30a0bbbc87 fix: fixed omni e2e 2026-06-23 15:34:52 +08:00
clh02467605 9761932b4c feat(omni): add voice listing functionality and update voice options 2026-06-23 14:50:09 +08:00
wb-liuxuehuan c0d30fee3d feat(tokenplan): 添加 tokenplan seats 命令以列出订阅座位详情
新增 tokenplan seats 命令,支持分页和状态过滤,提供详细的座位信息查询功能。相关文档已更新。
2026-06-23 14:05:11 +08:00
qcq01083097 9cbd4aab85 feat: Add default login site selection for agent 2026-06-22 17:09:17 +08:00
Gong Shiqi 19c4f5f2ab Merge pull request #70 from modelstudioai/feat/switch-model
feat(video): upgrade happyhorse model from 1.0 to 1.1, video-edit has not been updated and is still 1.0.
2026-06-22 17:08:13 +08:00
若麒 af524e5487 chore(release): 1.4.1 2026-06-22 17:03:14 +08:00
clh02467605 4e025dda8d feat(video): upgrade happyhorse model from 1.0 to 1.1, video-edit has not been updated and is still 1.0.
- Update default models in bl-api pipeline from happyhorse-1.0 to 1.1
- Replace happyhorse-1.0-t2v/i2v/r2v references with 1.1 versions in commands
2026-06-22 14:30:48 +08:00
Gong Shiqi 1ffcbdd80c Merge pull request #65 from modelstudioai/feat/optimize-skill
Feat/optimize skill
2026-06-18 18:05:07 +08:00
clh02467605 8aedeca4ac chore(skill): bump skill version to 1.3.4 2026-06-18 17:50:43 +08:00
clh02467605 f5a7dd494b docs(bailian-cli): Update SKILL.md to reference new pre-flight checklist procedure 2026-06-18 15:25:27 +08:00
qcq01083097 07dafc0fd4 feat: The method to modify and update skills 2026-06-18 14:07:06 +08:00
Gong Shiqi 0cd0daa18d Merge pull request #68 from modelstudioai/release/1.4.0
Release/1.4.0
2026-06-17 21:12:36 +08:00
若麒 5e39d1abc3 docs(changelog): note video resolution/ratio flag fix 2026-06-17 21:10:40 +08:00
若麒 c3df659ef0 Merge remote-tracking branch 'origin/main' into release/1.4.0 2026-06-17 21:07:39 +08:00
若麒 1d803bb4b9 fix(skill): require version check before any bl command, ask user before upgrading 2026-06-17 20:54:36 +08:00
Gong Shiqi 847b291ccc Merge pull request #67 from modelstudioai/fix/video-params-accuracy-v2
fix(video): correct resolution/ratio flag descriptions
2026-06-17 20:29:15 +08:00
若麒 0ceb15b0be fix(video): correct resolution/ratio flag descriptions 2026-06-17 20:26:55 +08:00
故璃 dc3c02f68c fix: e2e test update logic 2026-06-17 19:51:21 +08:00
clh02467605 f1eeeff682 chore(skill): opt bailian-cli skill 2026-06-17 18:12:47 +08:00
clh02467605 f184c60357 chore(skill): opt bailian-cli skill 2026-06-17 18:10:38 +08:00
若麒 8906af8ad1 docs(readme): sync China-site-only notice to zh and cli-package READMEs 2026-06-17 17:21:25 +08:00
xxlaura 3b705f2b0d docs(readme): add China-site-only notice to Features section
Separate globally available features from China-site-only features
with a blockquote note clarifying that Knowledge base, App calls,
MCP integration, Web search, Model recommendation, Console
capabilities, and Local file auto-upload are currently exclusive
to China site (aliyun.com) account holders.
2026-06-17 16:46:49 +08:00
若麒 2260c51c7e docs(changelog): add advisor model upgrade and JSON output changes to 1.4.0 2026-06-17 16:14:17 +08:00
若麒 8c893a59ef Merge remote-tracking branch 'origin/main' into release/1.4.0 2026-06-17 16:06:14 +08:00
若麒 611ecc1e68 chore(release): 1.4.0 2026-06-17 15:58:08 +08:00
gujieye 17c1f5f7b2 Merge pull request #59 from modelstudioai/feat/update-intent-model
feat: change output & table name into English
2026-06-17 15:24:20 +08:00
故璃 7593baf4f6 Merge branch 'feat/update-intent-model' of https://github.com/modelstudioai/cli into feat/update-intent-model 2026-06-17 15:20:46 +08:00
故璃 edb34658a1 feat: temp save 2026-06-17 15:16:54 +08:00
gujieye cbeb2bf071 Merge branch 'main' into feat/update-intent-model 2026-06-17 15:03:51 +08:00
ls 1fd08fe1c8 Merge pull request #56 from modelstudioai/feat/console-gateway-region-site
feat(console): resolve gateway URL from region + site, add switchAgent
2026-06-17 14:49:28 +08:00
故璃 8cb24b719b feat: merge main 2026-06-17 14:43:13 +08:00
qcq01083097 d4c0809951 feat: resolve conflict 2026-06-17 14:43:01 +08:00
故璃 b01d35c246 Merge branch 'main' into feat/update-intent-model 2026-06-17 14:35:14 +08:00
Gong Shiqi 67ff4d2811 Merge pull request #63 from modelstudioai/feat/i18n-english-only
feat(cli): standardize user-facing CLI text to English
2026-06-17 14:27:27 +08:00
clh02467605 a830965feb feat(omni): add new voice option to omni chat command 2026-06-17 14:19:47 +08:00
故璃 14583cf6d1 feat: update access token logic 2026-06-17 11:58:09 +08:00
Gong Shiqi c32f03e8dc Merge pull request #62 from modelstudioai/feat/project-optimization
Feat/project optimization
2026-06-17 11:53:02 +08:00
lishengzxc e9feb380f5 Merge branch 'main' of github.com:modelstudioai/cli into feat/console-gateway-region-site 2026-06-17 11:45:49 +08:00
clh02467605 62ad8768e3 merge: merge main into feat/i18n-english-only 2026-06-17 11:28:24 +08:00
clh02467605 e8a40cdad0 Merge remote-tracking branch 'refs/remotes/origin/main' into feat/i18n-english-only
# Conflicts:
#	packages/cli/src/commands/knowledge/retrieve.ts
#	skills/bailian-cli/reference/knowledge.md
2026-06-17 11:26:04 +08:00
qcq01083097 9bcb66b1ff feat: Resolve conflicts 2026-06-17 10:02:00 +08:00
lishengzxc 60cad1001c chore: remove redundant "(global flag)" from option descriptions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-16 19:44:08 +08:00
qcq01083097 a5d078b8a0 Merge branch 'feat/console-gateway-region-site' of github.com:modelstudioai/cli into feat/console-gateway-region-site 2026-06-16 18:23:09 +08:00
qcq01083097 685c0176ff feat: clean up region remnants 2026-06-16 18:22:18 +08:00
qcq01083097 d24b41d452 feat: Remove the logic related to region 2026-06-16 18:16:10 +08:00
lishengzxc d320d36ba7 docs: add console gateway flags convention to AGENTS.md and command help
Add console global flags (--console-region, --console-site, --console-switch-agent)
to the options of all 12 commands that depend on callConsoleGateway, so they appear
in --help output. Document this as convention #4 in AGENTS.md for future commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-16 17:50:58 +08:00
故璃 f816d5cb1f Merge branch 'main' into feat/update-intent-model 2026-06-16 17:38:10 +08:00
故璃 93cecc35de feat: model recommend use english output 2026-06-16 17:37:40 +08:00
qcq01083097 a5d055f45a feat: Adjust the priority of base_url in config to be higher than that of the environment variable DASHSCOPE_BASE_URL 2026-06-16 17:16:25 +08:00
qcq01083097 631a9c1818 feat: Complete the missing changes for E2E Test 2026-06-16 16:51:04 +08:00
qcq01083097 682321247b feat: Delete invalid code 2026-06-16 16:39:05 +08:00
Gong Shiqi d82f334a99 Merge pull request #58 from modelstudioai/feat/knowledge-api-key
docs(agents): add CHANGELOG checklist to publish guide
2026-06-16 15:57:00 +08:00
若麒 f7c18276bc docs(agents): add CHANGELOG checklist to publish guide 2026-06-16 15:54:15 +08:00
故璃 c54f6a64d7 feat: model recommend use english prompt & output 2026-06-16 15:49:01 +08:00
Gong Shiqi 73143dbae2 Merge pull request #57 from modelstudioai/feat/knowledge-api-key
feat(knowledge): update deprecation notices for access key options in CLI and documentation
2026-06-16 15:11:58 +08:00
qcq01083097 cb25bc4149 feat: When console login is not performed, throw more explicit errors and prompts 2026-06-16 15:11:10 +08:00
若麒 35d681f0c7 chore(release): prepare 1.3.3 2026-06-16 15:09:32 +08:00
zeyu.fz a16afb3f0b chore(changelog): update to version 1.3.3 with improvements to CLI help output and command notes 2026-06-16 15:01:57 +08:00
qcq01083097 2ed513124e feat: Use command.skipDefaultApiKeySetup instead of NO_AUTH_SETUP to determine whether an API key is required 2026-06-16 14:59:14 +08:00
clh02467605 062bbd4052 feat(cli): standardize user-facing CLI text to English 2026-06-16 14:41:33 +08:00
lishengzxc f847476016 refactor(auth): 修改 --console 标志以简化登录命令 2026-06-16 14:15:58 +08:00
qcq01083097 83ea0dfd03 feat: Clear invalid remnants of the command "model list" 2026-06-16 13:58:34 +08:00
lishengzxc 9a5797da1e Merge branch 'feat/console-gateway-region-site' of github.com:modelstudioai/cli into feat/console-gateway-region-site 2026-06-16 13:58:07 +08:00
lishengzxc 8c398bae57 refactor(console): promote --console-region, --console-site, --console-switch-agent to global flags
Eliminate per-command --region/--site/--switch-agent duplication across 11 console gateway commands.
These values now flow through config (CLI flags → config file → defaults) and are consumed by
callConsoleGateway automatically. Also wire consoleSite into resolveConsoleOrigin so --console-site
selects the correct login URL (domestic vs international).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-16 13:56:20 +08:00
故璃 14fc293ef6 feat: opt json output & reduce Chinese table name 2026-06-16 11:39:16 +08:00
qcq01083097 2bcbf56282 feat: Fix lint errors in mcp.ts 2026-06-16 11:27:29 +08:00
qcq01083097 ce64d628bb feat: The command "config show" does not display the "region" field, but displays all fields in the "config.json" file 2026-06-16 11:08:33 +08:00
zeyu.fz 3ca8da8e75 refactor(knowledge): update deprecation notices for access key options in CLI and documentation 2026-06-15 19:57:20 +08:00
zeyu.fz 6c4ac80882 feat(cli): add support for displaying command notes in help output 2026-06-15 19:49:09 +08:00
zeyu.fz bd4644448a chore(core): 更新核心包版本至1.3.3
- 将版本号从1.3.2提升至1.3.3
- 保持其他核心包配置不变
2026-06-15 19:41:30 +08:00
zeyu.fz a0ab35acf1 refactor(knowledge): update authentication options and documentation 2026-06-15 19:22:35 +08:00
故璃 3f29f93ef5 feat: replace model 2026-06-15 14:45:38 +08:00
lishengzxc e5abd1b554 feat(auth): 更新默认控制台登录页为正式中国站地址 2026-06-15 13:53:41 +08:00
lishengzxc 4749b493da feat(auth): 更新默认控制台登录页为中国站地址 2026-06-15 13:11:49 +08:00
lishengzxc 7a65fb850c feat(auth): parse and persist workspace_id from console login callback
Adapts to bailian-cli-login af06baf which added workspace_id to the
notifyToken payload. The field is parsed from query/body and written
to config.json as workspace_id.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 01:05:00 +08:00
lishengzxc f45b19c261 chore: remove console_gateway_url remnants from schema and tests
Field was replaced by region+site gateway resolution but ConfigFile
definition, parseConfigFile logic, and test case were left behind.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 01:01:26 +08:00
lishengzxc 270412d146 refactor(auth): deduplicate canRetry/validateKey between login.ts and login-console.ts
Export validateAndPersistApiKey from login-console.ts and reuse in
login.ts. Remove duplicated canRetry, RETRY_DELAY_BASE_MS, and
validateKeyAndPersist from login.ts. Unify validation model to
qwen3.7-max. Reorder base_url write before apiKey validation in
the --api-key path to avoid double read-modify-write.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 00:26:57 +08:00
lishengzxc 36a60a2848 chore(pnpm): 移除 vite 和 vitest 的 overrides 配置 2026-06-14 23:46:35 +08:00
lishengzxc 6c716e5120 feat(auth): add --base-url flag to bl auth login
When used with --api-key, validates the key against the specified
base URL and persists it to config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 23:44:17 +08:00
lishengzxc 2182a2239f refactor(auth): unify console callback persistence — validate apiKey with callback's baseUrl
Move apiKey validation into login-console.ts so it uses the baseUrl
from the same callback (not stale config). All fields are now persisted
in one place: config fields first, then apiKey validated + written.

Remove onApiKey callback indirection from runConsoleLogin signature.
Clean up debug logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 23:20:30 +08:00
lishengzxc 51d9833a3d fix(auth): parse baseUrl/consoleSite/consoleRegion/consoleSwitchAgent from POST body
These fields were only extracted from query params but the console
sends them in the JSON POST body. Add parseExtrasFromRawBody() to
handle JSON and form-urlencoded bodies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 22:56:54 +08:00
lishengzxc e68abb6975 refactor(auth): simplify login-console config persistence logic
Extract hasConfig variable to deduplicate the multi-field condition check,
remove the changed flag pattern in favor of direct write-through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 22:35:33 +08:00
lishengzxc 067e96689a feat(console): resolve gateway URL from region + site, add switchAgent support
Console gateway URL and action are now resolved from a region + site
mapping table instead of a single hardcoded config value. Supports
cn-beijing and ap-southeast-1 with domestic/international site variants.

- Add ConsoleSite type, REGION_GATEWAYS mapping, and resolveGateway()
- Add switchAgent to cornerstoneParam for delegated access
- Add console_site, console_region, console_switch_agent to config
- Remove consoleGatewayUrl from Config (replaced by region+site resolution)
- login-console callback now persists baseUrl, site, region, switchAgent
- bl console call gains --site and --switch-agent flags
- All callers delegate region default to callConsoleGateway (no more hardcoded cn-beijing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 20:17:46 +08:00
660 changed files with 64720 additions and 14576 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ body:
attributes:
label: Node version
description: "Output of node --version"
placeholder: "v22.12.0"
placeholder: "v18.17.0"
validations:
required: true
+62 -8
View File
@@ -3,6 +3,13 @@ name: Publish
on:
workflow_dispatch:
inputs:
package:
description: "Which package set to publish"
required: true
type: choice
options:
- bailian-cli
- knowledge-studio-cli
mode:
description: "Publish mode"
required: true
@@ -11,22 +18,22 @@ on:
- channel
- stable
channel:
description: "dist-tag (channel mode only, e.g. mcp/plugin/advisor)"
description: "Required when mode=channel. npm dist-tag only (lowercase, digits, dashes), e.g. mcp / plugin / sync-release. bailian-cli binary CDN always overwrites sync-release.json; knowledge-studio-cli is npm-only."
required: false
type: string
concurrency:
group: publish-${{ inputs.mode }}-${{ inputs.channel }}
group: publish-${{ inputs.package }}-${{ inputs.mode }}-${{ inputs.channel }}
cancel-in-progress: false
jobs:
publish-stable:
if: inputs.mode == 'stable'
name: publish stable to npm + tag
name: publish stable (${{ inputs.package }}) to npm + binary + tag
runs-on: ubuntu-latest
environment: production # Required Reviewers gate
permissions:
contents: write # push lightweight tag to origin
contents: write # push tag + create GitHub Release with binary assets
id-token: write # OIDC for npm Trusted Publishing + provenance
steps:
- uses: actions/checkout@v6
@@ -48,19 +55,47 @@ jobs:
| sudo tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Ensure zip (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip
- run: pnpm install --frozen-lockfile
# Binary compile uses `bun build --compile` CLI (not Bun.build API).
# Keep this pin in sync with any local smoke tests of binary-compile.mjs.
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.19"
- name: publish-stable
run: node tools/release/publish-stable.mjs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# OSS release channel runs fully in CI: upload + reconcile + manifest.json.
# All values come from repo Settings → Secrets — no OSS defaults live in
# code. Leave AK/SK unset to skip the OSS channel; once enabled,
# bucket/region/prefix are required.
BAILIAN_OSS_AK: ${{ secrets.BAILIAN_OSS_AK }}
BAILIAN_OSS_SK: ${{ secrets.BAILIAN_OSS_SK }}
BAILIAN_OSS_BUCKET: ${{ secrets.BAILIAN_OSS_BUCKET }}
BAILIAN_OSS_REGION: ${{ secrets.BAILIAN_OSS_REGION }}
BAILIAN_OSS_ENDPOINT: ${{ secrets.BAILIAN_OSS_ENDPOINT }}
BAILIAN_RELEASE_PREFIX: ${{ secrets.BAILIAN_RELEASE_PREFIX }}
BAILIAN_STATIC_PREFIX: ${{ secrets.BAILIAN_STATIC_PREFIX }}
run: node tools/release/publish-stable.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }}
publish-channel:
if: inputs.mode == 'channel'
name: publish beta to npm
name: publish channel (${{ inputs.package }}) to npm + binary
runs-on: ubuntu-latest
permissions:
contents: read # no tag, no Release; just publish
contents: write # create prerelease GitHub Release with binary assets
id-token: write # OIDC for npm Trusted Publishing + provenance
steps:
- name: Require channel input
if: ${{ inputs.channel == '' }}
run: |
echo "::error::mode=channel requires the workflow input \"channel\" (npm dist-tag, e.g. mcp / plugin / sync-release). Leave mode=stable if you do not need a dist-tag."
exit 1
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
@@ -80,7 +115,26 @@ jobs:
| sudo tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Ensure zip (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip
- run: pnpm install --frozen-lockfile
# Binary compile uses `bun build --compile` CLI (not Bun.build API).
# Keep this pin in sync with any local smoke tests of binary-compile.mjs.
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.19"
- name: publish-channel
run: node tools/release/publish-channel.mjs --channel "${{ inputs.channel }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# OSS release channel — same Settings-injected values as stable.
BAILIAN_OSS_AK: ${{ secrets.BAILIAN_OSS_AK }}
BAILIAN_OSS_SK: ${{ secrets.BAILIAN_OSS_SK }}
BAILIAN_OSS_BUCKET: ${{ secrets.BAILIAN_OSS_BUCKET }}
BAILIAN_OSS_REGION: ${{ secrets.BAILIAN_OSS_REGION }}
BAILIAN_OSS_ENDPOINT: ${{ secrets.BAILIAN_OSS_ENDPOINT }}
BAILIAN_RELEASE_PREFIX: ${{ secrets.BAILIAN_RELEASE_PREFIX }}
BAILIAN_STATIC_PREFIX: ${{ secrets.BAILIAN_STATIC_PREFIX }}
run: node tools/release/publish-channel.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }} --channel "${{ inputs.channel }}"
+7
View File
@@ -10,8 +10,10 @@ lerna-debug.log*
# Dependencies & build output
node_modules
dist
dist-bin
dist-ssr
tools/generated
.node-version
*.local
@@ -35,7 +37,9 @@ tools/generated
.claude/settings.local.json
.claude/scheduled_tasks.lock
.cursor/
.qoder/
.qwen/
.qoder
.playwright-mcp/
.pnpm-store/
@@ -45,3 +49,6 @@ packages/cli/scene/**/outputs/
# Environment variables (sensitive data)
.env
# Local scratch / plan drafts (never commit)
.scratch/
+11 -2
View File
@@ -1,10 +1,19 @@
#!/usr/bin/env sh
set -eu
# Regenerate skill reference + SKILL metadata (needs bailian-cli-core dist).
# Regenerate skill reference + SKILL metadata from source (no package build).
pnpm run sync:skill-assets
# Stage generator output so it is included in this commit.
git add skills/bailian-cli/reference skills/bailian-cli/SKILL.md
git add \
skills/bailian-protocol/SKILL.md \
skills/bailian-cli/SKILL.md \
skills/bailian-cli/reference \
skills/bailian-gen/SKILL.md \
skills/bailian-gen/reference \
skills/bailian-finetune/SKILL.md \
skills/bailian-finetune/reference \
skills/bailian-managed-agent/SKILL.md \
skills/bailian-managed-agent/reference
vp staged
+85 -56
View File
@@ -1,82 +1,96 @@
# bailian-cli — AI 维护指南
本文件是 AI agent 维护本仓库时的契约。每次进入项目先读这里,从下方"业务场景索引"挑一条,跳到对应的详细文档,按它的清单完成改动
本文件是 AI agent 维护本仓库时的契约。每次进入项目先读这里,从"业务场景索引"挑一条,再进入对应 `docs/agents/*.md` 清单
## 项目地图
monorepo 双包结构:
monorepo 现在按"纯逻辑 → 运行时框架 → 命令库 → 产品入口"分层:
- `packages/cli``bailian-cli` 包,CLI 命令、UI、入口
- `packages/core``bailian-cli-core` 包,鉴权 / HTTP / 类型,纯逻辑层
- `packages/core``bailian-cli-core`,纯逻辑层:鉴权、配置、HTTP client、错误、类型、文件工具
- `packages/runtime``bailian-cli-runtime`,通用 CLI 运行时:`createCli`、参数解析、registry/help、middleware、error handler、输出、pipeline、Command Pack host
- `packages/commands``bailian-cli-commands`,可复用命令实现库,只导出 command,不决定产品路径
- `packages/cli``bailian-cli`,完整 `bl` 产品入口;`src/commands.ts` 组装 `bl` 暴露的命令路径
- `packages/kscli``knowledge-studio-cli`,Knowledge Studio 专用入口;`src/main.ts` 复用 commands 并重映射为 `kscli` 路径
### `packages/cli` 目录要点
### 关键文件
```
packages/cli/
├── src/
│ ├── main.ts # 入口、鉴权分支、调用 registry
│ ├── registry.ts # 命令树解析、动态 help(读 catalog)
│ ├── commands/
│ │ ├── catalog.ts # 命令总表(登记处,构建脚本也读它)
│ │ ├── index.ts # re-export commands
│ │ └── <group>/...ts # 各命令 defineCommand 实现
│ ├── output/ # CLI 输出、prompt、progress
│ └── urls.ts # 控制台/文档 URL(仅 cli)
└── tests/e2e/
packages/cli/src/main.ts # bl 入口,注入 binName/version/clientName/npmPackage
packages/cli/src/commands.ts # bl 产品命令 map,tools/generate-reference.ts 也读它
packages/cli/src/command-pack-policy.ts # bl 的 Command Pack policy
packages/kscli/src/main.ts # kscli 入口和命令 map
packages/commands/src/index.ts # re-export 单个命令实现
packages/commands/src/commands/ # defineCommand({ auth, flags, usageArgs, exampleArgs, run })
packages/runtime/src/create-cli.ts # createCli(commands, options)
packages/runtime/src/registry.ts # 命令树解析 + 动态 help
packages/runtime/src/middleware.ts # auth / telemetry / update / run command
packages/runtime/src/command-packs/ # 通用 Command Pack 加载、校验、隔离安装目录和管理命令
packages/runtime/src/urls.ts # 用户面控制台 URL
packages/core/src/types/command.ts # Command / flags / auth 类型
packages/core/src/config/ # ConfigFile / Settings / source 解析
packages/core/src/auth/ # apiKey / console credential 解析与落盘
packages/core/src/client/ # HTTP client / endpoints / console gateway
```
Skill / 命令手册随 `skills/bailian-cli/``npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts``catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 一起在 **pre-commit**`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行
非代码资产:
- `tools/release/` — 发版自动化CI 驱动,见 `.github/workflows/publish.yml`
- `tools/generate-reference.ts` — 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`
- `tools/sync-skill-metadata.ts` — 从 `packages/cli/package.json` 同步 `skills/bailian-cli/SKILL.md``metadata.version`(与 `generate:reference` 一并由根目录 `pnpm run sync:skill-assets` 及 pre-commit 执行)
- `README.md` / `README.zh.md` — npm 和 GitHub 主页
Skill / 命令手册随 `skills/bailian-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` `packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)
约定:
- core 是纯库,不依赖 cli(详见下方通用约定)
- 文件路径与命令路径一一对应:`commands/text/chat.ts` `bl text chat`
- 单级命令:`commands/<name>.ts`(如 `update.ts`);两级:`commands/<group>/<action>.ts`
- 命令登记在 **`catalog.ts`**;`bl --help` `tools/generate-reference.ts` 生成的命令手册同源,见 [command-add-remove.md](docs/agents/command-add-remove.md)
- 命令实现文件路径仍按能力放置:`packages/commands/src/commands/text/chat.ts`
- 产品命令路径由入口 map 决定:同一个实现可暴露为 `bl knowledge retrieve` `kscli retrieve`
- `defineCommand` 只写命令元数据与逻辑: `auth``flags``usageArgs``exampleArgs``validate``run`
- `usageArgs` / `exampleArgs` 不写 `bl` `kscli` 前缀;runtime / reference 生成器按产品路径补前缀
- 不再使用 `catalog.ts` 作为登记处;新增/重命名命令必须同时看命令库导出和产品入口 map
非代码资产:
- `tools/release/` — 发版自动化CI 驱动,见 `.github/workflows/publish.yml`
- `tools/generate-reference.ts` — 从 `packages/cli/src/commands.ts` 按归属表生成各 `skills/<skill>/reference/`
- `tools/sync-skill-metadata.ts` — 同步各 `skills/*/SKILL.md``metadata.version`(含 `bailian-protocol`
- `README.md` / `README.zh.md` — npm 和 GitHub 主页
## 业务场景索引
按当前任务从下表挑一条进入对应文档:
| 场景 | 何时进入 | 详见 |
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
| 错误文案变更 | 改 `BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
| 发布 | channel / stable 发布到 npmCI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
| 场景 | 何时进入 | 详见 |
| ----------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- |
| 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) |
| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) |
| 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) |
| 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) |
| 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) |
| Skill 文案 / 路由 | 改 SKILL 路由、安装约定、hand-off、hub/领域边界 | [docs/agents/skill-change.md](docs/agents/skill-change.md) |
| 错误文案变更 | 改 `BailianError` 的 message 或 hint | [docs/agents/error-hint-change.md](docs/agents/error-hint-change.md) |
| URL / 渠道变更 | 控制台域名 / 文档站 / 追踪参数 | [docs/agents/url-change.md](docs/agents/url-change.md) |
| 埋点变更 | 改 AEM 命令事件、后端渠道 header、User-Agent | [docs/agents/telemetry-change.md](docs/agents/telemetry-change.md) |
| 鉴权扩展 | 加 OAuth / SSO / 换 token 来源 | [docs/agents/auth-change.md](docs/agents/auth-change.md) |
| 配置项扩展 | 新 env var 或 `~/.bailian/config.json` 字段 | [docs/agents/config-add.md](docs/agents/config-add.md) |
| Profile / 激活 | 改命名 Profile、预设或 `active_config` | [docs/agents/config-profile-change.md](docs/agents/config-profile-change.md) |
| 安装文档 | 改安装、鉴权、验证流程或线上 install 页面 | [docs/agents/install-doc-change.md](docs/agents/install-doc-change.md) |
| 发布 | channel / stable 发布到 npmCI 驱动) | [docs/agents/publish.md](docs/agents/publish.md) |
| Change Log | 发版说明 / 历史版本说明 | [docs/agents/changelog-write.md](docs/agents/changelog-write.md) |
| 工具链调整 | lint 规则 / 构建配置 / 依赖升级 | [docs/agents/lint-toolchain.md](docs/agents/lint-toolchain.md) |
| Command Pack | 扩展包 / 白名单 / plugin 管理命令 | [docs/agents/command-pack.md](docs/agents/command-pack.md) |
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增一份 `docs/agents/<scenario>.md`,把清单沉淀下来。
如果当前任务无法对应任何场景,先按经验完成,然后**回来评估这是不是一类新场景** —— 是就新增 `docs/agents/<scenario>.md`,把清单沉淀下来。
## 通用约定
下面两条与场景无关,任何改动都适用。每次完成改动后自查。
### 1. 发布包版本号同步
### 1. cli 和 core 版本号同步
源码包的 `version` 当前保持一致: `packages/core``packages/runtime``packages/commands``packages/cli``packages/kscli`。做版本 bump 时一动多动。release 工具当前强校验 / 发布范围以 `tools/release/lib/packages.mjs` 为准;把新包纳入发布前必须同步该清单和 [publish.md](docs/agents/publish.md)。
`packages/cli/package.json``packages/core/package.json``version` 字段必须始终相等。一动两动。
### 2. 分层边界
### 2. core 是纯库,cli 是 core 的 UI 层
core 不应该知道 cli 的存在。具体表现:
- core 不写 stderr,不调 `process.exit`(用 `console.*``throw`)
- core 抛的 `BailianError`,hint 字符串不出现 `bl xxx` 命令名
- core 不写死域名 / region / 追踪参数(URL 集中在 `packages/cli/src/urls.ts`)
- core 接收 cli 通过 `Config` 注入的 metadata(`clientName` / `clientVersion`)
- `core` 是纯库:不依赖 `runtime` / `commands` / 产品入口;不调 `process.exit`;新增/改动时不硬编码 `bl` / `kscli` 命令名、控制台 URL 或渠道追踪参数。当前遗留项见 [error-hint-change.md](docs/agents/error-hint-change.md) 与 [url-change.md](docs/agents/url-change.md),触碰相关代码时顺手收敛
- `runtime` 是通用 CLI 框架:可以处理 TTY、help、错误输出、middleware,但不写具体业务命令逻辑
- `commands` 是命令实现库:不决定产品路径;不在 `usageArgs` / `exampleArgs` / hint 里硬编码产品 bin 前缀
- `cli` / `kscli` 是产品层:负责命令路径 map、产品 identity、README、技能 reference、发版入口
- URL 集中在 `packages/runtime/src/urls.ts`(用户面控制台)和 `packages/core/src/config/schema.ts` / client 层(API)
### 3. 错误处理边界:CLI 不翻译服务端错误
@@ -86,18 +100,33 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的
| ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
| 命令解析、缺 flag、参数校验 | **内部** | `BailianError(USAGE)` |
| 文件 I/O(ENOENT/EACCES/...) | **内部** | `BailianError(GENERAL)` + errno-specific hint |
| 本地 credentials 缺失(resolver/ensure-key/AK-SK 等) | **内部** | `BailianError(AUTH)` |
| 本地 credentials 缺失(resolver / auth stage 等) | **内部** | `BailianError(AUTH)` |
| `fetch` 自身失败(DNS/TCP/TLS/proxy) | **内部** | `BailianError(NETWORK)` + 读 `err.cause.code` 给 errno-hint |
| polling 客户端超时 | **内部** | `BailianError(TIMEOUT)` |
| HTTP 4xx/5xx、HTTP 200 + 业务错码、async task FAILED | **服务** | `BailianError(GENERAL)`,**message 原样透传**,不分类、不替换 |
不要扮演服务端错误的翻译官——我们没有最新的错误码体系认知,二次包装只会撒谎(详见 `docs/agents/error-hint-change.md` 中的反面 case)
不要扮演服务端错误的翻译官——我们没有最新的错误码体系认知,二次包装只会撒谎。
### 4. Console Gateway 命令必须声明鉴权域
如果命令调用 Console Gateway,`defineCommand` 必须设置 `auth: "console"`。runtime 会基于 `CONSOLE_AUTH_FLAGS` 自动在 help 中展示 `--console-region``--console-site``--console-switch-agent``--workspace-id`,并由 `authStage` 解析/注入 console credential。命令不要重复声明这些凭证域 flag,也不要手动从 env/config 解析 token。
### 5. 禁止单字母变量命名
所有变量、参数、回调形参必须使用有语义的命名,不允许单字母(如 `i``m``p``t``e``s`)。具体表现:
- 回调参数: `.map((m) => ...)``.map((model) => ...)`, `.find((t) => ...)``.find((template) => ...)`
- catch 变量: `catch (e)``catch (error)`
- for-of 循环: `for (const i of items)``for (const item of items)`
- 临时变量: `const s = ...``const strategy = ...`
例外: 仅当作用域极小(≤3 行)且语义从上下文完全明确时,可使用 `k`/`v`(Object.entries 的 key/value)。
## 完成改动后的快速验证
```sh
vp check # format + lint + type check
vp test # unit + e2e (e2e 需 API key)
vp test # unit + e2e (真实集成需 API key / console token)
```
## 这份指南本身怎么演化
+377 -2
View File
@@ -1,11 +1,386 @@
# Changelog
All notable changes to `bailian-cli` and `bailian-cli-core` are documented here.
All notable changes to the `bailian-cli` packages are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The two packages share a single version number — they are always released together.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The `bailian-cli`, `bailian-cli-core`, `bailian-cli-runtime`, `bailian-cli-commands`, and `knowledge-studio-cli` packages share a single version number.
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.15.1] - 2026-08-17
### Added
- **Model permission management** — `bl permission list` shows per-model inference / fine-tune / deploy grants; `bl permission grant` and `bl permission revoke` manage them, with `--all` to one-key grant inference for every model in the workspace (including future ones).
### Changed
- **`bl quota request` renamed to `bl quota update`** — set per-model QPM/TPM via `--rpm`/`--tpm` and clear custom limits with the new `--delete`; omitted fields keep their current values, and the old `quota request` path keeps working as an alias.
- **`bl quota list` reworked** — now reads the model-limits API and shows per-model and workspace-level request/usage limits plus async queue/concurrency limits in a single table.
- **`bl model list` no longer requires Console login** — the model catalog and `--enrich` parameter-schema endpoints are public.
- **`bl skill init` output simplified** — per-skill status is now `success`/`failed` (previously `installed`) with an aggregate `success`/`partial`/`failed` result; the `publishedAt` and `agents` fields were removed.
## [1.15.0] - 2026-08-14
### Added
- **Responses API for `bl text chat`** — Use `--api responses` to call the DashScope Responses API with streaming, tool definitions, and structured JSON output; Chat Completions remains the default.
- **Subscription plan usage views** — `bl usage token-plan` displays 5-hour and weekly quota usage, while `bl usage coding-plan` displays 5-hour, weekly, and monthly usage; both support text and JSON output.
- **Authentication requirements in command help** — Help output now states whether a command requires an API Key, Console login, or Alibaba Cloud OpenAPI credentials.
### Changed
- **Broader speech-recognition model support** — `bl speech recognize` now routes asynchronous file-transcription and synchronous Flash ASR models to the appropriate DashScope APIs, with clear guidance for unsupported realtime models.
- **MCP transport compatibility** — MCP commands now fall back from Streamable HTTP to classic SSE for compatible Bailian and custom endpoints.
### Fixed
- Binary updates now refresh installed Agent Skills after a successful CLI upgrade.
- Fixed unavailable Token Plan quota values and missing reset times.
- Fixed Qwen3 file-transcription result handling so waiting mode and `--out` work correctly.
- Fixed MCP SSE chunk parsing, header timeouts, abort cleanup, and fallback status matching.
- Network failures in JSON output now preserve the errno value in `cause.code`.
## [1.14.3] - 2026-08-12
### Fixed
- **Free-tier quota compatibility** — `bl usage free` and `bl usage freetier` now use the current Bailian Commerce console APIs for quota queries, activation, and deactivation, with consistent asynchronous-task polling.
## [1.14.2] - 2026-08-07
### Added
- **`bl skill init`** — Install all first-party `bailian-*` skills into detected local AI Agents in one step.
### Changed
- **Skill command interface** — Skill management commands now default to JSON output for Agent workflows; `bl skill add` and `bl skill update` use explicit `--all` and `--name` selectors.
## [1.14.1] - 2026-08-05
### Added
- **Focused Bailian Skills** — `npx skills add modelstudioai/cli --all -g` now installs dedicated skills for media generation, fine-tuning, Managed Agent, and shared execution rules, improving task routing while reducing irrelevant context.
### Changed
- **Default image model upgraded to Qwen-Image 3.0** — image generation, image editing, pipelines, the config UI, and related documentation now default to `qwen-image-3.0` for API Key users.
- **Broader coding-agent compatibility** — Skill installation and updates now detect more coding agents, preserve existing installation links, and automatically backfill skills into newly detected agents.
## [1.14.0] - 2026-08-04
### Added
- **Standalone installation without Node.js** — binary packages are available for macOS on Apple Silicon and Intel, Linux x64, and Windows x64; npm installation remains supported.
- **Exact-version updates** — binary and npm installations can use `bl update --to <version>` to update or switch to a specified version.
### Changed
- **Binary self-updates** — binary installations now check and download updates through a dedicated release channel. `bl update` no longer replaces the running executable, and the next invocation automatically uses the new version.
## [1.13.1] - 2026-08-03
### Changed
- **Default text model upgraded to Qwen3.8-Max** — `bl text chat`, pipelines, API key validation, the config UI, and Managed Agent init templates now default to `qwen3.8-max`; Token Plan also moves from the preview model to the stable release.
## [1.13.0] - 2026-07-30
### Added
- **`bl config ui` Skills / MCP / Agents / Assets inventory** — browse installed skills, MCP servers, coding agents, and generated assets in the local Web UI with click-to-open detail drawers:
- Skills: render `SKILL.md` as Markdown (GFM tables supported), show local vs remote origin badges, and install a skill by uploading a `.zip` archive into any supported agent's skills root.
- MCP: view and edit JSON configuration with secret masking and mask-preserving writes; create, update, and delete MCP entries across Claude Code, Qwen Code, OpenCode, Cursor, Windsurf, Gemini, Qoder Work, OpenClaw, and Claude Desktop.
- Agents: quick-launch coding agents directly from the UI (gated on the CLI binary being on PATH).
- Assets: categorized, time-sorted browser with preview, open-locally, and delete.
- **Model catalog suggestion chips** — per-category model names surfaced as click-to-fill chips under each `default_*_model` field in the config UI.
- **Profiles tile grid** — profiles displayed as a tile grid with an add-tile and a design-consistent new-profile modal.
### Changed
- Config UI layout: collapsible grouped sidebar with icons and persistent state, responsive breakpoint, wider main area, sticky view headers, and right-side drawers for editing.
### Fixed
- Symlinked skill directories are now correctly identified as an installed source.
- Config file detection now supports environment-variable-based paths and legacy configuration schemes.
## [1.12.0] - 2026-07-28
### Added
- **`bl config agent --key` / `--region`** — run commands generated by the Model Studio web console as-is: `--key` accepts the console's encoded API key and decodes it locally (use instead of `--api-key`), and `--region` derives the Token Plan endpoint from a region name (use instead of `--base-url`).
- **`bl config agent --context-window`** — set the context window written to the OpenClaw configuration (default 256000).
- **`bl config agent --wire-api`** — choose the wire protocol written to the Codex configuration; `chat` is kept for legacy Codex 0.80.0 and earlier (a warning is shown).
### Changed
- `bl config agent` for Codex now writes `wire_api = "responses"` by default, matching current Codex releases that no longer accept `chat`.
- `bl config agent` for Qwen Code now writes the `DASHSCOPE_API_KEY` environment variable instead of `BAILIAN_CLI_API_KEY`.
### Fixed
- `bl config agent` configurations now match each agent's official format: Claude Code honors `CLAUDE_CONFIG_DIR` and removes a stale `ANTHROPIC_API_KEY`; Qwen Code uses the v3 settings schema and writes credentials so a system-level `OPENAI_API_KEY` no longer takes precedence; OpenCode accepts JSONC config files (comments and trailing commas); OpenClaw registers the primary model in the model allowlist with complete cost metadata; Hermes uses the official flat `model.*` layout; Codex writes the official `env_key` with an `auth.json` fallback.
- `bl config agent` now preserves existing user configuration when writing: it merges instead of overwriting, avoids duplicate provider entries, and keeps custom display names.
## [1.11.2] - 2026-07-28
### Changed
- MCP tools and WebSearch now provide activation guidance and direct marketplace links when Bailian reports that the corresponding service is not activated. WebSearch also guides users with legacy SSE connections to reactivate the service using Streamable HTTP.
### Fixed
- Fixed text chat and API Key validation compatibility failures caused by sending unsupported `enable_thinking` values. Text chat now sends the parameter only when thinking is explicitly enabled, while validation uses a compatible model without sending it.
## [1.11.1] - 2026-07-28
### Added
- `bl image edit` now supports `--function` for specifying edit operations with Wanx image-edit models such as `wanx2.1-imageedit`.
### Fixed
- Fixed image generation and editing failures and incorrect size parameters for some image models, improving compatibility with Qwen-Image, Wan/Wanx, Z-Image, and dated `wanx-v1` variants.
## [1.11.0] - 2026-07-28
### Added
- **`bl managed-agent`** — declaratively manage Managed Agent infrastructure through a unified CLI. The Bailian provider connects to AgentStudio, with Claude, Qoder, and Ark providers also supported:
- `init` / `validate` / `plan` / `apply` / `destroy` — initialize and validate `agents.yaml`, preview and apply resource changes, and destroy managed resources.
- `state list` / `state show` / `state rm` / `state import` — inspect and manage local resource state, including adopting an existing remote resource or removing it from local state without destroying it remotely.
- `session create` / `session list` / `session get` / `session delete` / `session run` / `session send` / `session events` — manage the full session lifecycle with streaming responses and structured `--output json` output.
- `skill-list` — browse custom and official skills; use `--source all` to return both catalogs in one call.
### Changed
- Model Base URLs are now normalized to the URL origin; paths, query parameters, and fragments supplied in the Base URL are no longer included when constructing API request paths.
### Fixed
- The installation guide no longer recommends the removed `--non-interactive` flag and now documents explicit required arguments, `--output json`, and `NO_COLOR=1` for non-interactive environments.
## [1.10.1] - 2026-07-22
### Changed
- Token Plan defaults now use the current text, image, and dedicated text-to-video, image-to-video, and reference-to-video models.
- The Bailian CLI Skill now distinguishes Bailian-specific tasks from ordinary host-agent work more accurately and avoids repeated consent prompts within an approved workflow.
- Published CLI packages now support Node.js 18.17 and later, lowering the previous minimum requirement from Node.js 22.12.
### Fixed
- Token Plan now handles local images correctly for image editing, image-to-video, reference-to-video, and vision understanding without requiring a separately hosted URL.
## [1.10.0] - 2026-07-19
### Added
- **`bl config agent`** — configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope in one command.
### Changed
- The Bailian CLI Skill now routes only matching Bailian and multimodal tasks to `bl`, and asks for consent before provider-neutral remote or billable calls.
### Fixed
- Full `bl auth logout` now clears the model Base URL so later logins cannot inherit a stale custom or Token Plan endpoint.
## [1.9.0] - 2026-07-17
### Added
- **Token Plan support** — log in and call supported models directly without manually configuring the endpoint.
- **Named Config Profiles** — create, switch, and manage isolated configurations; logging in to a named Profile activates it automatically.
- **Console Access Token automation** — generate and automatically refresh Console Access Tokens.
- **`bl workspace init`** — initialize a Bailian workspace and activate the required services in one workflow.
### Fixed
- Improved configuration safety and consistency, including secret masking and preservation of custom configuration fields.
## [1.8.3] - 2026-07-16
### Fixed
- Fixed `bl text chat --messages-file -` failing on Windows by treating standard input as a `/dev/stdin` file path; piped JSON messages are now read from standard input correctly. (#103)
## [1.8.2] - 2026-07-15
### Changed
- `bl model list` now defaults to JSON output; pass `--output text` for the table view.
### Fixed
- `bl model list --enrich` now returns each model's input parameter schema (predictConfig); it was previously always empty because the console gateway response envelope was not unwrapped.
## [1.8.1] - 2026-07-14
### Changed
- Expanded the Command Pack allowlist to accept an additional internal command extension.
## [1.8.0] - 2026-07-13
### Added
- **`bl model list`** — browse the Bailian model marketplace: list model families or show full details for a single family (`--model`), with filters for provider, capability, feature, and context-window, pagination (`--page` / `--page-size`), pricing, and `--enrich` for richer metadata.
- **`bl usage summary`** — a unified usage view combining free-tier quota and a recent usage overview; `--days` sets the overview window (default 7).
- **Command Pack host support** — added support for allowlisted internal command extensions.
- **Audio & image fine-tuning** — `bl finetune audio create` (CosyVoice TTS) and `bl finetune image create` (Wan image generation) join the existing text flow. `bl finetune image create` supports `--generation-type t2i|i2i` to select text-to-image or image-to-image training.
- **Audio & image deployment** — `bl deploy audio create` and `bl deploy image create` deploy fine-tuned TTS and image models as endpoints.
- **Multimodal dataset validation** — `bl dataset upload` and `bl dataset validate` now accept `.zip` archives with `tts` and `image` schemas, validate referenced media files, and allow image archives up to 1 GB.
### Changed
- **Fine-tune and deploy commands are now split by modality (BREAKING)**: `bl finetune create``bl finetune text create`, and `bl deploy create``bl deploy text create`. Update any scripts that use the old paths.
- **Deployment option renamed (BREAKING)**: `--template-id``--deploy-spec` on deployment creation commands.
- **Fine-tune status exit behavior changed (BREAKING)**: `bl finetune watch` no longer reserves exit code 3 for running jobs. Running and succeeded jobs return 0; failed and canceled jobs use normal CLI errors.
- `bl deploy audio create` now defaults to `--plan mu` (model-unit billing, per the CosyVoice deployment contract); text and image continue to default to `lora`.
- `bl finetune audio create` now validates CosyVoice training data: audio files must be `.wav`, each `wav_fn` must start with `train/`, and exactly one training file is accepted.
- `bl quota list` and `bl quota check` now report real RPM/TPM usage against limits, adding `RPM Left` / `TPM Left` columns with remaining-quota progress bars sourced from monitoring data.
- `bl usage free` output now shares its rendering with `bl usage summary` for consistent free-tier tables.
- `bl advisor recommend` no longer depends on a dedicated intent-detection model to analyze your request.
### Removed
- **Removed the `tongyi-intent-detect-v3` integration (BREAKING)** used by `bl advisor recommend`, along with the `intent_detect_base_url` config field and the `DASHSCOPE_INTENT_DETECT_BASE_URL` environment variable.
### Fixed
- Skill command-reference generation now reads product command maps directly from source and produces stable formatting during release checks.
## [1.7.0] - 2026-07-09
### Added
- `bl auth login --open-api` now stores Alibaba Cloud OpenAPI AK/SK credentials for Token Plan commands; `bl auth status` reports API key, console, and OpenAPI credential state separately, and `bl auth logout --open-api` clears only OpenAPI credentials.
- `kscli` help and examples now render as Knowledge Studio paths such as `kscli search`, `kscli chat`, and `kscli retrieve`, matching the standalone CLI.
### Changed
- Token Plan commands now use the shared OpenAPI AK/SK credential flow, including persisted credentials and `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` environment variables.
- Auth flags are now scoped to the commands that can use them. Passing model, console, or OpenAPI credential flags to the wrong command now reports an unknown flag instead of being accepted and ignored.
- Help and command reference output now show only the flags that apply to each command's auth mode, making model, console, and OpenAPI credentials easier to distinguish.
- Missing required flags now return usage errors with exit code 2 instead of opening interactive prompts or printing help with exit code 0.
- Image, video, and speech task commands now use `--async` consistently for returning task IDs without waiting; `--concurrent` is shown only on commands that support parallel requests.
- Default command output is text unless `--output json`, `DASHSCOPE_OUTPUT=json`, or config explicitly requests JSON.
- Update checks are throttled to once per day and can surface in non-TTY/agent runs.
- Proxy setup now reads uppercase `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` only; lowercase proxy environment variables are ignored.
- `bl auth login` no longer prints the onboarding quick start block after a successful login.
### Removed
- Deprecated AK/SK authentication for `bl knowledge retrieve`; use DashScope API key auth for knowledge commands.
- Removed `--no-color`, `--non-interactive`, and `--no-wait`. Use `NO_COLOR=1` for plain output and `--async` for task submission without waiting.
- Removed `--yes` and interactive confirmation prompts from delete/logout commands; use `--dry-run` to preview before running destructive operations.
### Fixed
- Credential-gated `--dry-run` paths now skip auth preflight so commands such as Token Plan can print request details without configured credentials.
- `--verbose` model requests again print request method, URL, auth source, and response status details.
## [1.6.1] - 2026-07-03
### Changed
- `bl vision describe` examples and skill reference now use `qwen3-vl-plus` instead of the legacy `qwen-vl-plus` model id, matching the command's default model.
## [1.6.0] - 2026-07-02
### Added
- `bl knowledge search` — semantic search across knowledge bases using the new workspace-based RAG API. Supports `--query`, `--agent-id`, `--workspace-id`, `--image` (multimodal retrieval, repeatable), and `--query-history` (JSON conversation context for multi-turn query rewriting).
- `bl knowledge chat` — knowledge-base Q&A with SSE streaming. Supports `--message` (repeatable, with `role:content` prefix for multi-turn history), `--agent-id`, `--workspace-id`, and `--image` (multimodal). Displays real-time progress with step-change labels (retrieval, planning, generation) in interactive mode.
- `bailian-cli-core` gains new types and endpoints for the workspace-based knowledge API: `KnowledgeSearchRequest` / `KnowledgeSearchResponse`, `KnowledgeChatRequest` / `KnowledgeChatStreamChunk` / `KnowledgeChatMessage` / `KnowledgeChatContentPart`, and `knowledgeSearchEndpoint` / `knowledgeChatEndpoint`.
- `kscli` now ships `search` and `chat` commands alongside the existing `retrieve`.
### Changed
- `bl knowledge retrieve` is now marked as deprecated in its description; use `bl knowledge search` instead.
- `kscli` README (EN + ZH) updated to feature `search` and `chat` as the primary commands, with `retrieve` marked deprecated.
## [1.5.0] - 2026-07-01
### Added
- Model fine-tuning — `bl finetune`: create, list, get, watch, and cancel jobs; fetch training logs; list checkpoints; export a checkpoint as a deployable model; and query training capability (by model or by training type). Supports `sft`, `sft-lora`, `dpo`, `dpo-lora`, and `cpt` training types.
- Model deployment — `bl deploy`: create, list, get, update (rate limits), scale, and delete deployments; list deployable models and plans.
- Dataset management — `bl dataset`: upload, list, get, and delete dataset files, plus `bl dataset validate` to check a local `.jsonl` before uploading (ChatML / DPO / CPT formats).
- Token Plan management — `bl token-plan`: list subscription seats, add members, batch-assign seats, and create a per-seat API key.
- Automatic update check: after a command finishes, the CLI checks npm for a newer release (throttled) and shows an `Update available` hint; a major stable-version gap upgrades itself automatically. Skipped with `--quiet` or when running `bl update`.
- Composable packages: `bailian-cli-runtime` (CLI framework) and `bailian-cli-commands` (command library) are now published alongside `bailian-cli-core`, and a new sibling CLI `knowledge-studio-cli` (`kscli`) ships on top of them. `bl` behavior is unchanged.
### Removed
- `bl config export-schema` (exported CLI commands as Anthropic/OpenAI-compatible JSON tool schemas) has been removed.
### Fixed
- Console gateway commands (`bl console call`, etc.) now surface a readable message when the gateway returns a non-string `errorCode`, instead of `[object Object]`.
## [1.4.2] - 2026-06-24
### Added
- `bl omni --list-voices` prints the built-in output voices (ID, name, description, language) and exits without needing an API key. The built-in voice table is expanded from 6 to 17 voices, including dialect voices such as Dylan, Sunny, and Kiki.
### Changed
- `bl omni` default `--voice` is now `Tina` (previously `Cherry`). The `--voice` help points at `--list-voices` instead of listing every option inline.
- `bl speech synthesize --list-voices` and its missing-`--voice` hint now include a link to the official CosyVoice voice documentation.
- Agent skill setup guidance now covers console site selection (`--console-site domestic` / `international`) for console login and gateway commands.
### Fixed
- `bl speech synthesize` corrects the `cosyvoice-v3-flash` built-in voice ID from `longanhuan` to `longanhuan_v3`.
## [1.4.1] - 2026-06-22
### Changed
- Video generation now defaults to the upgraded HappyHorse 1.1 model for better quality. The 1.0 models are still available via `--model`.
- `bl update` now keeps the agent skill in sync across all your agent apps (Claude Code, Cursor, etc.), and refreshes it even when the CLI is already up to date.
## [1.4.0] - 2026-06-17
### Added
- Console gateway now supports multiple regions and sites: `cn-beijing` and `ap-southeast-1`, each with domestic and international variants, plus `switchAgent` for delegated access.
- New global flags `--console-region`, `--console-site`, and `--console-switch-agent`; `bl console call` also gains `--site` and `--switch-agent`.
- `bl auth login --base-url <url>` to specify the base URL when logging in with an API key.
- `bl omni` gains a `--voice` option (Chelsie, Cherry, Ethan, Serena, Sunny, Tina; default Cherry).
### Changed
- All user-facing CLI text is now standardized to English.
- `bl advisor recommend` internal intent/ranking model upgraded from `qwen-turbo` to `qwen-flash`.
- Cleaner JSON output for `usage`, `quota`, and `workspace` commands.
- `base_url` from the config file now takes priority over the `DASHSCOPE_BASE_URL` environment variable.
- `bl config show` now displays all fields from `config.json`, with sensitive values masked.
### Removed
- The legacy `region` config field and its related options.
- Invalid leftover code for the removed `model list` command.
### Fixed
- When the console session is not logged in or has expired, the CLI now shows a clear sign-in prompt instead of a generic gateway error.
- Corrected `--resolution` / `--ratio` / `--duration` flag descriptions for `bl video` commands.
## [1.3.3] - 2026-06-16
### Changed
- `bl knowledge retrieve --help` now clearly indicates that `--api-key` is the recommended authentication method; AK/SK flags are explicitly marked as deprecated with guidance to use `--api-key` instead.
### Added
- `notes` field for command definitions — commands can now include contextual notes (auth requirements, deprecation notices, etc.) that are displayed in both `--help` output and the generated reference docs.
## [1.3.2] - 2026-06-12
### Fixed
+377 -2
View File
@@ -1,11 +1,386 @@
# 更新日志
`bailian-cli` `bailian-cli-core` 的所有重要变更都记录在此。
`bailian-cli` 系列包的所有重要变更都记录在此。
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。两个包共享一个版本号,总是一起发布
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。`bailian-cli``bailian-cli-core``bailian-cli-runtime``bailian-cli-commands``knowledge-studio-cli` 共享一个版本号
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.15.1] - 2026-08-17
### 新增
- **模型权限管理** —— `bl permission list` 查看各模型的推理 / 微调 / 部署授权;`bl permission grant``bl permission revoke` 负责授予和回收,支持 `--all` 一键为工作区全部模型(含后续新增模型)开启推理授权。
### 变更
- **`bl quota request` 更名为 `bl quota update`** —— 通过 `--rpm`/`--tpm` 设置单模型 QPM/TPM新增 `--delete` 一键清除自定义限制;未指定的字段保持当前值,旧命令 `quota request` 仍作为别名可用。
- **`bl quota list` 重构** —— 改从模型限制接口读取数据,单表展示模型级与工作区级的请求/用量限制及异步队列/并发限制。
- **`bl model list` 不再需要控制台登录** —— 模型目录与 `--enrich` 参数结构端点均为公开接口。
- **`bl skill init` 输出精简** —— 单技能状态改为 `success`/`failed`(原为 `installed`),新增 `success`/`partial`/`failed` 汇总结果;移除 `publishedAt``agents` 字段。
## [1.15.0] - 2026-08-14
### 新增
- **`bl text chat` 支持 Responses API** —— 可通过 `--api responses` 调用 DashScope Responses API支持流式输出、工具定义和结构化 JSON 输出;默认仍使用 Chat Completions。
- **订阅套餐用量视图** —— `bl usage token-plan` 支持查看 5 小时和每周额度,`bl usage coding-plan` 支持查看 5 小时、每周和每月额度;两者均提供文本与 JSON 输出。
- **命令帮助展示鉴权要求** —— Help 输出现在会明确标注命令需要 API Key、控制台登录还是阿里云 OpenAPI 凭证。
### 变更
- **扩展语音识别模型支持** —— `bl speech recognize` 现在会将异步文件转写和同步 Flash ASR 模型路由至对应的 DashScope API并为暂不支持的实时模型提供明确提示。
- **增强 MCP 传输兼容性** —— MCP 命令现在可为兼容的百炼及自定义端点从 Streamable HTTP 自动回退至经典 SSE。
### 修复
- 二进制方式升级 CLI 成功后,现在会同步刷新已安装的 Agent Skills。
- 修复 Token Plan 额度不可用或缺少重置时间时的展示问题。
- 修复 Qwen3 文件转写结果处理,使等待模式和 `--out` 能够正常工作。
- 修复 MCP SSE 分块解析、响应头超时、中止清理和回退状态匹配问题。
- JSON 输出中的网络错误现在会在 `cause.code` 中保留 errno。
## [1.14.3] - 2026-08-12
### 修复
- **免费额度兼容性** —— `bl usage free``bl usage freetier` 现在使用最新的 Bailian Commerce 控制台 API 查询、开通和关闭免费额度,并统一处理异步任务轮询。
## [1.14.2] - 2026-08-07
### 新增
- **`bl skill init`** —— 一次性将全部官方 `bailian-*` Skill 安装到本机检测到的 AI Agent。
### 变更
- **Skill 命令接口** —— Skill 管理命令现在默认输出适合 Agent 工作流的 JSON`bl skill add``bl skill update` 使用明确的 `--all``--name` 选择参数。
## [1.14.1] - 2026-08-05
### 新增
- **百炼 Skill 按领域拆分** —— 通过 `npx skills add modelstudioai/cli --all -g` 可统一安装图片与视频生成、模型微调、Managed Agent 和共享执行协议等专用 Skill提升任务路由准确性并减少无关上下文。
### 变更
- **默认图片模型升级至 Qwen-Image 3.0** —— 普通 API Key 用户的图片生成、图片编辑、Pipeline、配置 UI 和相关文档现在默认使用 `qwen-image-3.0`
- **扩展 Coding Agent 兼容范围** —— Skill 安装与更新现在能够识别更多 Coding Agent保留已有安装链接并自动将 Skill 补充到新识别的 Agent。
## [1.14.0] - 2026-08-04
### 新增
- **免 Node.js 的二进制安装** — 支持 macOS Apple Silicon / Intel、Linux x64 和 Windows x64npm 安装方式继续保留。
- **指定版本更新** — 二进制和 npm 安装均可通过 `bl update --to <version>` 更新或切换到指定版本。
### 变更
- **二进制自更新** — 二进制安装现在通过独立的发布通道检查和下载更新;执行 `bl update` 时不会覆盖正在运行的程序,下次运行自动使用新版本。
## [1.13.1] - 2026-08-03
### 变更
- **默认文本模型升级至 Qwen3.8-Max** — `bl text chat`、Pipeline、API Key 登录校验、配置 UI 和 Managed Agent 初始化模板现在默认使用 `qwen3.8-max`Token Plan 也由预览版切换至正式版。
## [1.13.0] - 2026-07-30
### 新增
- **`bl config ui` 技能 / MCP / 代理 / 资产清单** — 在本地 Web UI 中浏览已安装的技能、MCP 服务器、编码代理和生成的资产,点击打开右侧详情抽屉:
- 技能:将 `SKILL.md` 渲染为 Markdown支持 GFM 表格),展示本地/远程来源徽章,支持上传 `.zip` 压缩包将技能安装到任意受支持代理的技能目录。
- MCP查看和编辑 JSON 配置,支持密钥掩码与掩码保真写回;支持在 Claude Code、Qwen Code、OpenCode、Cursor、Windsurf、Gemini、Qoder Work、OpenClaw 和 Claude Desktop 中创建、更新、删除 MCP 条目。
- 代理:从 UI 一键启动编码代理(需对应 CLI 二进制在 PATH 中)。
- 资产:按类别分组、按时间排序的浏览器,支持预览、本地打开和删除。
- **模型目录建议芯片** — 在配置 UI 的每个 `default_*_model` 字段下方展示按类别分组的模型名称,点击即可填入。
- **Profile 磁贴网格** — 配置文件以磁贴网格展示,新增添加磁贴和设计一致的新建 Profile 弹窗。
### 变更
- 配置 UI 布局:可折叠分组侧边栏(带图标和持久化状态)、响应式断点、更宽的主区域、吸顶视图标题、右侧抽屉式编辑。
### 修复
- 修复软链接技能目录未被正确识别为已安装来源的问题。
- 配置文件检测现支持基于环境变量的路径和旧版配置方案。
## [1.12.0] - 2026-07-28
### 新增
- **`bl config agent --key` / `--region`** —— 百炼控制台生成的命令可直接运行:`--key` 接收控制台编码后的 API Key 并在本地解码(与 `--api-key` 二选一);`--region` 根据地域名自动派生 Token Plan 接入地址(与 `--base-url` 二选一)。
- **`bl config agent --context-window`** —— 设置写入 OpenClaw 配置的上下文窗口大小(默认 256000
- **`bl config agent --wire-api`** —— 选择写入 Codex 配置的通信协议;`chat` 仅保留给 Codex 0.80.0 及更早版本(会显示警告)。
### 变更
- `bl config agent` 配置 Codex 时默认写入 `wire_api = "responses"`,以适配已不再支持 `chat` 的新版 Codex。
- `bl config agent` 配置 Qwen Code 时改用 `DASHSCOPE_API_KEY` 环境变量,不再使用 `BAILIAN_CLI_API_KEY`
### 修复
- `bl config agent` 写入的配置现已与各 Agent 官方格式对齐Claude Code 尊重 `CLAUDE_CONFIG_DIR` 并清理残留的 `ANTHROPIC_API_KEY`Qwen Code 采用 v3 配置 schema 并正确写入凭证,避免被系统级 `OPENAI_API_KEY` 干扰OpenCode 支持带注释和尾部逗号的 JSONC 配置文件OpenClaw 会将主模型注册进模型白名单并补齐计费元数据Hermes 改用官方扁平 `model.*` 结构Codex 写入官方 `env_key` 并支持 `auth.json` 兜底。
- `bl config agent` 写入配置时现会保留用户已有配置:合并而非覆盖,避免重复添加 provider 条目,并保留用户自定义的显示名。
## [1.11.2] - 2026-07-28
### 变更
- MCP 工具或 WebSearch 因对应服务未开通而不可用时CLI 现在会提供开通指引和市场直达链接;对于使用旧版 SSE 连接的 WebSearch还会提示重新开通以切换至 Streamable HTTP。
### 修复
- 修复文本对话与 API Key 登录校验因传递不受支持的 `enable_thinking` 参数值而产生的兼容性错误。文本对话仅在用户明确开启思考模式时传递该参数,登录校验则改用兼容模型且不再传递该参数。
## [1.11.1] - 2026-07-28
### 新增
- `bl image edit` 新增 `--function` 参数,支持为万相图片编辑模型(如 `wanx2.1-imageedit`)指定编辑功能。
### 修复
- 修复部分图片模型在图片生成与编辑时的调用失败和尺寸参数错误,并完善 Qwen-Image、Wan/Wanx、Z-Image 系列及 `wanx-v1` 日期版本的兼容性。
## [1.11.0] - 2026-07-28
### 新增
- **`bl managed-agent`** —— 通过统一 CLI 声明式管理 Managed Agent 基础设施;百炼 Provider 对接 AgentStudio并支持 Claude、Qoder 和 Ark
- `init` / `validate` / `plan` / `apply` / `destroy` —— 基于 `agents.yaml` 初始化、校验、预览和执行资源变更,以及销毁已托管资源。
- `state list` / `state show` / `state rm` / `state import` —— 查看和管理本地资源状态,包括纳管已有远端资源或仅解除本地跟踪。
- `session create` / `session list` / `session get` / `session delete` / `session run` / `session send` / `session events` —— 完整的会话生命周期操作,支持流式响应和结构化的 `--output json` 输出。
- `skill-list` —— 浏览自定义与官方 Skill使用 `--source all` 可一次返回两个来源。
### 变更
- 模型 Base URL 现在统一仅保留 URL Origin传入的路径、查询参数和 Fragment 不再参与后续 API 请求路径拼接。
### 修复
- 安装指南不再推荐已移除的 `--non-interactive`,改为说明显式传入必填参数,并使用 `--output json``NO_COLOR=1` 适配非交互环境。
## [1.10.1] - 2026-07-22
### 变更
- Token Plan 默认模型已更新为当前文本、图片,以及文生视频、图生视频和参考生视频的专用模型。
- 百炼 CLI Skill 现在能更准确地区分百炼专属任务与普通宿主 Agent 任务,并避免在已授权的工作流中重复征求同意。
- 已发布的 CLI 包现在支持 Node.js 18.17 及以上版本,最低版本要求由 Node.js 22.12 下调至 18.17。
### 修复
- Token Plan 现在能在图片编辑、图生视频、参考生视频和视觉理解中正确处理本地图片,无需另行托管为 URL。
## [1.10.0] - 2026-07-19
### 新增
- **`bl config agent`** —— 一键配置 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 和 Codex 接入百炼模型服务。
### 变更
- 百炼 CLI Skill 现在只将匹配的百炼任务与多模态任务路由到 `bl`,并会在调用与平台无关的远程或计费能力前征求同意。
### 修复
- 完整执行 `bl auth logout` 时会同时清除模型 Base URL避免后续登录继承失效的自定义或 Token Plan 接入地址。
## [1.9.0] - 2026-07-17
### 新增
- **支持 Token Plan** —— 登录后即可直接调用支持的模型,无需手动配置接入地址。
- **命名 Config Profile** —— 支持创建、切换和管理相互隔离的配置,登录后会自动激活当前 Profile。
- **Console Access Token 自动化** —— 支持生成并自动刷新 Console Access Token。
- **`bl workspace init`** —— 一站式完成百炼工作空间初始化和所需服务开通。
### 修复
- 提升配置安全性与一致性,包括密钥脱敏和自定义配置字段保留。
## [1.8.3] - 2026-07-16
### 修复
- 修复 Windows 上 `bl text chat --messages-file -` 将标准输入当作 `/dev/stdin` 文件路径读取的问题;通过管道传入的 JSON 消息现在可以从标准输入正常读取。(#103
## [1.8.2] - 2026-07-15
### 变更
- `bl model list` 现在默认以 JSON 输出;需要表格视图请传 `--output text`
### 修复
- `bl model list --enrich` 现在能正确返回每个模型的输入参数 schemapredictConfig此前因未解包控制台网关响应信封而始终为空。
## [1.8.1] - 2026-07-14
### 变更
- 扩展 Command Pack 白名单,允许加载额外的内部命令扩展。
## [1.8.0] - 2026-07-13
### 新增
- **`bl model list`** —— 浏览百炼模型市场:列出模型家族,或用 `--model` 查看单个家族的完整详情;支持按 provider、能力、特性、上下文窗口过滤分页`--page` / `--page-size`)、价格展示,以及 `--enrich` 获取更丰富的元数据。
- **`bl usage summary`** —— 统一用量视图,一屏合并免费额度与近期用量概览;`--days` 设置概览时间窗口(默认 7 天)。
- **Command Pack 宿主支持** —— 新增面向白名单内部命令扩展包的加载能力。
- **音频与图像精调** —— 在原有文本流程之外新增 `bl finetune audio create`CosyVoice 语音合成)与 `bl finetune image create`(万相图像生成)。`bl finetune image create` 支持 `--generation-type t2i|i2i` 显式选择文生图或图生图训练。
- **音频与图像部署** —— `bl deploy audio create``bl deploy image create` 可将精调后的语音合成与图像模型部署为推理接入点。
- **多模态数据集校验** —— `bl dataset upload``bl dataset validate` 现在支持使用 `tts``image` schema 的 `.zip` 压缩包,可校验包内引用的媒体文件,图像数据压缩包上限提升至 1 GB。
### 变更
- **精调与部署命令按模态拆分BREAKING**`bl finetune create``bl finetune text create``bl deploy create``bl deploy text create`。请更新使用旧路径的脚本。
- **部署参数重命名BREAKING**:部署创建命令的 `--template-id` 更名为 `--deploy-spec`
- **精调状态退出行为变更BREAKING**`bl finetune watch` 不再使用退出码 3 表示任务运行中;运行中与成功均返回 0失败与取消使用 CLI 的常规错误流程。
- `bl deploy audio create` 默认使用 `--plan mu`(按模型单元计费,符合 CosyVoice 部署契约);文本与图像仍默认 `lora`
- `bl finetune audio create` 现在会校验 CosyVoice 训练数据:音频必须为 `.wav`,每条 `wav_fn` 必须以 `train/` 开头,且只接受一个训练文件。
- `bl quota list``bl quota check` 现在会基于监控数据展示真实的 RPM/TPM 用量与限额,新增 `RPM Left` / `TPM Left` 列及剩余额度进度条。
- `bl usage free` 的输出现在与 `bl usage summary` 共用渲染逻辑,免费额度表格更一致。
- `bl advisor recommend` 不再依赖独立的意图识别模型来分析你的需求。
### 已移除
- **移除 `bl advisor recommend` 使用的 `tongyi-intent-detect-v3` 集成BREAKING**,同时移除 `intent_detect_base_url` 配置字段与 `DASHSCOPE_INTENT_DETECT_BASE_URL` 环境变量。
### 修复
- Skill 命令参考文档生成现在直接读取产品命令源码,并在发布检查中保持稳定格式。
## [1.7.0] - 2026-07-09
### 新增
- `bl auth login --open-api` 现在可以保存阿里云 OpenAPI AK/SK 凭据,供 Token Plan 命令使用;`bl auth status` 会分别展示 API Key、控制台和 OpenAPI 凭据状态,`bl auth logout --open-api` 可只清除 OpenAPI 凭据。
- `kscli` 的 help 与示例现在展示为 `kscli search``kscli chat``kscli retrieve` 等 Knowledge Studio 独立入口路径。
### 变更
- Token Plan 命令统一使用 OpenAPI AK/SK 凭据流程,支持登录持久化凭据和 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` 环境变量。
- 鉴权 flag 现在只对可使用它们的命令生效。把模型、控制台或 OpenAPI 凭据 flag 传给错误的命令时,现在会报 unknown flag而不是接受后忽略。
- help 与命令参考现在只展示当前命令鉴权域适用的 flag更容易区分模型、控制台和 OpenAPI 凭据。
- 缺少必填 flag 时现在返回用法错误并以退出码 2 退出,不再进入交互式补全或打印 help 后以退出码 0 退出。
- 图片、视频、语音任务类命令现在统一用 `--async` 表示提交任务后不等待;`--concurrent` 只在支持并发请求的命令上展示。
- 命令默认输出为文本;仅在显式设置 `--output json``DASHSCOPE_OUTPUT=json` 或配置文件要求 JSON 时输出 JSON。
- 更新检查节流调整为每天一次,并可在非 TTY / agent 场景展示更新提示。
- 代理配置现在只读取大写 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`,忽略小写代理环境变量。
- `bl auth login` 成功后不再额外打印 onboarding quick start 内容。
### 已移除
- 移除 `bl knowledge retrieve` 已废弃的 AK/SK 鉴权;知识库命令请使用 DashScope API Key。
- 移除 `--no-color``--non-interactive``--no-wait`。纯文本输出使用 `NO_COLOR=1`,提交任务后不等待使用 `--async`
- 移除删除 / 登出类命令的 `--yes` 与交互式确认提示;执行破坏性操作前请用 `--dry-run` 预览。
### 修复
- 需要凭据的 `--dry-run` 路径现在会跳过鉴权前置检查,例如 Token Plan 可在未配置凭据时先打印请求信息。
- `--verbose` 的模型请求日志恢复输出请求方法、URL、鉴权来源与响应状态等信息。
## [1.6.1] - 2026-07-03
### 变更
- `bl vision describe` 的示例与 skill 参考文档中的模型 id 由旧版 `qwen-vl-plus` 更新为 `qwen3-vl-plus`,与命令默认模型保持一致。
## [1.6.0] - 2026-07-02
### 新增
- `bl knowledge search` — 基于新版 workspace RAG API 的知识库语义检索。支持 `--query``--agent-id``--workspace-id``--image`(多模态检索,可重复)和 `--query-history`(多轮对话上下文 JSON用于查询重写
- `bl knowledge chat` — 知识库 SSE 流式问答。支持 `--message`(可重复,支持 `角色:内容` 前缀传入多轮历史)、`--agent-id``--workspace-id``--image`(多模态)。交互模式下实时展示检索、规划、生成等步骤进度。
- `bailian-cli-core` 新增 workspace 级知识 API 类型与端点:`KnowledgeSearchRequest` / `KnowledgeSearchResponse``KnowledgeChatRequest` / `KnowledgeChatStreamChunk` / `KnowledgeChatMessage` / `KnowledgeChatContentPart`,以及 `knowledgeSearchEndpoint` / `knowledgeChatEndpoint`
- `kscli` 现已包含 `search``chat` 命令。
### 变更
- `bl knowledge retrieve` 描述中已标记为废弃,请改用 `bl knowledge search`
- `kscli` README中英文更新`search``chat` 为主推命令,`retrieve` 标记为废弃。
## [1.5.0] - 2026-07-01
### 新增
- 模型精调 —— `bl finetune`:创建、列出、查询、观察、取消训练任务;拉取训练日志;列出 checkpoint;将 checkpoint 导出为可部署模型;查询训练能力(按模型或按训练类型)。支持 `sft``sft-lora``dpo``dpo-lora``cpt` 训练类型。
- 模型部署 —— `bl deploy`:创建、列出、查询、更新(限流)、扩缩容、删除部署;列出可部署模型与套餐。
- 数据集管理 —— `bl dataset`:上传、列出、查询、删除数据集文件,并新增 `bl dataset validate` 在上传前本地校验 `.jsonl`(ChatML / DPO / CPT 格式)。
- Token Plan 管理 —— `bl token-plan`:列出订阅座位、添加成员、批量分配座位、为座位创建 API Key。
- 自动更新检查:命令执行完成后,CLI 会(节流地)检查 npm 上是否有新版本并提示 `Update available`;若与稳定版存在大版本差距则自动升级。`--quiet` 或执行 `bl update` 时跳过。
- 可组合包:`bailian-cli-runtime`(CLI 框架)与 `bailian-cli-commands`(命令库)现在与 `bailian-cli-core` 一起发布,并在其之上新增了同家族 CLI `knowledge-studio-cli`(`kscli`)。`bl` 行为保持不变。
### 已移除
- 移除 `bl config export-schema` 命令(原用于把 CLI 命令导出为 Anthropic/OpenAI 兼容的 JSON tool schema)。
### 修复
- 控制台网关类命令(`bl console call` 等)在网关返回非字符串 `errorCode` 时,现在会给出可读的错误信息,而不是 `[object Object]`
## [1.4.2] - 2026-06-24
### 新增
- `bl omni --list-voices` 无需 API key 即可打印内置输出音色列表(ID、名称、描述、语言)并退出。内置音色表从 6 个扩展到 17 个,新增 Dylan、Sunny、Kiki 等方言音色。
### 变更
- `bl omni` 默认 `--voice` 改为 `Tina`(原为 `Cherry`)。`--voice` 帮助文案改为指向 `--list-voices`,不再内联列出全部音色。
- `bl speech synthesize --list-voices` 输出及缺少 `--voice` 时的提示中,新增官方 CosyVoice 音色文档链接。
- Agent skill 配置指引新增 console 站点选择说明(`--console-site domestic` / `international`),适用于 console 登录与网关类命令。
### 修复
- `bl speech synthesize` 修正 `cosyvoice-v3-flash` 内置音色 ID,由 `longanhuan` 改为 `longanhuan_v3`
## [1.4.1] - 2026-06-22
### 变更
- 视频生成默认升级到 HappyHorse 1.1 模型,画面质量更佳。如需使用 1.0 模型,可通过 `--model` 指定。
- `bl update` 现在会把 agent skill 同步更新到所有 agent 应用(Claude Code、Cursor 等),即使 CLI 已是最新版本也会刷新 skill。
## [1.4.0] - 2026-06-17
### 新增
- 控制台网关支持多 region 与多站点:`cn-beijing``ap-southeast-1`,各含国内站 / 国际站变体,并新增 `switchAgent` 委托访问。
- 新增全局标志 `--console-region``--console-site``--console-switch-agent``bl console call` 另外新增 `--site``--switch-agent`
- `bl auth login --base-url <url>`:使用 API Key 登录时可指定 base URL。
- `bl omni` 新增 `--voice` 选项Chelsie、Cherry、Ethan、Serena、Sunny、Tina默认 Cherry
### 变更
- 所有面向用户的 CLI 文案统一为英文。
- `bl advisor recommend` 内部意图 / 排序模型由 `qwen-turbo` 升级为 `qwen-flash`
- 优化 `usage``quota``workspace` 命令的 JSON 输出。
- 配置文件中的 `base_url` 现在优先级高于环境变量 `DASHSCOPE_BASE_URL`
- `bl config show` 现在展示 `config.json` 中的全部字段(敏感值已脱敏)。
### 移除
- 移除遗留的 `region` 配置字段及其相关选项。
- 清理 `model list` 命令移除后遗留的无效代码。
### 修复
- 当控制台会话未登录或已过期时CLI 现在会给出明确的登录提示,不再是笼统的网关错误。
- 修正 `bl video` 命令 `--resolution` / `--ratio` / `--duration` 的帮助文案。
## [1.3.3] - 2026-06-16
### 变更
- `bl knowledge retrieve --help` 现在明确指出 `--api-key` 是推荐的鉴权方式AK/SK 相关选项已标注废弃并引导用户使用 `--api-key`
### 新增
- 命令定义新增 `notes` 字段 — 命令可以附带上下文说明(鉴权要求、废弃提示等),同时展示在 `--help` 输出和生成的命令手册中。
## [1.3.2] - 2026-06-12
### 修复
+2 -7
View File
@@ -6,7 +6,8 @@ Developer guide for `bailian-cli` — the official CLI for Aliyun Model Studio (
## Prerequisites
- Node.js ≥ 22.12
- Node.js ≥ 22.12 (required for developing this repo)
- End users installing the published CLI only need Node.js >= 18.17
- pnpm 10.33.2 (`npm i -g pnpm@10.33.2`)
- A DashScope API key for running e2e tests
@@ -33,13 +34,7 @@ pnpm install
### Running the CLI from source
Open two terminals:
```bash
# Terminal 1 — watch-build core
pnpm dev
# Terminal 2 — run any bl command
pnpm bl auth login --api-key sk-xxxxx
pnpm bl text chat --message "hello"
pnpm bl video generate --prompt "a cat walking"
+3 -8
View File
@@ -6,8 +6,9 @@
## 环境要求
- Node.js ≥ 22.12
- pnpm 10.33.2(`npm i -g pnpm@10.33.2`)
- 参与本仓库开发:Node.js ≥ 22.12
- 仅安装/使用已发布的 CLINode.js >= 18.17
- pnpm 10.33.2`npm i -g pnpm@10.33.2`
- 跑 e2e 需要一个百炼 API Key
## 仓库结构
@@ -33,13 +34,7 @@ pnpm install
### 从源码运行 CLI
开两个终端:
```bash
# 终端 1 —— core watch 重建
pnpm dev
# 终端 2 —— 跑任意 bl 命令
pnpm bl auth login --api-key sk-xxxxx
pnpm bl text chat --message "你好"
pnpm bl video generate --prompt "一只走路的猫"
+66 -73
View File
@@ -1,117 +1,110 @@
# 阿里云百炼CLI 安装说明(供 AI Agent 阅读)
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**npm 包名 `bailian-cli`命令 `bl` / `bailian`)。用户通常通过 **npm 全局安装** 使用,**无需**访问本仓库源码。不要臆造版本号或路径;以用户环境为准。
本文档面向 **AI Agent**:按下列步骤为用户安装并校验 **阿里云百炼CLI**(命令 `bl` / `bailian`)。**优先**在已有 Node.js**≥ 18.17.0**)的环境用 npm 安装;若没有可用 Node / npm再走二进制一键安装。不要臆造版本号或路径;以用户环境为准。
## 1. 安装前检查
## 1. 推荐npm 安装(要求 **≥ 18.17.0**
**原则**:先执行检查;若 **Node / 包管理器 / 网络** 任一不满足下文要求Agent 应 **在本机终端内主动完成安装或配置**(例如升级 Node、切换镜像、配置 `npm` 代理等),再继续 `bailian-cli` 安装。仅在无权限或环境无法自动化时,再向用户说明原因并请其配合
1. **Node.js**
- 要求 **≥ 22.12.0**,执行 `node -v` 确认。
- 未安装或版本不足:由 Agent 自行安装或升级 Node。
2. **npm****唯一**允许的包管理器)
- 执行 `npm -v` 确认可用。
- 未安装或不可用:由 Agent **安装或修复 npm**(不要改用 pnpm/yarn 装 `bailian-cli`),再执行 `npm install -g bailian-cli`
- 即使本机已安装 `pnpm``yarn` 等,**也仅用于其它项目**;装 `bailian-cli` 时仍必须用 `npm`
3. **网络**
- 需能访问 npm registry默认 **registry.npmjs.org**)。
- 不可达:由 Agent 自行配置镜像或代理后重试。
---
## 2. 安装 CLI 与 Skills
**仅允许以下命令**(不要用 `pnpm add -g``yarn global add` 等)。按顺序执行,上一步通过后再进行下一步。
**2.1 安装 CLI**
1. `node -v` 确认版本 ≥ 18.17.0
2. `npm -v` 确认可用(**仅允许 npm** 全局安装,不要用 pnpm/yarn
3. 执行:
```bash
npm install -g bailian-cli
```
安装成功后,应能在 PATH 中找到:
4. 校验:`bl --version`
- `bl`(短别名)
- `bailian`(全名)
**校验**Agent 应执行并检查退出码与输出):
安装 skillsCLI 内置,无需 Git / npx skills
```bash
bl --version
which bl # Windows 可用 where bl
bl skill init
```
`command not found`:检查全局 bin 是否在 PATH`npm config get prefix`,其下 `bin` 目录应加入 PATH)。
**Supported** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
**2.2 安装 Skills**
CLI 校验通过后,在本机终端执行:
**Advanced / 按需子集:**
```bash
npx skills add modelstudioai/cli --all -g
bl skill add --name bailian-protocol,bailian-gen
```
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
## 2. 备选:二进制安装(无需 Node
当环境没有 Node / npm或 Node 版本过低无法走 npm 时,使用二进制安装脚本。脚本安装 CLI 成功后会自动执行 `bl skill init`
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
```
可选:正式安装读 CDN 上的 `manifest.json`。预发 / channel 验证一律读 `sync-release.json`bailian-cli 的 channel 发版都会覆盖它):
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash -s -- --channel sync-release
```
也可用 `--version` / `--cdn`(或环境变量 `BAILIAN_CLI_CDN`)覆盖资源根。安装脚本本身不在本仓库维护。
Windows PowerShell
```powershell
# 正式manifest.json
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# channel / 预发验证sync-release.json
$env:BAILIAN_CHANNEL = 'sync-release'; irm 'https://bailian.aliyun.com/cli/install.ps1' | iex
```
带其它参时先落盘再执行(`irm | iex` 不便传参),或使用仓外静态资源文档中的预发入口。
二进制安装布局为 `versions/<ver>/` + `current` 指针;`bl update` 只切换指针并清理旧版本(保留当前与上一版)。更新进程退出后,下次执行 `bl` 即使用新版本(无需「重启应用」)。
校验:
```bash
bl --version
which bl # Windows: where.exe bl
```
若自动 skill 安装失败,再手动执行:`bl skill init`
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node回退到上方 npm 安装。
---
## 3. 鉴权(安装后必做才能调 API
### 推荐:浏览器登录(控制台会话)
适用于本机交互式安装,无需用户手动复制 API Key
1. 执行 `bl auth status --output json`,判断是否已配置。
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`;命令会拉起浏览器完成阿里云控制台登录授权
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`
3. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整凭据。
> 此方式同时打通 `app list`、`usage free` 等控制台能力,并自动配置 API Key 调用所需的鉴权信息。
### 备选API Key / Token Plan
### 备选:由 Agent 引导用户输入 API Key 后登录
适用于无法拉起浏览器的对话式安装(远程 SSH、CI 调试、纯终端环境等):
- 获取入口:[百炼控制台 API Key](https://bailian.console.aliyun.com/cn-beijing/?tab=app#/api-key)
1. 执行 `bl auth status --output json`,判断是否已配置。
2. 若未配置或后续 API 校验失败,**请用户粘贴 API Key**(可说明从上述控制台复制;勿要求用户发到公开渠道)。
3. 用户提供了 Key 之后,在**用户本机终端**执行Agent 用终端工具跑,勿把 Key 写进回复正文):`bl auth login --api-key <用户提供的_Key>`
4. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整 Key。
### 其他方式
- **环境变量**(不落盘到配置文件):在 shell 中配置 API Key 环境变量;变量名见 `bl auth status --help`,勿在对话中向用户解释底层命名。
- **写入配置文件**(持久化,与 `auth login` 落盘相同):`bl config set --key api_key --value <key>``--key api-key` 亦可)。**不会**像 `bl auth login --api-key` 那样先校验 Key 是否可用Agent 引导安装时仍**优先**用 `auth login`
- **命令行临时传入**:需要 API Key 的 `bl` 子命令可在**当次**执行附加全局 `--api-key <key>`,仅本次生效、不落盘(例:`bl text chat --api-key sk-xxx --message "你好"`)。与上文持久化方式不是同一用途。
- 普通 Key`bl auth login --api-key <Key>`
- Token Plan`bl auth login --config token-plan --api-key <Key>`
### Agent 安全约束
- **禁止**把真实 API Key 写入仓库、日志、Skill、聊天记录的可公开部分。
- CI / 非交互环境:使用 `bl ... --non-interactive`通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
- CI / 非交互环境:显式传入必填参数并使用 `--output json` 获取机器可读结果;如需纯文本输出,设置 `NO_COLOR=1`通过密钥管理或环境变量注入,勿在脚本中硬编码 Key。
---
## 4. 最小功能验证
在鉴权配置完成后执行:
## 4. 配置验证
```bash
bl auth status --output json
bl text chat --message "ping" --non-interactive --output json
```
若失败:根据 stderr / JSON 中的 `hint``message` 排查网络、Key 无效、region 等)。全局 region`--region cn|us|intl`,默认 `cn`
## 5. 常见问题
---
## 5. 常见问题Agent 排障清单)
| 现象 | 可能原因 | 建议动作 |
| ----------------------- | -------------------- | --------------------------------------------------------------- |
| `bl: command not found` | 全局 bin 不在 PATH | 检查 `npm prefix -g` 与 PATH |
| 安装报错 engines | Node 版本过低 | 升级到 ≥ 22.12 |
| 401 / 鉴权失败 | 未 login 或 Key 无效 | 引导用户更新 Key 并 `bl auth login --api-key` |
| 企业网络无法访问 npm | 代理 / 镜像 | 配置 registry 或代理后再装 |
| 本机只有 pnpm、没有 npm | Agent 误用 pnpm 安装 | 先装/修好 **npm**,再用 `npm install -g bailian-cli`;勿用 pnpm |
| 现象 | 可能原因 | 建议动作 |
| ------------------------ | ---------------------------- | ------------------------------------------------ |
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin``npm prefix -g` |
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node或改用 npm 版 CLI |
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
+99 -102
View File
@@ -5,7 +5,7 @@
**The official command-line interface for Aliyun Model Studio (DashScope) AI Platform**
[![npm version](https://img.shields.io/npm/v/bailian-cli?color=0969da&label=npm)](https://www.npmjs.com/package/bailian-cli)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18.17-brightgreen)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
@@ -13,8 +13,9 @@
---
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Built for AI Agents. Every command works as a structured tool call._
@@ -22,23 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
- **Text chat** — Qwen3.7-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 2.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — HappyHorse-1.0 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 520s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Console capabilities** — Browse Bailian apps (`app list`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
## Showcase: One-Sentence Cinematic Video
## Showcase 1: A Cinematic Short Film from One Sentence
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -51,118 +45,114 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.0**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2分钟左右的视频尺寸是16:9")_
### How it works
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.0** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
## Installation
```bash
npm install -g bailian-cli
npx skills add modelstudioai/cli --all -g
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
> Requires Node.js >= 22.12.
**Install with NPM**
```bash
npm install -g bailian-cli
bl skill init
```
> Requires Node.js >= 18.17.
**Install on macOS/Linux**
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
```
> No Node.js required. The installer automatically installs Bailian Skills.
**Install on Windows**
```powershell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
```
> No Node.js required. The installer automatically installs Bailian Skills.
## Quick Start
```bash
# Authenticate, recommended
bl auth login --console
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Browse apps / free-tier quota / usage statistics / workspaces
bl app list
bl usage free --model qwen3-max
bl usage free --expiring 30 # Quotas expiring within 30 days
bl usage free --sort remaining # Sort by remaining % ascending
bl usage stats --workspace-id <id> # Usage overview for a workspace
bl usage stats --model qwen-turbo --workspace-id <id> # Per-model usage
bl workspace list # List all workspaces
# Rate limit management
bl quota list # View RPM/TPM limits for all models
bl quota list --model qwen3.6-plus # View limits for a specific model
bl quota check # Current usage vs rate limits
bl quota check --model qwen3.6-plus --period 5 # Check usage over last 5 minutes
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota change history
```
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## Authentication
### DashScope API Key
### API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
```
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
### Console Login (OAuth)
Required for console capability commands (`app list`, `usage free`, `usage stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
```
### Alibaba Cloud AK/SK (Knowledge Base only)
### Alibaba Cloud OpenAPI AK/SK
Required for `knowledge retrieve`. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
```
## Configuration
@@ -171,17 +161,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# View current config
bl config show
# Set defaults
bl config set --key region --value us
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# List all config profiles
bl config list
# Self-update to latest version
bl update
# Switch config profile
bl config use --name token-plan
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -191,12 +195,5 @@ Config file location: `~/.bailian/config.json`
| Qwen Model List | https://help.aliyun.com/zh/model-studio/getting-started/models |
| Aliyun Model Studio Console | https://bailian.console.aliyun.com/?source_channel=cli_github |
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+99 -98
View File
@@ -5,7 +5,7 @@
**阿里云百炼 (DashScope) AI 平台命令行工具**
[![npm version](https://img.shields.io/npm/v/bailian-cli?color=0969da&label=npm)](https://www.npmjs.com/package/bailian-cli)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18.17-brightgreen)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
@@ -22,23 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.7-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 2.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — HappyHorse-1.0 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **控制台能力** — 浏览百炼应用(`app list`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -48,116 +41,117 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.0**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
> _帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2 分钟左右的视频尺寸是 16:9。”_
### 工作流程
## 示例 2一句话构建短片导演 Managed Agent
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.0**
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用能够实现短片拍摄导演专家生成视频然后也能进行设计对应的分镜图。”_
## 安装
```bash
npm install -g bailian-cli
npx skills add modelstudioai/cli --all -g
**Agent 安装(推荐)**
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
> 需要预先安装 Node.js >= 22.12。
**NPM 安装**
```bash
npm install -g bailian-cli
bl skill init
```
> 需要预先安装 Node.js >= 18.17。
**macOS/Linux 安装**
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
```
> 无需预先安装 Node.js安装脚本会自动安装 Bailian Skills。
**Windows 安装**
```powershell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
```
> 无需预先安装 Node.js安装脚本会自动安装 Bailian Skills。
## 快速开始
```bash
# 认证
bl auth login --api-key sk-xxxxx
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 浏览应用 / 免费额度 / 用量统计 / 业务空间
bl app list
bl usage free --model qwen3-max
bl usage free --expiring 30 # 30 天内过期的额度
bl usage free --sort remaining # 按剩余百分比升序排列
bl usage stats --workspace-id <id> # 指定空间的用量概览
bl usage stats --model qwen-turbo --workspace-id <id> # 指定模型用量
bl workspace list # 列出所有业务空间
# 限流管理与提额
bl quota list # 查看所有模型的 RPM/TPM 限额
bl quota list --model qwen3.6-plus # 查看指定模型限额
bl quota check # 查看当前用量 vs 限流阈值
bl quota check --model qwen3.6-plus --period 5 # 查看最近 5 分钟用量
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
```
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### DashScope API Key
### API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json
bl auth login --api-key sk-xxxxx
```
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
### 控制台登录OAuth
控制台能力命令(`app list``usage free``usage stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 AK/SK仅知识库检索
### 阿里云 OpenAPI AK/SK
`knowledge retrieve` 命令需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
```
## 配置
@@ -166,17 +160,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# 查看当前配置
bl config show
# 设置默认值
bl config set --key region --value us
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 查看全部配置档
bl config list
# 自更新到最新版本
bl update
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -186,12 +194,5 @@ bl update
| 通义千问模型列表 | https://help.aliyun.com/zh/model-studio/getting-started/models |
| 阿里云百炼控制台 | https://bailian.console.aliyun.com/?source_channel=cli_github |
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+119 -67
View File
@@ -2,117 +2,169 @@
## 触发条件
- 增加新的鉴权方式(OAuth、SSO、控制台回调登录)
- 增加新的 token 来源(env / config / flag / 文件)
- 调整凭证解析优先级
-`bl auth login` 流程
- 增加新的鉴权域或 token 来源(env / config / flag / 文件)
- 调整 API Key / Console token 解析优先级
- `bl auth login` / `auth status` / `auth logout` 流程
-runtime 对 command `auth` 的 gating 或 credential 注入
## 鉴权链路
```
flag 优先 ─→ config 文件 ─→ env var
│ │ │
──── resolveCredential() (core) ───┐
cli/utils/ensure-key.ts (启动时拦)
命令注入 Authorization 头
argv flags ─┐
env var ──┼─ buildSources(flags) ─┐
config ──┘ │
├─ buildSettings(sources) → ctx.settings
├─ resolveApiKey(sources) → model-domain Client
├─ resolveConsole(sources) → console-domain Client
└─ resolveOpenApi(sources) → OpenAPI Client
defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx)
```
凭证类型(`AuthMethod`):
当前 command 鉴权域(`AuthRequirement`):
- `api-key` — DashScope SK(`sk-...`),走 Bearer 头
- `access-token` — 控制台 OAuth 回调拿到的临时 token,走 Bearer + 不同 endpoint
- `ak/sk` — Alibaba Cloud 标准 AK/SK,走 ROA 签名(只用于知识库)
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
### 凭证并存API Key + Console
### 凭证并存
`~/.bailian/config.json` 可同时保存 `api_key``access_token`**登录任一种方式不得删除另一种**`bl auth login --api-key` / `--console` 只更新对应字段)。
`~/.bailian/config.json` 可同时保存 `api_key``access_token``access_key_*`。登录任一种方式不得删除另一种:
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi``console` 域凭证
- `bl auth logout --console` 只清 `access_token`
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
- `bl auth logout``api_key` + `base_url` + `access_token` + `access_key_*`
解析分工:
- `resolveCredential()` — DashScope API 命令(`text chat``file upload`config 里两者都有时 **优先 `api_key`**
- `resolveConsoleGatewayCredential()` — 控制台网关(`app list``usage free``console call`**只用** env/file 的 `access_token`,忽略 `api_key`
- `resolveApiKey()``auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin仅保留协议、host 和显式端口,去除 path、query、fragment
- `--config` 只选择 config 文件 block不提升该 block 的字段优先级;内置套餐 Profile当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile未传
`--config` 时继续写当前激活项,失败和 dry-run 不切换
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
- `resolveOpenApi()``auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
- `describeAuthState()``auth status` / banner / telemetry 使用的只读快照
必改调用点: 凡 `callConsoleGateway` 必须用 `resolveConsoleGatewayCredential`,不能误用 `resolveCredential`(否则 config 仅有 api_key 时会拿 sk- 打网关)
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘
`bl auth logout --console` 只清 `access_token`;全量 `bl auth logout` 清两者。
### 例外:agent 命令的分层鉴权与 SDK 凭证内存注入
`bl managed-agent *` 按调用链分两层:
- **离线命令** — `init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
- **联网命令** — `plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁authStage 都经 `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state不查 provider key其中 `--dry-run` 连登录也不要求authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK管道五步:
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
## 必查清单
### A. core 层(类型 + 解析)
- [ ] `packages/core/src/types/command.ts`:
- 如新增鉴权域,扩展 `AuthRequirement`
- 更新 `credentialFlagDefs()` 暴露该域可见的 flag
- 必要时新增 `*_AUTH_FLAGS`
- `workspace_id` 是作用域字段而非 credential,不要把它放进 `ConsoleCredential`;读取方式按命令 `auth` 域区分:
- `auth: "console"` 命令通过 `CONSOLE_AUTH_FLAGS` 自动获得 `--workspace-id`,由 `buildSettings()` 解析到 `settings.workspaceId`,命令统一从 `settings.workspaceId` 读取
- `auth: "apiKey"`/`"openapi"`/`"none"` 命令如需 `--workspace-id`,必须自声明 flag;因它不会进入 credential/global flags,命令从 `ctx.flags.workspaceId` 读取(可回退到 `settings.workspaceId`)
- [ ] `packages/core/src/auth/types.ts`:
- 新增 `AuthMethod` 字面量
- 新增 `ResolvedCredential` 字段(如 token 类型 / 过期时间)
- 新增 credential 类型 / source / scope 字段
- [ ] `packages/core/src/auth/resolver.ts`:
- `resolveCredential()` 增加新分支
- 控制台网关命令用 `resolveConsoleGatewayCredential()`(与 DashScope 解析分离)
- 优先级注释保持清晰(数字标号)
- [ ] `packages/core/src/auth/credentials.ts`:
- 如果新方式需要持久化,加 `save*` / `load*` / `clear*`
- 新增或调整 resolver,保持优先级注释清晰
- 新增/调整 resolver hint 时保持产品无关,不要新增 `bl` / `kscli` 硬编码;当前遗留的 `bl auth login` hint 如被触碰,迁到 runtime `enhanceHint`
- [ ] `packages/core/src/auth/store.ts`:
- 如果新方式需要持久化,扩展 `AuthStore` / `AuthPersistPatch`
- [ ] `packages/core/src/config/schema.ts`:
- `Config` 接口加新字段(如 `fileAccessToken``accessTokenEnv`)
- `ConfigFile` 接口加对应 disk 字段(snake_case)
- `ConfigFile` 加 disk 字段(snake_case)
- `Settings` 加运行时字段(如果命令需要读取)
- [ ] `packages/core/src/config/loader.ts`:
- `loadConfig()` 把 env / 文件读到 Config 上
- `buildSources()` / `buildSettings()` 把 flag/env/file 读到正确层
### B. core 客户端
### B. runtime 层
- [ ] `packages/core/src/client/http.ts`:
- 不同 `credential.method` 走不同分支(参考已有 `access-token` 分支走 console gateway)
- Authorization 头注入正确
- [ ] `packages/runtime/src/create-cli.ts`:
- parse flags 时纳入新的全局/凭证域 flag
- `globalFlags``ownFlags` 分流正确
- [ ] `packages/runtime/src/middleware.ts:authStage`:
- 根据 `command.auth` 解析 credential 并注入 `ctx.client`
- `settings.dryRun` 下是否允许缺 credential 的策略明确
- [ ] `packages/runtime/src/error-handler.ts`:
- AUTH hint 增强使用 `binName`,不要硬编码 `bl`
- URL 从 `packages/runtime/src/urls.ts` import
### C. cli
### C. command
- [ ] `packages/cli/src/utils/ensure-key.ts`:
- 启动时检查新凭证方式是否已配置,缺的话提示
- 如果是交互式 setup(类似 `bl auth login --console`),增加新分支
- [ ] `packages/cli/src/commands/auth/login.ts`:
- 新增 `--xxx` flag 触发新登录流程
- 持久化到 config(调用 core 的 save 函数)
- [ ] `packages/cli/src/commands/auth/status.ts`:
- 分别显示 `api_key` / `access_token` 是否已配置,以及 DashScope vs 控制台网关各自生效的 credential
- [ ] `packages/cli/src/output/status-bar.ts`:
- 顶部状态条显示新凭证 method
- [ ] `packages/commands/src/commands/auth/login.ts`:
- 新增/调整登录 flag 与流程
- 持久化只走 `ctx.authStore.login(...)`
- [ ] `packages/commands/src/commands/auth/status.ts`:
- 分别显示 model / console / openapi 鉴权状态,并 mask token
- [ ] `packages/commands/src/commands/auth/logout.ts`:
- 清理范围与双凭证并存规则一致
- [ ] 新的业务命令设置正确 `auth`:
- 模型域请求 → `auth: "apiKey"`
- Console Gateway → `auth: "console"`
- 阿里云 OpenAPI 请求 → `auth: "openapi"`
- 本地/登录/配置 → `auth: "none"`
### D. main 启动逻辑
- [ ] `packages/cli/src/main.ts:NO_AUTH_SETUP` 列表:
- 如果新增的命令"自己管鉴权或不需要鉴权",加进去绕开 ensureApiKey 拦截
- 当前清单以 `main.ts:NO_AUTH_SETUP` 为准
### E. 错误文案
- [ ] core 的 `BailianError` 鉴权失败 hint **保持通用**(不写 cli 命令名,见 [error-hint-change.md](error-hint-change.md))
- [ ] cli 的 `enhanceHint` (error-handler.ts) 按 `ExitCode.AUTH` 注入新方式的 cli 命令引导
### F. 用户面文档
### D. 用户面文档
- [ ] `README.md` / `README.zh.md` "Authentication" 段落
- [ ]`skills/<skill>/reference/` 通过 `pnpm run sync:skill-assets` 重建
### G. 测试
### E. 测试
- [ ] `packages/cli/tests/e2e/auth.e2e.test.ts` 增加新方式的 happy / failure 路径
- [ ] mask token 的输出格式不变(避免泄漏)
- [ ] 如调整 resolver 优先级,补 core/runtime 单测覆盖 flag > env > file
## 完成后自查
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
```sh
# 各种凭证组合
unset DASHSCOPE_API_KEY DASHSCOPE_ACCESS_TOKEN
HOME=/tmp/empty node packages/cli/src/main.ts auth status
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts auth status
# flag 注入
node packages/cli/src/main.ts auth status --api-key sk-xxx
# flag 注入(凭证域 flag 只在对应业务命令可见,auth status 不接收)
pnpm -F bailian-cli exec tsx src/main.ts text chat --message hi --api-key sk-xxx --dry-run
pnpm -F bailian-cli exec tsx src/main.ts token-plan list-seats --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
pnpm -F bailian-cli exec tsx src/main.ts auth login --open-api --access-key-id ak-xxx --access-key-secret sec-xxx --dry-run
# env 注入
DASHSCOPE_ACCESS_TOKEN=xxx node packages/cli/src/main.ts auth status
DASHSCOPE_API_KEY=sk-xxx pnpm -F bailian-cli exec tsx src/main.ts auth status
ALIBABA_CLOUD_ACCESS_KEY_ID=ak-xxx ALIBABA_CLOUD_ACCESS_KEY_SECRET=sec-xxx pnpm -F bailian-cli exec tsx src/main.ts auth status
```
Console 登录/网关相关改动:
```sh
pnpm -F bailian-cli exec tsx src/main.ts auth login --console
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json --workspace-id ws-xxx
```
注意:`usage stats --dry-run` 仍会先校验 workspace,必须传入 `--workspace-id`(或 `BAILIAN_WORKSPACE_ID` / config `workspace_id`)。
## 常见漏点
- ✗ 加了新 token 来源但忘了改 `resolveCredential` 优先级,实际不生效
-`Config` 加字段但 `loadConfig` 没读 → 字段永远 undefined
-`bl auth login` 写成功但 `bl auth status` 不识别(两边走的 storage path 不一致)
- ✗ 加了新 token 来源但忘了改 resolver 优先级,实际不生效
-`ConfigFile` / `Settings` 加字段但 `parseConfigFile``buildSettings` 没读
-`auth login` 写成功但 `auth status` 不识别(两边走的 storage path 不一致)
- ✗ token mask 显示完整 token,日志泄漏
-`auth: "console"` 命令误用 `apiKey` 域,config 只有 API key 时会把 `sk-...` 发到网关
- ✗ 新增 core resolver hint 时写死产品命令,导致 `kscli` 等入口提示错误
+11 -11
View File
@@ -50,13 +50,13 @@ git diff --name-only <base>...<head>
- [ ] **`package.json` 没破坏发布元数据**:`bin` / `exports` / `files` / `inlinedDependencies` 字段任何删除或改名都要单独评估
- [ ] **公共依赖没被悄悄升级**:catalog / 根 lockfile 改动要列出来
- [ ] **`package.json` version 没倒退**:目标分支已经更高时(如 main 1.0.3 vs head 1.0.0-beta.1),手动对齐版本号,不要被 head 覆盖
- [ ] **全局表没冲突**:`registry.ts``NO_AUTH_SETUP`(`packages/cli/src/main.ts`)`ExitCode` 三个全局表新增项不和现有项冲突
- [ ] **全局表没冲突**:`packages/cli/src/commands.ts` / `packages/kscli/src/main.ts` command map、`defineCommand({ auth })``GLOBAL_FLAGS` / `MODEL_AUTH_FLAGS` / `CONSOLE_AUTH_FLAGS` / `OPENAPI_AUTH_FLAGS``ExitCode` 新增项不和现有项冲突
## 清单 B:用户透出(用户可见的新东西必看)
- [ ] **新命令 / 新 flag** 已同步到用户面文档:
- [README.md](README.md) + [README.zh.md](README.zh.md)(中英文都要,常漏 `_CN`)
- (SKILL.md 已迁出本仓库,由 `npx add skills` 机制独立维护,不在本仓库 review 范围)
- `skills/<skill>/reference/` + 对应 `SKILL.md` 通过 `pnpm run sync:skill-assets` 更新并提交
- [ ] **`bl <cmd> --help`** 文案完整:`description` / `examples` 都填了
- [ ] **demo / quickstart**:用户可调用的新命令至少有一个示例
- [ ] **行为变化的老命令**:在 commit message / CHANGELOG 注明用户感知的差异
@@ -80,7 +80,7 @@ git diff --name-only <base>...<head>
解冲突要点(merge 时不要漏):
- <冲突文件> + <字段/段落> + <怎么取舍>
↑ 放"合并那一刻才会出现"的细节,例如 package.json 的 files/scripts/devDependencies 各取并集、
NO_AUTH_SETUP 这种全局表两边都加项时不要丢一侧、pnpm-lock.yaml 直接 rm 后 pnpm install 重生等。
command map / `auth` / 全局 flags 这类元数据两边都加项时不要丢一侧、pnpm-lock.yaml 直接 rm 后 pnpm install 重生等。
建议修(可后置):
- ...
仅信息(无需动作,告知即可):
@@ -94,11 +94,11 @@ git diff --name-only <base>...<head>
## 常见漏点(基于历史踩坑)
| 漏点 | 后果 |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `pnpm-workspace.yaml``packages/*` 收窄成显式列表 | 合并后目标分支的新子包不再被 workspace 识别,`pnpm install` 看似正常但子包失联 |
| 源分支 version 比目标分支低,直接 merge 覆盖 | npm 上版本号回退,latest tag 错乱 |
| `registry.ts` 注册新命令但忘了 [README](README.md) / [README.zh](README.zh.md) | 用户完全感知不到新功能 |
| 共享 util 重构(抽公共函数)只改了一处调用方 | 其它调用方静默走旧分支,行为分裂 |
| `NO_AUTH_SETUP` 加了不该免登录的命令 | 安全风险,用户没登录也能调付费 API |
| `NO_AUTH_SETUP` / `registry.ts` 这类全局表两边都加项,解冲突时被合掉一侧 | 某个命令突然要求登录 / 某个新命令注册丢失,编译能过、回归不易察觉 |
| 漏点 | 后果 |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `pnpm-workspace.yaml``packages/*` 收窄成显式列表 | 合并后目标分支的新子包不再被 workspace 识别,`pnpm install` 看似正常但子包失联 |
| 源分支 version 比目标分支低,直接 merge 覆盖 | npm 上版本号回退,latest tag 错乱 |
| `packages/cli/src/commands.ts` 注册新命令但忘了 [README](README.md) / [README.zh](README.zh.md) | 用户完全感知不到新功能 |
| 共享 util 重构(抽公共函数)只改了一处调用方 | 其它调用方静默走旧分支,行为分裂 |
| 命令 `auth` 域设错(如 Console Gateway 用了 `apiKey` | 凭证域 flag/help/credential 注入都错,运行期才暴露 |
| `packages/cli/src/commands.ts` / `packages/kscli/src/main.ts` 这类 map 两边都加项,解冲突时被合掉一侧 | 某个新命令注册丢失,编译能过、回归不易察觉 |
+4 -3
View File
@@ -81,11 +81,12 @@ git merge-base --is-ancestor 12f2b1b 3fc54ae && echo "IN" || echo "NOT IN"
光看 commit 还不够,要确认目标功能的代码 / 文件在 release commit 上真的存在:
```sh
# 列出 release commit 下某目录的文件
git ls-tree -r <releaseCommit> --name-only -- packages/cli/src/commands/
# 列出 release commit 下命令实现与产品入口
git ls-tree -r <releaseCommit> --name-only -- packages/commands/src/commands/
git show <releaseCommit>:packages/cli/src/commands.ts | head
# 看 release commit 下某文件的内容
git show <releaseCommit>:packages/cli/src/commands/console/call.ts | head
git show <releaseCommit>:packages/commands/src/commands/console/call.ts | head
```
特别注意被一行带过的"杂项" commit。本仓库历史踩过坑:`feat(cli): enhance output options and add new commands` 这种标题里藏了**新命令** + **新输出格式** + **logout 增强**三件事,粗看会全部漏掉。
+76 -31
View File
@@ -1,30 +1,58 @@
# CLI E2E 测试规范
## 架构分层
| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup`private`,不发布) |
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、livegated每用例最小路由 |
| **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 拒绝 |
**依赖边界**`e2e``core``commands/tests``e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
## 触发条件
- 新增/修改 `packages/cli/src` 下的 command`commands/catalog.ts` 登记、`defineCommand` 实现、options/usage
-建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例
- 为命令补 help / 缺参 / dry-run / 真实集成测试
- 新增/修改 `packages/commands/src/commands` 下的 command 实现
-增/修改 `packages/cli/src/commands.ts``bl` 命令路径 map
- 新建或扩展 `packages/commands/tests/e2e/<topic>.e2e.test.ts`
- 新增 bl 产品 path → `registry.smoke` 自动覆盖 leaf pathcommands topic 测试在 `topic-routes.ts` 补最小路由
以上情况必须同步维护 `packages/cli/tests/e2e/<topic>.e2e.test.ts`跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`
跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`
> **规则**:共享 command 行为在 `commands/tests/e2e`;产品 map、identity、CLI-only 命令留在对应产品 `tests/e2e`。
## 文件与工具
- 路径:`packages/cli/tests/e2e/<kebab-topic>.e2e.test.ts`
- 框架:`vite-plus/test`;子进程跑 CLI`runCli` from `./helpers.ts`
### commands E2E
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`
- 子进程:`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
### 产品 smoke
- bl`runCli` from `packages/cli/tests/e2e/helpers.ts`
- kscli`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
### 共享
- gating / output / runner`e2e/gating``e2e/output``e2e/runner`
- globalSetup`vite.config.ts``packages/e2e/src/global-setup.ts`
- 解析 JSON stdout`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
## 双层 describe固定结构
```ts
// 1) 不 skip分组 + --help无密钥、无真实 API
// 1) 不 skip--help无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
describe("e2e: <topic>", () => {
test("<group> 分组展示子命令帮助且成功退出", ...);
test("<subcommand> --help 正常退出", ...);
});
// 2) skipIf缺参 / dry-run / 真实集成;原有集成用例放最后、勿改逻辑
// 2) skipIf缺参 / dry-run / 真实集成
describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
test("缺少 --<flag> 时退出为用法错误 (2)", ...);
test("<cmd> --dry-run ...", ...); // 若适用
@@ -32,57 +60,74 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
});
```
## skip 条件(helpers.ts
## skip 条件(`e2e/gating`commands helpers re-export
| 场景 | 条件 |
| ------------------- | ----------------------------------------------------- |
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
| 知识库 | `isKnowledgeE2EReady()` |
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
| 场景 | 条件 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
| OpenAPI AK/SK | `isOpenApiE2EReady()``.env` 中必须同时提供完整 AK/SK |
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` |
| 知识库 chat/search live | `isChatE2EReady()` / `isSearchE2EReady()``knowledge chat/search`,需 `BAILIAN_WORKSPACE_ID` + agent ID |
## 用例类型
1. **分组 help**`runCli(["image"])``exitCode === 0`stdout+stderr 含子命令名
2. **--help**`runCli([..., "--help"])` → stderr 含主要 flags
3. **缺参**`--non-interactive` 且不传 required flag → `exitCode === 2`stderr 匹配 `--flag|Missing required argument`
4. **--dry-run**:仅当实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本,不入网
5. **真实集成**:保留既有用例名称与断言;放在 skip 块**末尾**
1. **--help**`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
2. **缺参**:带无害全局 flag`--quiet`)且不传 required flag → `exitCode === 2`
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
4. **真实集成**:放在 skip 块**末尾**
## 增删命令同步
- **commands export** + **topic 路由**`topic-routes.ts` 或测试文件内 `ROUTES`+ **产品 map**`cli/commands.ts` / `kscli/commands.ts`
- 分组 help 由产品 `registry.smoke` 负责,无需在 commands 重复
## 安全与例外
- **禁止真实破坏性操作**`auth logout` 只用 `--dry-run``config set` 只用 `--dry-run`
- **禁止破坏真实用户配置**`auth logout` 默认只用 `--dry-run`需要验证实际落盘时,必须通过
`BAILIAN_CONFIG_DIR` 指向隔离 fixture`config set` 只用 `--dry-run`
- **不加 dry-run**`dryRun``resolveFileUrl` / `resolveCredential` / 上传**之后**的命令(如 `image edit``speech recognize``--url`
- **`--list-voices` 等旁路**:先于 `--text` 校验的 flag缺参用例勿带该 flag
- 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言
## 新增 command 检查清单
- [ ] `commands/catalog.ts` 登记 + `tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] 若改了 `usage` / `options` / `examples`,跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/` 并提交
- [ ] 顶层:分组 help + 子命令 `--help`(多子命令则各一条 help
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
- [ ] 至少一条真实集成(或说明为何仅 smoke不破坏已有集成用例顺序
- [ ] `pnpm test packages/cli/tests/e2e/<file>` 通过
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
## 调试命令
```sh
pnpm --filter bailian-cli-commands exec vp test packages/commands/tests/e2e/text-chat.e2e.test.ts
pnpm --filter bailian-cli exec vp test packages/cli/tests/e2e/registry.smoke.e2e.test.ts
pnpm --filter knowledge-studio-cli exec vp test packages/kscli/tests/e2e/registry.smoke.e2e.test.ts
pnpm --filter bailian-cli-runtime exec vp test packages/runtime/tests/proxy.e2e.test.ts
```
## 示例片段
```ts
import { FOO_ROUTES } from "./topic-routes.ts";
test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["foo", "bar", "--non-interactive"]);
const { stderr, exitCode } = await runCommandE2e(FOO_ROUTES, ["foo", "bar", "--quiet"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--prompt|Missing required argument/i);
});
test("foo bar --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCli([
const { stdout, stderr, exitCode } = await runCommandE2e(FOO_ROUTES, [
"foo",
"bar",
"--dry-run",
"--prompt",
"x",
"--non-interactive",
"--output",
"json",
]);
@@ -97,4 +142,4 @@ test("foo bar --dry-run 仅输出计划", async () => {
- **E2E**:单条/少量调用、断言固定、可进 `vp test`(见上文 skip 条件)
- **批量压测**`packages/cli/tests/stress/run.mjs` + `targets/*.mjs`,并发 + 报告,**仅手动** `pnpm run test:stress -- <target>`
勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md)
勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md).
+94 -51
View File
@@ -5,89 +5,132 @@
- 增加新的 `bl xxx` 命令
- 删除已有命令
- 重命名命令(包括从单级 `bl x` 改成 `bl x y` 或反向)
- 调整某个 shared command 在 `bl` / `kscli` 等产品入口里的暴露路径
## 命令路径与文件路径的对应规则
## 命令实现与产品路径的关系
命令实现住在 `packages/commands`,产品路径由入口包决定。实现文件路径按能力组织,但不再等同于最终命令路径。
```
单级命令(无 group): commands/<name>.ts ↔ bl <name>
例: commands/update.ts ↔ bl update
两级命令(有 group): commands/<group>/<action>.ts ↔ bl <group> <action>
例: commands/text/chat.ts ↔ bl text chat
三级命令(子组,慎用): commands/<group>/<sub>/<action>.ts ↔ bl <group> <sub> <action>
例: commands/memory/profile/create.ts ↔ bl memory profile create
仅当子组下有 ≥2 个 action 时合理(否则拍平到两级)
实现文件:
packages/commands/src/commands/knowledge/retrieve.ts
↓ packages/commands/src/index.ts export { default as knowledgeRetrieve }
产品入口:
packages/cli/src/commands.ts "knowledge retrieve": knowledgeRetrieve ↔ bl knowledge retrieve
packages/kscli/src/main.ts "retrieve": knowledgeRetrieve ↔ kscli retrieve
```
文件路径与命令路径必须 1:1 对齐。
常见路径形态:
```
单级命令: packages/commands/src/commands/update.ts ↔ bl update
两级命令: packages/commands/src/commands/text/chat.ts ↔ bl text chat
子组命令: packages/commands/src/commands/memory/profile-get.ts ↔ bl memory profile get
```
子组要慎用:只有子组下有 ≥2 个 action 时才合理,否则优先拍平到两级。
## CLI 命令注册架构(必读)
命令元数据以 **`catalog.ts` 为单一登记处**;`registry.ts` 负责解析与打印 help,不再内嵌命令表或手写 Resources 列表
`packages/commands` 是命令库,只导出单个 command;不内置 path presets,不关心 `bl` / `kscli`。每个产品入口传入自己的 command map,`runtime` 负责解析、help、鉴权、遥测、执行
```
commands/<...>.ts defineCommand({ name, description, usage, options, examples, run })
packages/commands/src/commands/<...>.ts
defineCommand({ auth, flags, usageArgs, exampleArgs, validate, run })
commands/catalog.ts export const commands: Record<string, Command>
packages/commands/src/index.ts
export { default as xxxCommand } from "./commands/...ts"
┌──────────────────────────────┬─────────────────────┐
↓ ↓ ↓ ↓
registry.ts main.ts tools/generate-reference.ts export-schema.ts
(解析/help) (入口) → skills/bailian-cli/reference/index.md + <group>.md
┌──────────────────────────────┬──────────────────────────────
│ packages/cli/src/commands.ts │ packages/kscli/src/main.ts │
│ { "text chat": textChat } │ { "retrieve": knowledge... } │
└──────────────┬───────────────┴──────────────┬───────────────┘
↓ ↓
createCli(commands, identity) → runtime registry/help/middleware
tools/generate-reference.ts reads packages/cli/src/commands.ts
```
- **`packages/cli/src/commands/catalog.ts`**: `import` 命令模块 + `"<path>": handler` 映射;**不** `import registry.ts`(避免构建时循环依赖)
- **`packages/cli/src/commands/index.ts`**: `export { commands } from "./catalog.ts"`(给包内 re-export 用)
- **`packages/cli/src/registry.ts`**: `import { commands } from "./commands/catalog.ts"`,建树、`resolve``printHelp`;Commands / Global Flags 从 `Command` 元数据与 `GLOBAL_OPTIONS` **动态生成**
- **`tools/generate-reference.ts`**: pre-commit / `pnpm run sync:skill-assets` 时读 `catalog.ts`,写 `skills/bailian-cli/reference/index.md`(索引) + `skills/bailian-cli/reference/<一级命令>.md`(详情,勿手改)。该目录**纳入 git**,随 `npx skills add modelstudioai/cli` 分发
- **`packages/commands/src/commands/<...>.ts`**:命令实现;`usageArgs` / `exampleArgs` 只写参数片段,不写 `bl` / `kscli` 前缀
- **`packages/commands/src/index.ts`**:导出命令实现;新增命令必须在这里 re-export
- **`packages/cli/src/commands.ts`**:`bl` 产品命令 map;新增/删除/重命名 `bl` 命令必须改这里
- **`packages/kscli/src/main.ts`**:`kscli` 产品命令 map;只有该入口需要暴露/变更时才改
- **`packages/runtime/src/registry.ts`**:通用 registry,从传入 map 建树;不要在这里登记业务命令
- **`tools/generate-reference.ts`**:pre-commit / `pnpm run sync:skill-assets` 时读 `packages/cli/src/commands.ts`,按 `GROUP_OWNER_SKILL` 归属表分流写到各 `skills/<skill>/reference/index.md` + `<一级命令>.md`。未显式归属的一级组默认进 `bailian-cli`。各目录**纳入 git**,勿手改。新增一级命令组若应归领域 skill,记得改归属表。
已删除勿再引用:`commands/help.ts``registry.ts` 内联 `new CommandRegistry({...})``printRootHelp` 手写命令行
已删除/勿再引用:旧的 `packages/cli/src/commands/catalog.ts`、旧的 `packages/cli/src/commands/index.ts` catalog re-export、`packages/cli/src/registry.ts``skipDefaultApiKeySetup``ensureApiKey` 启动拦截、`config/export-schema.ts`
## 必查清单
### A. 代码层
### A. 命令库
- [ ] **新建/删除/移动**对应的 `packages/cli/src/commands/<...>.ts` 文件
- [ ] **`packages/cli/src/commands/catalog.ts`**:
- 增删 `import xxx from "./.../xxx.ts"`
- `export const commands` 里增删 `"<group> <action>": xxx`(key 与 `defineCommand({ name })` 一致)
- [ ] **不要**在 `registry.ts` 里重复登记命令(已从 catalog 读取)
- [ ] 如果命令需要鉴权之外的特殊路径,看 `packages/cli/src/main.ts``NO_AUTH_SETUP`
- [ ] **`config/export-schema.ts`**: 若新命令不适合作为 agent tool,评估是否加入 `SKIP_PREFIXES`;该文件在 `run()``import("../catalog.ts")`,勿顶层 import catalog 以免循环依赖
- [ ] 新建/删除/移动对应的 `packages/commands/src/commands/<...>.ts`
- [ ] `defineCommand` 字段使用当前 schema:
- `auth: "apiKey" | "console" | "openapi" | "none"`
- `flags`(camelCase key,由 runtime 渲染为 kebab-case)
- `usageArgs`(不含 bin/path 前缀)
- `exampleArgs`(不含 bin/path 前缀)
- `validate`(跨 flag 校验)
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
- [ ] `packages/commands/src/index.ts`:新增或移除对应 export
- [ ] 如果命令调用 Console Gateway,设置 `auth: "console"`;不要重复声明 console 凭证域 flags
- [ ] 如果命令不需要网络或自己管理配置/登录,设置 `auth: "none"`;不要绕过 runtime auth stage
### B. 文档层
### B. 产品入口
- [ ] 运行 `pnpm run sync:skill-assets`(或正常 `git commit` 走 pre-commit),刷新 `skills/bailian-cli/reference/``SKILL.md` `metadata.version` 并提交
- [ ] `README.md` / `README.zh.md`: Quick Start、命令一览(用户向,与 help 对齐即可)
- [ ] `skills/bailian-cli/SKILL.md`: 若安装说明或能力边界有变,同步更新
- [ ] `packages/cli/src/commands.ts`:按需增删 `import` `commands` map key
- [ ] 新 map key 就是 `bl` 下的命令路径;重命名时全仓 grep 旧路径字符串
- [ ] 如果 `kscli` 入口也要暴露/移除该能力,同步 `packages/kscli/src/main.ts`
- [ ] 不要在 `packages/runtime/src/registry.ts``create-cli.ts` 里写业务命令表
### C. 测试
### C. 文档
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/cli/tests/e2e/<topic>.e2e.test.ts`
- [ ] 删除命令时一并删对应 e2e
- [ ] 运行 `pnpm run sync:skill-assets`(或正常 `git commit` 走 pre-commit),刷新各 `skills/<skill>/reference/``SKILL.md``metadata.version` 并提交
- [ ] `README.md` / `README.zh.md`:Quick Start、命令一览、认证说明(用户向,与 help 对齐)
- [ ] 相关 `skills/<skill>/SKILL.md`:若安装说明或能力边界有变,同步更新;新一级命令组若属领域 skill,同步改 `tools/generate-reference.ts``GROUP_OWNER_SKILL`
- [ ] **拥有方** skill 的「When to use which command」(或等价路由表)补上新意图;hub `bailian-cli` 仅加/改 hand-off 行,**不要**把领域子命令与默认模型抄进 hub 表(约定见 [skill-change.md](skill-change.md))
### D. 重命名特殊处理
### D. 测试层
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/commands/tests/e2e/<topic>.e2e.test.ts`
- [ ] 同步 `packages/commands/tests/e2e/topic-routes.ts`(该 topic 的最小 path → export 映射)
- [ ] bl 产品 path 变更由 `registry.smoke` 自动覆盖kscli 变更同步 `kscli/src/commands.ts``registry.smoke`
- [ ] 删除命令时一并删对应 commands e2e / README 示例 / reference / topic 路由条目
- [ ] 如果 shared command 在不同入口路径下复用,至少确保 commands e2e 覆盖 `bl` path`kscli` 入口改动需补对应 smoke 或说明不测 flat path live
### E. 重命名特殊处理
- [ ] 全仓 grep **旧命令名字符串**,确保以下位置全部更新:
- `catalog.ts` key
- error hints(cli 层)
- `skills/bailian-cli/reference/`(重建后检查并提交)
- README 示例
- 测试断言
- `packages/cli/src/commands.ts` map key
- `packages/kscli/src/commands.ts` map key(如适用)
- 用户可见 hint / README / tests
- `skills/*/reference/`(重建后检查并提交)
- [ ] 检查 `usageArgs` / `exampleArgs` 没有硬编码旧的 `bl <path>` 前缀
## 完成后自查
```sh
pnpm run sync:skill-assets # reference/ + SKILL metadata.version 与 catalog / package.json 一致
node packages/cli/src/main.ts <new-command> --help
node packages/cli/src/main.ts # 根 help 列表含新命令
vp test packages/cli/tests/e2e/<topic>.e2e.test.ts # 相关 e2e
pnpm run sync:skill-assets
pnpm -F bailian-cli exec tsx src/main.ts <new-command> --help
pnpm -F bailian-cli exec tsx src/main.ts
vp test packages/commands/tests/e2e/<topic>.e2e.test.ts
```
如改了 `kscli` 入口:
```sh
pnpm -F knowledge-studio-cli exec tsx src/main.ts <command> --help
```
## 常见漏点
- ✗ 只改了命令文件,忘了 **`catalog.ts`** → 命令不存在或 help 里没有
-手改 **`skills/bailian-cli/reference/*.md`** → 下次 generate 被覆盖;应改 `defineCommand` 后重新 generate 并提交
- `export-schema.ts` 顶层 `import catalog` → 可能与 registry 循环依赖
- ✗ 只新增 `packages/commands/src/commands/...` 文件,忘了在 `packages/commands/src/index.ts` 导出
-只导出了命令实现,忘了在 `packages/cli/src/commands.ts` 暴露路径 → `bl --help` 看不到
-手改 `skills/*/reference/*.md` → 下次 generate 被覆盖;应改 command metadata 后重新 generate 并提交
- ✗ 新一级命令组忘改 `tools/generate-reference.ts``GROUP_OWNER_SKILL` → reference 会落到 hub `bailian-cli`(未必是预期)
- ✗ 只改 reference / hub,忘改拥有方 skill 路由表;或把领域命令明细重新抄回 `bailian-cli` SKILL → 与 [skill-change.md](skill-change.md) 分层冲突
- ✗ 在 `usageArgs` / `exampleArgs` 写死 `bl text chat``kscli` 等入口复用时 help 错
- ✗ Console Gateway 命令忘设 `auth: "console"` → console flags / credential 注入都不生效
- ✗ 单 action 的子组是反模式,新增时优先拍平为两级
+17 -16
View File
@@ -11,25 +11,26 @@
### A. 命令文件本身
- [ ] `packages/cli/src/commands/<group>/<action>.ts`:
- `defineCommand({ options: [...] })` 数组里增删/改 `{ flag, description, type, required }`
- `usage` 字段(如 `"bl text chat --message <text> [flags]"`)反映新签名
- `examples` 数组覆盖新 flag 至少一个示例
- `run()`读取 flag 的代码:
- 类型转换正确(`type: "number"``flags.x as number`,`"array"``as string[]`)
- 必填校验:`if (!flags.x) failIfMissing("x", ...)` 或交互式 prompt
- 默认值 fallback
- [ ] `packages/commands/src/commands/<group>/<action>.ts`:
- `defineCommand({ flags: { ... } })` 里增删/改 camelCase flag key 与 `{ type, valueHint, description, required }`
- `usageArgs` 字段只写参数片段(如 `"--message <text> [flags]"`),不写 `bl <path>`
- `exampleArgs` 数组覆盖新 flag 至少一个示例,同样不写 bin/path 前缀
- `run()`只从 `ctx.flags` 读取本命令 flag,从 `ctx.settings` 读取全局/config 解析结果
- 类型`ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
### B. 鉴权 / 全局选项
- [ ] 如果是**全局 flag**(所有命令通用),改 `packages/core/src/types/command.ts``GLOBAL_OPTIONS`
- [ ] 如果 flag 影响 `Config`,改 `packages/core/src/config/schema.ts` `Config` 接口
- [ ] 如果对应 env var,改 `packages/core/src/config/loader.ts``loadConfig`
- [ ] 如果是**全局 flag**(所有命令通用),改 `packages/core/src/types/command.ts``GLOBAL_FLAGS`
- [ ] 如果是凭证域 flag,优先确认是否属于 `MODEL_AUTH_FLAGS` `CONSOLE_AUTH_FLAGS`;不要在单个命令里重复声明
- [ ] 如果新 flag 影响有效配置面,改 `packages/core/src/config/schema.ts``Settings` 接口
- [ ] 如果对应 env var 或 config 文件字段,改 `packages/core/src/config/loader.ts``buildSettings`
### C. 文档层
- [ ] `README.md` / `README.zh.md` 如果在示例里展示了相关命令,补充新 flag
- [ ]`pnpm --filter bailian-cli run generate:reference`,让 `skills/bailian-cli/reference/` 与命令一致(勿手改;改完提交)
- [ ]`pnpm --filter bailian-cli run generate:reference`,让 `skills/<skill>/reference/` 与命令一致(勿手改;改完提交)
### D. 测试层
@@ -44,13 +45,13 @@
## 完成后自查
```sh
node packages/cli/src/main.ts <command> --help # 看新 flag 出现在 Options
node packages/cli/src/main.ts <command> --new-flag x # 实测一遍
pnpm -F bailian-cli exec tsx src/main.ts <command> --help # 看新 flag 出现在 Flags
pnpm -F bailian-cli exec tsx src/main.ts <command> --new-flag x # 实测一遍
```
## 常见漏点
- ✗ 加 `type: "number"``String(flags.x)` 触发 lint 警告(参考已修过的 memory/list.ts)
- ✗ 加了 array 型 flag 但没考虑用户可能传多次
- ✗ 改默认值忘记更新 description 里的 "(default: xxx)" 文案
-Required flag 缺失时直接抛硬错而不是 prompt(交互友好性问题,参考已实现 prompt 的命令文件作为示例)
-`usageArgs` / `exampleArgs` 里写死 `bl <path>`,导致其它产品入口复用时 help 错
- ✗ required flag 缺失又在 `run()` 里重复手写校验,与 parser/`validate` 的错误文案不一致
+56
View File
@@ -0,0 +1,56 @@
# Command Pack 维护
## 触发条件
- 新增或移除 Command Pack 包
- 调整包白名单、允许的命令前缀或协议字段
- 修改 `plugin install/link/list/remove`
- 修改 Command Pack 加载、隔离、兼容性或独立安装目录
## 分层边界
- `packages/core/src/types/command-pack.ts`:稳定的协议元数据和导出类型,不知道具体产品或白名单。
- `packages/runtime/src/command-packs/`:所有 CLI 共用的加载、校验、API 适配、产品隔离安装目录和 manager 实现。
- `packages/runtime/src/create-cli.ts`:始终接收静态 command map`CliOptions.commandPacks` 统一合并 pack并把已绑定产品 identity/policy 的 manager 注入 `ctx.commandPacks`
- `packages/commands/src/commands/plugin/`:普通共享管理命令,只依赖 `ctx.commandPacks`,不 import 任何产品 policy。
- `packages/cli/src/command-pack-policy.ts``bl` 支持的包、命令前缀和凭据授权。
- `kscli` 当前不传 `commandPacks`,使用 runtime 的默认空 policy。
- 当前只有 `bl``bailian-cli-commands` 导入并登记 `plugin *`;使用默认空 policy 的产品不提前暴露管理命令。
不要把产品白名单写进 core/runtime也不要通过扫描全局 `node_modules` 自动发现包。通用机制放 runtime产品差异只由 policy 表达。
## 安全与兼容性清单
- [ ] 包名必须精确命中当前产品 policy 的 `supported`,命令路径必须位于该包允许的前缀。
- [ ] 正式安装只接受包名加 version/tag本地目录只走 `plugin link`
- [ ] npm 使用独立安装目录和 `--ignore-scripts`,不污染 CLI 自身依赖树。
- [ ] npm 子进程只继承明确允许的 registry/config/cache/proxy/TLS 配置,不通配透传 pnpm 注入的 `npm_config_*`
- [ ] 安装目录按 `identity.npmPackage` 隔离,不能让一个产品安装/删除另一个产品的 pack。
- [ ] 安装目录只隔离依赖位置不隔离执行权限Command Pack 必须视为 CLI 进程内的完全可信代码。
- [ ] 入口 realpath 不能逃逸包根目录。
- [ ] 加载前检查 `type``apiVersion``minCliVersion`;报告状态只使用 `loaded/failed`,具体原因写入 `error`
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore``configStore`
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager避免 pack 安装或删除其他 pack。
- [ ] 单包失败必须 fail-open保留内置命令和其他合法 pack。
- [ ] 破坏协议前优先在适配层兼容;确实无法兼容时才提升 `apiVersion`
## 测试与文档
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
- [ ] 更新生成的 `skills/bailian-cli/reference/plugin.md`(或归属表指定的 skill reference公开 `README.md` / `README.zh.md` 等正式对外发布时再补。
验证:
```sh
vp test packages/runtime/tests/command-packs.test.ts
vp test packages/cli/tests/e2e/command-packs.e2e.test.ts
vp test packages/kscli/tests/e2e/command-packs.e2e.test.ts
pnpm run sync:skill-assets
vp check
```
+19 -19
View File
@@ -11,46 +11,46 @@
```
flag (--xxx) ─┐
├─ loadConfig() 合并 ─→ Config(运行时单一对象)
├─ buildSources() + buildSettings() ─→ Settings(命令读取面)
env (XXX=yyy) ─┤
config 文件 ─┘
~/.bailian/config.json
```
优先级一般是 **flag > env > config 文件 > 默认值**,具体见 `core/config/loader.ts`
优先级一般是 **flag > env > config 文件 > 默认值**,具体见 `packages/core/src/config/loader.ts`
## 必查清单
### A. 类型定义
- [ ] `packages/core/src/config/schema.ts`:
- `Config`(运行时形状)加新字段
- `Settings`(运行时有效配置面)加新字段
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
- `parseConfigFile()` 解析新字段
- 如果是 enum 字段,加校验
### B. 加载逻辑
- [ ] `packages/core/src/config/loader.ts:loadConfig()`:
- 加新字段的合并逻辑(`flags.x ?? process.env.XXX ?? file.x ?? default`)
- [ ] `packages/core/src/config/loader.ts`:
- `buildSources()` 如需新增来源,把 flag/file/env 纳入 sources
- `buildSettings()` 加新字段的合并逻辑(`flags.x ?? process.env.XXX ?? file.x ?? default`)
- 校验(数值范围、枚举合法性等)
- 校验失败抛 `BailianError(USAGE)`
### C. 全局 flag(如果加的是 flag)
- [ ] `packages/core/src/types/command.ts:GLOBAL_OPTIONS` 数组
- [ ] `registry.ts``buildGlobalFlagLines` 会**自动**从 `GLOBAL_OPTIONS` 生成 `bl --help` `reference/index.md` 的全局 flag 段,无需手写
- [ ] flag 的 type 标注(`boolean` / `number` / `array`),让 args.ts 正确解析
- [ ] `packages/core/src/types/command.ts:GLOBAL_FLAGS`
- [ ] `packages/runtime/src/registry.ts` 会**自动**从 `GLOBAL_FLAGS` 生成 root help;`tools/generate-reference.ts` 会生成 `reference/index.md` 的全局 flag 段
- [ ] flag 的 type 标注(`switch` / `boolean` / `number` / `array` / `string`),让 `packages/runtime/src/args.ts` 正确解析
- [ ] 改完全局 flag 后跑 `pnpm --filter bailian-cli run generate:reference`
### D. 命令使用方
- [ ] 用到新字段的命令文件直接读 `config.xxx`,不要重复解析
- [ ] 用到新字段的命令文件直接读 `ctx.settings.xxx`,不要重复解析 env/config
- [ ] 配置展示 / 修改命令同步:
- `packages/cli/src/commands/config/show.ts` 显示新字段
- `packages/cli/src/commands/config/set.ts` 允许 set
- `packages/cli/src/commands/config/export-schema.ts` 在 schema 输出里
- `packages/commands/src/commands/config/show.ts` 显示新字段
- `packages/commands/src/commands/config/set.ts``VALID_KEYS` / `KEY_ALIASES` / description 允许 set
### E. 文档
@@ -66,19 +66,19 @@ config 文件 ─┘
```sh
# 三个来源都试一遍
node packages/cli/src/main.ts config show --output json | grep <new-field>
XXX=value node packages/cli/src/main.ts config show --output json | grep <new-field>
node packages/cli/src/main.ts config show --xxx value --output json | grep <new-field>
pnpm -F bailian-cli exec tsx src/main.ts config show --output json | grep <new-field>
XXX=value pnpm -F bailian-cli exec tsx src/main.ts config show --output json | grep <new-field>
pnpm -F bailian-cli exec tsx src/main.ts config show --xxx value --output json | grep <new-field>
# 写到文件
node packages/cli/src/main.ts config set --key <key> --value <value>
# 写到文件(会改用户 HOME,必要时先用临时 HOME)
pnpm -F bailian-cli exec tsx src/main.ts config set --key <key> --value <value>
cat ~/.bailian/config.json
```
## 常见漏点
-`Config` 接口加字段但 `loadConfig` 没填,运行时永远 undefined
-`Settings` 接口加字段但 `buildSettings` 没填,运行时永远 undefined
-`ConfigFile` 用 camelCase 字段名(disk schema 应该是 snake_case)
- ✗ 全局 flag 没标 `type: "boolean"`,被当成需要值的 `--xxx <value>`
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
-`config show` 不显示新字段,用户改了无法回查
+78
View File
@@ -0,0 +1,78 @@
# Config Profile 与激活状态变更清单
适用于新增 Profile 预设、修改命名 Profile 选择规则、调整 `active_config`,或新增/修改 `bl config list/use/show/ui` 等 Profile 管理能力。
## 1. 保持存储边界
- Profile 业务字段继续由 `ConfigFile` / `CONFIG_FILE_KEYS` 管理。
- `active_config``config.json` 顶层元数据,不得进入命名 Profile block也不得被 `config set` 当作普通字段写入。
- 识别命名 Profile 时必须排除业务字段和顶层元数据。
- 旧配置缺少 `active_config` 时继续等价于激活 `default`
## 2. 保持选择语义
```text
显式 --config <name> > active_config > default
```
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
- 激活状态只选择配置 block不改变字段优先级字段仍为 flag > env > selected config > 默认值。
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
## 3. 保持读写命令交互一致
- `auth login``config set` 等写命令未传 `--config` 时修改当前激活项。
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
创建和激活;`config set --config <name>` 可创建但不自动激活。
- `config show``auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
- 按凭证域退出时必须清理该域的完整字段集合,例如 OpenAPI 同时清理 AK、SK 和 STS `security_token`
- 所有生产代码读取“当前配置”时优先经过 `buildSources()` 或携带解析后的 `configName`;直接调用无名称的 `readConfigFile()` / `writeConfigFile()` 只适用于明确操作顶层 default 的底层能力。
## 4. 保持状态一致性
- `config use` 只能激活已经存在的命名 Profile`default` 始终有效。
- 配置文件中的 `active_config` 指向不存在的 Profile 时返回 usage error不静默回退。
- 删除当前激活的命名 Profile 时,同一次落盘切回 `default`,不得留下悬空引用。
- 配置写入继续使用临时文件 + rename避免中断后留下半写文件。
## 5. 命令与展示联动
- 新增/重命名命令时同步 `packages/commands/src/index.ts` 和产品入口 `packages/cli/src/commands.ts`
- `config list` 标识所有 Profile 与当前激活项。
- `config show``auth status` 只输出本次最终选择的 `config``config_file`,不重复携带激活状态。
- `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`
- `config ui` 展示并可编辑完整 `ConfigFile`(含 `console_*``telemetry`),保存时按类型(数字/布尔/枚举)归一化写回;`config set` 仍只暴露较窄的 `VALID_KEYS`。UI 未管理的顶层元数据(如 `active_config`)不进入 Profile block仍由写盘逻辑单独保留。
- `config ui` 只读展示本地 agent 生态Skills 跨全部 agent skill 目录(`~/.agents/skills` 及各 agent 的 `skills/`,含软链接)按 id 聚合并标注安装来源MCP、Agents 从各 agent 本地配置读取。
- `config ui` 提供 Assets 资产管理:扫描 `output_dir`(默认 `~/bailian-output`)下的 `images/videos/speech/omni` 分类及根目录散落文件按分类与生成时间mtime标记支持按分类筛选、内联预览图/视频/音频)与删除单个文件;文件读取与删除均通过限定在输出目录内的路径校验(防目录穿越)。
- 同步 E2E topic routes、Skill setup 和自动生成 reference。
## 6. 最小测试矩阵
- 旧配置无 `active_config` -> `default`
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
- 显式命名 `--config``--config default` 均覆盖激活项且不修改磁盘状态。
- 激活不存在的 Profile 失败且不写盘。
- 悬空 `active_config` 明确失败。
- 删除激活 Profile 后切回 `default`
- 登录、退出、`config set` 分别覆盖“当前激活项”和“显式不存在名称成功后创建”。
- 显式 `auth login --config <name>` 成功后激活该 Profile失败或 dry-run 不创建、不切换;
`--config default` 成功后切回 `default`
- Console token 自动刷新不从其他 Profile 借用 AK/SK也不把新 token 写入其他 Profile。
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。
## 7. 完成检查
```sh
pnpm run sync:skill-assets
vp check
vp test
```
命令 E2E 会启动本地子进程Config UI 测试还会监听 `127.0.0.1` 临时端口;受限沙箱内出现 `EPERM` 时,需要在允许本地进程和端口的环境中复跑。
+27 -21
View File
@@ -3,8 +3,8 @@
## 触发条件
- 修改 `BailianError` 的 message 或 hint
- 调整 cli 的 hint 增强逻辑(`enhanceHint`)
-ensure-key 的 setup 流程文案
- 调整 runtime 的 hint 增强逻辑(`enhanceHint`)
-auth stage / resolver 的鉴权失败文案
- 改任何抛错位置的分类(exitCode)
> 注意:`mapApiError` **不再做错误分类**(参见下方"边界原则")。如果你想给某种 HTTP 错误码加白名单分类,请先回到本文档读完"边界原则"再说。
@@ -17,7 +17,7 @@
| ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
| 命令解析、缺 flag、参数校验 | **内部** | `BailianError(USAGE)` |
| 文件 I/O(ENOENT/EACCES/...) | **内部** | `BailianError(GENERAL)` + errno-specific hint |
| 本地 credentials 缺失(resolver/ensure-key/AK-SK 等) | **内部** | `BailianError(AUTH)` |
| 本地 credentials 缺失(resolver / authStage 等) | **内部** | `BailianError(AUTH)` |
| `fetch` 自身失败(DNS/TCP/TLS/proxy) | **内部** | `BailianError(NETWORK)` + 读 `err.cause.code` 给 errno-hint |
| polling 客户端超时 | **内部** | `BailianError(TIMEOUT)` |
| HTTP 4xx/5xx、HTTP 200 + 业务错码、async task FAILED | **服务** | `BailianError(GENERAL)`,**message 原样透传**,不分类、不替换 |
@@ -39,9 +39,9 @@
```
core 抛出 BailianError(message, exitCode, hint, cause?)
↓ 沿调用栈冒泡
cli/main.ts: main().catch(handleError)
runtime/create-cli.ts: dispatch().catch(handleError)
cli/error-handler.ts:
runtime/error-handler.ts:
- 服务端错误(BailianError(GENERAL)) → text 直接打 message
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
@@ -62,36 +62,42 @@ process.exit(err.exitCode)
- ❌ 不要回退到"401 → AUTH、429 → QUOTA"那套白名单
- ✅ message 把 status / apiCode / request_id 拼进去就够,exit 统一 GENERAL
- 例外:CLI **自己**因为本地状态产生的 BailianError(resolver、ensure-key 等)可以用语义化 exitCode
- 例外:CLI/runtime **自己**因为本地状态产生的 BailianError(resolver、authStage 等)可以用语义化 exitCode
### 3. core 的 hint 必须不含 cli 关切
- ❌ 不写 `bl xxx` 命令名
-不写控制台 URL 或 region
-不写渠道追踪参数(`source_channel=xxx`)
-新增/改动时不写 `bl xxx` 命令名
-新增/改动时不写 `kscli xxx` 等产品入口命令名
-新增/改动时不写控制台 URL 或 region
- ❌ 新增/改动时不写渠道追踪参数(`source_channel=xxx`)
- ✅ 只描述抽象做法(如 `"Set DASHSCOPE_API_KEY environment variable, or pass --api-key."`)
- 当前遗留:`packages/core/src/auth/resolver.ts` 仍含 `bl auth login` hint;触碰鉴权错误时迁到 runtime `enhanceHint`
### 4. cli 端可以自由使用 cli 命令名 + URL
### 4. runtime / 产品层可以使用入口名 + URL
- 命令文件、`error-handler.ts``utils/ensure-key.ts` 是 cli 层,内部可以写 `bl xxx`
- URL 必须从 `packages/cli/src/urls.ts` import,不能硬编码
- `packages/runtime/src/error-handler.ts` 通过 `binName` 渲染 `bl` / `kscli` 等入口名,不要硬编码
- 产品入口 / README / E2E 可以写具体入口命令
- shared command 实现不写 `bl` / `kscli` 前缀;`usageArgs` / `exampleArgs` 只写参数片段
- URL 必须从 `packages/runtime/src/urls.ts` import,不能硬编码
## 必查清单
### A. core 改动(message / hint)
- [ ] `packages/core/src/errors/api.ts``mapApiError`:**保持透传形态**,不要加白名单分支
- [ ] `packages/core/src/auth/resolver.ts` 改 throw 语句:hint 不含 cli 关切
- [ ] 任何 core 文件 throw 的 BailianError:同上
- [ ] `packages/core/src/auth/resolver.ts` 新增/改 throw 语句:hint 不含 cli 关切;已有 `bl auth login` 遗留点被触碰时要收敛
- [ ] 任何 core 文件新增/改 BailianError:同上
### B. cli 增强(`enhanceHint`)
### B. runtime 增强(`enhanceHint`)
- [ ] `packages/cli/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为只有 resolver/ensure-key 等内部位置会发 AUTH)
- [ ] `packages/runtime/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为 resolver / authStage 等内部位置会发 AUTH)
- [ ] 命令名使用 `binName`,不要硬编码 `bl`
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`
### C. cli 直接抛错(`ensure-key`、命令文件)
### C. command / runtime 直接抛错
- [ ] cli 层抛 BailianError 时,hint 里可以放 cli 命令,但 **URL 一律走 `urls.ts` import**
- [ ] runtime 层抛 BailianError 时,hint 里可以放 `binName` 渲染的入口命令,但 **URL 一律走 `urls.ts` import**
- [ ] `packages/commands` 作为 shared command 库,默认不硬编码产品 bin;如果确需用户操作提示,优先依赖 runtime error handler 或 `ctx.identity.binName`
- [ ] 抛错位置如果**已经在调用服务端**,catch 时不要替换 message——重新评估是否需要 catch
### D. 文案一致性
@@ -104,14 +110,14 @@ process.exit(err.exitCode)
```sh
# 触发对应错误,看 text 输出
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message "x"
# 看 JSON 输出(应包含 cause 字段当 cause 存在时)
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive --output json
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message "x" --output json
# 模拟网络层错误,验证 errno 透传
DASHSCOPE_BASE_URL=https://nonexistent-host.invalid \
node packages/cli/src/main.ts text chat --message hi
pnpm -F bailian-cli exec tsx src/main.ts text chat --message hi
# 预期:"Network request failed: ENOTFOUND ..." + Caused by 链
```
+42
View File
@@ -0,0 +1,42 @@
# 安装文档变更
## 触发条件
- 修改根目录 `INSTALL.md` 的安装、鉴权或验证流程
- 修改发布包 Node.js 要求、全局 flag 或安装文档引用的命令
- 同步或发布 `https://bailian.aliyun.com/cli/install.md`
## 必查清单
### A. CLI 契约
- [ ] `INSTALL.md` 中的 `bl` 命令路径存在于 `packages/cli/src/commands.ts`
- [ ] 示例 flag 属于 `GLOBAL_FLAGS`、命令鉴权域 flag 或命令自身 `flags`
- [ ] Node.js 用户安装要求与 `packages/cli/package.json``engines.node` 一致,不使用根 `package.json` 的开发环境要求
- [ ] 鉴权流程与 `packages/commands/src/commands/auth/` 的实际校验、保存和 Profile 激活行为一致
### B. 静态副本
- [ ]`INSTALL.md` 同步到 `bailian-cli-static-resources/public/install.txt`
- [ ] 使用 `cmp -s` 确认两份文档逐字节一致
- [ ] 静态资源仓库单独创建分支、提交和发布,不把跨仓库改动遗漏在 CLI PR 之外
### C. 线上验证
- [ ] 发布后读取 `https://bailian.aliyun.com/cli/install.md`,确认内容来自最新静态副本
- [ ] 带随机 query 参数复查,区分 CDN 缓存与源站未更新
- [ ] 验证线上文档中的安装命令、Node.js 要求和配置验证段落,不只检查页面可访问
## 完成后自查
```sh
pnpm -F bailian-cli test -- tests/install-doc.test.ts
cmp -s INSTALL.md ../bailian-cli-static-resources/public/install.txt
curl -L -s "https://bailian.aliyun.com/cli/install.md?verify=$(date +%s)"
```
## 常见漏点
- `--non-interactive` 已从 CLI 移除,但旧安装文档和静态副本仍把它当作全局 flag
-`package.json` 是开发工具链 Node.js 要求;用户安装要求以 `packages/cli/package.json` 为准
- 静态仓库文件名是 `public/install.txt`,线上稳定地址是 `/cli/install.md`;只更新其中一侧不会自动证明发布成功
+12 -10
View File
@@ -12,9 +12,9 @@
### A. 版本一致性
- [ ] `package.json``engines.node` 与 README 的 Node.js 徽章一致
- [ ] 发布包(`cli` 等)`engines.node` 与 README 的 Node.js 徽章一致;根/e2e 开发要求(`>=22.12`)与 CONTRIBUTING 一致
- [ ] `pnpm-lock.yaml` 同步生成(运行 `pnpm install`)
- [ ] 三处 `tsconfig.json`(根 + cli + core)的 target / module 设置一致
- [ ] 各源码包 `tsconfig.json`(根 + core + runtime + commands + cli + kscli)的 target / module 设置一致
### B. lint / format 规则改动
@@ -26,13 +26,15 @@
### C. 构建配置
- [ ] `packages/cli/vite.config.ts``packages/core/vite.config.ts` 的 entry / external / dts 设置
- [ ] cli 的 bundle 必须把 `bailian-cli-core`**external**(不内联),确认 `dist/bailian.mjs` 第一行有 `from "bailian-cli-core"`
- [ ] cli 的 bundle 第一行必须有 `#!/usr/bin/env node` shebang(`tools/release.mjs check` 会断言)
- [ ] `packages/*/vite.config.ts` 的 entry / dts / exports 设置符合包类型:
- library 包(core/runtime/commands):本地 `exports` 默认指向 `src/index.ts`;`publishConfig.exports` 覆盖发布入口为 `dist/index.mjs`;dts 产物正常生成
- binary 包(cli/kscli):entry 指向 `src/main.ts`,有 shebang,`exports: true`
- [ ] cli / kscli 的 bundle 必须把 workspace 包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands`)当 **external**(不内联),确认 dist 中仍是 package import
- [ ] cli / kscli 的 binary bundle 第一行必须有 `#!/usr/bin/env node` shebang
### D. 依赖升级
- [ ] 检查 `bailian-cli-core` 在 cli 的 `dependencies` 里仍是 `"workspace:*"`(不要成实际版本号`tools/release.mjs` 会拦)
- [ ] 检查 workspace 内部依赖在 `dependencies` 里仍是 `"workspace:*"`(不要手改成实际版本号;发布时由 pack/publish 流程解析)
- [ ] 升级后跑 `vp check && vp test`
- [ ] 升级 `@types/node` 时注意 Node API 变化(如 fs.existsSync 行为)
@@ -40,11 +42,11 @@
- [ ] `.vite-hooks/pre-commit` 改动后,`pnpm install` 重新软链(走 `prepare: vp config`)
- [ ] 增加 hook 时,确认在干净 clone 后能自动激活
- [ ] pre-commit 会跑 `pnpm run sync:skill-assets`(先 build core,再 `generate:reference` + `sync:skill-version`)并 `git add` skill 资产,最后 `vp staged`
- [ ] pre-commit 会跑 `pnpm run sync:skill-assets`(`generate:reference` 含格式化 + `sync:skill-version`,直接读源码、无需先 build)并 `git add` skill 资产,最后 `vp staged`
### F. CI / 发版工具
- [ ] `tools/release.mjs` 中如有版本/规则相关的硬编码,同步更新
- [ ] `tools/release/` 中如有版本/规则相关的硬编码,同步更新
- [ ] 比如 `secretPatterns` 添加新的敏感值识别
## 完成后自查
@@ -54,13 +56,13 @@
pnpm install --frozen-lockfile
vp check
vp test
node tools/release.mjs check
node tools/release/check.mjs
```
## 常见漏点
- ✗ 升级 Node engines 但忘了 README 徽章
- ✗ 改 lint 规则后没全仓 `--fix`,新人 PR 报红一片
- ✗ 改 cli 的 vite config 把 core 不小心打成 inline,bundle 体积暴涨
- ✗ 改 cli/kscli 的 vite config 把 core/runtime/commands 不小心打成 inline,bundle 体积暴涨
- ✗ Oxlint 配置改了但 IDE 缓存还是旧的(IDE 可能要重启 ts server)
- ✗ 升级依赖一并升 lockfile,改动量大但没拆 commit
+1 -1
View File
@@ -109,7 +109,7 @@
### Do
- 写清晰的 **must / must-not / 必查**,不写"建议"性语气
- 用 file path + 具体 action 的句式(`packages/cli/src/commands/catalog.ts:增加 import 与 commands 条目`)
- 用 file path + 具体 action 的句式(`packages/cli/src/commands.ts:增加产品命令 map 条目`)
- 在每份场景末尾留**常见漏点**段,持续累积真实经验
- 在跨场景的不变量上互相引用,不复制
+9 -6
View File
@@ -12,12 +12,13 @@
### A. 命令实现
- [ ] `packages/cli/src/commands/<group>/<action>.ts`:
- [ ] `packages/commands/src/commands/<group>/<action>.ts`:
- `--model` flag 的 description 里"default:"反映新默认值
- 命令内部 `const model = (flags.model as string) || "<default>"` 的 fallback 字符串
- 命令内部 `const model = flags.model || settings.defaultXxxModel || "<default>"` 的 fallback 字符串
- 如果命令维护一个 supported-models 列表(如 `speech/synthesize.ts:MODEL_VOICES`),增删条目
- 如果不同模型有不同 endpoint / 请求体形状,确保 `if (model.startsWith("xxx"))` 分支覆盖
- [ ] 模型如有特殊 endpoint,看 `packages/core/src/client/endpoints.ts`
- [ ] 如果新增的是某产品入口专属能力,确认 `packages/cli/src/commands.ts` 或其它入口 map 是否需要暴露/隐藏
### B. 类型层
@@ -25,7 +26,8 @@
### C. 命令手册
- [ ]`--model` 的 description 含 default,改命令后跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/<group>.md` 并提交
- [ ]`--model` 的 description 含 default,改命令后跑 `pnpm --filter bailian-cli run generate:reference` 更新对应 `skills/<skill>/reference/<group>.md` 并提交
- [ ] 同步**拥有该命令的领域 skill**「When to use which command」表中的 Default model(现主要是 `bailian-gen`;精调相关看 `bailian-finetune` 正文示例)。hub `bailian-cli` 已瘦身,一般**不必**再写领域默认模型(见 [skill-change.md](skill-change.md))
### D. 用户面文档
@@ -41,13 +43,14 @@
```sh
# 默认模型走通
node packages/cli/src/main.ts <command> --message "test"
pnpm -F bailian-cli exec tsx src/main.ts <command> --message "test"
# 显式指定新模型
node packages/cli/src/main.ts <command> --model <new-model> --message "test"
pnpm -F bailian-cli exec tsx src/main.ts <command> --model <new-model> --message "test"
```
## 常见漏点
- ✗ 改了命令默认模型,但 SKILL.md frontmatter 仍写老型号 → AI agent 调用时仍按老型号宣传
- ✗ 改了命令默认模型,但 SKILL.md frontmatter 或领域路由表 Default model 仍写老型号 → AI agent 调用时仍按老型号宣传
- ✗ 只改了 `reference/` / flag description,忘改 `bailian-gen`(等) SKILL 路由表
- ✗ 废弃模型时只删了代码,e2e 测试还在跑,CI 红
- ✗ 新模型 endpoint 不一致,但只改了 default,没加 endpoint 分支判断
+83 -37
View File
@@ -1,91 +1,137 @@
# 发布npm publish
# 发布npm + GitHub Release 二进制
## 触发条件
- 准备发布 channelbeta/mcp/plugin 等)或正式版到 npm
- 准备打 git tag
- 准备发布 channelmcp/plugin 等)或正式版到 npm **与** GitHub Releases 二进制
- 准备打 git tag(仅 stable
## 发布方式GitHub Actions + npm OIDC
## 发布方式GitHub Actions 总入口
发版**必须**通过 CI 完成,不要本地手动 `pnpm publish`
入口GitHub Actions → **Publish** workflow`.github/workflows/publish.yml`)→ Run workflow。
**编排关系(重要):**
```text
publish-stable.mjs / publish-channel.mjs ← 唯一发版入口
├─ npmpnpm publish
└─ binarylib/binary-release
→ binary-build
→ gh-release
→ oss-direct-upload
```
`tools/release/lib/binary-release.mjs` 等是实现,一般不要单独当发版入口(调试可用)。
两种模式:
| 模式 | 用途 | 触发方式 |
| ------- | ------------------------------ | -------------------------------------------------- |
| channel | 发 channel 版本到指定 dist-tag | 选 mode=channel填 dist-tag 名称(如 mcp/plugin |
| stable | 正式发版到 latest | 选 mode=stable需 production environment 审批 |
| 模式 | 用途 | 触发方式 |
| ------- | --------------------------------------------------------------------------------------- | -------------------------------------------- |
| channel | npm dist-tag +(仅 bailian-cli二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channelchannel 填 **npm dist-tag** |
| stable | npm latest + GitHub Release `v<ver>` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable需 production environment 审批 |
可选 flag`--skip-binary`(仅发 npm紧急逃生
### CDN 滚动指针bailian-cli
| 发布模式 | CDN 指针 | 本机安装 / 更新 |
| -------- | ---------------------------------- | ----------------------------------------------------------------- |
| channel | 始终覆盖 `sync-release.json` | `BAILIAN_CHANNEL=sync-release` / `install --channel sync-release` |
| stable | `manifest.json`+ `latest.json` | 默认安装 / `bl update`(无 channel |
workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` / `sync-release`**不再**生成 `release-test.json` 这类旁路文件。
### channel 发布
1. 在 GitHub 触发 Publish workflowmode 选 `channel`channel 填 dist-tag 名(如 `mcp`
2. CI 自动:生成 `0.0.0-beta-<sha7>-<date>` 版本号 → 自检 → 构建 → 发布到指定 dist-tag
1. 在 GitHub 触发 Publish workflowmode 选 `channel`channel 填 npm dist-tag 名
- **`bailian-cli`**npm 发到该 tag二进制同时刷新 CDN `sync-release.json`(与 tag 名无关)。本机验证:`BAILIAN_CHANNEL=sync-release`
- **`knowledge-studio-cli`**:仅 npm自动跳过 binary不碰 `sync-release.json`
2. CI 自动:生成 `0.0.0-beta-<sha7>-<YYYYMMDDHHMM>`UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag**bailian-cli**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json
3. 对应脚本:`tools/release/publish-channel.mjs`
### stable 发布
1. 确保 `packages/cli/package.json` `packages/core/package.json` 已升到目标版本且一致
2. 在 GitHub 触发 Publish workflowmode 选 `stable`
1. 确保当前 release tooling 覆盖的包(`tools/release/lib/packages.mjs`)已升到目标版本且一致;当前基础集合为 `packages/core` / `packages/runtime` / `packages/commands` / `packages/cli``knowledge-studio-cli` 发布会额外包含 `packages/kscli`
2. 在 GitHub 触发 Publish workflowpackage 选目标包集合,mode 选 `stable`
3. 需要 production environment 审批人批准
4. CI 自动:自检 → 构建 →到 latest → 打 git tag
5. 对应脚本:`tools/release/publish-stable.mjs`
4. CI 自动:自检 → **npm 发到 latest****推送 git tag `v<ver>`****Bun 编二进制并创建/更新 GitHub Release**bailian-cli维护 CDN **`manifest.json`** → 完成
5. 如果所选发布集合的当前版本已全部存在于 npmstable 发布会失败并提示先升级版本号如果只有部分包已发布CI 会继续补发缺失包
6. 对应脚本:`tools/release/publish-stable.mjs`
## 自检(`tools/release/check.mjs`
两种模式都会先跑 `check.mjs`,覆盖以下检查:
| 检查项 | 说明 |
| -------------------------------- | ----------------------------------------- |
| `pnpm install --frozen-lockfile` | lockfile 一致性 |
| README 同步 | `packages/cli/README.md` 与根 README 一致 |
| 版本号一致 | cli 与 core 的 version 字段相同 |
| `workspace:*` 替换 | cli 对 core 依赖解析为真实版本号 |
| 构建 core + cli | `pnpm build` |
| pnpm pack | 打 tarball |
| publint | 包元数据校验 |
| gitleaks | 敏感信息扫描 |
| 检查项 | 说明 |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `pnpm install --frozen-lockfile` | lockfile 一致性 |
| README 同步 | `packages/cli/README.md` 与根 README 一致 |
| 版本号一致 | `tools/release/lib/packages.mjs` 中待发布包集合 version 相同 |
| `workspace:*` 替换 | 发布包间 workspace 依赖解析为真实版本号 |
| 构建 | 基础发布构建 core/runtime/commands 依赖和 cli;`--knowledge` 额外构建 `knowledge-studio-cli` |
| 生成资产 | 重建各 `skills/<skill>/reference/`;非 channel 模式还同步各 `skills/*/SKILL.md` version`bailian-protocol` |
| pnpm pack | 打 tarball |
| publint | 包元数据校验 |
| gitleaks | 敏感信息扫描 |
本地可以 dry-run 验证:
```sh
node tools/release/publish-channel.mjs --channel test --dry-run
node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
```
## CI 基础设施
- **认证**npm OIDC Trusted Publishing无 token需要 `id-token: write` 权限
- **GitHub Release**`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`stable / channel 均需)
- **Node 版本**24npm 11.5+ 才支持 OIDC token 交换)
- **Bun**`oven-sh/setup-bun`,版本钉死在 workflow 中
- **Actions 版本**checkout/setup-node/pnpm-action 均为 v6Node 24 兼容)
- **npm 配置**两个包的 Trusted Publisher 指向 `modelstudioai/cli``publish.yml`environment 留空
- **npm 配置**当前 release tooling 发布的包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands` / `bailian-cli` / `knowledge-studio-cli`)的 Trusted Publisher 指向 `modelstudioai/cli``publish.yml`;新增发布包时同步 npm Trusted Publisher
## `check.mjs` 不覆盖的(手动确认)
### 版本号目标(仅 stable
- [ ] `packages/cli/package.json``packages/core/package.json` 已升到目标版本
- [ ] `tools/release/lib/packages.mjs` 覆盖的目标包集合已升到目标版本且一致
- [ ] 源码包 `packages/core/package.json``packages/runtime/package.json``packages/commands/package.json``packages/cli/package.json``packages/kscli/package.json` 是否需要同步升版已人工确认;当前仓库通常保持五包版本一致
- [ ] `tools/release/lib/packages.mjs``PACKAGES` 覆盖基础发布包;`KSCLI_PACKAGE` / `ALL_PACKAGES` 覆盖 `knowledge-studio-cli` 发布路径;如果新增发布包,同步 `publish-stable.mjs` / `publish-channel.mjs` 的 bump、publish、idempotency 逻辑和 `.github/workflows/publish.yml` 的 package 选项
- [ ] pre-release 格式正确(`1.0.0-beta.0` / `1.0.0-rc.1`**不要直接用 `1.0.0` 当 beta**
### CHANGELOG仅 stable
- [ ] `CHANGELOG.md``CHANGELOG.zh.md` 都已新增目标版本条目,中英文一一对应
- [ ] 分类标题用 Keep a Changelog 规范的 `Added` / `Changed` / `Deprecated` / `Removed` / `Fixed` / `Security`(中文版对应 `新增` / `变更` / `已弃用` / `已移除` / `修复` / `安全`**不要自创 `Improved` / `优化` 等规范外分类**
- [ ] 条目日期与发版日期一致
### 用户面文档
- [ ] `README.md` / `README.zh.md` 的 Quick Start 命令仍能跑通
- [ ] README 的 Node.js 徽章版本与 `cli/package.json.engines.node` 一致
- [ ] README 宣传的 bin 名称在 `cli/package.json.bin` 都真的注册
- [ ] `LICENSE` 文件存在(根 + cli + core 各一份)
- [ ] `packages/kscli/README.md` / `README.zh.md``knowledge-studio-cli` 的 bin、控制台 URL、认证方式一致
- [ ] `LICENSE` 文件存在(根 + 当前实际发布包;新增发布包时补该包 LICENSE
## 完成后
- [ ] 验证 npm 上能装:`npm view bailian-cli@<tag> version`
- [ ] 试装一次:`npm i -g bailian-cli@<tag> && bl --version`
- [ ] 验证 npm 上能装:`npm view bailian-cli@<tag> version`;如发布 `knowledge-studio-cli`,同时 `npm view knowledge-studio-cli@<tag> version`
- [ ] 试装一次:`npm i -g bailian-cli@<tag> && bl --version`;如发布 `knowledge-studio-cli`,同时 `npm i -g knowledge-studio-cli@<tag> && kscli --version`
## 常见漏点(基于历史踩坑)
| 漏点 | 后果 |
| ----------------------------------------------------- | -------------------------------------------------- |
| cli 升版号但 core 没升 | check.mjs 会拦下 |
| `1.0.0` 当 beta 直接发 | 占了 `latest` tag所有用户被强升撤回成本极高 |
| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` |
| Node 徽章 `>=18`、engines `>=22.12` 不一致 | 用户在 Node 18 上 `npm i` 被 engine 警告或直接失败 |
| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上publish 报 404 |
| CI 用 Node 22npm 10跑 publish | npm 10 不支持 OIDC token 交换publish 报 404 |
| 漏点 | 后果 |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli |
| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 |
| cli 升版号但 core 没升 | check.mjs 会拦下 |
| 发版漏更 CHANGELOG或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 |
| `1.0.0` 当 beta 直接发 | 占了 `latest` tag所有用户被强升撤回成本极高 |
| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` |
| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17` | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 |
| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上publish 报 404 |
| CI 用 Node 22npm 10跑 publish | npm 10 不支持 OIDC token 交换publish 报 404 |
| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npmCI 明确报错并要求先升级版本号 |
| channel job 缺少 `contents: write` | `gh release create` 失败 |
| stable 未先推 tag 就建 Release | `--verify-tag` 失败 |
+77
View File
@@ -0,0 +1,77 @@
# Skill 文案 / 路由 / 安装约定
## 触发条件
-`skills/*/SKILL.md` 的 description、路由表、consent、安全闸、hand-off、references 落款
- 调整 `bailian-protocol` 与业务 skill 的关系,或业务 skill 之间的软 hand-off 约定
- 新增 / 拆分 / 合并 `bailian-*` 业务 skill或改 `tools/generate-reference.ts``GROUP_OWNER_SKILL` 归属(与命令增删改交叉时两边都看)
- 给业务 skill 补安装说明、README或统一「勿猜 flag → `reference/`」类约定
纯改生成物 `skills/*/reference/*.md`(由命令 metadata 驱动)→ 走 [command-add-remove.md](command-add-remove.md) / [command-flag-change.md](command-flag-change.md)**不要手改 reference**。
## 统一口径(安装)
1. **Supported install** `bl skill init`(装齐 registry 中全部 `bailian-*`,含 `bailian-protocol`
2. **`bailian-protocol` 是共享协议 skill**,业务 skill 执行前应 Read 它
3. **不要**在 frontmatter 写 `companions`也不要对外说「companions = 安装器硬依赖」
4. 子集安装:`bl skill add --name bailian-protocol,<skill>`;漏装 protocol 会导致相对路径 Read 失败
5. **`bl skill add --all`** 安装 registry 全量(含 `spark-video` 等非 bailian 技能);一键安装 / `bl update``skill init`,不要用 `--all`
## 概念图
```text
bailian-protocol ← 共享协议consent / 鉴权 / 版本 / 错误上报)
▲ 靠 `bl skill init` 与业务 skill 同装;非安装器强制 companions
┌───────┴────────┬────────────────┬──────────────────┐
bailian-gen bailian-finetune bailian-managed-agent
(领域路由表) (领域工作流) IaC 安全闸)
│ │ │
└────────────────┼──────────────────┘
▼ 软 hand-off按 skill 名)
bailian-clihub
hub 路由表:本职命令 + 领域 hand-off 行
细节 → 各 skill reference/(生成)
```
## 必查清单
### A. 分层边界
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
- [ ] **协议读取**CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
- [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提
- [ ] **Hub vs 领域**`bailian-cli` 的「When to use which command」只列 hub 拥有的意图;媒体 / 精调 / managed-agent 各留 hand-off 行,**不抄**领域默认模型与子命令明细
- [ ] **渐进披露**SKILL 写意图路由与领域硬规则flags / usage / examples 以 `reference/``bl <command> --help` 为准,表后保留「勿猜 flag」指向句
### B. 文案与落款一致性
- [ ] 领域 skillgen / finetune / managed-agent路由或命令表后有指向 `reference/` 的句;文末 `## references`protocol + reference与家族对齐
- [ ] description 含 WHAT + WHEN + 反触发;安装说明指向 `bl skill init`,不写 companions 必装
- [ ] Quick examples 只演示本 skill 职责hub 不示范 `bl image` / `bl video` 等)
- [ ] 若改了安装方式:同步 `README.md` / `README.zh.md` / `INSTALL.md` / `skills/*/README*` / `skills/bailian-protocol/assets/setup.md` 中的 `bl skill init` / `bl skill add …` 示例(改 `INSTALL.md` 时按 [install-doc-change.md](install-doc-change.md) 同步静态页)
### C. 归属与生成
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts``GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表hub 最多加一行 hand-off
- [ ]`pnpm run sync:skill-assets`(或 commit 走 pre-commit提交生成的 `reference/` 与 version 同步结果
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
## 完成后自查
```sh
pnpm run sync:skill-assets
# 已发布版本试装
bl skill init
```
抽查:打开 `skills/bailian-cli/SKILL.md` 确认无领域子命令明细表、无 `companions`;打开对应领域 skill 确认有「勿猜 flag」与 hand-off。
## 常见漏点
- ✗ hub 路由表再次抄回 image / video / finetune / managed-agent 明细 → token 膨胀且与领域 skill 双份漂移
- ✗ 重新加回 `companions` 并宣称安装器硬依赖 → 与 `bl skill add` 合同不符
- ✗ 软 hand-off 写成硬路径 `../bailian-*/SKILL.md` 当执行前提 → 子集安装断链
- ✗ 只改 SKILL、忘改 `GROUP_OWNER_SKILL` → reference 落错 skill
- ✗ 手改 `skills/*/reference/*.md` → 下次 generate 被覆盖
- ✗ 改默认模型只动 flag description / reference忘改领域 SKILL「When to use which command」表见 [model-add-remove.md](model-add-remove.md)
+4 -5
View File
@@ -115,19 +115,18 @@ pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3
### 子进程调用方式
- **实际执行**`node packages/cli/src/main.ts <args>``cwd``packages/cli`
- **实际执行**仓库本地 `tsx src/main.ts <args>``cwd``packages/cli`
- **禁止**用 `pnpm run dev` 跑子任务:`pnpm` 会向 stdout 打生命周期日志,污染 JSON 解析
- **报告中的「完整命令」**:用 `pnpm run dev ...` 展示(`buildDisplayCommand`
### 必须带的 CLI 参数(通用)
- `--non-interactive`
-`speech recognize` 外,压测子进程宜带 `--output json`(语音识别以 `--out` 文件为准 stdout 可能为纯文本)
- 异步类命令带 `--timeout`、对应 `--poll-interval`
**禁止**对子进程加 `--quiet`(与 `--output json` 并存时可能丢 `urls` / `video_url`)。
**禁止**对视频相关子进程加 `--no-wait`;须阻塞到任务完成(及下载路径正确时落盘)。
**禁止**对视频相关子进程加 `--async`;须阻塞到任务完成(及下载路径正确时落盘)。
### 成功 / 失败判定(概要)
@@ -192,8 +191,8 @@ pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3
### 只改压测脚本时
- [ ] `lib/paths.mjs` 解析的 `CLI_PACKAGE` / `MONOREPO_ROOT` 仍正确
- [ ] 子进程仍为 `node` + `src/main.ts`,未改回裸 `pnpm run dev` 执行任务
- [ ] 未对子进程加 `--quiet`,视频未加 `--no-wait`
- [ ] 子进程仍为仓库本地 `tsx` + `src/main.ts`,未改回裸 `pnpm run dev` 执行任务
- [ ] 未对子进程加 `--quiet`,视频未加 `--async`
- [ ] `parsers.mjs` 与文档中的成功判定一致
- [ ]`package.json` 仅保留 `test:stress` 入口指向 `run.mjs`
- [ ] `node --check` 对相关 `.mjs` 通过,`pnpm run test:stress -- list` 可运行
+165
View File
@@ -0,0 +1,165 @@
# 埋点变更
## 触发条件
- 调整 AEM 命令事件、事件字段或参数 allowlist
- 调整 `User-Agent``x-dashscope-source-config` 或其他后端渠道标识
- 新增鉴权域、请求网关或绕开统一 Client 的网络出口
- 排查命令量、成功率、版本、鉴权域或后端渠道数据不一致
## 当前数据流
三套鉴权对应三套请求域,但不代表三套网关使用相同的后端埋点。命令侧另有一套覆盖所有实际执行命令的 AEM 客户端事件,两者必须分开理解。
```text
命令进入 run
├─ telemetryStage
│ ├─ ~/.bailian/telemetry.jsonl
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
└─ authStage
├─ apiKey → DashScope / 模型域
├─ console → Bailian Console Gateway
├─ openapi → 阿里云 OpenAPI
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
```
### 1. 三套鉴权与埋点标识
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识AEM |
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
| `auth: "apiKey"` | API KeyDashScope / OpenAI-compatible 模型域 | `Client.request/requestJson``McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent``x-dashscope-source-config` | 有:`pid=bailian-cli-node``authMethod=apiKey` |
| `auth: "console"` | Console access tokenBailian Console Gateway | `callConsoleGateway()``/cli/api.json` | 无 | 有:`pid=bailian-cli-node``authMethod=console` |
| `auth: "openapi"` | AccessKey ID/Secret可选 STS token阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config` | 有:`pid=bailian-cli-node``authMethod=openapi` |
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node``authMethod=none` |
`authMethod` 记录的是命令声明的鉴权域,不是凭证来源。它不会区分 API Key 来自 flag、env 还是 config。
鉴权域是命令的准入门槛和主请求域,不保证命令内部只有一种网络出口;例如部分 `apiKey` 命令也可能读取匿名 Console 公共目录Managed Agent 还可能访问其他 provider。
表中的后端埋点按该鉴权域的主要业务请求填写:
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 仅对阿里云 host 注入
- DashScope 上传策略 `getPolicy` 只有 `x-dashscope-source-config`,没有显式 CLI `User-Agent`
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product``action``api` 是鉴权或路由字段,不计为埋点标识
### 2. 后端渠道参数
当前 `x-dashscope-source-config` 结构为:
```json
{
"channel": "bailian-cli",
"tags": {
"t1": "public",
"t2": "bl 或 kscli",
"t3": "实际 CLI 版本"
}
}
```
- `t2` 取产品 `identity.binName`:完整 CLI 为 `bl`Knowledge Studio CLI 为 `kscli`
- `t3` 取产品 `identity.version`,由产品入口的 `package.json` 注入
- `channel``t1` 是当前固定口径
- `User-Agent` 是独立标识:`bl``bailian-cli/<version>``kscli``knowledge-studio-cli/<version>`
source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网络传输:
| 请求 | source-config |
| ------------------------------------ | ------------- |
| 模型 API、任务提交与轮询 | 有 |
| Bailian MCP / OpenAPI | 有 |
| DashScope 上传策略 `getPolicy` | 有 |
| OSS 文件上传 | 无 |
| 图片、视频、音频、转录结果下载 | 无 |
| npm / 二进制更新检查、Skill registry | 无 |
当前已知例外Pipeline runtime 自建的 `Identity.version``0.0.0-dev`,因此 Pipeline 内部模型请求的 `t3` 不代表产品包版本;现阶段不纳入本轮收敛。
### 3. 全命令 AEM 客户端埋点
`packages/runtime/src/middleware.ts``telemetryStage` 包裹 `authStage` 与命令执行,因此成功、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`
- 根帮助、子命令 `--help``--version`
- 未识别命令、参数解析失败、缺少必填参数
- `defineCommand.validate` 在 dispatch 阶段拒绝的请求
遥测默认开启;`DO_NOT_TRACK=1` 一票否决,配置文件 `telemetry: false` 也可关闭。关闭后本地和远端均不记录。
单条 `TrackingEvent` 当前包含:
- `command``timestamp``durationMs``success`
- `cliVersion``nodeVersion``os`
- `authMethod`
- 失败时的 `errorMessage``httpStatus``requestId`
- 安全 allowlist 过滤后的 `params`
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts``PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
事件同时写入两处:
1. 本地 `~/.bailian/telemetry.jsonl`:权限 `0600`,超过 5 MB 后重建
2. AEM`pid=bailian-cli-node`,源码运行自动使用 `env=dev`npm 安装或编译二进制使用 `env=prod`
底层 Node tracker 还会附加公共设备字段OS 类型/版本、Node 应用名与版本、平台,以及由本机网络标识计算的 MD5 `device_id`
当前 AEM 事件没有 `binName``clientName` 产品维度,并且 `bl``kscli` 共用 `pid=bailian-cli-node`。两边相同路径的 `config show``config set``update` 无法仅凭当前事件稳定区分产品Knowledge 命令虽然因路径映射不同而表现为 `knowledge chat``chat`,也不应把命令路径当作长期产品标识。后端 source-config 的 `t2` 已能区分 `bl/kscli`,但这个维度尚未进入 AEM 客户端事件。
AEM 映射:
| AEM 字段 | 内容 |
| ---------- | ----------------------------------------- |
| event name | 命令路径 |
| `et` | `EXP` |
| `ext` | 除 `command``params` 外的结构化事件字段 |
| `c1` | allowlist 参数 |
| `c2` | `success` / `failure` |
| `c3` | HTTP status |
| `c4` | 错误文案,最多 500 字符 |
| `c5` | request ID |
远端发送是 best-effort不得阻塞命令或改变退出码。正常退出最多等待 1 秒SIGINT 最多等待 500 ms。
## 必查清单
### A. 新增或调整命令
- [ ] `defineCommand({ auth })` 必须声明真实请求域AEM 的 `authMethod` 直接读取该值
- [ ] 新命令进入 `run` 后自动有基础事件,不得在命令内重复发送同名事件
- [ ] 需要按产品分析 AEM 数据时,必须显式设计产品字段;不得从命令路径推断 `bl/kscli`
- [ ] 只有可枚举、数值或布尔等低风险字段才可加入 `PARAM_ALLOWLIST`
- [ ] 新增 console raw API flag 时只允许记录公开 API 名,不得记录请求 `data`
### B. 调整后端渠道参数
- [ ] 同时核对 `packages/core/src/client/http.ts``mcp.ts``instrumented-fetch.ts``client.ts``files/upload.ts`
- [ ] 产品身份必须来自 `Identity`;不得从命令路径、环境变量或 `process.argv` 猜测
- [ ] `bl``kscli` 必须分别验证 `binName``clientName``version`
- [ ] OSS、结果文件、npm、二进制和 Skill 下载不得为了业务渠道统计新增 source-config
- [ ] 改 URL / host 范围时同时执行 [URL / 渠道变更](url-change.md) 清单
### C. 调整 AEM 事件
- [ ] 更新 `TrackingEvent``createTrackingEvent()``buildRemoteAemOptions()` 的字段映射
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
- [ ] 检查 `DO_NOT_TRACK=1``telemetry: false` 两个关闭入口
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
## 完成后自查
```sh
rg -n "trackingHeaders|x-dashscope-source-config|User-Agent" packages --glob '*.ts'
rg -n "trackCommandExecution|PARAM_ALLOWLIST|buildRemoteAemOptions" packages/core packages/runtime --glob '*.ts'
vp check
vp test packages/core/tests packages/commands/tests/e2e/auth.e2e.test.ts
```
## 常见漏点
- ✗ 只看 AEM 命令事件,误以为它能替代网关侧请求渠道统计
- ✗ 把 `authMethod` 当成实际凭证来源;它只是命令声明的鉴权域
- ✗ 新增 bypass `fetch` 后漏掉应由网关消费的 source-config或把它发给 OSS / npm / 第三方下载地址
- ✗ 只改 `bl` 入口,导致 `kscli` 的产品名或版本标签错误
- ✗ 把帮助、版本或参数校验失败算进“全部命令”;这些路径当前没有进入 telemetry middleware
+18 -13
View File
@@ -13,12 +13,15 @@
core/config/schema.ts ← API endpoint / 文档站(region-aware)
REGIONS{cn, us, intl} dashscope.aliyuncs.com 等
DOCS_HOSTS{cn, us, intl} help.aliyun.com/zh/model-studio
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (POP API)
BAILIAN_HOST bailian.cn-beijing.aliyuncs.com (OpenAPI)
cli/src/urls.ts ← 用户面控制台 URL(cn-only)
runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
BAILIAN_CONSOLE_ROOT bailian.console.aliyun.com
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
@@ -29,14 +32,16 @@ core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
### A. TS 源码(必须 import,不准硬编码)
- [ ] `packages/core/src/config/schema.ts` 是所有 API/docs 基址的源头
- [ ] `packages/cli/src/urls.ts` 是所有用户面控制台 URL 的源头
- [ ] `packages/runtime/src/urls.ts` 是所有用户面控制台 URL 的源头
- [ ] 改完后 grep 验证:
```sh
# 控制台 URL — 应只在 urls.ts 出现
grep -rnE "https://bailian\.console\.aliyun\.com" packages/ --include="*.ts" \
| grep -v "node_modules" | grep -v "/dist/"
# 期望:匹配 packages/cli/src/urls.ts
# 期望:匹配 packages/runtime/src/urls.ts;
# 当前遗留例外:packages/commands/src/commands/auth/login-console.ts(登录站点映射)、
# packages/core/src/advisor/recommend.ts(模型文档 deep link)。触碰时优先收敛到统一 URL 模块。
# API endpoint — 应只在 schema.ts 和 upload.ts 出现
grep -rnE "https://dashscope[a-z-]*\.aliyuncs\.com" packages/ --include="*.ts" \
@@ -46,29 +51,29 @@ grep -rnE "https://dashscope[a-z-]*\.aliyuncs\.com" packages/ --include="*.ts" \
### B. 非 TS 文件(只能人工同步,无法 import)
- [ ] `skills/bailian-cli/reference/``<group>.md` 中 API/控制台 URL(`generate:reference` 重建后核对并提交)
- [ ] `skills/*/reference/``<group>.md` 中 API/控制台 URL(`generate:reference` 重建后核对并提交)
- [ ] `README.md` / `README.zh.md` 中所有 URL
### C. 渠道追踪参数
- [ ] **当前现状**:全仓不带 `source_channel=aliway`追踪参数
- [ ] 如未来要恢复以收集分析数据,**统一评估再加回**(不要单点恢复造成不一致)
- [ ] 全仓 grep `source_channel=`,确认无残留
- [ ] **当前现状**:TS 源码不带 `source_channel=...`;README / package README 中保留 `cli_github` / `key_github` 等用户入口追踪参数
- [ ] 如未来调整追踪参数,统一评估 README、`packages/cli/README*``packages/core/README*` 与 package homepage,不要单点造成不一致
- [ ] grep `source_channel=`,确认每个残留都属于预期用户面文档或已批准的追踪入口
## 完成后自查
```sh
# 验证错误 hint 不再泄漏旧 URL
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message x --non-interactive
HOME=/tmp/empty pnpm -F bailian-cli exec tsx src/main.ts text chat --message x
# 看输出的 Get API Key URL 是否走新值
# 验证 banner / help
node packages/cli/src/main.ts # banner
node packages/cli/src/main.ts help # help 命令
pnpm -F bailian-cli exec tsx src/main.ts # banner
pnpm -F bailian-cli exec tsx src/main.ts help # help 命令
```
## 常见漏点
- ✗ 改了 `urls.ts` 但忘记同步 README(用户最先看到)
- ✗ 在 cli 命令文件里 inline `https://bailian.console.aliyun.com/...` 而不是 `${API_KEY_PAGE}`
- ✗ 改了 `urls.ts` / 登录站点 / 文档 deep link 但忘记同步 README(用户最先看到)
- ✗ 在 runtime / command 文件里 inline `https://bailian.console.aliyun.com/...` 而不是 `urls.ts` import
- ✗ 在 core 的 hint 里写 URL(违反 [error-hint-change.md](error-hint-change.md) 不变量 1)
+4 -1
View File
@@ -16,15 +16,18 @@
"ready": "vp check && vp run -r test && vp run -r build",
"prepare": "vp config",
"check": "vp check",
"sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
"sync:skill-assets": "pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version",
"dev": "pnpm -F bailian-cli-core dev",
"bl": "pnpm -F bailian-cli dev",
"kscli": "pnpm -F knowledge-studio-cli dev",
"test": "vp test",
"release:check": "node tools/release/check.mjs",
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
"test:stress": "node packages/cli/tests/stress/run.mjs"
},
"dependencies": {},
"devDependencies": {
"tsx": "catalog:",
"vite-plus": "catalog:"
},
"engines": {
+4 -1
View File
@@ -2,4 +2,7 @@ node_modules
dist
*.log
.DS_Store
outputs/
outputs/
# agents
agents.state.json
.env
+99 -102
View File
@@ -5,7 +5,7 @@
**The official command-line interface for Aliyun Model Studio (DashScope) AI Platform**
[![npm version](https://img.shields.io/npm/v/bailian-cli?color=0969da&label=npm)](https://www.npmjs.com/package/bailian-cli)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18.17-brightgreen)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
@@ -13,8 +13,9 @@
---
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Built for AI Agents. Every command works as a structured tool call._
@@ -22,23 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
- **Text chat** — Qwen3.7-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 2.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — HappyHorse-1.0 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 520s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Console capabilities** — Browse Bailian apps (`app list`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
## Showcase: One-Sentence Cinematic Video
## Showcase 1: A Cinematic Short Film from One Sentence
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -51,118 +45,114 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.0**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2分钟左右的视频尺寸是16:9")_
### How it works
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.0** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
## Installation
```bash
npm install -g bailian-cli
npx skills add modelstudioai/cli --all -g
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
> Requires Node.js >= 22.12.
**Install with NPM**
```bash
npm install -g bailian-cli
bl skill init
```
> Requires Node.js >= 18.17.
**Install on macOS/Linux**
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
```
> No Node.js required. The installer automatically installs Bailian Skills.
**Install on Windows**
```powershell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
```
> No Node.js required. The installer automatically installs Bailian Skills.
## Quick Start
```bash
# Authenticate, recommended
bl auth login --console
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Browse apps / free-tier quota / usage statistics / workspaces
bl app list
bl usage free --model qwen3-max
bl usage free --expiring 30 # Quotas expiring within 30 days
bl usage free --sort remaining # Sort by remaining % ascending
bl usage stats --workspace-id <id> # Usage overview for a workspace
bl usage stats --model qwen-turbo --workspace-id <id> # Per-model usage
bl workspace list # List all workspaces
# Rate limit management
bl quota list # View RPM/TPM limits for all models
bl quota list --model qwen3.6-plus # View limits for a specific model
bl quota check # Current usage vs rate limits
bl quota check --model qwen3.6-plus --period 5 # Check usage over last 5 minutes
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota change history
```
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## Authentication
### DashScope API Key
### API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
```
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
### Console Login (OAuth)
Required for console capability commands (`app list`, `usage free`, `usage stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
```
### Alibaba Cloud AK/SK (Knowledge Base only)
### Alibaba Cloud OpenAPI AK/SK
Required for `knowledge retrieve`. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
```
## Configuration
@@ -171,17 +161,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# View current config
bl config show
# Set defaults
bl config set --key region --value us
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# List all config profiles
bl config list
# Self-update to latest version
bl update
# Switch config profile
bl config use --name token-plan
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -191,12 +195,5 @@ Config file location: `~/.bailian/config.json`
| Qwen Model List | https://help.aliyun.com/zh/model-studio/getting-started/models |
| Aliyun Model Studio Console | https://bailian.console.aliyun.com/?source_channel=cli_github |
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+99 -98
View File
@@ -5,7 +5,7 @@
**阿里云百炼 (DashScope) AI 平台命令行工具**
[![npm version](https://img.shields.io/npm/v/bailian-cli?color=0969da&label=npm)](https://www.npmjs.com/package/bailian-cli)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org)
[![Node.js](https://img.shields.io/badge/node-%3E%3D18.17-brightgreen)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
@@ -22,23 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.7-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 2.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — HappyHorse-1.0 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **控制台能力** — 浏览百炼应用(`app list`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -48,116 +41,117 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.0**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
> _帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2 分钟左右的视频尺寸是 16:9。”_
### 工作流程
## 示例 2一句话构建短片导演 Managed Agent
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.0**
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用能够实现短片拍摄导演专家生成视频然后也能进行设计对应的分镜图。”_
## 安装
```bash
npm install -g bailian-cli
npx skills add modelstudioai/cli --all -g
**Agent 安装(推荐)**
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
> 需要预先安装 Node.js >= 22.12。
**NPM 安装**
```bash
npm install -g bailian-cli
bl skill init
```
> 需要预先安装 Node.js >= 18.17。
**macOS/Linux 安装**
```bash
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
```
> 无需预先安装 Node.js安装脚本会自动安装 Bailian Skills。
**Windows 安装**
```powershell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
```
> 无需预先安装 Node.js安装脚本会自动安装 Bailian Skills。
## 快速开始
```bash
# 认证
bl auth login --api-key sk-xxxxx
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 浏览应用 / 免费额度 / 用量统计 / 业务空间
bl app list
bl usage free --model qwen3-max
bl usage free --expiring 30 # 30 天内过期的额度
bl usage free --sort remaining # 按剩余百分比升序排列
bl usage stats --workspace-id <id> # 指定空间的用量概览
bl usage stats --model qwen-turbo --workspace-id <id> # 指定模型用量
bl workspace list # 列出所有业务空间
# 限流管理与提额
bl quota list # 查看所有模型的 RPM/TPM 限额
bl quota list --model qwen3.6-plus # 查看指定模型限额
bl quota check # 查看当前用量 vs 限流阈值
bl quota check --model qwen3.6-plus --period 5 # 查看最近 5 分钟用量
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
```
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### DashScope API Key
### API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json
bl auth login --api-key sk-xxxxx
```
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
### 控制台登录OAuth
控制台能力命令(`app list``usage free``usage stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 AK/SK仅知识库检索
### 阿里云 OpenAPI AK/SK
`knowledge retrieve` 命令需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
```
## 配置
@@ -166,17 +160,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# 查看当前配置
bl config show
# 设置默认值
bl config set --key region --value us
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 查看全部配置档
bl config list
# 自更新到最新版本
bl update
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -186,12 +194,5 @@ bl update
| 通义千问模型列表 | https://help.aliyun.com/zh/model-studio/getting-started/models |
| 阿里云百炼控制台 | https://bailian.console.aliyun.com/?source_channel=cli_github |
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+21 -22
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.3.2",
"version": "1.15.1",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
@@ -25,7 +25,8 @@
},
"files": [
"dist",
"README.zh.md"
"README.zh.md",
"postinstall.js"
],
"type": "module",
"exports": {
@@ -33,43 +34,41 @@
"./package.json": "./package.json"
},
"publishConfig": {
"exports": {
".": "./dist/bailian.mjs",
"./package.json": "./package.json"
},
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"generate:reference": "node --experimental-strip-types ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference'",
"sync:skill-version": "node --experimental-strip-types ../../tools/sync-skill-metadata.ts",
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference'",
"sync:skill-version": "tsx ../../tools/sync-skill-metadata.ts",
"build": "vp pack",
"dev": "node src/main.ts",
"dev": "tsx src/main.ts",
"test": "vp test",
"check": "vp check"
"check": "vp check",
"postinstall": "node postinstall.js"
},
"dependencies": {
"bailian-cli-commands": "workspace:*",
"bailian-cli-core": "workspace:*",
"boxen": "catalog:",
"chalk": "catalog:",
"undici": "catalog:"
"bailian-cli-runtime": "workspace:*",
"tar-stream": "catalog:"
},
"devDependencies": {
"@clack/prompts": "^0.7.0",
"@types/node": "catalog:",
"@typescript/native-preview": "7.0.0-dev.20260328.1",
"ajv": "catalog:",
"boxen": "catalog:",
"chalk": "catalog:",
"e2e": "workspace:*",
"typescript": "^6.0.2",
"vite-plus": "catalog:",
"undici": "catalog:",
"vite-plus": "0.1.22",
"yaml": "catalog:"
},
"engines": {
"node": ">=22.12.0"
},
"inlinedDependencies": {
"@clack/core": "0.3.5",
"@clack/prompts": "0.7.0",
"ajv": "8.20.0",
"fast-deep-equal": "3.1.3",
"fast-uri": "3.1.2",
"json-schema-traverse": "1.0.0",
"picocolors": "1.1.1",
"sisteransi": "1.0.5",
"yaml": "2.8.3"
"node": ">=18.17.0"
}
}
+253
View File
@@ -0,0 +1,253 @@
/**
* postinstall.js — Wiki data sync (layer 1: triggered by npm install)
*
* Runs automatically after npm/pnpm installs bailian-cli: unconditionally downloads the full Wiki data
* package and overwrites the local directory, ensuring data is in place the first time the user runs
* `bl advisor recommend`.
*
* Flow (unified skill publishing protocol: skills/index.json + one content-addressed object per skill):
* 1. Download skills/index.json from public-read OSS, get the bailian-docs-llm-wiki entry
* 2. Download skills/bailian-docs-llm-wiki/<entry.object> (sha256-<hex>.tar.br, brotli q6, ~2.3MB);
* legacy fallback to skill.tar.br when the entry has no valid object field
* 3. Node built-in brotli decompress + tar-stream extract (per-entry path safety check) to same-volume temp dir,
* then recompute contentHash over the extracted files and reject on mismatch (symmetric with core installer)
* 4. renameSync atomic swap into ~/.bailian/skills/bailian-docs-llm-wiki/
* 5. Write ~/.bailian/wiki-sync-state.json
* 6. Write ~/.bailian/skills/skill-lock.json record (same ledger as bl skill)
*
* Design constraints:
* - Unconditional overwrite: every install fully replaces, no version comparison
* - Silent failure: any step failure → console.warn → process.exit(0), never blocks install
* - Standalone implementation: does not import bailian-cli-core, avoiding ESM path issues after bundling
* - Depends on Node built-in modules + tar-stream (consistent with sync.ts / publisher skills-publish.mjs)
*/
import { createHash } from "node:crypto";
import {
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createBrotliDecompress } from "node:zlib";
import tar from "tar-stream";
const REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills";
const WIKI_SKILL_NAME = "bailian-docs-llm-wiki";
const CONFIG_DIR_NAME = ".bailian";
const SKILL_DIR_NAME = "skills/bailian-docs-llm-wiki";
const STATE_FILE_NAME = "wiki-sync-state.json";
const INDEX_KEY = "index.json";
/** Legacy fixed asset key (entries without a valid content-addressed object field) */
const LEGACY_ASSET_NAME = "skill.tar.br";
/** Same strict shape check as core registry.ts: only a valid object name may enter the URL */
const OBJECT_FILE_RE = /^sha256-[0-9a-f]{64}\.tar\.br$/;
const INDEX_TIMEOUT_MS = 3000;
const DOWNLOAD_TIMEOUT_MS = 30000;
function getConfigDir() {
if (process.env.BAILIAN_CONFIG_DIR) return process.env.BAILIAN_CONFIG_DIR;
return join(homedir(), CONFIG_DIR_NAME);
}
function getCatalogDir() {
return join(getConfigDir(), SKILL_DIR_NAME);
}
function getStatePath() {
return join(getConfigDir(), STATE_FILE_NAME);
}
function getSkillLockPath() {
return join(getConfigDir(), "skills", "skill-lock.json");
}
/**
* Record this sync in skill-lock.json (same ledger as bl skill; list shows installed).
* Semantics aligned with upsertSkillLockEntry in core/src/skills/lock.ts: shallow-merge with the existing
* entry, preserving fields like links written by bl skill add; rebuild as empty table if lock is corrupted/unrecognized.
* best-effort: failure does not affect data sync results.
*/
function upsertSkillLock(name, entry) {
try {
let lock = { version: 1, skills: {} };
try {
const parsed = JSON.parse(readFileSync(getSkillLockPath(), "utf-8"));
if (parsed?.version === 1 && parsed.skills && typeof parsed.skills === "object") {
lock = parsed;
}
} catch {
/* absent/corrupted → empty table */
}
lock.skills[name] = { ...lock.skills[name], ...entry };
mkdirSync(dirname(getSkillLockPath()), { recursive: true });
writeFileSync(getSkillLockPath(), JSON.stringify(lock, null, 2) + "\n");
} catch {
/* Bookkeeping failure does not block install; advisor-side sync will backfill */
}
}
async function fetchJson(url, timeoutMs) {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function downloadBuffer(url) {
const res = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
/** tar 条目路径必须是相对路径且不含 ..,防止 tar-slip 逃逸解包目录 */
function isSafeEntryName(name) {
// Symmetric with core skills/extract.ts: backslashes can escape the extraction
// dir on Windows (path.join expands "\.." segments, leading "\" hits drive root)
if (name.includes("\\") || name.includes("\0")) return false;
if (name.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(name)) return false;
return !name.split("/").includes("..");
}
/** Brotli decompress + tar-stream extract into destDir (symmetric with publisher tar.pack()). */
async function extractTarBr(tarBrBuffer, destDir) {
const extract = tar.extract();
extract.on("entry", (header, stream, next) => {
if (!isSafeEntryName(header.name)) {
// Same semantics as core skills/extract.ts: destroy so the pipeline rejects with this
// error; silence the entry stream to avoid its companion error becoming unhandled
stream.on("error", () => {});
stream.resume();
extract.destroy(new Error(`unsafe tar entry: ${header.name}`));
return;
}
const filePath = join(destDir, header.name);
if (header.type === "directory") {
mkdirSync(filePath, { recursive: true });
stream.resume();
stream.on("end", next);
return;
}
mkdirSync(dirname(filePath), { recursive: true });
const ws = createWriteStream(filePath);
stream.pipe(ws);
ws.on("finish", next);
ws.on("error", next);
});
await pipeline(Readable.from(tarBrBuffer), createBrotliDecompress(), extract);
}
/**
* Recompute the publisher's deterministic content hash over an extracted directory
* (same accumulation as core skills/extract.ts computeDirContentHash): regular files
* sorted by "/"-separated relative path, sha256 over relPath + bytes.
*/
function computeDirContentHash(dir) {
const relPaths = [];
const walk = (sub) => {
for (const dirent of readdirSync(sub ? join(dir, sub) : dir, { withFileTypes: true })) {
const rel = sub ? `${sub}/${dirent.name}` : dirent.name;
if (dirent.isDirectory()) walk(rel);
else if (dirent.isFile()) relPaths.push(rel);
}
};
walk("");
relPaths.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
const hash = createHash("sha256");
for (const rel of relPaths) {
hash.update(rel);
hash.update(readFileSync(join(dir, rel)));
}
return `sha256:${hash.digest("hex")}`;
}
/** Atomic swap: tmpDir (same volume) → catalogDir. */
function atomicSwap(tmpDir, catalogDir) {
mkdirSync(dirname(catalogDir), { recursive: true });
const backup = `${catalogDir}.old-${Date.now()}`;
if (existsSync(catalogDir)) renameSync(catalogDir, backup);
try {
renameSync(tmpDir, catalogDir);
} catch (err) {
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
throw err;
}
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
}
async function main() {
// 1. Download skills/index.json and get the wiki entry
const index = await fetchJson(`${REGISTRY_BASE_URL}/${INDEX_KEY}`, INDEX_TIMEOUT_MS);
const entry = index?.skills?.[WIKI_SKILL_NAME];
if (!entry?.contentHash)
throw new Error("no bailian-docs-llm-wiki entry (or contentHash) in index.json");
// 2. Download the skill archive: content-addressed object first, legacy fixed key as fallback
const assetName =
entry.object && OBJECT_FILE_RE.test(entry.object) ? entry.object : LEGACY_ASSET_NAME;
const tarBuf = await downloadBuffer(`${REGISTRY_BASE_URL}/${WIKI_SKILL_NAME}/${assetName}`);
// 3. Extract to same-volume temp dir + integrity check + atomic swap
const catalogDir = getCatalogDir();
const tmpDir = `${catalogDir}.tmp-${process.pid}-${Date.now()}`;
try {
mkdirSync(tmpDir, { recursive: true });
await extractTarBr(tarBuf, tmpDir);
// Symmetric with layer 2 (core installer): reject archive/index fingerprint mismatch
// before touching the canonical dir
if (entry.contentHash.startsWith("sha256:")) {
const actualContentHash = computeDirContentHash(tmpDir);
if (actualContentHash !== entry.contentHash) {
throw new Error(
`content hash mismatch: index says ${entry.contentHash}, archive is ${actualContentHash}`,
);
}
}
atomicSwap(tmpDir, catalogDir);
} catch (err) {
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
// 4. Write state
try {
writeFileSync(
getStatePath(),
JSON.stringify({ lastChecked: Date.now(), contentHash: entry.contentHash }),
);
} catch {
/* state write failure has no impact: first recommend will re-check */
}
// 5. skill-lock.json record: wiki shares the same ledger as bl skill
upsertSkillLock(WIKI_SKILL_NAME, {
contentHash: entry.contentHash,
...(entry.publishedAt ? { publishedAt: entry.publishedAt } : {}),
installedAt: new Date().toISOString(),
sourceType: "oss",
...(entry.description ? { description: entry.description } : {}),
});
process.stdout.write(`bailian-cli: wiki data ready (${entry.publishedAt ?? "latest"})\n`);
}
main().catch((err) => {
// Unconditional pass-through: install-time network/permission issues should not block npm install;
// sync.ts will fall back to syncing on the first `bl advisor recommend`.
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(
`bailian-cli: wiki data pre-download skipped (${msg}); will sync automatically on first use.\n`,
);
// Force a success exit code so a download failure never fails `npm install`.
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
});
-186
View File
@@ -1,186 +0,0 @@
import type { GlobalFlags } from "bailian-cli-core";
import type { OptionDef } from "bailian-cli-core";
import { BailianError, ExitCode } from "bailian-cli-core";
function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
}
/** Extract camelCase flag name from an OptionDef.flag string, e.g. '--max-tokens <n>' → 'maxTokens' */
function flagKey(def: OptionDef): string | null {
const m = def.flag.match(/^--([a-z][a-z0-9-]*)/i);
return m ? kebabToCamel(m[1]!) : null;
}
/** Boolean when no value placeholder and type is not string/number/array */
function isBooleanDef(def: OptionDef): boolean {
if (def.type === "boolean") return true;
if (def.type === "string" || def.type === "number" || def.type === "array") return false;
return !def.flag.includes("<") && !def.flag.includes("[");
}
interface FlagSchema {
booleans: Set<string>;
numbers: Set<string>;
arrays: Set<string>;
}
function buildAllowedFlagKeys(options: OptionDef[]): Set<string> {
const keys = new Set<string>();
for (const opt of options) {
const key = flagKey(opt);
if (key) keys.add(key);
}
return keys;
}
function buildSchema(options: OptionDef[]): FlagSchema {
const booleans = new Set<string>();
const numbers = new Set<string>();
const arrays = new Set<string>();
for (const opt of options) {
const key = flagKey(opt);
if (!key) continue;
if (isBooleanDef(opt)) booleans.add(key);
else if (opt.type === "number") numbers.add(key);
else if (opt.type === "array") arrays.add(key);
}
return { booleans, numbers, arrays };
}
/**
* Quick scan: collect positional (non-dash) args to determine the command path.
* Skips global flags and their values so that e.g. `--output json text chat`
* correctly produces ['text', 'chat'] instead of ['json', 'text', 'chat'].
*/
export function scanCommandPath(argv: string[], globalOptions: OptionDef[] = []): string[] {
const globalSchema = buildSchema(globalOptions);
const path: string[] = [];
let i = 0;
while (i < argv.length) {
const arg = argv[i]!;
if (arg === "--") break;
if (arg.startsWith("--")) {
const eqIdx = arg.indexOf("=");
const key = eqIdx !== -1 ? arg.slice(2, eqIdx) : arg.slice(2);
const camelKey = kebabToCamel(key);
if (!globalSchema.booleans.has(camelKey) && eqIdx === -1) {
const next = argv[i + 1];
// Command-local booleans (e.g. `--console`) are not in GLOBAL_OPTIONS; if the next
// token is another flag, do not consume it as this flag's value.
if (next === undefined || next.startsWith("-")) {
i += 1;
} else {
i += 2;
}
} else {
i += 1;
}
continue;
}
if (arg.startsWith("-")) {
i++;
continue;
}
path.push(arg);
i++;
}
return path;
}
/**
* Full flag parse. Types are derived entirely from the provided OptionDef schema:
* - boolean: no <value> placeholder in flag string (or type: 'boolean')
* - number: type: 'number'
* - array: type: 'array' (repeatable via multiple --flag occurrences)
* - default: string
*/
export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
const allowedKeys = buildAllowedFlagKeys(options);
const schema = buildSchema(options);
const flags: GlobalFlags = {
quiet: false,
verbose: false,
noColor: false,
yes: false,
dryRun: false,
help: false,
nonInteractive: false,
async: false,
};
let i = 0;
while (i < argv.length) {
const arg = argv[i]!;
if (arg === "--help" || arg === "-h") {
flags.help = true;
i++;
continue;
}
if (arg === "--") {
break;
}
if (arg.startsWith("--")) {
const eqIdx = arg.indexOf("=");
let key: string;
let value: string | undefined;
if (eqIdx !== -1) {
key = arg.slice(2, eqIdx);
value = arg.slice(eqIdx + 1);
} else {
key = arg.slice(2);
}
const camelKey = kebabToCamel(key);
if (!allowedKeys.has(camelKey)) {
throw new BailianError(
`Unknown flag "--${key}". Run with --help to see available options.`,
ExitCode.USAGE,
);
}
// Switch-style flags (--quiet, --dry-run): no value. Value flags need a non-flag next token.
if (schema.booleans.has(camelKey)) {
(flags as Record<string, unknown>)[camelKey] = true;
i++;
continue;
}
// --prompt <text>, --watermark <bool>, …
if (value === undefined) {
i++;
const next = argv[i];
if (next === undefined || next.startsWith("-")) {
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
}
value = next;
}
if (schema.arrays.has(camelKey)) {
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
if (arr) arr.push(value);
else (flags as Record<string, unknown>)[camelKey] = [value];
} else if (schema.numbers.has(camelKey)) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
throw new BailianError(`Flag --${key} requires a finite number.`, ExitCode.USAGE);
}
(flags as Record<string, unknown>)[camelKey] = numericValue;
} else {
(flags as Record<string, unknown>)[camelKey] = value;
}
}
i++;
}
return flags;
}
+14
View File
@@ -0,0 +1,14 @@
import type { CommandPackPolicy } from "bailian-cli-runtime";
/** Command Packs accepted by the bl product. */
export const commandPackPolicy = {
supported: {
"@ali/bailian-plugin-agent": {
commandPrefixes: ["agent"],
credentialAccess: ["apiKey"],
},
"@ali/bailian-plugin-inner-console-call": {
commandPrefixes: ["inner-console"],
},
},
} as const satisfies CommandPackPolicy;
+253
View File
@@ -0,0 +1,253 @@
import type { AnyCommand } from "bailian-cli-core";
import {
authLogin,
authStatus,
authLogout,
authGenerateAccessToken,
textChat,
textOmni,
imageGenerate,
imageEdit,
videoGenerate,
videoEdit,
videoRef,
videoTaskGet,
videoDownload,
visionDescribe,
configShow,
configSet,
configList,
configUse,
configUi,
configAgent,
update,
appCall,
appList,
memoryAdd,
memorySearch,
memoryList,
memoryUpdate,
memoryDelete,
memoryProfileCreate,
memoryProfileGet,
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
mcpCall,
mcpList,
mcpTools,
searchWeb,
speechSynthesize,
speechRecognize,
fileUpload,
consoleCall,
usageFree,
usageFreetier,
usageStats,
usageSummary,
usageTokenPlan,
usageCodingPlan,
pipelineRun,
pipelineValidate,
advisorRecommend,
modelList,
workspaceList,
quotaList,
quotaUpdate,
quotaHistory,
quotaCheck,
permissionList,
permissionGrant,
permissionRevoke,
datasetUpload,
datasetList,
datasetGet,
datasetDelete,
datasetValidate,
finetuneTextCreate,
finetuneAudioCreate,
finetuneImageCreate,
finetuneList,
finetuneGet,
finetuneCancel,
finetuneDelete,
finetuneLogs,
finetuneCheckpoints,
finetuneExport,
finetuneWatch,
finetuneCapability,
deployTextCreate,
deployAudioCreate,
deployImageCreate,
deployList,
deployGet,
deployModels,
deployScale,
deployUpdate,
deployDelete,
tokenPlanListSeats,
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
workspaceInit,
pluginInstall,
pluginLink,
pluginList,
pluginRemove,
skillAdd,
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentPlan,
managedAgentApply,
managedAgentDestroy,
managedAgentStateList,
managedAgentStateShow,
managedAgentStateRm,
managedAgentStateImport,
managedAgentSessionCreate,
managedAgentSessionList,
managedAgentSessionGet,
managedAgentSessionDelete,
managedAgentSessionRun,
managedAgentSessionSend,
managedAgentSessionEvents,
managedAgentSkillList,
} from "bailian-cli-commands";
// Full bailian-cli product: every command, exposed under the `bl` binary.
// The command paths below are this product's decision — the command library
// ships no presets, so the map is spelled out here. Kept in its own module
// (no side effects) so tools like generate-reference.ts can import it without
// starting the CLI.
export const commands: Record<string, AnyCommand> = {
"auth login": authLogin,
"auth status": authStatus,
"auth logout": authLogout,
"auth generate-access-token": authGenerateAccessToken,
"text chat": textChat,
omni: textOmni,
"image generate": imageGenerate,
"image edit": imageEdit,
"video generate": videoGenerate,
"video edit": videoEdit,
"video ref": videoRef,
"video task get": videoTaskGet,
"video download": videoDownload,
"vision describe": visionDescribe,
"config show": configShow,
"config set": configSet,
"config list": configList,
"config use": configUse,
"config ui": configUi,
"config agent": configAgent,
update,
"app call": appCall,
"app list": appList,
"memory add": memoryAdd,
"memory search": memorySearch,
"memory list": memoryList,
"memory update": memoryUpdate,
"memory delete": memoryDelete,
"memory profile create": memoryProfileCreate,
"memory profile get": memoryProfileGet,
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
"knowledge chat": knowledgeChat,
"mcp call": mcpCall,
"mcp list": mcpList,
"mcp tools": mcpTools,
"search web": searchWeb,
"speech synthesize": speechSynthesize,
"speech recognize": speechRecognize,
"file upload": fileUpload,
"console call": consoleCall,
"usage free": usageFree,
"usage freetier": usageFreetier,
"usage stats": usageStats,
"usage summary": usageSummary,
"usage token-plan": usageTokenPlan,
"usage coding-plan": usageCodingPlan,
"pipeline run": pipelineRun,
"pipeline validate": pipelineValidate,
"advisor recommend": advisorRecommend,
"model list": modelList,
"workspace list": workspaceList,
"quota list": quotaList,
"quota update": quotaUpdate,
"quota history": quotaHistory,
"quota check": quotaCheck,
"permission list": permissionList,
"permission grant": permissionGrant,
"permission revoke": permissionRevoke,
"dataset upload": datasetUpload,
"dataset list": datasetList,
"dataset get": datasetGet,
"dataset delete": datasetDelete,
"dataset validate": datasetValidate,
"finetune text create": finetuneTextCreate,
"finetune audio create": finetuneAudioCreate,
"finetune image create": finetuneImageCreate,
"finetune list": finetuneList,
"finetune get": finetuneGet,
"finetune cancel": finetuneCancel,
"finetune delete": finetuneDelete,
"finetune logs": finetuneLogs,
"finetune checkpoints": finetuneCheckpoints,
"finetune export": finetuneExport,
"finetune watch": finetuneWatch,
"finetune capability": finetuneCapability,
"deploy text create": deployTextCreate,
"deploy audio create": deployAudioCreate,
"deploy image create": deployImageCreate,
"deploy list": deployList,
"deploy get": deployGet,
"deploy models": deployModels,
"deploy scale": deployScale,
"deploy update": deployUpdate,
"deploy delete": deployDelete,
"token-plan list-seats": tokenPlanListSeats,
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
"plugin list": pluginList,
"plugin remove": pluginRemove,
"skill add": skillAdd,
"skill update": skillUpdate,
"skill remove": skillRemove,
"skill list": skillList,
"skill init": skillInit,
"managed-agent init": managedAgentInit,
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply,
"managed-agent destroy": managedAgentDestroy,
"managed-agent state list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow,
"managed-agent state rm": managedAgentStateRm,
"managed-agent state import": managedAgentStateImport,
"managed-agent session create": managedAgentSessionCreate,
"managed-agent session list": managedAgentSessionList,
"managed-agent session get": managedAgentSessionGet,
"managed-agent session delete": managedAgentSessionDelete,
"managed-agent session run": managedAgentSessionRun,
"managed-agent session send": managedAgentSessionSend,
"managed-agent session events": managedAgentSessionEvents,
"managed-agent skill-list": managedAgentSkillList,
};
/**
* Runtime-only aliases for renamed commands: dispatched by the CLI (merged in
* main.ts) but kept out of the canonical map so generate-reference.ts only
* documents the canonical path.
*/
export const commandAliases: Record<string, AnyCommand> = {
// Pre-migration name of "quota update".
"quota request": quotaUpdate,
};
@@ -1,321 +0,0 @@
import {
analyzeIntent,
buildDocLink,
type Config,
defineCommand,
detectOutputFormat,
type GetModelsOptions,
type GlobalFlags,
getModels,
type IntentProfile,
isInteractive,
type PipelineStep,
type RecommendedModel,
type RecommendResult,
rankModels,
recallSemantic,
} from "bailian-cli-core";
import boxen from "boxen";
import chalk, { Chalk, type ChalkInstance } from "chalk";
import { emitBare, emitResult } from "../../output/output.ts";
import { createSpinner } from "../../output/progress.ts";
import { failIfMissing, promptText } from "../../output/prompt.ts";
function formatContextWindow(tokens: number): string {
if (tokens >= 1_000_000)
return `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`;
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(tokens % 1_000 === 0 ? 0 : 1)}K`;
return String(tokens);
}
const MODALITY_LABELS: Record<string, string> = {
Text: "文本",
Image: "图片",
Video: "视频",
Audio: "音频",
};
const CAPABILITY_LABELS: Record<string, string> = {
TG: "文本生成",
VU: "视觉理解",
IG: "图像生成",
VG: "视频生成",
TTS: "语音合成",
ASR: "语音识别",
Reasoning: "推理",
};
const BUDGET_LABELS: Record<string, string> = {
low: "低成本优先",
medium: "适中",
high: "高投入",
};
const QUALITY_LABELS: Record<string, string> = {
flagship: "旗舰优先",
balanced: "均衡",
"cost-optimized": "性价比优先",
};
const PREFERENCE_MODE_LABELS: Record<string, string> = {
scoped: "限定范围",
comparison: "对比评估",
alternative: "替代推荐",
};
function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
const lines: string[] = [];
lines.push(colorize.cyan.bold("需求理解"));
if (intent.taskSummary) {
lines.push("");
lines.push(intent.taskSummary);
}
if (intent.scenarioHints.length) {
lines.push("");
lines.push(`${colorize.dim("场景特征")} ${intent.scenarioHints.join(" · ")}`);
}
const inputLabels = intent.inputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
const outputLabels = intent.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
if (inputLabels.length || outputLabels.length) {
lines.push("");
const parts: string[] = [];
if (inputLabels.length) parts.push(`${colorize.dim("输入")} ${inputLabels.join(", ")}`);
if (outputLabels.length) parts.push(`${colorize.dim("输出")} ${outputLabels.join(", ")}`);
lines.push(parts.join(" "));
}
const capLabels = intent.requiredCapabilities.map((cap) => CAPABILITY_LABELS[cap] ?? cap);
if (capLabels.length) {
lines.push(`${colorize.dim("所需能力")} ${capLabels.join(", ")}`);
}
const budgetLabel = BUDGET_LABELS[intent.budget] ?? intent.budget;
const qualityLabel = QUALITY_LABELS[intent.qualityPreference] ?? intent.qualityPreference;
lines.push("");
lines.push(
`${colorize.dim("预算倾向")} ${budgetLabel} ${colorize.dim("质量偏好")} ${qualityLabel}`,
);
const preference = intent.modelPreference;
if (preference && preference.mode !== "unconstrained") {
lines.push("");
const modeLabel = PREFERENCE_MODE_LABELS[preference.mode] ?? preference.mode;
const prefParts = [colorize.dim("推荐模式") + ` ${colorize.yellow(modeLabel)}`];
if (preference.targets?.length) {
prefParts.push(colorize.dim("目标") + ` ${preference.targets.join(", ")}`);
}
if (preference.excludes?.length) {
prefParts.push(colorize.dim("排除") + ` ${preference.excludes.join(", ")}`);
}
lines.push(prefParts.join(" "));
}
if (intent.segments?.length) {
lines.push("");
lines.push(colorize.dim("任务拆解"));
for (const [idx, segment] of intent.segments.entries()) {
const outMods = segment.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod).join(", ");
lines.push(
` ${colorize.dim(`${idx + 1}.`)} ${segment.step}${outMods ? colorize.dim(`${outMods}`) : ""}`,
);
}
}
return boxen(lines.join("\n"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0, left: 1, right: 0 },
borderColor: "cyan",
borderStyle: "round",
dimBorder: true,
});
}
const RECOMMEND_LABELS = ["最佳推荐", "次优选择", "备选参考"];
function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstance): string {
const labelColors = [colorize.green.bold, colorize.blue.bold, colorize.magenta.bold];
const colorFn = labelColors[index] ?? colorize.white.bold;
const label = RECOMMEND_LABELS[index] ?? `推荐 #${index + 1}`;
const lines: string[] = [];
lines.push(colorFn(`⬢ 推荐 #${index + 1}${label}`));
lines.push("");
lines.push(`${colorize.bold(rec.name)} ${colorize.dim(`(${rec.model})`)}`);
lines.push("");
lines.push(`${colorize.cyan("推荐理由")} ${rec.reason}`);
if (rec.highlights.length) {
lines.push("");
lines.push(
rec.highlights.map((highlight) => colorize.bgGray.white(` ${highlight} `)).join(" "),
);
}
const meta: string[] = [];
if (rec.contextWindow) meta.push(`上下文 ${formatContextWindow(rec.contextWindow)}`);
if (rec.maxOutputTokens) meta.push(`最大输出 ${formatContextWindow(rec.maxOutputTokens)}`);
if (meta.length) {
lines.push("");
lines.push(colorize.dim(meta.join(" · ")));
}
const docLink = buildDocLink(rec.docUrl);
if (docLink) {
lines.push("");
lines.push(colorize.dim(`文档 ${docLink}`));
}
return boxen(lines.join("\n"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0, left: 1, right: 0 },
borderColor: "gray",
borderStyle: "round",
dimBorder: true,
});
}
function formatSingleResult(results: RecommendedModel[], noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
return results.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n");
}
function formatPipelineResult(summary: string, steps: PipelineStep[], noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
const lines: string[] = [];
lines.push(` ${colorize.yellow.bold("⚡ 组合方案")} ${summary}`);
for (const [stepIdx, { step, recommendations, warnings }] of steps.entries()) {
lines.push("");
lines.push(colorize.bold(` ━━━ Step ${stepIdx + 1}: ${step} ━━━`));
if (warnings?.length) {
for (const warning of warnings) {
lines.push(` ${colorize.yellow("⚠")} ${colorize.yellow(warning)}`);
}
}
lines.push("");
lines.push(recommendations.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n"));
}
return lines.join("\n");
}
function formatResult(result: RecommendResult, noColor: boolean): string {
if (result.type === "pipeline") {
return formatPipelineResult(result.summary, result.steps, noColor);
}
return formatSingleResult(result.recommendations, noColor);
}
function isEmptyResult(result: RecommendResult): boolean {
if (result.type === "pipeline") return result.steps.length === 0;
return result.recommendations.length === 0;
}
export default defineCommand({
name: "advisor recommend",
description:
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
usage: "bl advisor recommend <prompt> [flags]",
options: [
{
flag: "--message <text>",
description: "Describe your requirements (alternative to positional prompt)",
},
{
flag: "--dry-run",
description: "Show intent analysis and candidate list without LLM ranking",
},
{
flag: "--output <format>",
description: "Output format: text (default in TTY), json, yaml",
},
],
examples: [
'bl advisor recommend --message "I need a visual-understanding chatbot"',
'bl advisor recommend --message "Build an Agent that auto-generates animations"',
'bl advisor recommend --message "Legal contract review, high precision required"',
'bl advisor recommend --message "Low-cost high-concurrency online customer service" --output json',
'bl advisor recommend --message "Long document summarization" --dry-run',
"bl advisor recommend # Interactive input",
],
async run(config: Config, flags: GlobalFlags) {
const positional = ((flags as Record<string, unknown>)._positional as string[]) ?? [];
let userInput = (flags.message as string) || positional.join(" ");
if (!userInput.trim()) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "描述你的需求:" });
if (!hint) {
process.stderr.write("已取消。\n");
process.exit(1);
}
userInput = hint;
} else {
failIfMissing("message", 'bl advisor recommend "你的需求"');
}
}
const top = 3;
const format = detectOutputFormat(config.output);
const modelsOptions: GetModelsOptions = {
onPrepareStart: () => process.stderr.write("初始化中...\n"),
};
process.stderr.write("正在分析需求...\n");
const [allModels, intent] = await Promise.all([
getModels(config, modelsOptions),
analyzeIntent(config, userInput),
]);
if (intent.confidence === 0) {
process.stderr.write("需求分析超时,使用默认参数继续...\n");
} else {
process.stderr.write("\n");
}
// Stage 2: Candidate Recall (semantic recall, auto-builds embeddings on first run)
const candidates = await recallSemantic(config, allModels, userInput, 50, intent);
if (config.dryRun) {
emitResult(
{
userInput,
intent,
candidateCount: candidates.length,
candidates: candidates.map(({ model, score }) => ({
model: model.model,
score,
})),
top,
},
format,
);
return;
}
// Stage 3: LLM Ranking
const spinner = createSpinner("正在推荐最佳模型...");
spinner.start();
const result = await rankModels(config, candidates, intent, userInput, top);
spinner.stop();
if (isEmptyResult(result)) {
emitBare("暂无满足该需求的模型。");
return;
}
if (format !== "text") {
emitResult(result, format);
return;
}
emitBare(formatIntentSummary(intent, config.noColor));
emitBare("");
emitBare(formatResult(result, config.noColor));
},
});
-187
View File
@@ -1,187 +0,0 @@
import {
defineCommand,
request,
requestJson,
appCompletionEndpoint,
parseSSE,
detectOutputFormat,
type Config,
type GlobalFlags,
type AppCompletionRequest,
type AppStreamChunk,
type AppCompletionResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "app call",
description: "Call a Bailian application (agent or workflow)",
usage: "bl app call --app-id <id> --prompt <text> [flags]",
options: [
{ flag: "--app-id <id>", description: "Application ID (required)", required: true },
{ flag: "--prompt <text>", description: "Input prompt text", required: true },
{
flag: "--image <url>",
description: "Image URL(s) to pass to the app (repeatable)",
type: "array",
},
{ flag: "--file-id <id>", description: "Pre-uploaded file ID(s) (repeatable)", type: "array" },
{ flag: "--session-id <id>", description: "Session ID for multi-turn conversation" },
{ flag: "--stream", description: "Stream response (default: on in TTY)" },
{ flag: "--pipeline-ids <ids>", description: "Knowledge base pipeline IDs (comma-separated)" },
{ flag: "--memory-id <id>", description: "Memory ID for long-term memory" },
{ flag: "--biz-params <json>", description: "Business parameters JSON (workflow variables)" },
{ flag: "--has-thoughts", description: "Show agent thinking process" },
],
examples: [
'bl app call --app-id abc123 --prompt "你好"',
'bl app call --app-id abc123 --prompt "描述这张图片" --image https://example.com/photo.jpg',
'bl app call --app-id abc123 --prompt "分析图片" --image img1.jpg --image img2.jpg',
'bl app call --app-id abc123 --prompt "继续" --session-id sess_xxx --stream',
'bl app call --app-id abc123 --prompt "搜索资料" --pipeline-ids pipe1,pipe2',
'bl app call --app-id abc123 --prompt "开始" --biz-params \'{"key":"value"}\'',
],
async run(config: Config, flags: GlobalFlags) {
const appId = flags.appId as string;
if (!appId) failIfMissing("app-id", "bl app call --app-id <id> --prompt <text>");
const prompt = flags.prompt as string;
if (!prompt) failIfMissing("prompt", "bl app call --app-id <id> --prompt <text>");
const shouldStream =
flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
const format = detectOutputFormat(config.output);
const body: AppCompletionRequest = {
input: { prompt },
parameters: {
incremental_output: shouldStream,
},
};
if (flags.sessionId) {
body.input.session_id = flags.sessionId as string;
}
// Pass image URLs via image_list
const imageUrls = flags.image as string[] | undefined;
if (imageUrls && imageUrls.length > 0) {
body.input.image_list = imageUrls;
}
// Pass pre-uploaded file IDs
const fileIds = flags.fileId as string[] | undefined;
if (fileIds && fileIds.length > 0) {
body.input.file_ids = fileIds;
}
if (flags.hasThoughts) {
body.parameters!.has_thoughts = true;
}
if (flags.pipelineIds) {
const ids = (flags.pipelineIds as string)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
body.parameters!.rag_options = { pipeline_ids: ids };
}
if (flags.memoryId) {
body.parameters!.memory_id = flags.memoryId as string;
}
if (flags.bizParams) {
try {
body.input.biz_params = JSON.parse(flags.bizParams as string);
} catch {
process.stderr.write("Error: --biz-params must be valid JSON\n");
process.exit(1);
}
}
if (config.dryRun) {
emitResult({ endpoint: appCompletionEndpoint(config.baseUrl, appId), request: body }, format);
return;
}
const url = appCompletionEndpoint(config.baseUrl, appId);
if (shouldStream) {
const headers: Record<string, string> = { "X-DashScope-SSE": "enable" };
const res = await request(config, {
url,
method: "POST",
body,
headers,
stream: true,
});
let fullText = "";
let sessionId = "";
const writesStreamingStdout = format === "text";
const dim = config.noColor ? "" : "\x1b[2m";
const reset = config.noColor ? "" : "\x1b[0m";
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
try {
const chunk = JSON.parse(event.data) as AppStreamChunk;
const text = chunk.output?.text;
if (text) {
// incremental_output: text is delta
if (writesStreamingStdout) process.stdout.write(text);
fullText += text;
}
// Capture session_id for multi-turn
if (chunk.output?.session_id) {
sessionId = chunk.output.session_id;
}
// Show thoughts if available
if (chunk.output?.thoughts && flags.hasThoughts) {
for (const t of chunk.output.thoughts) {
if (t.thought) process.stderr.write(`${dim}[Thinking] ${t.thought}${reset}\n`);
if (t.action_name)
process.stderr.write(
`${dim}[Action] ${t.action_name}: ${t.action_input || ""}${reset}\n`,
);
if (t.observation)
process.stderr.write(`${dim}[Observation] ${t.observation}${reset}\n`);
}
}
} catch {
// skip unparseable
}
}
// Show session_id for multi-turn conversation
if (sessionId && !config.quiet) {
process.stderr.write(`${dim}Session ID: ${sessionId}${reset}\n`);
}
if (format === "json") {
emitResult({ text: fullText, session_id: sessionId }, format);
} else {
process.stdout.write("\n");
}
} else {
const response = await requestJson<AppCompletionResponse>(config, {
url,
method: "POST",
body,
});
const text = response.output?.text ?? "";
if (config.quiet || format === "text") {
emitBare(text);
} else {
emitResult(response, format);
}
}
},
});
-88
View File
@@ -1,88 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
const APP_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list";
export default defineCommand({
name: "app list",
description: "List Bailian applications",
usage: "bl app list [flags]",
options: [
{
flag: "--name <name>",
description: "Filter by app name (keyword search)",
},
{
flag: "--page <n>",
description: "Page number (default: 1)",
type: "number",
},
{
flag: "--page-size <n>",
description: "Results per page (default: 30)",
type: "number",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl app list",
"bl app list --name 客服",
"bl app list --page 2 --page-size 10",
"bl app list --output json",
],
async run(config: Config, flags: GlobalFlags) {
const name = (flags.name as string) || "";
const pageNo = (flags.page as number) || 1;
const pageSize = (flags.pageSize as number) || 30;
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
const data = {
reqDTO: {
name,
notInTypes: [10],
type: 5,
statuses: [1, 4],
page_no: pageNo,
page_size: pageSize,
},
};
if (config.dryRun) {
emitResult(
{ api: APP_LIST_API, data, region, token: credential.token.slice(0, 8) + "..." },
format,
);
return;
}
const result = (await callConsoleGateway(config, credential.token, {
api: APP_LIST_API,
data,
region,
})) as any;
const list: unknown[] = result?.data?.DataV2?.data?.data?.list ?? [];
const total: number = result?.data?.DataV2?.data?.data?.total ?? 0;
const apps = list.map((item: any) => ({
code: item.code,
name: item.name,
user_prompt_params: item.config?.user_prompt_params ?? [],
}));
emitResult({ total, apps }, format);
},
});
-140
View File
@@ -1,140 +0,0 @@
import {
BailianError,
ExitCode,
chatEndpoint,
defineCommand,
getConfigPath,
isInteractive,
maskToken,
readConfigFile,
requestJson,
writeConfigFile,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { printQuickStart } from "../../output/banner.ts";
import { emitBare } from "../../output/output.ts";
import { promptConfirm } from "../../output/prompt.ts";
import { printCurrentCommandHelp } from "../../utils/command-help.ts";
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
const RETRY_DELAY_BASE_MS = 500;
function canRetry(err: unknown): boolean {
if (err instanceof BailianError) {
if (err.exitCode === ExitCode.NETWORK || err.exitCode === ExitCode.TIMEOUT) {
return true;
}
const status = err.api?.httpStatus;
return status === 401 || (status !== undefined && status >= 500);
}
if (err instanceof Error) {
return (
err.name === "AbortError" ||
err.name === "TimeoutError" ||
err.message.includes("timed out") ||
err.message === "fetch failed"
);
}
return false;
}
async function validateKeyAndPersist(config: Config, key: string): Promise<void> {
process.stderr.write("Testing key... ");
const testConfig = { ...config, apiKey: key };
const requestOpts = {
url: chatEndpoint(testConfig.baseUrl),
method: "POST",
timeout: Math.min(config.timeout, 30),
body: {
model: "qwen3.7-max",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
},
};
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(testConfig, requestOpts);
break;
} catch (err) {
if (attempt >= 3 || !canRetry(err)) {
process.stderr.write("\n");
throw new BailianError("API key validation failed", ExitCode.AUTH, "Invalid API key.", {
cause: err,
});
}
// retry delay: 500ms, 1000ms, 2000ms
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
process.stderr.write("Valid\n");
const existing = readConfigFile() as Record<string, unknown>;
existing.api_key = key;
await writeConfigFile(existing);
process.stderr.write(`Saved to ${getConfigPath()}\n`);
}
export default defineCommand({
name: "auth login",
description: "Authenticate with API key or console browser login (credentials can coexist)",
usage: "bl auth login --api-key <key> | bl auth login --console",
options: [
{ flag: "--api-key <key>", description: "DashScope API key to store" },
{
flag: "--console",
description: "Sign in via browser; opens the console login URL in your default browser",
type: "boolean",
},
],
examples: ["bl auth login --api-key sk-xxxxx", "bl auth login --console"],
async run(config: Config, flags: GlobalFlags) {
if (flags.console) {
if (config.dryRun) {
emitBare(
"Would bind a free port on 127.0.0.1 and open the console login URL in your browser.",
);
return;
}
const hasApiKey = !!(config.apiKey || config.fileApiKey);
await runConsoleLogin(resolveConsoleOrigin(), {
needApiKey: !hasApiKey,
onApiKey: (key) => validateKeyAndPersist(config, key),
});
return;
}
const envKey = process.env.DASHSCOPE_API_KEY;
if (envKey && !flags.apiKey) {
const maskedEnvKey = maskToken(envKey);
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const proceed = await promptConfirm({
message: `Detected DASHSCOPE_API_KEY in environment (${maskedEnvKey}).\nYou are already authenticated via env.\nDo you still want to configure local persistent credentials?`,
initialValue: false,
});
if (!proceed) {
process.stdout.write("Login skipped. Using environment variables.\n");
process.exit(0);
}
} else {
process.stderr.write(`Warning: DASHSCOPE_API_KEY is already set in environment.\n`);
}
}
const key = (flags.apiKey as string) || config.apiKey;
if (!key) {
printCurrentCommandHelp(process.stderr);
process.exit(0);
}
if (!config.dryRun) {
await validateKeyAndPersist(config, key);
printQuickStart();
} else {
emitBare("Would validate and save API key.");
}
},
});
-79
View File
@@ -1,79 +0,0 @@
import {
defineCommand,
clearApiKey,
readConfigFile,
writeConfigFile,
getConfigPath,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitBare } from "../../output/output.ts";
async function clearConsoleToken(): Promise<boolean> {
const file = readConfigFile() as Record<string, unknown>;
if (!file.access_token) return false;
delete file.access_token;
await writeConfigFile(file);
return true;
}
export default defineCommand({
name: "auth logout",
description: "Clear stored credentials",
usage: "bl auth logout [--console] [--yes] [--dry-run]",
options: [
{
flag: "--console",
description: "Only clear the console access_token, keep api_key intact",
type: "boolean",
},
{ flag: "--yes", description: "Skip confirmation prompt" },
],
examples: [
"bl auth logout",
"bl auth logout --console",
"bl auth logout --dry-run",
"bl auth logout --yes",
],
async run(config: Config, flags: GlobalFlags) {
const file = readConfigFile();
if (flags.console) {
const hasToken = !!file.access_token;
if (config.dryRun) {
if (hasToken) emitBare("Would clear access_token from ~/.bailian/config.json");
else emitBare("No console access_token to clear.");
emitBare("No changes made.");
return;
}
if (hasToken) {
await clearConsoleToken();
process.stderr.write(`Cleared access_token from ${getConfigPath()}\n`);
if (file.api_key) {
process.stderr.write(
"api_key is still configured and will be used for authentication.\n",
);
}
} else {
process.stderr.write("No console access_token to clear.\n");
}
return;
}
const hasKey = !!(file.api_key || file.access_token);
if (config.dryRun) {
if (hasKey) emitBare("Would clear api_key / access_token from ~/.bailian/config.json");
else emitBare("No credentials to clear.");
emitBare("No changes made.");
return;
}
if (hasKey) {
await clearApiKey();
process.stderr.write("Cleared api_key / access_token from ~/.bailian/config.json\n");
} else {
process.stderr.write("No credentials to clear.\n");
}
},
});
-172
View File
@@ -1,172 +0,0 @@
import {
defineCommand,
resolveCredential,
resolveConsoleGatewayCredential,
detectOutputFormat,
maskToken,
type Config,
type GlobalFlags,
type ResolvedCredential,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
import { API_KEY_PAGE } from "../../urls.ts";
interface StoredCredential {
configured: boolean;
source?: string;
masked?: string;
}
interface AuthStatusPayload {
api_key: StoredCredential;
access_token: StoredCredential;
dashscope_commands?: { method: string; source: string; masked: string };
console_gateway_commands?: { method: string; source: string; masked: string };
}
function storedApiKey(config: Config): StoredCredential {
if (config.apiKey) {
return { configured: true, source: "flag", masked: maskToken(config.apiKey) };
}
if (config.fileApiKey) {
return { configured: true, source: "config.json", masked: maskToken(config.fileApiKey) };
}
const env = process.env.DASHSCOPE_API_KEY?.trim();
if (env) {
return { configured: true, source: "DASHSCOPE_API_KEY", masked: maskToken(env) };
}
return { configured: false };
}
function storedAccessToken(config: Config): StoredCredential {
if (config.accessTokenEnv) {
return {
configured: true,
source: "DASHSCOPE_ACCESS_TOKEN",
masked: maskToken(config.accessTokenEnv),
};
}
if (config.fileAccessToken) {
return {
configured: true,
source: "config.json",
masked: maskToken(config.fileAccessToken),
};
}
return { configured: false };
}
async function tryResolveDashscope(config: Config): Promise<ResolvedCredential | undefined> {
try {
return await resolveCredential(config);
} catch {
return undefined;
}
}
async function tryResolveConsole(config: Config): Promise<ResolvedCredential | undefined> {
try {
return await resolveConsoleGatewayCredential(config);
} catch {
return undefined;
}
}
async function buildStatus(config: Config): Promise<AuthStatusPayload> {
const status: AuthStatusPayload = {
api_key: storedApiKey(config),
access_token: storedAccessToken(config),
};
const dashscope = await tryResolveDashscope(config);
if (dashscope) {
status.dashscope_commands = {
method: dashscope.method,
source: dashscope.source,
masked: maskToken(dashscope.token),
};
}
const consoleGw = await tryResolveConsole(config);
if (consoleGw) {
status.console_gateway_commands = {
method: consoleGw.method,
source: consoleGw.source,
masked: maskToken(consoleGw.token),
};
}
return status;
}
function hasAnyAuth(status: AuthStatusPayload): boolean {
return (
status.api_key.configured ||
status.access_token.configured ||
!!status.dashscope_commands ||
!!status.console_gateway_commands
);
}
function emitTextStatus(status: AuthStatusPayload): void {
emitBare("Authentication Status:");
emitBare(" Stored credentials (can coexist):");
if (status.api_key.configured) {
emitBare(` API key: ${status.api_key.source} ${status.api_key.masked}`);
} else {
emitBare(" API key: not configured");
}
if (status.access_token.configured) {
emitBare(` Console token: ${status.access_token.source} ${status.access_token.masked}`);
} else {
emitBare(" Console token: not configured");
}
emitBare(" Effective credential per command family:");
if (status.dashscope_commands) {
emitBare(
` DashScope API: ${status.dashscope_commands.method} (${status.dashscope_commands.source}) ${status.dashscope_commands.masked}`,
);
} else {
emitBare(" DashScope API: unavailable");
}
if (status.console_gateway_commands) {
emitBare(
` Console gateway: ${status.console_gateway_commands.method} (${status.console_gateway_commands.source}) ${status.console_gateway_commands.masked}`,
);
} else {
emitBare(" Console gateway: unavailable (run bl auth login --console)");
}
}
export default defineCommand({
name: "auth status",
description: "Show current authentication state",
usage: "bl auth status",
examples: ["bl auth status", "bl auth status --output json"],
async run(config: Config, _flags: GlobalFlags) {
const format = detectOutputFormat(config.output);
const status = await buildStatus(config);
if (!hasAnyAuth(status)) {
const result = {
authenticated: false,
message: "Not authenticated.",
hint: [
"DashScope API: bl auth login --api-key <key> or DASHSCOPE_API_KEY",
"Console gateway: bl auth login --console or DASHSCOPE_ACCESS_TOKEN",
`Get API Key: ${API_KEY_PAGE}`,
].join("\n"),
...status,
};
emitResult(result, format);
return;
}
if (format !== "text") {
emitResult({ authenticated: true, ...status }, format);
return;
}
emitTextStatus(status);
},
});
-98
View File
@@ -1,98 +0,0 @@
import type { Command } from "bailian-cli-core";
import authLogin from "./auth/login.ts";
import authStatus from "./auth/status.ts";
import authLogout from "./auth/logout.ts";
import textChat from "./text/chat.ts";
import textOmni from "./omni/chat.ts";
import imageGenerate from "./image/generate.ts";
import imageEdit from "./image/edit.ts";
import videoGenerate from "./video/generate.ts";
import videoEdit from "./video/edit.ts";
import videoRef from "./video/ref.ts";
import videoTaskGet from "./video/task-get.ts";
import videoDownload from "./video/download.ts";
import visionDescribe from "./vision/describe.ts";
import configShow from "./config/show.ts";
import configSet from "./config/set.ts";
import configExportSchema from "./config/export-schema.ts";
import update from "./update.ts";
import appCall from "./app/call.ts";
import appList from "./app/list.ts";
import memoryAdd from "./memory/add.ts";
import memorySearch from "./memory/search.ts";
import memoryList from "./memory/list.ts";
import memoryUpdate from "./memory/update.ts";
import memoryDelete from "./memory/delete.ts";
import memoryProfileCreate from "./memory/profile-create.ts";
import memoryProfileGet from "./memory/profile-get.ts";
import knowledgeRetrieve from "./knowledge/retrieve.ts";
import mcpCall from "./mcp/call.ts";
import mcpList from "./mcp/list.ts";
import mcpTools from "./mcp/tools.ts";
import searchWeb from "./search/web.ts";
import speechSynthesize from "./speech/synthesize.ts";
import speechRecognize from "./speech/recognize.ts";
import fileUpload from "./file/upload.ts";
import consoleCall from "./console/call.ts";
import usageFree from "./usage/free.ts";
import usageFreetier from "./usage/freetier.ts";
import usageStats from "./usage/stats.ts";
import pipelineRun from "./pipeline/run.ts";
import pipelineValidate from "./pipeline/validate.ts";
import advisorRecommend from "./advisor/recommend.ts";
import workspaceList from "./workspace/list.ts";
import quotaList from "./quota/list.ts";
import quotaRequest from "./quota/request.ts";
import quotaHistory from "./quota/history.ts";
import quotaCheck from "./quota/check.ts";
/** Command registry map (no dependency on registry.ts — safe for build-time import). */
export const commands: Record<string, Command> = {
"auth login": authLogin,
"auth status": authStatus,
"auth logout": authLogout,
"text chat": textChat,
omni: textOmni,
"image generate": imageGenerate,
"image edit": imageEdit,
"video generate": videoGenerate,
"video edit": videoEdit,
"video ref": videoRef,
"video task get": videoTaskGet,
"video download": videoDownload,
"vision describe": visionDescribe,
"app call": appCall,
"app list": appList,
"memory add": memoryAdd,
"memory search": memorySearch,
"memory list": memoryList,
"memory update": memoryUpdate,
"memory delete": memoryDelete,
"memory profile create": memoryProfileCreate,
"memory profile get": memoryProfileGet,
"knowledge retrieve": knowledgeRetrieve,
"mcp list": mcpList,
"mcp tools": mcpTools,
"mcp call": mcpCall,
"search web": searchWeb,
"speech synthesize": speechSynthesize,
"speech recognize": speechRecognize,
"file upload": fileUpload,
"console call": consoleCall,
"usage free": usageFree,
"usage freetier": usageFreetier,
"usage stats": usageStats,
"pipeline run": pipelineRun,
"pipeline validate": pipelineValidate,
"config show": configShow,
"config set": configSet,
"config export-schema": configExportSchema,
"advisor recommend": advisorRecommend,
"workspace list": workspaceList,
"quota list": quotaList,
"quota request": quotaRequest,
"quota history": quotaHistory,
"quota check": quotaCheck,
update: update,
};
@@ -1,46 +0,0 @@
import { defineCommand, generateToolSchema } from "bailian-cli-core";
import type { Config } from "bailian-cli-core";
import type { GlobalFlags } from "bailian-cli-core";
import { BailianError } from "bailian-cli-core";
import { ExitCode } from "bailian-cli-core";
/**
* Commands that are infrastructure/auth-related and not suitable as Agent tools.
*/
const SKIP_PREFIXES = ["auth ", "config ", "update"];
export default defineCommand({
name: "config export-schema",
description:
"Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas",
usage: 'bl config export-schema [--command "<name>"]',
options: [
{
flag: "--command <name>",
description: 'Export schema for a specific command only (e.g. "image generate")',
},
],
examples: ["bl config export-schema", 'bl config export-schema --command "video generate"'],
async run(config: Config, flags: GlobalFlags) {
const { commands } = await import("../catalog.ts");
const targetCommand = flags.command as string | undefined;
if (targetCommand) {
const command = commands[targetCommand];
if (!command) {
throw new BailianError(`Command "${targetCommand}" not found.`, ExitCode.USAGE);
}
const schema = generateToolSchema(command);
process.stdout.write(JSON.stringify(schema, null, 2) + "\n");
return;
}
// Export all suitable commands
const allCommands = Object.values(commands);
const schemas = allCommands
.filter((c) => !SKIP_PREFIXES.some((p) => c.name.startsWith(p)))
.map((c) => generateToolSchema(c));
process.stdout.write(JSON.stringify(schemas, null, 2) + "\n");
},
});
-135
View File
@@ -1,135 +0,0 @@
import {
defineCommand,
detectOutputFormat,
maskToken,
readConfigFile,
writeConfigFile,
BailianError,
type Config,
type GlobalFlags,
ExitCode,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
const VALID_KEYS = [
"region",
"base_url",
"output",
"output_dir",
"timeout",
"api_key",
"access_token",
"default_text_model",
"default_video_model",
"default_image_model",
"default_speech_model",
"default_omni_model",
"access_key_id",
"access_key_secret",
"workspace_id",
];
// Keys whose values are secrets. Their stored value must never be echoed back in
// cleartext (CI logs, pipes, shared terminals); show a masked form instead — the
// same policy `config show` and `auth status` already follow.
const SECRET_KEYS = new Set(["api_key", "access_token", "access_key_id", "access_key_secret"]);
// Allow hyphen-style keys (e.g. default-text-model → default_text_model)
const KEY_ALIASES: Record<string, string> = {
"base-url": "base_url",
"output-dir": "output_dir",
"api-key": "api_key",
"access-token": "access_token",
"default-text-model": "default_text_model",
"default-video-model": "default_video_model",
"default-image-model": "default_image_model",
"default-speech-model": "default_speech_model",
"default-omni-model": "default_omni_model",
"access-key-id": "access_key_id",
"access-key-secret": "access_key_secret",
"workspace-id": "workspace_id",
};
export default defineCommand({
name: "config set",
description: "Set a config value",
usage: "bl config set --key <key> --value <value>",
options: [
{
flag: "--key <key>",
description:
"Config key (region, base_url, output, output_dir, timeout, api_key, access_token, default_*_model, access_key_id, access_key_secret, workspace_id)",
},
{ flag: "--value <value>", description: "Value to set" },
],
examples: [
"bl config set --key output --value json",
"bl config set --key timeout --value 600",
"bl config set --key base_url --value https://dashscope.aliyuncs.com",
],
async run(config: Config, flags: GlobalFlags) {
const key = flags.key as string | undefined;
const value = flags.value as string | undefined;
if (!key || value === undefined) {
throw new BailianError(
"--key and --value are required.",
ExitCode.USAGE,
"bl config set --key <key> --value <value>",
);
}
// Resolve hyphen aliases to underscore keys
const resolvedKey: string = KEY_ALIASES[key] || key;
if (!VALID_KEYS.includes(resolvedKey)) {
throw new BailianError(
`Invalid config key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`,
ExitCode.USAGE,
);
}
// Validate specific values
if (resolvedKey === "region" && !["cn", "us", "intl"].includes(value)) {
throw new BailianError(
`Invalid region "${value}". Valid values: cn, us, intl`,
ExitCode.USAGE,
);
}
if (resolvedKey === "output" && !["text", "json"].includes(value)) {
throw new BailianError(
`Invalid output format "${value}". Valid values: text, json`,
ExitCode.USAGE,
);
}
if (resolvedKey === "timeout") {
const num = Number(value);
if (isNaN(num) || num <= 0) {
throw new BailianError(
`Invalid timeout "${value}". Must be a positive number.`,
ExitCode.USAGE,
);
}
}
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ would_set: { [resolvedKey]: value } }, format);
return;
}
const existing = readConfigFile() as Record<string, unknown>;
existing[resolvedKey] = resolvedKey === "timeout" ? Number(value) : value;
await writeConfigFile(existing);
if (!config.quiet) {
const shown = SECRET_KEYS.has(resolvedKey)
? maskToken(String(existing[resolvedKey]))
: existing[resolvedKey];
emitResult({ [resolvedKey]: shown }, format);
}
},
});
-44
View File
@@ -1,44 +0,0 @@
import {
defineCommand,
readConfigFile as loadConfigFile,
getConfigPath,
detectOutputFormat,
maskToken,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
export default defineCommand({
name: "config show",
description: "Display current configuration",
usage: "bl config show",
examples: ["bl config show", "bl config show --output json"],
async run(config: Config, _flags: GlobalFlags) {
const file = loadConfigFile();
const format = detectOutputFormat(config.output);
const result: Record<string, unknown> = {
region: config.region,
base_url: config.baseUrl,
output: config.output,
timeout: config.timeout,
config_file: getConfigPath(),
};
// Mask API key if present
if (file.api_key) {
result.api_key = maskToken(file.api_key);
}
if (file.access_token) {
result.access_token = maskToken(file.access_token);
}
// Default models
if (file.default_text_model) result.default_text_model = file.default_text_model;
if (file.default_video_model) result.default_video_model = file.default_video_model;
if (file.default_image_model) result.default_image_model = file.default_image_model;
emitResult(result, format);
},
});
-78
View File
@@ -1,78 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
CONSOLE_GATEWAY_NO_TOKEN_MESSAGE,
BailianError,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult } from "../../output/output.ts";
export default defineCommand({
name: "console call",
description: "Call a Bailian console API via the CLI gateway",
usage: "bl console call --api <api> --data <json> [flags]",
options: [
{
flag: "--api <api>",
description: "API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries)",
required: true,
},
{
flag: "--data <json>",
description: "Request data as JSON string",
required: true,
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
`bl console call --api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`,
`bl console call --api some.api.name --data '{"key":"value"}' --region cn-beijing`,
],
async run(config: Config, flags: GlobalFlags) {
const api = flags.api as string;
if (!api) failIfMissing("api", "bl console call --api <api> --data <json>");
const dataRaw = flags.data as string;
if (!dataRaw) failIfMissing("data", "bl console call --api <api> --data <json>");
let data: Record<string, unknown>;
try {
data = JSON.parse(dataRaw) as Record<string, unknown>;
} catch {
process.stderr.write("Error: --data must be valid JSON\n");
process.exit(1);
}
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
let token: string | undefined;
try {
token = (await resolveConsoleGatewayCredential(config)).token;
} catch (err) {
if (!(err instanceof BailianError && err.message === CONSOLE_GATEWAY_NO_TOKEN_MESSAGE)) {
throw err;
}
}
if (config.dryRun) {
emitResult({ api, data, region, token: token ? token.slice(0, 8) + "..." : null }, format);
return;
}
const result = await callConsoleGateway(config, token, {
api,
data,
region,
});
emitResult(result, format);
},
});
-75
View File
@@ -1,75 +0,0 @@
import {
defineCommand,
resolveCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
uploadFile,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "file upload",
description: "Upload a local file to DashScope temporary storage (48h)",
usage: "bl file upload --file <path> --model <model>",
options: [
{
flag: "--file <path>",
description: "Local file to upload (image, video, audio)",
required: true,
},
{
flag: "--model <model>",
description: "Target model name (file is bound to this model)",
required: true,
},
],
examples: [
"bl file upload --file photo.jpg --model qwen3-vl-plus",
"bl file upload --file video.mp4 --model wan2.1-t2v-plus",
"bl file upload --file audio.wav --model qwen3-asr-flash",
"bl file upload --file cat.png --model qwen-image-2.0",
],
async run(config: Config, flags: GlobalFlags) {
const filePath = flags.file as string | undefined;
if (!filePath) {
failIfMissing("file", "bl file upload --file <path> --model <model>");
}
const model = flags.model as string | undefined;
if (!model) {
failIfMissing("model", "bl file upload --file <path> --model <model>");
}
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ action: "upload", file: filePath, model }, format);
return;
}
// Resolve API key for upload
const credential = await resolveCredential(config);
const ossUrl = await uploadFile({
apiKey: credential.token,
model: model!,
filePath: filePath!,
});
if (config.quiet) {
emitBare(ossUrl);
} else {
emitResult(
{
url: ossUrl,
model,
expires_in: "48 hours",
note: "When using this URL in API calls, add header: X-DashScope-OssResourceResolve: enable",
},
format,
);
}
},
});
-201
View File
@@ -1,201 +0,0 @@
import {
defineCommand,
requestJson,
imageSyncEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
resolveCredential,
resolveFileUrl,
resolveOutputDir,
generateFilename,
isInteractive,
stripUndefined,
type DashScopeImageRequest,
type DashScopeImageSyncResponse,
ExitCode,
BailianError,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { downloadFile } from "../../utils/download.ts";
import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { resolveImageSize } from "../../utils/image-size.ts";
import { join } from "path";
import {
BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "image edit",
description: "Edit an existing image with text instructions (Qwen-Image)",
usage: "bl image edit --image <url> --prompt <text> [flags]",
options: [
{
flag: "--image <url>",
description: "Source image URL or local file path (repeatable for multi-image merge)",
required: true,
type: "array",
},
{ flag: "--prompt <text>", description: "Edit instruction text", required: true },
{ flag: "--model <model>", description: "Model ID (default: qwen-image-2.0)" },
{
flag: "--size <W*H>",
description: "Output image size: ratio (3:4, 16:9) or pixels (2048*2048)",
},
{ flag: "--n <count>", description: "Number of images (default: 1, max: 6)", type: "number" },
{ flag: "--seed <n>", description: "Random seed for reproducible results", type: "number" },
{
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--out-dir <dir>", description: "Download images to directory" },
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: edited)" },
],
examples: [
'bl image edit --image ./photo.png --prompt "把背景换成海滩"',
'bl image edit --image https://example.com/logo.png --prompt "Change color to blue" --n 3',
'bl image edit --image ./a.png --image ./b.png --prompt "把两张图合并成一张拼图"',
'bl image edit --image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'bl image edit --image ./photo.png --prompt "把背景换成海滩" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// Normalize --image to string array (supports both single and repeated flags)
let rawImages: string[] = [];
if (Array.isArray(flags.image)) {
rawImages = flags.image as string[];
} else if (typeof flags.image === "string") {
rawImages = [flags.image];
}
if (rawImages.length === 0) {
failIfMissing("image", "bl image edit --image <url> --prompt <text>");
}
let prompt = flags.prompt as string | undefined;
if (!prompt) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({
message: "Enter your edit instruction:",
});
if (!hint) {
process.stderr.write("Image editing cancelled.\n");
process.exit(1);
}
prompt = hint;
} else {
failIfMissing("prompt", "bl image edit --image <url> --prompt <text>");
}
}
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
// Auto-upload local files (resolve all images in parallel)
const credential = await resolveCredential(config);
const resolvedImages = await Promise.all(
rawImages.map((img) => resolveFileUrl(img, credential.token, model)),
);
const n = (flags.n as number) ?? 1;
const promptExtend = resolveBooleanFlag(flags.promptExtend, true, "prompt-extend");
// Build content: all images first, then text prompt
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
(u: string) => ({ image: u }),
);
contentItems.push({ text: prompt! });
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
input: {
messages: [
{
role: "user",
content: contentItems,
},
],
},
parameters: {
size: resolveImageSize(flags.size as string | undefined, true),
n,
seed: flags.seed as number | undefined,
prompt_extend: promptExtend,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
},
};
// Remove undefined parameters
stripUndefined(body.parameters as Record<string, unknown>);
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ request: body }, format);
return;
}
if (!config.quiet) {
process.stderr.write(`[Model: ${model}] [Mode: sync] [Images: ${resolvedImages.length}]\n`);
}
const url = imageSyncEndpoint(config.baseUrl);
const concurrent = getConcurrency(flags);
const results = await runConcurrent(concurrent, config, () =>
requestJson<DashScopeImageSyncResponse>(config, {
url,
method: "POST",
body,
}),
);
// Extract image URLs from all responses
const imageUrls = results
.flatMap((r) => r.output.choices || [])
.flatMap((c) => c.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
if (imageUrls.length === 0) {
throw new BailianError("Edit completed but no images returned.", ExitCode.GENERAL);
}
const outDir = resolveOutputDir(config, {
flagDir: flags.outDir as string | undefined,
subDir: flags.outDir ? undefined : "images",
});
const prefix =
(flags.outPrefix as string) || generateFilename("edited", flags?.prompt as string);
// Parallel download all images
const items =
imageUrls.length > 1
? imageUrls.map((url, i) => {
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
return { url, destPath: join(outDir, filename) };
})
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
const saved = await downloadParallel(items, downloadFile, { quiet: config.quiet });
if (config.quiet) {
emitBare(saved.join("\n"));
} else {
emitResult({ urls: imageUrls, saved, total: imageUrls.length }, format);
}
},
});
-314
View File
@@ -1,314 +0,0 @@
import {
defineCommand,
requestJson,
imageEndpoint,
imageSyncEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
resolveOutputDir,
isInteractive,
type DashScopeImageRequest,
type DashScopeImageSyncResponse,
BailianError,
ExitCode,
type DashScopeAsyncResponse,
type OutputFormat,
type DashScopeTaskResponse,
generateFilename,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile } from "../../utils/download.ts";
import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { resolveImageSize } from "../../utils/image-size.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
import { join } from "path";
// qwen-image-2.0 series uses the sync multimodal-generation endpoint
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
}
export default defineCommand({
name: "image generate",
description: "Generate images (Qwen-Image / wan2.x)",
usage: "bl image generate --prompt <text> [flags]",
options: [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID (default: qwen-image-2.0)" },
{
flag: "--size <W*H>",
description: "Image size: ratio (3:4, 16:9, 1:1) or pixels (2048*2048)",
},
{
flag: "--n <count>",
description: "Number of images per request (default: 1, max: 6)",
type: "number",
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{
flag: "--no-wait",
description: "Return task ID immediately without waiting (async models only)",
},
{ flag: "--out-dir <dir>", description: "Download images to directory" },
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: image)" },
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 3)",
type: "number",
},
],
examples: [
'bl image generate --prompt "一只穿太空服的猫在火星上"',
'bl image generate --prompt "Logo design" --n 3 --out-dir ./generated/',
'bl image generate --prompt "Mountain landscape" --size 2688*1536',
'bl image generate --prompt "A castle" --seed 42 --prompt-extend false',
'bl image generate --prompt "Logo" --watermark false',
'bl image generate --prompt "An alien in the space" --watermark false',
'bl image generate --prompt "sunset" --model wan2.6-t2i --no-wait --quiet',
'bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro',
'bl image generate --prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
],
async run(config: Config, flags: GlobalFlags) {
let prompt = (flags.prompt ?? (flags._positional as string[] | undefined)?.[0]) as
| string
| undefined;
if (!prompt) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({
message: "Enter your image prompt:",
});
if (!hint) {
process.stderr.write("Image generation cancelled.\n");
process.exit(1);
}
prompt = hint;
} else {
failIfMissing("prompt", "bl image generate --prompt <text>");
}
}
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
const useSync = isSyncModel(model);
const defaultSize = useSync ? "1:1" : "1:1";
const sizeInput = (flags.size as string) || defaultSize;
const size = resolveImageSize(sizeInput, useSync);
const n = (flags.n as number) ?? 1;
const concurrent = getConcurrency(flags);
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
useSync ? true : undefined,
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
input: {
messages: [{ role: "user", content: [{ text: prompt! }] }],
},
parameters: {
size,
n,
seed: flags.seed as number | undefined,
prompt_extend: promptExtend,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
},
};
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ request: body, mode: useSync ? "sync" : "async" }, format);
return;
}
if (!config.quiet) {
process.stderr.write(`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}]\n`);
}
if (useSync) {
await handleSyncMode(config, model, body, flags, format, concurrent);
} else {
await handleAsyncMode(config, model, body, flags, format, concurrent);
}
},
});
// ---- Sync mode: qwen-image-2.0 series ----
async function handleSyncMode(
config: Config,
_model: string,
body: DashScopeImageRequest,
flags: GlobalFlags,
format: string,
concurrent: number,
): Promise<void> {
const url = imageSyncEndpoint(config.baseUrl);
const results = await runConcurrent(concurrent, config, () =>
requestJson<DashScopeImageSyncResponse>(config, { url, method: "POST", body }),
);
const imageUrls = results
.flatMap((r) => r.output.choices || [])
.flatMap((c) => c.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
if (imageUrls.length === 0) {
throw new BailianError("Generation completed but no images returned.", ExitCode.GENERAL);
}
await saveImages(imageUrls, flags, config, format);
}
// ---- Async mode: wan2.x / qwen-image-plus ----
async function handleAsyncMode(
config: Config,
_model: string,
body: DashScopeImageRequest,
flags: GlobalFlags,
format: string,
concurrent: number,
): Promise<void> {
const url = imageEndpoint(config.baseUrl);
const responses = await runConcurrent(
concurrent,
config,
() => requestJson<DashScopeAsyncResponse>(config, { url, method: "POST", body, async: true }),
"tasks",
);
const taskIds = responses.map((r) => r.output.task_id);
// --no-wait: return all task IDs immediately
if (flags.noWait || config.async) {
emitResult({ task_ids: taskIds }, format as OutputFormat);
return;
}
// Poll all tasks concurrently
const pollInterval = (flags.pollInterval as number) ?? 3;
const pollPromises = taskIds.map((taskId) => {
const pollUrl = taskEndpoint(config.baseUrl, taskId);
return poll<DashScopeTaskResponse>(config, {
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: config.timeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
},
});
});
const results = await Promise.all(pollPromises);
let imageUrls: string[] = [];
for (const result of results) {
if (result.output.choices) {
const urls = result.output.choices
.flatMap((c) => c.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
imageUrls.push(...urls);
}
if (result.output.results) {
const urls = result.output.results.map((r) => r.url).filter(Boolean);
if (urls.length > 0 && imageUrls.length === 0) {
imageUrls.push(...urls);
}
}
}
if (imageUrls.length === 0) {
throw new BailianError("All tasks completed but no images returned.", ExitCode.GENERAL);
}
await saveImages(
imageUrls,
flags,
config,
format,
taskIds.length === 1 ? taskIds[0] : undefined,
taskIds,
);
}
// ---- Shared: download & save ----
async function saveImages(
imageUrls: string[],
flags: GlobalFlags,
config: Config,
format: string,
taskId?: string,
taskIds?: string[],
): Promise<void> {
const outDir = resolveOutputDir(config, {
flagDir: flags.outDir as string | undefined,
subDir: flags.outDir ? undefined : "images",
});
const promptText =
(flags.prompt as string) || (flags._positional as string[] | undefined)?.[0] || "";
const prefix = (flags.outPrefix as string) || generateFilename("image", promptText);
// Parallel download all images
const items =
imageUrls.length > 1
? imageUrls.map((url, i) => {
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
return { url, destPath: join(outDir, filename) };
})
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
const results = await downloadParallel(items, downloadFile, { quiet: config.quiet });
if (config.quiet) {
emitBare(results.join("\n"));
} else {
const output: Record<string, unknown> = {
urls: imageUrls,
saved: results,
total: imageUrls.length,
};
if (taskId) output.task_id = taskId;
if (taskIds && taskIds.length > 1) output.task_ids = taskIds;
emitResult(output, format as OutputFormat);
}
}
-1
View File
@@ -1 +0,0 @@
export { commands } from "./catalog.ts";
@@ -1,304 +0,0 @@
import {
defineCommand,
knowledgeRetrieveEndpoint,
signRequest,
requestJson,
detectOutputFormat,
maskToken,
resolveCredential,
trackingHeaders,
type Config,
type GlobalFlags,
type KnowledgeRetrieveRequest,
type KnowledgeRetrieveResponse,
type DashScopeKnowledgeRetrieveRequest,
type DashScopeKnowledgeRetrieveResponse,
type OutputFormat,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
export default defineCommand({
name: "knowledge retrieve",
description: "Retrieve from a Bailian knowledge base",
usage: "bl knowledge retrieve --index-id <id> --query <text> [flags]",
options: [
{ flag: "--index-id <id>", description: "Knowledge base index ID (required)", required: true },
{ flag: "--query <text>", description: "Search query (required)", required: true },
{
flag: "--dense-similarity-top-k <n>",
description: "Dense retrieval top K",
type: "number",
},
{
flag: "--sparse-similarity-top-k <n>",
description: "Sparse retrieval top K",
type: "number",
},
{ flag: "--rerank", description: "Enable reranking" },
{ flag: "--rerank-top-n <n>", description: "Rerank top N results", type: "number" },
{
flag: "--rerank-model <name>",
description: "Rerank model, e.g. qwen3-rerank-hybrid",
},
{
flag: "--rerank-mode <mode>",
description: "Rerank mode: qa, similar, or custom",
},
{
flag: "--rerank-instruct <text>",
description: "Custom rerank instruction, when mode=custom",
},
{
flag: "--top-k <n>",
description: "Number of results (deprecated, use --rerank-top-n)",
type: "number",
},
{
flag: "--workspace-id <id>",
description: "Bailian workspace ID (required for AK/SK auth)",
},
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
{
flag: "--access-key-secret <key>",
description: "Alibaba Cloud Access Key Secret (deprecated)",
},
],
examples: [
'bl knowledge retrieve --index-id idx_xxx --query "如何使用阿里云百炼"',
'bl knowledge retrieve --index-id idx_xxx --query "API限流" --rerank --rerank-model qwen3-rerank-hybrid',
],
async run(config: Config, flags: GlobalFlags) {
const indexId = flags.indexId as string;
if (!indexId) failIfMissing("index-id", "bl knowledge retrieve --index-id <id> --query <text>");
const query = flags.query as string;
if (!query) failIfMissing("query", "bl knowledge retrieve --index-id <id> --query <text>");
const format = detectOutputFormat(config.output);
const hasExplicitApiKey = !!config.apiKey;
const hasExplicitAkSk = !!(flags.accessKeyId && flags.accessKeySecret);
if (hasExplicitApiKey) {
await runWithApiKey(config, flags, indexId, query, format);
} else if (hasExplicitAkSk) {
await runWithAkSk(config, flags, indexId, query, format);
} else {
let useApiKey = false;
try {
await resolveCredential(config);
useApiKey = true;
} catch {
// No API-KEY credential available
}
if (useApiKey) {
await runWithApiKey(config, flags, indexId, query, format);
} else {
await runWithAkSk(config, flags, indexId, query, format);
}
}
},
});
// ---- API-KEY path (DashScope gateway, snake_case) ----
async function runWithApiKey(
config: Config,
flags: GlobalFlags,
indexId: string,
query: string,
format: OutputFormat,
): Promise<void> {
if (flags.topK !== undefined && flags.rerankTopN === undefined) {
process.stderr.write("Warning: --top-k is deprecated. Use --rerank-top-n instead.\n");
flags.rerankTopN = flags.topK;
}
const body: DashScopeKnowledgeRetrieveRequest = {
index_id: indexId,
query,
search_filters: [],
};
if (flags.denseSimilarityTopK !== undefined)
body.dense_similarity_top_k = flags.denseSimilarityTopK as number;
if (flags.sparseSimilarityTopK !== undefined)
body.sparse_similarity_top_k = flags.sparseSimilarityTopK as number;
if (flags.rerank) body.enable_reranking = true;
if (flags.rerankTopN !== undefined) body.rerank_top_n = flags.rerankTopN as number;
if (flags.rerankModel) {
const rerankEntry: { model_name: string; rerank_mode?: string; rerank_instruct?: string } = {
model_name: flags.rerankModel as string,
};
if (flags.rerankMode) rerankEntry.rerank_mode = flags.rerankMode as string;
if (flags.rerankInstruct) rerankEntry.rerank_instruct = flags.rerankInstruct as string;
body.rerank = [rerankEntry];
}
const url = knowledgeRetrieveEndpoint(config.baseUrl);
if (config.dryRun) {
emitResult({ endpoint: url, request: body }, format);
return;
}
const response = await requestJson<DashScopeKnowledgeRetrieveResponse>(config, {
url,
method: "POST",
body,
});
const nodes = response.data?.nodes || [];
if (config.quiet || format === "text") {
emitTextNodes(nodes.map((n) => ({ text: n.text, score: n.score })));
} else {
emitResult(response, format);
}
}
// ---- AK/SK path (Bailian OpenAPI gateway, PascalCase) ----
async function runWithAkSk(
config: Config,
flags: GlobalFlags,
indexId: string,
query: string,
format: OutputFormat,
): Promise<void> {
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
if (!accessKeyId || !accessKeySecret) {
throw new BailianError(
"No credentials found.\n" +
"Preferred: set DASHSCOPE_API_KEY or pass --api-key.\n" +
"Legacy (deprecated): set ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
ExitCode.AUTH,
);
}
if (!workspaceId) {
throw new BailianError(
"Knowledge retrieve requires a workspace ID.\n" +
"Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
ExitCode.USAGE,
);
}
process.stderr.write(
"Warning: AK/SK auth for knowledge retrieve is deprecated. Prefer --api-key or DASHSCOPE_API_KEY.\n",
);
const body: KnowledgeRetrieveRequest = {
IndexId: indexId,
Query: query,
};
if (flags.topK !== undefined && flags.rerankTopN === undefined) {
process.stderr.write("Warning: --top-k is deprecated. Use --rerank-top-n instead.\n");
flags.rerankTopN = flags.topK;
}
if (flags.rerank) body.EnableReranking = true;
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
if (flags.denseSimilarityTopK !== undefined)
body.DenseSimilarityTopK = flags.denseSimilarityTopK as number;
if (flags.sparseSimilarityTopK !== undefined)
body.SparseSimilarityTopK = flags.sparseSimilarityTopK as number;
if (flags.rerankModel) {
const rerank: { ModelName: string; RerankMode?: string; RerankInstruct?: string } = {
ModelName: flags.rerankModel as string,
};
if (flags.rerankMode) rerank.RerankMode = flags.rerankMode as string;
if (flags.rerankInstruct) rerank.RerankInstruct = flags.rerankInstruct as string;
body.Rerank = [rerank];
}
const pathname = `/${workspaceId}/index/retrieve`;
if (config.dryRun) {
emitResult(
{
endpoint: `https://${BAILIAN_HOST}${pathname}`,
workspaceId,
request: body,
},
format,
);
return;
}
const bodyStr = JSON.stringify(body);
const headers = signRequest({
accessKeyId,
accessKeySecret,
action: "Retrieve",
version: "2023-12-29",
body: bodyStr,
host: BAILIAN_HOST,
pathname,
});
const url = `https://${BAILIAN_HOST}${pathname}`;
if (config.verbose) {
process.stderr.write(`> POST ${url}\n`);
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
}
const timeoutMs = config.timeout * 1000;
const res = await fetch(url, {
method: "POST",
headers: { ...headers, ...trackingHeaders() },
body: bodyStr,
signal: AbortSignal.timeout(timeoutMs),
});
if (config.verbose) {
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
}
const data = (await res.json()) as KnowledgeRetrieveResponse & {
Code?: string;
Message?: string;
};
if (!res.ok || (data.Code && data.Code !== "Success")) {
throw new BailianError(
`Knowledge retrieve failed: ${data.Code || res.status} - ${data.Message || res.statusText}`,
ExitCode.GENERAL,
);
}
const nodes = data.Data?.Nodes || [];
if (config.quiet || format === "text") {
emitTextNodes(nodes.map((n) => ({ text: n.Text, score: n.Score })));
} else {
emitResult(data, format);
}
}
// ---- Shared text output ----
function emitTextNodes(nodes: Array<{ text: string; score: number }>): void {
if (nodes.length === 0) {
emitBare("No results found.");
} else {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
emitBare(`[${i + 1}] (score: ${node.score.toFixed(4)})`);
emitBare(node.text);
emitBare("");
}
}
}
-123
View File
@@ -1,123 +0,0 @@
import {
defineCommand,
McpClient,
bailianMcpUrl,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult } from "../../output/output.ts";
import { ensureApiKey } from "../../utils/ensure-key.ts";
function parseArgFlags(raw: string[]): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const item of raw) {
const idx = item.indexOf("=");
if (idx <= 0) {
process.stderr.write(`Error: --arg must be in K=V form, got: ${item}\n`);
process.exit(1);
}
const key = item.slice(0, idx).trim();
const rawVal = item.slice(idx + 1);
try {
out[key] = JSON.parse(rawVal);
} catch {
out[key] = rawVal;
}
}
return out;
}
export default defineCommand({
name: "mcp call",
description: "Call a tool on an MCP server (tools/call)",
usage: "bl mcp call <server-code>.<tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
options: [
{
flag: "<server-code>.<tool>",
description:
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
required: true,
},
{
flag: "--arg <kv>",
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
type: "array",
},
{
flag: "--json <obj>",
description: "Full arguments object as JSON; merged with --arg (arg wins).",
},
{
flag: "--query <text>",
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
},
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
],
examples: [
'bl mcp call market-cmapi00073529.SmartStockSelection --query "筛选ROE>15%的消费股"',
'bl mcp call market-cmapi00073529.FinQuery --json \'{"q":"贵州茅台","limit":5}\'',
"bl mcp call market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
],
async run(config: Config, flags: GlobalFlags) {
const positional =
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
const target = positional[0];
if (!target) failIfMissing("<server-code>.<tool>", "bl mcp call <server-code>.<tool>");
const dot = target!.indexOf(".");
if (dot <= 0 || dot === target!.length - 1) {
process.stderr.write(`Error: target must be <server-code>.<tool>, got "${target}".\n`);
process.exit(1);
}
const serverCode = target!.slice(0, dot);
const toolName = target!.slice(dot + 1);
let toolArgs: Record<string, unknown> = {};
if (flags.json) {
try {
const parsed = JSON.parse(flags.json as string);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
process.stderr.write("Error: --json must decode to an object.\n");
process.exit(1);
}
toolArgs = parsed as Record<string, unknown>;
} catch (err) {
process.stderr.write(`Error: --json is not valid JSON — ${(err as Error).message}\n`);
process.exit(1);
}
}
Object.assign(toolArgs, parseArgFlags((flags.arg as string[] | undefined) ?? []));
if (flags.query !== undefined) toolArgs.query = flags.query;
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, serverCode);
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult(
{
server: serverCode,
url,
tool: toolName,
arguments: toolArgs,
},
format,
);
return;
}
await ensureApiKey(config);
const client = new McpClient(config, url);
await client.initialize();
const result = await client.callTool(toolName, toolArgs);
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
process.stderr.write(`Tool error: ${errText}\n`);
process.exit(1);
}
emitResult(result, format);
},
});
-50
View File
@@ -1,50 +0,0 @@
import {
defineCommand,
McpClient,
bailianMcpUrl,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult } from "../../output/output.ts";
import { ensureApiKey } from "../../utils/ensure-key.ts";
export default defineCommand({
name: "mcp tools",
description: "List tools exposed by an MCP server (tools/list)",
usage: "bl mcp tools <server-code> [--url <url>]",
options: [
{
flag: "<server-code>",
description: "Server code from `bl mcp list` (e.g. market-cmapi00073529)",
required: true,
},
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
],
examples: [
"bl mcp tools market-cmapi00073529",
"bl mcp tools market-cmapi00073529 --output json",
"bl mcp tools my-server --url https://example.com/mcp",
],
async run(config: Config, flags: GlobalFlags) {
const positional =
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
const code = positional[0];
if (!code) failIfMissing("server-code", "bl mcp tools <server-code>");
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code!);
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ server: code, url, action: "tools/list" }, format);
return;
}
await ensureApiKey(config);
const client = new McpClient(config, url);
await client.initialize();
const tools = await client.listTools();
emitResult({ server: code, url, tools }, format);
},
});
-81
View File
@@ -1,81 +0,0 @@
import {
defineCommand,
requestJson,
memoryAddEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemoryAddRequest,
type MemoryAddResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory add",
description: "Add memory from messages or custom content",
usage: "bl memory add --user-id <id> [--messages <json>] [--content <text>] [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{
flag: "--messages <json>",
description: 'Messages JSON array: [{"role":"user","content":"..."},...]',
},
{ flag: "--content <text>", description: "Custom content text to memorize" },
{ flag: "--profile-schema <id>", description: "Profile schema ID for user profiling" },
{ flag: "--memory-library-id <id>", description: "Memory library ID (isolate memory space)" },
],
examples: [
'bl memory add --user-id user1 --content "用户喜欢Python编程"',
'bl memory add --user-id user1 --messages \'[{"role":"user","content":"我喜欢旅行"}]\'',
'bl memory add --user-id user1 --content "住在北京" --profile-schema schema_xxx',
],
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
if (!userId) failIfMissing("user-id", "bl memory add --user-id <id>");
const body: MemoryAddRequest = { user_id: userId };
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages as string);
} catch {
process.stderr.write("Error: --messages must be valid JSON array\n");
process.exit(1);
}
}
if (flags.content) {
body.custom_content = flags.content as string;
}
if (!body.messages && !body.custom_content) {
process.stderr.write("Error: at least one of --messages or --content is required\n");
process.exit(1);
}
if (flags.profileSchema) body.profile_schema = flags.profileSchema as string;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ endpoint: memoryAddEndpoint(config.baseUrl), request: body }, format);
return;
}
const url = memoryAddEndpoint(config.baseUrl);
const response = await requestJson<MemoryAddResponse>(config, {
url,
method: "POST",
body,
});
if (config.quiet || format === "text") {
const ids = response.memory_ids?.join(", ") || "none";
emitBare(`Memory added. IDs: ${ids}`);
} else {
emitResult(response, format);
}
},
});
@@ -1,50 +0,0 @@
import {
defineCommand,
requestJson,
memoryNodeEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory delete",
description: "Delete a memory node",
usage: "bl memory delete --node-id <id> --user-id <id>",
options: [
{ flag: "--node-id <id>", description: "Memory node ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--memory-library-id <id>", description: "Memory library ID (non-default library)" },
],
examples: ["bl memory delete --node-id node_xxx --user-id user1"],
async run(config: Config, flags: GlobalFlags) {
const nodeId = flags.nodeId as string;
if (!nodeId) failIfMissing("node-id", "bl memory delete --node-id <id> --user-id <id>");
const userId = flags.userId as string;
if (!userId) failIfMissing("user-id", "bl memory delete --node-id <id> --user-id <id>");
const format = detectOutputFormat(config.output);
const params = new URLSearchParams({ user_id: userId });
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId as string);
const url = `${memoryNodeEndpoint(config.baseUrl, nodeId)}?${params.toString()}`;
if (config.dryRun) {
emitResult({ endpoint: url, method: "DELETE" }, format);
return;
}
const response = await requestJson<{ request_id: string }>(config, {
url,
method: "DELETE",
});
if (config.quiet || format === "text") {
emitBare(`Memory node ${nodeId} deleted.`);
} else {
emitResult(response, format);
}
},
});
-65
View File
@@ -1,65 +0,0 @@
import {
defineCommand,
requestJson,
memoryListEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemoryNodeListResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory list",
description: "List memory nodes for a user",
usage: "bl memory list --user-id <id> [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--page-size <n>", description: "Results per page (default: 10)", type: "number" },
{ flag: "--page <n>", description: "Page number (default: 1)", type: "number" },
{ flag: "--memory-library-id <id>", description: "Memory library ID" },
],
examples: [
"bl memory list --user-id user1",
"bl memory list --user-id user1 --page-size 20 --page 2",
],
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
if (!userId) failIfMissing("user-id", "bl memory list --user-id <id>");
const format = detectOutputFormat(config.output);
const params = new URLSearchParams();
params.set("user_id", userId);
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize as number));
if (flags.page !== undefined) params.set("page_num", String(flags.page as number));
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId as string);
const url = `${memoryListEndpoint(config.baseUrl)}?${params.toString()}`;
if (config.dryRun) {
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await requestJson<MemoryNodeListResponse>(config, {
url,
method: "GET",
});
if (config.quiet || format === "text") {
if (!response.memory_nodes || response.memory_nodes.length === 0) {
emitBare("No memory nodes found.");
} else {
for (const node of response.memory_nodes) {
emitBare(`[${node.memory_node_id}] ${node.content}`);
}
if (response.total !== undefined) {
emitBare(`\nTotal: ${response.total}`);
}
}
} else {
emitResult(response, format);
}
},
});
@@ -1,69 +0,0 @@
import {
defineCommand,
requestJson,
profileSchemaEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type ProfileSchemaCreateRequest,
type ProfileSchemaCreateResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory profile create",
description: "Create a user profile schema for memory profiling",
usage: "bl memory profile create --name <name> --attributes <json> [flags]",
options: [
{ flag: "--name <name>", description: "Schema name (required)", required: true },
{ flag: "--description <text>", description: "Schema description" },
{
flag: "--attributes <json>",
description: 'Attributes JSON array: [{"name":"age","description":"年龄"}]',
required: true,
},
],
examples: [
'bl memory profile create --name "user_basic" --attributes \'[{"name":"age","description":"年龄"},{"name":"hobby","description":"爱好"}]\'',
],
async run(config: Config, flags: GlobalFlags) {
const name = flags.name as string;
if (!name) failIfMissing("name", "bl memory profile create --name <name> --attributes <json>");
const attrStr = flags.attributes as string;
if (!attrStr)
failIfMissing("attributes", "bl memory profile create --name <name> --attributes <json>");
let attributes;
try {
attributes = JSON.parse(attrStr);
} catch {
process.stderr.write("Error: --attributes must be valid JSON array\n");
process.exit(1);
}
const body: ProfileSchemaCreateRequest = { name, attributes };
if (flags.description) body.description = flags.description as string;
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ endpoint: profileSchemaEndpoint(config.baseUrl), request: body }, format);
return;
}
const url = profileSchemaEndpoint(config.baseUrl);
const response = await requestJson<ProfileSchemaCreateResponse>(config, {
url,
method: "POST",
body,
});
if (config.quiet || format === "text") {
emitBare(`Profile schema created: ${response.profile_schema_id}`);
} else {
emitResult(response, format);
}
},
});
@@ -1,56 +0,0 @@
import {
defineCommand,
requestJson,
userProfileEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type UserProfileResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory profile get",
description: "Get user profile by schema ID and user ID",
usage: "bl memory profile get --schema-id <id> --user-id <id>",
options: [
{ flag: "--schema-id <id>", description: "Profile schema ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
],
examples: ["bl memory profile get --schema-id schema_xxx --user-id user1"],
async run(config: Config, flags: GlobalFlags) {
const schemaId = flags.schemaId as string;
if (!schemaId)
failIfMissing("schema-id", "bl memory profile get --schema-id <id> --user-id <id>");
const userId = flags.userId as string;
if (!userId) failIfMissing("user-id", "bl memory profile get --schema-id <id> --user-id <id>");
const format = detectOutputFormat(config.output);
const params = new URLSearchParams({ user_id: userId });
const url = `${userProfileEndpoint(config.baseUrl, schemaId)}?${params.toString()}`;
if (config.dryRun) {
emitResult({ endpoint: url, method: "GET" }, format);
return;
}
const response = await requestJson<UserProfileResponse>(config, {
url,
method: "GET",
});
if (config.quiet || format === "text") {
if (response.profile?.attributes) {
for (const attr of response.profile.attributes) {
emitBare(`${attr.name}: ${attr.value ?? "(empty)"}`);
}
} else {
emitBare("No profile data found.");
}
} else {
emitResult(response, format);
}
},
});
@@ -1,89 +0,0 @@
import {
defineCommand,
requestJson,
memorySearchEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemorySearchRequest,
type MemorySearchResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory search",
description: "Search memory nodes by query or messages",
usage: "bl memory search --user-id <id> [--query <text>] [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--query <text>", description: "Search query text" },
{ flag: "--messages <json>", description: "Messages JSON array for context-based search" },
{
flag: "--top-k <n>",
description: "Number of results to return (default: 10)",
type: "number",
},
{ flag: "--memory-library-id <id>", description: "Memory library ID" },
],
examples: [
'bl memory search --user-id user1 --query "编程偏好"',
'bl memory search --user-id user1 --messages \'[{"role":"user","content":"推荐一本书"}]\' --top-k 5',
],
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
if (!userId) failIfMissing("user-id", "bl memory search --user-id <id>");
const body: MemorySearchRequest = { user_id: userId };
if (flags.query) body.query = flags.query as string;
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages as string);
} catch {
process.stderr.write("Error: --messages must be valid JSON array\n");
process.exit(1);
}
}
// API requires messages; if only query is given, wrap it as a user message
if (!body.messages && body.query) {
body.messages = [{ role: "user", content: body.query }];
}
if (!body.query && !body.messages) {
process.stderr.write("Error: at least one of --query or --messages is required\n");
process.exit(1);
}
if (flags.topK !== undefined) body.top_k = flags.topK as number;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ endpoint: memorySearchEndpoint(config.baseUrl), request: body }, format);
return;
}
const url = memorySearchEndpoint(config.baseUrl);
const response = await requestJson<MemorySearchResponse>(config, {
url,
method: "POST",
body,
});
if (config.quiet || format === "text") {
if (!response.memory_nodes || response.memory_nodes.length === 0) {
emitBare("No memory nodes found.");
} else {
for (const node of response.memory_nodes) {
emitBare(`[${node.memory_node_id}] ${node.content}`);
}
}
} else {
emitResult(response, format);
}
},
});
@@ -1,70 +0,0 @@
import {
defineCommand,
requestJson,
memoryNodeEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemoryNodeUpdateRequest,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "memory update",
description: "Update a memory node content",
usage: "bl memory update --node-id <id> --user-id <id> --content <text>",
options: [
{ flag: "--node-id <id>", description: "Memory node ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{
flag: "--content <text>",
description: "New content for the memory node (required)",
required: true,
},
{ flag: "--memory-library-id <id>", description: "Memory library ID (non-default library)" },
],
examples: ['bl memory update --node-id node_xxx --user-id user1 --content "更新后的记忆内容"'],
async run(config: Config, flags: GlobalFlags) {
const nodeId = flags.nodeId as string;
if (!nodeId)
failIfMissing("node-id", "bl memory update --node-id <id> --user-id <id> --content <text>");
const userId = flags.userId as string;
if (!userId)
failIfMissing("user-id", "bl memory update --node-id <id> --user-id <id> --content <text>");
const content = flags.content as string;
if (!content)
failIfMissing("content", "bl memory update --node-id <id> --user-id <id> --content <text>");
const body: MemoryNodeUpdateRequest = {
user_id: userId,
custom_content: content,
};
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult(
{ endpoint: memoryNodeEndpoint(config.baseUrl, nodeId), method: "PATCH", request: body },
format,
);
return;
}
const url = memoryNodeEndpoint(config.baseUrl, nodeId);
const response = await requestJson<{ request_id: string }>(config, {
url,
method: "PATCH",
body,
});
if (config.quiet || format === "text") {
emitBare(`Memory node ${nodeId} updated.`);
} else {
emitResult(response, format);
}
},
});
-348
View File
@@ -1,348 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
import { displayWidth, padEnd } from "../../output/cjk-width.ts";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
const MONITOR_API = "zeldaEasy.bailian-telemetry.monitor.getMonitorData";
interface QpmInfoItem {
count_limit: number;
count_limit_period: number;
usage_limit: number;
usage_limit_period: number;
usage_limit_field: string;
type: string;
}
interface ModelWithQpm {
model: string;
qpmInfo?: Record<string, QpmInfoItem>;
}
interface MonitorPoint {
value: number;
timestamp: number;
}
interface MonitorMetric {
aggMethod: string;
metricName: string;
points: MonitorPoint[];
}
function calculateRPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.count_limit_period || fallbackPeriod;
if (!period) return 0;
return Math.floor((item.count_limit * 60) / period);
}
function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.usage_limit_period || fallbackPeriod;
if (!period) return 0;
return Math.floor((item.usage_limit * 60) / period);
}
function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function formatRatio(usage: number, limit: number): string {
if (limit <= 0) return "-";
const pct = Math.round((usage / limit) * 100);
return `${formatNumber(usage)}/${formatNumber(limit)} (${pct}%)`;
}
function getStatus(usage: number, limit: number): string {
if (limit <= 0) return "-";
const pct = (usage / limit) * 100;
if (pct >= 100) return "已限流";
if (pct >= 80) return "接近限流";
return "正常";
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
async function fetchAllModelsWithQpm(
config: Config,
token: string,
region: string,
): Promise<ModelWithQpm[]> {
const allModels: ModelWithQpm[] = [];
let pageNo = 1;
while (true) {
const raw = await callConsoleGateway(config, token, {
api: MODEL_LIST_API,
data: {
input: {
pageNo,
pageSize: 50,
group: false,
queryQpmInfo: true,
ignoreWorkspaceServiceSite: true,
supports: { selfServiceLimitIncrease: true },
},
},
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
const list = (resp.list as ModelWithQpm[]) ?? [];
const total = (resp.total as number) ?? 0;
allModels.push(...list);
if (allModels.length >= total || list.length === 0) break;
pageNo++;
}
return allModels;
}
async function fetchMonitorData(
config: Config,
token: string,
region: string,
modelName: string,
windowMinutes: number,
): Promise<{ rpm: number; tpm: number }> {
const now = Date.now();
const startTime = now - windowMinutes * 60 * 1000;
try {
const raw = await callConsoleGateway(config, token, {
api: MONITOR_API,
data: {
reqDTO: {
monitorType: "Advanced",
metricFilters: [
{ aggMethod: "sum_pm", metricName: "model_total_amount" },
{ aggMethod: "sum_pm", metricName: "model_call_count" },
],
labelFilters: {
resourceId: modelName,
resourceType: "model",
},
startTime,
endTime: now,
},
},
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
const metrics = (resp.data ?? resp) as MonitorMetric[] | Record<string, unknown>;
if (!Array.isArray(metrics)) return { rpm: 0, tpm: 0 };
let rpm = 0;
let tpm = 0;
for (const metric of metrics) {
if (metric.aggMethod !== "sum_pm" || !metric.points?.length) continue;
const lastValue = metric.points[metric.points.length - 1].value ?? 0;
if (metric.metricName === "model_call_count") rpm = Math.round(lastValue);
if (metric.metricName === "model_total_amount") tpm = Math.round(lastValue);
}
return { rpm, tpm };
} catch {
return { rpm: -1, tpm: -1 };
}
}
interface CheckRow {
model: string;
rpmUsage: number;
rpmLimit: number;
tpmUsage: number;
tpmLimit: number;
}
function printTable(rows: CheckRow[], noColor: boolean): void {
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;
const green = noColor ? (t: string) => t : (t: string) => `\x1b[32m${t}\x1b[0m`;
const yellow = noColor ? (t: string) => t : (t: string) => `\x1b[33m${t}\x1b[0m`;
const red = noColor ? (t: string) => t : (t: string) => `\x1b[31m${t}\x1b[0m`;
const headersCn = ["模型", "RPM 用量/限额", "TPM 用量/限额", "状态"];
const headersEn = ["Model", "RPM Usage/Limit", "TPM Usage/Limit", "Status"];
const tableRows = rows.map((r) => {
const rpmStr = r.rpmUsage < 0 ? "-" : formatRatio(r.rpmUsage, r.rpmLimit);
const tpmStr = r.tpmUsage < 0 ? "-" : formatRatio(r.tpmUsage, r.tpmLimit);
const maxPct = Math.max(
r.rpmLimit > 0 ? (r.rpmUsage / r.rpmLimit) * 100 : 0,
r.tpmLimit > 0 ? (r.tpmUsage / r.tpmLimit) * 100 : 0,
);
const status =
r.rpmUsage < 0
? "-"
: getStatus(Math.max(r.rpmUsage, r.tpmUsage), Math.max(r.rpmLimit, r.tpmLimit));
return { cells: [r.model, rpmStr, tpmStr, status], maxPct };
});
if (tableRows.length === 0) {
process.stdout.write("No models found.\n");
return;
}
const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...tableRows.map((r) => displayWidth(r.cells[col])),
),
);
const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");
process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(separator + "\n");
const statusCol = 3;
for (const r of tableRows) {
const cells = r.cells.map((cell, col) => {
if (col === statusCol) {
if (cell === "已限流") return red(padEnd(cell, widths[col]));
if (cell === "接近限流") return yellow(padEnd(cell, widths[col]));
if (cell === "正常") return green(padEnd(cell, widths[col]));
}
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(dim(`\n共 ${rows.length} 个模型 (Total: ${rows.length})`) + "\n");
}
export default defineCommand({
name: "quota check",
description: "Check current usage against rate limits",
usage: "bl quota check [--model <model>] [flags]",
options: [
{
flag: "--model <model>",
description: "Model name(s), comma-separated",
},
{
flag: "--period <minutes>",
description: "Query usage for the last N minutes (default: 2)",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl quota check",
"bl quota check --model qwen3.6-plus",
"bl quota check --period 5",
"bl quota check --model qwen3.6-plus,qwen-turbo",
"bl quota check --output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const rawPeriod = Number(flags.period) || 2;
if (rawPeriod < 1) {
process.stderr.write("Error: --period must be at least 1 minute.\n");
process.exit(1);
}
const windowMinutes = rawPeriod;
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
if (config.dryRun) {
emitResult(
{
apis: [MODEL_LIST_API, MONITOR_API],
region,
},
format,
);
return;
}
let models = await fetchAllModelsWithQpm(config, credential.token, region);
if (modelFlag) {
const names = new Set(
modelFlag
.split(",")
.map((n) => n.trim())
.filter(Boolean),
);
models = models.filter((m) => names.has(m.model));
}
models = models.filter((m) => m.qpmInfo);
if (models.length === 0) {
process.stdout.write("No models found.\n");
return;
}
const monitorResults = await Promise.all(
models.map((m) => fetchMonitorData(config, credential.token, region, m.model, windowMinutes)),
);
const checkRows: CheckRow[] = models.map((m, idx) => {
const qpm = m.qpmInfo!;
const modelDefault = qpm["model-default"];
const userSpec = qpm["user-spec"];
const rpmLimit =
calculateRPM(userSpec, modelDefault?.count_limit_period) || calculateRPM(modelDefault);
const tpmLimit =
calculateTPM(userSpec, modelDefault?.usage_limit_period) || calculateTPM(modelDefault);
return {
model: m.model,
rpmUsage: monitorResults[idx].rpm,
rpmLimit,
tpmUsage: monitorResults[idx].tpm,
tpmLimit,
};
});
if (format === "json") {
emitResult(checkRows, format);
return;
}
printTable(checkRows, config.noColor);
},
});
-230
View File
@@ -1,230 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
import { displayWidth, padEnd } from "../../output/cjk-width.ts";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
interface QpmInfoItem {
count_limit: number;
count_limit_period: number;
usage_limit: number;
usage_limit_period: number;
usage_limit_field: string;
type: string;
}
interface ModelWithQpm {
model: string;
qpmInfo?: Record<string, QpmInfoItem>;
}
function calculateRPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.count_limit_period || fallbackPeriod;
if (!period) return 0;
return Math.floor((item.count_limit * 60) / period);
}
function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.usage_limit_period || fallbackPeriod;
if (!period) return 0;
return Math.floor((item.usage_limit * 60) / period);
}
function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
async function fetchAllModelsWithQpm(
config: Config,
token: string,
region: string,
onlySelfService: boolean,
): Promise<ModelWithQpm[]> {
const allModels: ModelWithQpm[] = [];
let pageNo = 1;
while (true) {
const input: Record<string, unknown> = {
pageNo,
pageSize: 50,
group: false,
queryQpmInfo: true,
ignoreWorkspaceServiceSite: true,
};
if (onlySelfService) {
input.supports = { selfServiceLimitIncrease: true };
}
const raw = await callConsoleGateway(config, token, {
api: MODEL_LIST_API,
data: { input },
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
const list = (resp.list as ModelWithQpm[]) ?? [];
const total = (resp.total as number) ?? 0;
allModels.push(...list);
if (allModels.length >= total || list.length === 0) break;
pageNo++;
}
return allModels;
}
function printTable(models: ModelWithQpm[], noColor: boolean): void {
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;
const headersCn = ["模型", "RPM", "TPM", "可设上限 TPM"];
const headersEn = ["Model", "Req/min", "Token/min", "Max TPM"];
const rows = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];
const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;
return [
m.model,
currentRPM > 0 ? formatNumber(currentRPM) : "-",
currentTPM > 0 ? formatNumber(currentTPM) : "-",
maxTPM > 0 ? formatNumber(maxTPM) : "-",
];
});
if (rows.length === 0) {
process.stdout.write("No models found.\n");
return;
}
const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...rows.map((row) => displayWidth(row[col])),
),
);
const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");
process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
process.stdout.write(row.map((cell, col) => padEnd(cell, widths[col])).join(" ") + "\n");
}
process.stdout.write(dim(`\n共 ${models.length} 个模型 (Total: ${models.length})`) + "\n");
}
export default defineCommand({
name: "quota list",
description: "View model RPM/TPM rate limits",
usage: "bl quota list [--model <model>] [flags]",
options: [
{
flag: "--model <model>",
description: "Model name(s), comma-separated",
},
{
flag: "--all",
description: "Show all models, not just self-service ones",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl quota list",
"bl quota list --model qwen3.6-plus",
"bl quota list --model qwen3.6-plus,qwen-turbo",
"bl quota list --all",
"bl quota list --output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const showAll = Boolean(flags.all);
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
if (config.dryRun) {
const input: Record<string, unknown> = {
pageNo: 1,
pageSize: 50,
group: false,
queryQpmInfo: true,
ignoreWorkspaceServiceSite: true,
};
if (!showAll) input.supports = { selfServiceLimitIncrease: true };
emitResult({ api: MODEL_LIST_API, data: { input }, region }, format);
return;
}
let models = await fetchAllModelsWithQpm(config, credential.token, region, !showAll);
if (modelFlag) {
const names = new Set(
modelFlag
.split(",")
.map((n) => n.trim())
.filter(Boolean),
);
models = models.filter((m) => names.has(m.model));
if (models.length === 0) {
process.stderr.write(`Error: no matching models found for "${modelFlag}".\n`);
process.exit(1);
}
}
if (format === "json") {
emitResult(models, format);
return;
}
printTable(models, config.noColor);
},
});
-220
View File
@@ -1,220 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
BailianError,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
const UPDATE_LIMITS_API = "zeldaEasy.broadscope-platform.modelInstance.updateFoundationModelLimits";
interface QpmInfoItem {
count_limit: number;
count_limit_period: number;
usage_limit: number;
usage_limit_period: number;
usage_limit_field: string;
type: string;
}
function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.usage_limit_period || fallbackPeriod;
if (!period) return 0;
return Math.floor((item.usage_limit * 60) / period);
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
async function fetchModelQpmInfo(
config: Config,
token: string,
region: string,
modelName: string,
): Promise<{ model: string; qpmInfo: Record<string, QpmInfoItem> } | undefined> {
const raw = await callConsoleGateway(config, token, {
api: MODEL_LIST_API,
data: {
input: {
pageNo: 1,
pageSize: 50,
name: modelName,
group: false,
queryQpmInfo: true,
ignoreWorkspaceServiceSite: true,
supports: { selfServiceLimitIncrease: true },
},
},
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
const list = (resp.list as Array<{ model: string; qpmInfo?: Record<string, QpmInfoItem> }>) ?? [];
return list.find((m) => m.model === modelName && m.qpmInfo) as
| { model: string; qpmInfo: Record<string, QpmInfoItem> }
| undefined;
}
export default defineCommand({
name: "quota request",
description: "Request a temporary quota increase",
usage: "bl quota request --model <model> --tpm <value> [flags]",
options: [
{
flag: "--model <model>",
description: "Model name (required)",
required: true,
},
{
flag: "--tpm <value>",
description: "Target TPM value (required)",
required: true,
},
{
flag: "--yes",
description: "Skip downgrade confirmation",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl quota request --model qwen-turbo --tpm 100000",
"bl quota request --model qwen3.6-plus --tpm 8000000 --yes",
"bl quota request --model qwen-turbo --tpm 100000 --output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelName = flags.model as string;
if (!modelName) {
process.stderr.write("Error: --model is required.\n");
process.exit(1);
}
const tpmValue = Number(flags.tpm);
if (!tpmValue || tpmValue <= 0) {
process.stderr.write("Error: --tpm must be a positive number.\n");
process.exit(1);
}
const autoConfirm = Boolean(flags.yes) || config.yes;
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
const modelInfo = await fetchModelQpmInfo(config, credential.token, region, modelName);
if (!modelInfo) {
process.stderr.write(
`Error: model "${modelName}" not found or does not support self-service quota increase.\n`,
);
process.stderr.write("Hint: run `bl quota list` to view available models.\n");
process.exit(1);
}
const modelDefault = modelInfo.qpmInfo["model-default"];
const userSpec = modelInfo.qpmInfo["user-spec"];
const minLimit = calculateTPM(modelDefault);
const currentLimit = calculateTPM(userSpec, modelDefault?.usage_limit_period) || minLimit;
const maxLimit = minLimit * 2;
if (tpmValue < minLimit || tpmValue > maxLimit) {
process.stderr.write(
`Error: TPM value ${tpmValue.toLocaleString()} is out of range.\n` +
` Current: ${currentLimit.toLocaleString()}\n` +
` Range: ${minLimit.toLocaleString()} ~ ${maxLimit.toLocaleString()}\n`,
);
process.exit(1);
}
const requestData = {
input: {
model: modelName,
limit: { usage_limit: tpmValue },
originalQpmInfo: modelInfo.qpmInfo,
} as Record<string, unknown>,
};
if (config.dryRun) {
emitResult({ api: UPDATE_LIMITS_API, data: requestData, region }, format);
return;
}
const submitRequest = async (confirmedDowngrade?: boolean): Promise<unknown> => {
if (confirmedDowngrade) {
requestData.input.confirmedDowngrade = true;
}
try {
return await callConsoleGateway(config, credential.token, {
api: UPDATE_LIMITS_API,
data: requestData,
region,
});
} catch (err) {
if (err instanceof BailianError && err.message.includes("NotLogined")) {
process.stderr.write(
"Error: session expired. Run `bl auth login --console` to re-authenticate.\n",
);
process.exit(1);
}
throw err;
}
};
let result = await submitRequest();
const resp = extractResponseData(result as Record<string, unknown>);
if (resp.needConfirm) {
const confirmCode = resp.confirmCode as string;
if (confirmCode === "Refresh_Required") {
process.stderr.write("Error: rate limit has been updated externally. Please retry.\n");
process.exit(1);
}
if (confirmCode === "Downgrade") {
if (!autoConfirm) {
process.stderr.write(
`Warning: target TPM (${tpmValue.toLocaleString()}) is lower than current (${currentLimit.toLocaleString()}).\n` +
"Use --yes to confirm downgrade.\n",
);
process.exit(1);
}
result = await submitRequest(true);
}
}
if (format === "json") {
emitResult(result, format);
return;
}
process.stdout.write(
`Quota updated for "${modelName}": TPM ${currentLimit.toLocaleString()}${tpmValue.toLocaleString()}\n`,
);
},
});
-138
View File
@@ -1,138 +0,0 @@
import {
defineCommand,
detectOutputFormat,
mcpWebSearchEndpoint,
type Config,
type GlobalFlags,
isInteractive,
McpClient,
} from "bailian-cli-core";
import { createSpinner } from "../../output/progress.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult } from "../../output/output.ts";
export default defineCommand({
name: "search web",
description: "Search the web using DashScope MCP WebSearch service",
usage: "bl search web --query <text> [flags]",
options: [
{ flag: "--query <text>", description: "Search query text", required: true },
{ flag: "--count <n>", description: "Number of search results (default: 10)", type: "number" },
{ flag: "--list-tools", description: "List available MCP tools and exit" },
],
examples: [
'bl search web --query "阿里云百炼最新功能"',
'bl search web --query "TypeScript 5.9 new features" --count 5',
'bl search web --query "今日新闻"',
"bl search web --list-tools",
],
async run(config: Config, flags: GlobalFlags) {
const mcpUrl = mcpWebSearchEndpoint(config.baseUrl);
const format = detectOutputFormat(config.output);
// --- List tools mode ---
if (flags.listTools) {
if (config.dryRun) {
emitResult({ endpoint: mcpUrl, action: "tools/list" }, format);
return;
}
const client = new McpClient(config, mcpUrl);
await client.initialize();
const tools = await client.listTools();
emitResult({ tools }, format);
return;
}
// --- Search mode ---
let query = flags.query as string | undefined;
if (!query) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter your search query:" });
if (!hint) {
process.stderr.write("Search cancelled.\n");
process.exit(1);
}
query = hint;
} else {
failIfMissing("query", "bl search web --query <text>");
}
}
if (config.dryRun) {
emitResult(
{
endpoint: mcpUrl,
action: "tools/call",
tool: "bailian_web_search",
arguments: {
query: query!,
count: (flags.count as number) || undefined,
},
},
format,
);
return;
}
// Initialize MCP client
const client = new McpClient(config, mcpUrl);
const spinner = createSpinner("Initializing search...");
if (!config.quiet) spinner.start();
try {
await client.initialize();
if (!config.quiet) spinner.update("Searching...");
// Build tool arguments
const toolArgs: Record<string, unknown> = { query: query! };
if (flags.count) toolArgs.count = flags.count as number;
// Call the search tool
const result = await client.callTool("bailian_web_search", toolArgs);
if (!config.quiet) spinner.stop("Done.");
// Handle error response
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
process.stderr.write(`Search error: ${errText}\n`);
process.exit(1);
}
// Output results — always structured to stdout
if (format === "json") {
emitResult(result, format);
} else {
// Text mode: try to extract pages for human-friendly display
for (const item of result.content) {
if (item.type === "text" && item.text) {
try {
const data = JSON.parse(item.text) as {
pages?: Array<{
title?: string;
url?: string;
snippet?: string;
hostname?: string;
}>;
};
if (data.pages && Array.isArray(data.pages)) {
emitResult({ pages: data.pages, total: data.pages.length }, format);
} else {
emitResult(data, format);
}
} catch {
emitResult({ text: item.text }, format);
}
}
}
}
} catch (error) {
spinner.stop("Failed.");
throw error;
}
},
});
@@ -1,248 +0,0 @@
import { writeFileSync } from "fs";
import {
BailianError,
defineCommand,
ExitCode,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeASRRequest,
type DashScopeASRTaskResult,
type DashScopeAsyncResponse,
resolveFileUrl,
resolveCredential,
trackingHeaders,
stripUndefined,
taskEndpoint,
requestJson,
type OutputFormat,
speechRecognizeEndpoint,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "speech recognize",
description: "Recognize speech from audio files (FunAudio-ASR)",
usage: "bl speech recognize --url <audio-url> [flags]",
options: [
{
flag: "--url <url>",
description: "Audio file URL or local file path (repeatable, max 100)",
required: true,
type: "array",
},
{ flag: "--model <model>", description: "Model ID (default: fun-asr)" },
{ flag: "--language <lang>", description: "Language hint (e.g. zh, en, ja)" },
{ flag: "--diarization", description: "Enable automatic speaker diarization" },
{
flag: "--speaker-count <n>",
description: "Expected number of speakers (requires --diarization)",
type: "number",
},
{ flag: "--vocabulary-id <id>", description: "Hot-word vocabulary ID for improved accuracy" },
{ flag: "--channel-id <n>", description: "Audio channel ID (default: 0)", type: "number" },
{ flag: "--out <path>", description: "Save full transcription result to JSON file" },
{ flag: "--no-wait", description: "Return task ID immediately without polling" },
{
flag: "--poll-interval <seconds>",
description: "Polling interval in seconds (default: 2)",
type: "number",
},
],
examples: [
"bl speech recognize --url https://example.com/audio.mp3",
"bl speech recognize --url https://example.com/a.mp3 --url https://example.com/b.mp3",
"bl speech recognize --url https://example.com/meeting.wav --diarization --speaker-count 3",
"bl speech recognize --url https://example.com/audio.mp3 --language zh",
"bl speech recognize --url https://example.com/audio.mp3 --vocabulary-id vocab-abc123",
"bl speech recognize --url https://example.com/audio.mp3 --out result.json",
"bl speech recognize --url https://example.com/audio.mp3 --no-wait --quiet",
],
async run(config: Config, flags: GlobalFlags) {
// Normalize --url to string[] (supports both single and repeated flags)
let rawUrls: string[] = [];
if (Array.isArray(flags.url)) {
rawUrls = flags.url as string[];
} else if (typeof flags.url === "string") {
rawUrls = [flags.url];
}
if (rawUrls.length === 0) {
failIfMissing("url", "bl speech recognize --url <audio-url>");
}
// Strict validation: --speaker-count requires --diarization
const speakerCount = flags.speakerCount as number | undefined;
const diarization = flags.diarization === true;
if (speakerCount !== undefined && !diarization) {
throw new BailianError(
"--speaker-count requires --diarization to be enabled.\nHint: Add --diarization flag to enable speaker separation.",
ExitCode.USAGE,
);
}
const model = (flags.model as string) || "fun-asr";
const format = detectOutputFormat(config.output);
// Auto-upload local files in parallel
const credential = await resolveCredential(config);
const resolvedUrls = await Promise.all(
rawUrls.map((u) => resolveFileUrl(u, credential.token, model)),
);
const channelId = flags.channelId as number | undefined;
const language = flags.language as string | undefined;
const vocabularyId = flags.vocabularyId as string | undefined;
const body: DashScopeASRRequest = {
model,
input: {
file_urls: resolvedUrls,
},
parameters: {
channel_id: channelId !== undefined ? [channelId] : [0],
language_hints: language ? [language] : undefined,
diarization_enabled: diarization ? true : undefined,
speaker_count: speakerCount,
vocabulary_id: vocabularyId,
},
};
// Remove undefined parameter fields
stripUndefined(body.parameters as Record<string, unknown>);
if (config.dryRun) {
emitResult({ request: body, mode: "async" }, format);
return;
}
if (!config.quiet) {
process.stderr.write(`[Model: ${model}] [Mode: async] [Files: ${resolvedUrls.length}]\n`);
}
const url = speechRecognizeEndpoint(config.baseUrl);
await handleAsyncMode(config, url, body, flags, format, resolvedUrls.length);
},
});
async function handleAsyncMode(
config: Config,
url: string,
body: DashScopeASRRequest,
flags: GlobalFlags,
format: OutputFormat,
fileCount: number,
): Promise<void> {
// Submit async task (always required for fun-asr)
const response = await requestJson<DashScopeAsyncResponse>(config, {
url,
method: "POST",
body,
async: true,
});
const taskId = response.output.task_id;
// --no-wait: return task ID immediately
if (flags.noWait || config.async) {
emitResult({ task_id: taskId }, format);
return;
}
// Poll until completion
const pollInterval = (flags.pollInterval as number) ?? 2;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const result = await poll<DashScopeASRTaskResult>(config, {
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: config.timeout,
isComplete: (d) => (d as DashScopeASRTaskResult).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeASRTaskResult).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeASRTaskResult).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeASRTaskResult).output;
return (o as unknown as Record<string, unknown>).message as string | undefined;
},
});
const results = result.output.results ?? [];
if (results.length === 0) {
emitResult({ task_id: taskId, status: result.output.task_status }, format);
return;
}
// Collect all transcription data for --out
const allTransData: Record<string, unknown>[] = [];
for (let i = 0; i < results.length; i++) {
const subResult = results[i]!;
const isMulti = fileCount > 1;
if (isMulti) {
process.stdout.write(`=== [${i + 1}/${results.length}] ${subResult.file_url ?? ""} ===\n`);
}
if (subResult.subtask_status === "FAILED") {
const errMsg = subResult.message ?? subResult.code ?? "unknown error";
process.stdout.write(`[FAILED] ${subResult.file_url ?? ""}${errMsg}\n`);
if (isMulti) process.stdout.write("\n");
continue;
}
if (!subResult.transcription_url) {
if (isMulti) process.stdout.write("\n");
continue;
}
// Fetch transcription JSON
const transRes = await fetch(subResult.transcription_url, {
headers: trackingHeaders(),
});
if (!transRes.ok) {
throw new BailianError(
`Failed to download transcription: HTTP ${transRes.status}`,
ExitCode.GENERAL,
);
}
const transData = (await transRes.json()) as Record<string, unknown>;
allTransData.push(transData);
// Extract and output text from transcripts[]
const transcripts = transData.transcripts as
| Array<{
text?: string;
sentences?: Array<{ text: string; speaker_id?: number }>;
}>
| undefined;
if (transcripts && transcripts.length > 0) {
for (const transcript of transcripts) {
if (transcript.sentences && transcript.sentences.length > 0) {
for (const sentence of transcript.sentences) {
const speakerTag =
sentence.speaker_id !== undefined ? ` [Speaker ${sentence.speaker_id}]` : "";
process.stdout.write(`${sentence.text}${speakerTag}\n`);
}
} else if (transcript.text) {
process.stdout.write(transcript.text + "\n");
}
}
} else {
emitBare(JSON.stringify(transData));
}
if (isMulti) process.stdout.write("\n");
}
// Save to --out file
if (flags.out) {
const outPath = flags.out as string;
const outData = allTransData.length === 1 ? allTransData[0] : allTransData;
writeFileSync(outPath, JSON.stringify(outData, null, 2) + "\n");
if (!config.quiet) {
process.stderr.write(`Full result saved to: ${outPath}\n`);
}
}
}
-257
View File
@@ -1,257 +0,0 @@
import {
defineCommand,
request,
requestJson,
chatEndpoint,
parseSSE,
detectOutputFormat,
type Config,
type GlobalFlags,
type ChatMessage,
type ChatRequest,
type ChatResponse,
type StreamChunk,
isInteractive,
} from "bailian-cli-core";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { readFileSync } from "fs";
interface ParsedMessages {
system?: string;
messages: ChatMessage[];
}
function parseMessages(flags: GlobalFlags): ParsedMessages {
const messages: ChatMessage[] = [];
let system: string | undefined;
if (flags.system) {
system = flags.system as string;
}
if (flags.messagesFile) {
const filePath = flags.messagesFile as string;
const raw =
filePath === "-" ? readFileSync("/dev/stdin", "utf-8") : readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw) as Array<{ role: string; content: string }>;
for (const m of parsed) {
if (m.role === "system") {
system = typeof m.content === "string" ? m.content : "";
} else {
messages.push(m as ChatMessage);
}
}
}
if (flags.message) {
const validRoles = new Set(["system", "user", "assistant"]);
const msgs = flags.message as string[];
for (const m of msgs) {
const colonIdx = m.indexOf(":");
const maybeRole = colonIdx !== -1 ? m.slice(0, colonIdx) : "";
if (validRoles.has(maybeRole)) {
const content = m.slice(colonIdx + 1);
if (maybeRole === "system") {
system = content;
} else {
messages.push({ role: maybeRole as "user" | "assistant", content });
}
} else {
messages.push({ role: "user", content: m });
}
}
}
return { system, messages };
}
export default defineCommand({
name: "text chat",
description: "Send a chat completion (OpenAI compatible, DashScope)",
usage: "bl text chat --message <text> [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: qwen3.7-max)" },
{
flag: "--message <text>",
description: "Message text (repeatable, prefix role: to set role)",
required: true,
type: "array",
},
{
flag: "--messages-file <path>",
description: "JSON file with messages array (use - for stdin)",
},
{ flag: "--system <text>", description: "System prompt" },
{
flag: "--max-tokens <n>",
description: "Maximum tokens to generate (default: 4096)",
type: "number",
},
{ flag: "--temperature <n>", description: "Sampling temperature (0.0, 2.0]", type: "number" },
{ flag: "--top-p <n>", description: "Nucleus sampling threshold", type: "number" },
{ flag: "--stream", description: "Stream response tokens (default: on in TTY)" },
{
flag: "--tool <json-or-path>",
description: "Tool definition as JSON or file path (repeatable)",
type: "array",
},
{
flag: "--enable-thinking",
description: "Enable thinking/reasoning mode (for qwen3/qwq models)",
},
{
flag: "--thinking-budget <n>",
description: "Max tokens for thinking (default: 4096)",
type: "number",
},
],
examples: [
'bl text chat --message "What is Qwen?"',
'bl text chat --model qwen-max --system "You are a coding assistant." --message "Write fizzbuzz in Python"',
'bl text chat --message "Hello" --message "assistant:Hi!" --message "How are you?"',
"cat conversation.json | bl text chat --messages-file - --stream",
'bl text chat --message "Hello" --output json',
'bl text chat --model qwq-plus --message "Solve 1+1" --enable-thinking',
],
async run(config: Config, flags: GlobalFlags) {
const { system, messages: parsedMessages } = parseMessages(flags);
let messages = parsedMessages;
if (messages.length === 0) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({
message: "Enter your message:",
});
if (!hint) {
process.stderr.write("Chat cancelled.\n");
process.exit(1);
}
messages = [{ role: "user", content: hint }];
} else {
failIfMissing("message", "bl text chat --message <text>");
}
}
const model = (flags.model as string) || config.defaultTextModel || "qwen3.7-max";
const shouldStream =
flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
const format = detectOutputFormat(config.output);
// Build messages array with system prompt
const allMessages: ChatMessage[] = [];
if (system) {
allMessages.push({ role: "system", content: system });
}
allMessages.push(...messages);
const body: ChatRequest = {
model,
messages: allMessages,
max_tokens: (flags.maxTokens as number) ?? 4096,
stream: shouldStream,
};
if (flags.temperature !== undefined) body.temperature = flags.temperature as number;
if (flags.topP !== undefined) body.top_p = flags.topP as number;
if (flags.enableThinking) {
body.enable_thinking = true;
if (flags.thinkingBudget !== undefined) {
body.thinking_budget = flags.thinkingBudget as number;
}
}
if (flags.tool) {
const tools = (flags.tool as string[]).map((t) => {
try {
return JSON.parse(t);
} catch {
const raw = readFileSync(t, "utf-8");
return JSON.parse(raw);
}
});
body.tools = tools;
}
if (config.dryRun) {
emitResult({ request: body }, format);
return;
}
const url = chatEndpoint(config.baseUrl);
if (shouldStream) {
const res = await request(config, {
url,
method: "POST",
body,
stream: true,
});
let textContent = "";
let inThinking = false;
const writesStreamingStdout = format === "text";
const dim = config.noColor ? "" : "\x1b[2m";
const reset = config.noColor ? "" : "\x1b[0m";
const isTTY = process.stdout.isTTY;
const statusOut =
format === "json" ? process.stderr : isTTY ? process.stdout : process.stderr;
const resultOut = process.stdout;
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
try {
const parsed = JSON.parse(event.data) as StreamChunk;
for (const choice of parsed.choices) {
const delta = choice.delta;
// Handle thinking/reasoning content
if (delta.reasoning_content) {
if (writesStreamingStdout && !inThinking) {
inThinking = true;
statusOut.write(`${dim}Thinking:\n`);
}
if (writesStreamingStdout) statusOut.write(delta.reasoning_content);
}
// Handle regular content
if (delta.content) {
if (writesStreamingStdout && inThinking) {
statusOut.write(`${reset}\n\nResponse:\n`);
inThinking = false;
}
textContent += delta.content;
if (writesStreamingStdout) resultOut.write(delta.content);
}
}
} catch {
// Skip unparseable chunks
}
}
if (inThinking) statusOut.write(reset);
if (format === "json") {
emitResult({ content: textContent }, format);
} else {
resultOut.write("\n");
}
} else {
const response = await requestJson<ChatResponse>(config, {
url,
method: "POST",
body,
});
const text = response.choices?.[0]?.message?.content ?? "";
if (config.quiet || format === "text") {
emitBare(text);
} else {
emitResult(response, format);
}
}
},
});
-86
View File
@@ -1,86 +0,0 @@
import { execSync } from "child_process";
import { writeFileSync } from "fs";
import { join } from "path";
import { defineCommand, getConfigDir } from "bailian-cli-core";
import { CLI_VERSION } from "../version.ts";
import { NPM_PACKAGE, fetchLatestVersion } from "../utils/update-checker.ts";
const SKILL_NAME = "bailian-cli";
/** Build the install command */
function detectInstallCommand(): { cmd: string; label: string } {
return { cmd: `npm install -g ${NPM_PACKAGE}@latest`, label: "npm" };
}
function updateAgentSkill(colors: { green: string; yellow: string; reset: string }): void {
const { green, yellow, reset } = colors;
process.stderr.write("\nUpdating agent skill...\n");
try {
execSync(`npx skills update ${SKILL_NAME} -g -y`, { stdio: "inherit" });
process.stderr.write(`${green}\u2713 Agent skill updated.${reset}\n`);
} catch {
process.stderr.write(
`${yellow}Agent skill update skipped. Run manually: npx skills update ${SKILL_NAME} -g -y${reset}\n`,
);
}
}
export default defineCommand({
name: "update",
description: "Update bl to the latest version",
usage: "bl update",
examples: ["bl update"],
async run() {
const isTTY = process.stderr.isTTY;
const green = isTTY ? "\x1b[32m" : "";
const yellow = isTTY ? "\x1b[33m" : "";
const reset = isTTY ? "\x1b[0m" : "";
process.stderr.write(`Current version: ${yellow}${CLI_VERSION}${reset}\n`);
// Check latest version first
process.stderr.write("Checking for updates...\n");
const latest = await fetchLatestVersion(5000);
if (latest && latest === CLI_VERSION) {
process.stderr.write(`${green}\u2713 Already up to date (${CLI_VERSION}).${reset}\n`);
return;
}
if (latest) {
process.stderr.write(`Latest version: ${green}${latest}${reset}\n\n`);
}
const { cmd, label } = detectInstallCommand();
process.stderr.write(`Updating ${NPM_PACKAGE} via ${label}...\n\n`);
try {
execSync(cmd, { stdio: "inherit" });
// Verify the installed version after update
try {
const rawVer = execSync("bl --version 2>/dev/null", { encoding: "utf-8" }).trim();
// bl --version outputs "bl X.Y.Z" — extract just the version number
const newVer = rawVer.replace(/^bl\s+/, "");
process.stderr.write(
`\n${green}\u2713 Update complete: ${CLI_VERSION} \u2192 ${newVer}${reset}\n`,
);
// Update the cached state so the post-run notification doesn't fire
try {
const stateFile = join(getConfigDir(), "update-state.json");
writeFileSync(
stateFile,
JSON.stringify({ lastChecked: Date.now(), latestVersion: newVer }),
);
} catch {
/* ignore */
}
} catch {
process.stderr.write(`\n${green}\u2713 Update complete.${reset}\n`);
}
updateAgentSkill({ green, yellow, reset });
} catch {
process.stderr.write("\nAutomatic update failed. Please run manually:\n");
process.stderr.write(` ${cmd}\n\n`);
}
},
});
-334
View File
@@ -1,334 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
fetchModelList,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
import { displayWidth, padEnd } from "../../output/cjk-width.ts";
const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota";
const FREE_TIER_ONLY_STATUS_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierOnlyStatus";
interface FreeTierQuota {
model: string;
quotaInitTotal: number;
quotaTotal: number;
quotaValidityPeriod: number;
quotaStatus: string;
}
interface FreeTierOnlyStatus {
model: string;
freeTierOnly: boolean;
}
function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function formatDate(ts: number): string {
const date = new Date(ts);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function formatUsage(quota: FreeTierQuota): string {
if (!quota.quotaInitTotal) return "-";
const used = quota.quotaInitTotal - quota.quotaTotal;
const percent = (used / quota.quotaInitTotal) * 100;
return `${percent.toFixed(1)}%`;
}
const CAPABILITY_TO_TYPE: Record<string, string> = {
Reasoning: "Text",
TG: "Text",
VU: "Text",
IG: "Vision",
VG: "Vision",
"Realtime-Omni": "Multimodal",
"Multimodal-Omni": "Multimodal",
ASR: "Audio",
TTS: "Audio",
"Voice-Replication": "Audio",
"Realtime-Text-to-Speech": "Audio",
"Realtime-Voice-Replication": "Audio",
"Realtime-ASR": "Audio",
"Realtime-Audio-Translate": "Audio",
ME: "Embedding",
TR: "Embedding",
};
function resolveModelType(capabilities: string[]): string {
for (const cap of capabilities) {
const type = CAPABILITY_TO_TYPE[cap];
if (type) return type;
}
return "-";
}
function printTable(
quotas: FreeTierQuota[],
stopMap: Map<string, boolean>,
typeMap: Map<string, string>,
noColor: boolean,
): void {
const headersCn = ["模型", "类型", "剩余/总量", "使用率", "过期时间", "用完即停"];
const headersEn = ["Model", "Type", "Remaining/Total", "Usage", "Expires", "Auto-Stop"];
const rows = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const remaining = hasQuota ? formatNumber(quota.quotaTotal) : "-";
const total = hasQuota ? formatNumber(quota.quotaInitTotal) : "-";
const stopStatus = stopMap.get(quota.model);
return [
quota.model,
typeMap.get(quota.model) || "-",
hasQuota ? `${remaining} / ${total}` : "-",
formatUsage(quota),
quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : "-",
quota.quotaStatus === "UNKNOWN"
? "Unsupported"
: stopStatus === true
? "ON"
: stopStatus === false
? "OFF"
: "-",
];
});
const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...rows.map((row) => displayWidth(row[col])),
),
);
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const green = noColor ? (text: string) => text : (text: string) => `\x1b[32m${text}\x1b[0m`;
const yellow = noColor ? (text: string) => text : (text: string) => `\x1b[33m${text}\x1b[0m`;
const autoStopCol = headersCn.length - 1;
const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
const cells = row.map((cell, col) => {
if (col === autoStopCol) {
if (cell === "ON") return green(padEnd(cell, widths[col]));
if (cell === "OFF") return yellow(padEnd(cell, widths[col]));
}
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
}
function extractQuotas(result: unknown): FreeTierQuota[] {
const root = result as Record<string, unknown>;
const data = root.data as Record<string, unknown> | undefined;
if (!data) return [];
const dataV2 = data.DataV2 as Record<string, unknown> | undefined;
if (dataV2) {
const inner = dataV2.data as Record<string, unknown> | undefined;
const innerData = inner?.data as Record<string, unknown> | undefined;
return (innerData?.freeTierQuotas as FreeTierQuota[]) || [];
}
const direct = data.data as Record<string, unknown> | undefined;
return (direct?.freeTierQuotas as FreeTierQuota[]) || [];
}
function extractFreeTierOnlyStatuses(result: unknown): FreeTierOnlyStatus[] {
const root = result as Record<string, unknown>;
const data = root.data as Record<string, unknown> | undefined;
if (!data) return [];
const dataV2 = data.DataV2 as Record<string, unknown> | undefined;
if (dataV2) {
const inner = dataV2.data as Record<string, unknown> | undefined;
const innerData = inner?.data as Record<string, unknown> | undefined;
return (innerData?.freeTierOnlyStatuses as FreeTierOnlyStatus[]) || [];
}
const direct = data.data as Record<string, unknown> | undefined;
return (direct?.freeTierOnlyStatuses as FreeTierOnlyStatus[]) || [];
}
interface ModelInfo {
name: string;
type: string;
}
async function fetchAllModels(config: Config, token: string): Promise<ModelInfo[]> {
const allModels: Record<string, unknown>[] = [];
let page = 1;
while (true) {
const result = await fetchModelList(config, token, { pageNo: page, pageSize: 50 });
allModels.push(...result.models);
if (allModels.length >= result.total) break;
page++;
}
return allModels
.filter((item) => typeof item.model === "string" && item.model)
.map((item) => ({
name: item.model as string,
type: resolveModelType((item.capabilities as string[]) || []),
}));
}
export default defineCommand({
name: "usage free",
description: "Query free-tier quota for models (all models if --model is omitted)",
usage: "bl usage free [--model <model>[,model2,...]] [flags]",
options: [
{
flag: "--model <model>",
description: "Model name(s) to query, comma-separated for multiple; omit for all models",
},
{
flag: "--expiring <days>",
description: "Only show quotas expiring within N days",
},
{
flag: "--sort <field>",
description: "Sort by: remaining (ascending), expires (ascending)",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl usage free",
"bl usage free --model qwen3-max",
"bl usage free --model qwen3-max,qwen-turbo",
"bl usage free --expiring 30",
"bl usage free --sort remaining",
"bl usage free --model qwen-turbo --output json",
"bl usage free --model qwen3-max --region cn-beijing",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const expiringDays = Number(flags.expiring) || 0;
const VALID_SORT_FIELDS = ["remaining", "expires"] as const;
const sortField = (flags.sort as string) || undefined;
if (sortField && !VALID_SORT_FIELDS.includes(sortField as (typeof VALID_SORT_FIELDS)[number])) {
process.stderr.write(
`Error: invalid --sort value "${sortField}". Must be one of: ${VALID_SORT_FIELDS.join(", ")}\n`,
);
process.exit(1);
}
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
let models: string[];
const typeMap = new Map<string, string>();
if (modelFlag) {
models = [
...new Set(
modelFlag
.split(",")
.map((name) => name.trim())
.filter(Boolean),
),
];
const searchResults = await Promise.all(
models.map((name) => fetchModelList(config, credential.token, { name, pageSize: 50 })),
);
for (let idx = 0; idx < models.length; idx++) {
const matched = searchResults[idx].models.find((item) => item.model === models[idx]);
if (matched) {
typeMap.set(models[idx], resolveModelType((matched.capabilities as string[]) || []));
}
}
} else {
const modelInfos = await fetchAllModels(config, credential.token);
models = modelInfos.map((info) => info.name);
for (const info of modelInfos) {
typeMap.set(info.name, info.type);
}
}
const requestData = {
queryFreeTierQuotaRequest: { models },
};
if (config.dryRun) {
emitResult(
{
api: FREE_TIER_API,
data: requestData,
region,
token: credential.token.slice(0, 8) + "...",
},
format,
);
return;
}
const [quotaResult, stopResult] = await Promise.all([
callConsoleGateway(config, credential.token, {
api: FREE_TIER_API,
data: requestData,
region,
}),
callConsoleGateway(config, credential.token, {
api: FREE_TIER_ONLY_STATUS_API,
data: { queryFreeTierOnlyStatusRequest: { models } },
region,
}),
]);
if (format === "json") {
emitResult(quotaResult, format);
return;
}
const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
: allQuotas.filter((quota) => quota.quotaStatus === "VALID" && quota.quotaInitTotal > 0);
if (expiringDays > 0) {
const cutoff = Date.now() + expiringDays * 24 * 60 * 60 * 1000;
quotas = quotas.filter((q) => q.quotaValidityPeriod > 0 && q.quotaValidityPeriod <= cutoff);
}
if (sortField === "remaining") {
quotas.sort((a, b) => {
const pctA = a.quotaInitTotal ? a.quotaTotal / a.quotaInitTotal : 0;
const pctB = b.quotaInitTotal ? b.quotaTotal / b.quotaInitTotal : 0;
return pctA - pctB;
});
} else if (sortField === "expires") {
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}
if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}
const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));
printTable(quotas, stopMap, typeMap, config.noColor);
},
});
-252
View File
@@ -1,252 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
fetchModelList,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
const ACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchActivateFreeTierOnly";
const DEACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchDeactivateFreeTierOnly";
const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota";
const FREE_TIER_ONLY_STATUS_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierOnlyStatus";
interface FreeTierQuota {
model: string;
quotaTotal: number;
quotaInitTotal: number;
}
interface FreeTierOnlyStatus {
model: string;
freeTierOnly: boolean;
}
interface BatchResultFailure {
failureModelId: string;
errorCode: string;
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
const POLL_INTERVAL_MS = 500;
const MAX_POLLS = 20;
async function pollUntilDone(
config: Config,
token: string,
api: string,
requestKey: string,
models: string[],
region: string,
): Promise<unknown> {
let nextTaskId: string | undefined;
for (let attempt = 0; attempt < MAX_POLLS; attempt++) {
const requestData = {
[requestKey]: nextTaskId ? { taskId: nextTaskId } : { models },
};
const raw = await callConsoleGateway(config, token, {
api,
data: requestData,
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
if (resp.taskId && Object.keys(resp).length === 1) {
nextTaskId = resp.taskId as string;
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
return raw;
}
return null;
}
async function fetchAllModelNames(config: Config, token: string): Promise<string[]> {
const allModels: Record<string, unknown>[] = [];
let page = 1;
while (true) {
const result = await fetchModelList(config, token, { pageNo: page, pageSize: 50 });
allModels.push(...result.models);
if (allModels.length >= result.total) break;
page++;
}
return allModels.map((item) => item.model as string).filter(Boolean);
}
export default defineCommand({
name: "usage freetier",
description:
"Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable",
usage: "bl usage freetier <--model <model>[,model2,...] | --all> [--off] [flags]",
options: [
{
flag: "--model <model>",
description: "Model name(s), comma-separated for multiple",
},
{
flag: "--all",
description: "Apply to all free-tier models",
},
{
flag: "--on",
description: "Enable auto-stop (default behavior)",
},
{
flag: "--off",
description: "Disable auto-stop",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl usage freetier --model qwen3-max",
"bl usage freetier --model qwen3-max,qwen-turbo",
"bl usage freetier --all",
"bl usage freetier --on --model qwen3-max",
"bl usage freetier --off --model qwen3-max",
"bl usage freetier --off --all",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const all = Boolean(flags.all);
const off = Boolean(flags.off);
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
if (!modelFlag && !all) {
process.stderr.write(
"Error: missing required flag. Specify --model <model>[,model2,...] or --all\n",
);
process.exit(1);
}
const credential = await resolveConsoleGatewayCredential(config);
let models: string[];
if (modelFlag) {
models = [
...new Set(
modelFlag
.split(",")
.map((name) => name.trim())
.filter(Boolean),
),
];
} else {
models = await fetchAllModelNames(config, credential.token);
}
const api = off ? DEACTIVATE_API : ACTIVATE_API;
const requestKey = off
? "BatchDeactivateFreeTierOnlyRequest"
: "BatchActivateFreeTierOnlyRequest";
if (config.dryRun) {
emitResult(
{
api,
data: { [requestKey]: { models } },
region,
token: credential.token.slice(0, 8) + "...",
},
format,
);
return;
}
if (off) {
const [quotaResult, stopResult] = await Promise.all([
callConsoleGateway(config, credential.token, {
api: FREE_TIER_API,
data: { queryFreeTierQuotaRequest: { models } },
region,
}),
callConsoleGateway(config, credential.token, {
api: FREE_TIER_ONLY_STATUS_API,
data: { queryFreeTierOnlyStatusRequest: { models } },
region,
}),
]);
const quotaData = extractResponseData(quotaResult as Record<string, unknown>);
const quotas = (quotaData.freeTierQuotas ?? []) as FreeTierQuota[];
const quotaMap = new Map(quotas.map((quota) => [quota.model, quota]));
const stopData = extractResponseData(stopResult as Record<string, unknown>);
const stopStatuses = (stopData.freeTierOnlyStatuses ?? []) as FreeTierOnlyStatus[];
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));
for (const name of models) {
if (stopMap.get(name) === false) {
process.stderr.write(`Auto-stop is already disabled for "${name}".\n`);
continue;
}
const quota = quotaMap.get(name);
if (quota && quota.quotaTotal > 0 && stopMap.get(name) === true) {
process.stderr.write(
`Cannot disable auto-stop for "${name}": free-tier quota has not been fully consumed. Please disable auto-stop after the quota is exhausted.\n`,
);
continue;
}
await pollUntilDone(config, credential.token, api, requestKey, [name], region);
process.stdout.write(`Disabled auto-stop for "${name}".\n`);
}
return;
}
const jsonResults: unknown[] = [];
for (const name of models) {
const result = await pollUntilDone(config, credential.token, api, requestKey, [name], region);
if (format === "json") {
jsonResults.push(result);
continue;
}
if (result) {
const resultData = extractResponseData(result as Record<string, unknown>);
const failureModels = (resultData.failureModels as BatchResultFailure[]) ?? [];
if (failureModels.length > 0) {
process.stderr.write(
`Failed to enable auto-stop for "${name}" (${failureModels[0].errorCode}).\n`,
);
} else {
process.stdout.write(`Enabled auto-stop for "${name}".\n`);
}
} else {
process.stderr.write(`Warning: operation timed out for "${name}".\n`);
}
}
if (format === "json") {
emitResult(jsonResults, format);
}
},
});
-442
View File
@@ -1,442 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
import { displayWidth, padEnd } from "../../output/cjk-width.ts";
const OVERVIEW_API = "zeldaEasy.bailian-telemetry.model.getModelUsageStatistic";
const LIST_API = "zeldaEasy.bailian-telemetry.model.listModelUsageStatisticData";
interface UsageItem {
key: string;
value: number;
unit: string;
}
interface OverviewStatistic {
callCount: number;
modelCount: number;
callSuccessCount: number;
usages: UsageItem[];
}
interface ModelStatisticItem {
model: string;
callSuccessCount: number;
usages?: UsageItem[];
usage?: Record<string, number | undefined>;
}
interface ListStatisticResponse {
list: ModelStatisticItem[];
totalCount: number;
maxResults: number;
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
const POLL_INTERVAL_MS = 500;
const MAX_POLLS = 30;
async function pollTelemetryApi(
config: Config,
token: string,
api: string,
reqDTO: Record<string, unknown>,
region: string,
): Promise<unknown> {
let nextTaskId: string | undefined;
for (let attempt = 0; attempt < MAX_POLLS; attempt++) {
const requestData = nextTaskId
? { reqDTO: { ...reqDTO, asyncTaskId: nextTaskId } }
: { reqDTO };
const raw = await callConsoleGateway(config, token, {
api,
data: requestData,
region,
});
const resp = extractResponseData(raw as Record<string, unknown>);
if (resp.taskId && Object.keys(resp).length === 1) {
nextTaskId = resp.taskId as string;
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
return raw;
}
return null;
}
function resolveWorkspaceId(config: Config, flagWorkspaceId?: string): string {
if (flagWorkspaceId) return flagWorkspaceId;
if (config.workspaceId) return config.workspaceId;
process.stderr.write(
"Error: workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or `bl config set workspace_id <id>`.\n",
);
process.stderr.write("Hint: run `bl workspace list` to view available workspaces.\n");
process.exit(1);
}
function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function formatDate(ts: number): string {
const date = new Date(ts);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function extractOverviewData(result: unknown): OverviewStatistic | undefined {
const resp = extractResponseData(result as Record<string, unknown>);
if (resp.callSuccessCount !== undefined || resp.usages !== undefined) {
return resp as unknown as OverviewStatistic;
}
return undefined;
}
function extractListData(result: unknown): ListStatisticResponse {
const resp = extractResponseData(result as Record<string, unknown>);
const list = (resp.list as ModelStatisticItem[]) ?? [];
const totalCount = (resp.totalCount as number) ?? 0;
const maxResults = (resp.maxResults as number) ?? 0;
return { list, totalCount, maxResults };
}
function resolveUsageMap(item: ModelStatisticItem): Record<string, number> {
const out: Record<string, number> = {};
if (item.usages && Array.isArray(item.usages)) {
for (const entry of item.usages) {
if (entry.key && entry.value != null) {
out[entry.key] = entry.value;
}
}
}
if (item.usage && typeof item.usage === "object") {
for (const [key, val] of Object.entries(item.usage)) {
if (val != null) out[key] = val;
}
}
return out;
}
interface UsageLabel {
cn: string;
en: string;
unit?: string;
}
const USAGE_KEY_LABELS: Record<string, UsageLabel> = {
total_token: { cn: "总 Token", en: "Total Tokens", unit: "tokens" },
input_token: { cn: "输入 Token", en: "Input Tokens", unit: "tokens" },
output_token: { cn: "输出 Token", en: "Output Tokens", unit: "tokens" },
input_token_cache: { cn: "缓存 Token", en: "Cached Tokens", unit: "tokens" },
input_token_cache_read: { cn: "缓存读取", en: "Cache Read", unit: "tokens" },
input_token_cache_creation: { cn: "缓存创建", en: "Cache Creation", unit: "tokens" },
thinking_input_token: { cn: "思考输入", en: "Thinking Input", unit: "tokens" },
thinking_output_token: { cn: "思考输出", en: "Thinking Output", unit: "tokens" },
text_input_token: { cn: "文本输入", en: "Text Input", unit: "tokens" },
purein_text_output_token: { cn: "文本输出", en: "Text Output", unit: "tokens" },
embedding_token: { cn: "向量", en: "Embedding", unit: "tokens" },
image_number: { cn: "图片数", en: "Images", unit: "张" },
video_duration: { cn: "视频时长", en: "Video Duration", unit: "秒" },
content_duration: { cn: "音频时长", en: "Audio Duration", unit: "秒" },
tts_text_number: { cn: "语音合成", en: "TTS Chars", unit: "字符" },
total_token_avg: { cn: "平均 Token/次", en: "Avg Tokens/Req" },
};
function formatLabel(label: UsageLabel): string {
const unitSuffix = label.unit ? ` [${label.unit}]` : "";
return `${label.cn} (${label.en})${unitSuffix}`;
}
function printOverview(
stat: OverviewStatistic,
startTime: number,
endTime: number,
days: number,
noColor: boolean,
): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
process.stdout.write(
`${dim("时间范围 Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${dim(`(${days} 天)`)}\n\n`,
);
const rows: [string, string][] = [
["调用模型数 (Models Called)", formatNumber(stat.modelCount ?? 0)],
["调用成功次数 (Successful Calls)", formatNumber(stat.callSuccessCount ?? 0)],
];
for (const usage of stat.usages ?? []) {
const label = USAGE_KEY_LABELS[usage.key];
const text = label ? formatLabel(label) : usage.key;
rows.push([text, formatNumber(usage.value)]);
}
const maxLabel = Math.max(...rows.map(([label]) => displayWidth(label)));
for (const [label, value] of rows) {
process.stdout.write(`${bold(padEnd(label, maxLabel + 2))}${value}\n`);
}
}
function printModelTable(
items: ModelStatisticItem[],
startTime: number,
endTime: number,
days: number,
noColor: boolean,
): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
process.stdout.write(
`${dim("时间范围 Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${dim(`(${days} 天)`)}\n\n`,
);
if (items.length === 0) {
process.stdout.write("No usage data found.\n");
return;
}
const usageKeys = new Set<string>();
const itemUsages = items.map((item) => {
const usage = resolveUsageMap(item);
for (const key of Object.keys(usage)) usageKeys.add(key);
return usage;
});
const orderedKeys = [...usageKeys].sort((keyA, keyB) => {
const order = [
"total_token",
"input_token",
"output_token",
"input_token_cache",
"image_number",
"video_duration",
"content_duration",
"tts_text_number",
];
const idxA = order.indexOf(keyA);
const idxB = order.indexOf(keyB);
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB);
});
const headersCn = [
"模型",
"调用次数",
...orderedKeys.map((key) => {
const label = USAGE_KEY_LABELS[key];
if (!label) return key;
return label.unit ? `${label.cn} [${label.unit}]` : label.cn;
}),
];
const headersEn = [
"Model",
"Calls",
...orderedKeys.map((key) => USAGE_KEY_LABELS[key]?.en ?? key),
];
const rows = items.map((item, idx) => [
item.model,
formatNumber(item.callSuccessCount ?? 0),
...orderedKeys.map((key) => {
const val = itemUsages[idx][key];
return val != null ? formatNumber(val) : "-";
}),
]);
const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...rows.map((row) => displayWidth(row[col])),
),
);
const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
const cells = row.map((cell, col) => padEnd(cell, widths[col]));
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(dim(`\n共 ${items.length} 个模型 (Total: ${items.length})`) + "\n");
}
export default defineCommand({
name: "usage stats",
description: "Query model usage statistics",
usage: "bl usage stats [--model <model>] [--days <days>] [flags]",
options: [
{
flag: "--model <model>",
description: "Model name(s), comma-separated; omit for overview",
},
{
flag: "--days <days>",
description: "Number of days (default: 7)",
},
{
flag: "--type <type>",
description: "Model type: Text, Vision, Multimodal, Audio, Embedding",
},
{
flag: "--workspace-id <id>",
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID)",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: [
"bl usage stats",
"bl usage stats --days 30",
"bl usage stats --model qwen-turbo",
"bl usage stats --model qwen-turbo --days 7",
"bl usage stats --model qwen3.6-plus,deepseek-v4-pro",
"bl usage stats --type Text --days 14",
"bl usage stats --output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
const daysFlag = Number(flags.days) || 7;
const typeFlag = (flags.type as string) || undefined;
const region = (flags.region as string) || "cn-beijing";
const format = detectOutputFormat(config.output);
const flagWorkspaceId = (flags.workspaceId as string) || undefined;
const workspaceId = resolveWorkspaceId(config, flagWorkspaceId);
const credential = await resolveConsoleGatewayCredential(config);
const endTime = Date.now();
const startTime = endTime - daysFlag * 24 * 60 * 60 * 1000;
if (modelFlag) {
const models = [
...new Set(
modelFlag
.split(",")
.map((name) => name.trim())
.filter(Boolean),
),
];
const baseReqDTO: Record<string, unknown> = {
startTime,
endTime,
modelCallSource: "Online",
filterWorkspaceId: workspaceId,
maxResults: 50,
skip: 0,
sortField: "success_count",
sortOrder: "DESC",
};
if (typeFlag) baseReqDTO.obsModelType = typeFlag;
if (config.dryRun) {
emitResult(
{ api: LIST_API, data: { reqDTO: { ...baseReqDTO, model: models.join(",") } }, region },
format,
);
return;
}
const results = await Promise.all(
models.map((model) =>
pollTelemetryApi(config, credential.token, LIST_API, { ...baseReqDTO, model }, region),
),
);
const allItems: ModelStatisticItem[] = [];
const jsonResults: unknown[] = [];
for (const result of results) {
if (!result) continue;
jsonResults.push(result);
const listData = extractListData(result);
allItems.push(...listData.list);
}
if (format === "json") {
emitResult(jsonResults.length === 1 ? jsonResults[0] : jsonResults, format);
return;
}
printModelTable(allItems, startTime, endTime, daysFlag, config.noColor);
} else {
const reqDTO: Record<string, unknown> = {
startTime,
endTime,
modelCallSource: "Online",
filterWorkspaceId: workspaceId,
};
if (typeFlag) reqDTO.obsModelType = typeFlag;
if (config.dryRun) {
emitResult({ api: OVERVIEW_API, data: { reqDTO }, region }, format);
return;
}
const result = await pollTelemetryApi(config, credential.token, OVERVIEW_API, reqDTO, region);
if (!result) {
process.stderr.write("Error: request timed out.\n");
process.exit(1);
}
if (format === "json") {
emitResult(result, format);
return;
}
const stat = extractOverviewData(result);
if (!stat) {
process.stdout.write("No usage data found.\n");
return;
}
printOverview(stat, startTime, endTime, daysFlag, config.noColor);
}
},
});
@@ -1,70 +0,0 @@
import {
defineCommand,
requestJson,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeTaskResponse,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "video download",
description: "Download a completed video by task ID",
usage: "bl video download --task-id <id> --out <path>",
options: [
{ flag: "--task-id <id>", description: "Task ID to download from" },
{ flag: "--out <path>", description: "Output file path" },
],
examples: [
"bl video download --task-id 3b256896-xxxx --out video.mp4",
"bl video download --task-id 3b256896-xxxx --out video.mp4 --quiet",
],
async run(config: Config, flags: GlobalFlags) {
const taskId = flags.taskId as string | undefined;
if (!taskId) failIfMissing("task-id", "bl video download --task-id <id> --out <path>");
const outPath = flags.out as string | undefined;
if (!outPath) failIfMissing("out", "bl video download --task-id <id> --out video.mp4");
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ task_id: taskId, action: "download", out: outPath }, format);
return;
}
// Get task info to find video URL
const url = taskEndpoint(config.baseUrl, taskId);
const taskInfo = await requestJson<DashScopeTaskResponse>(config, { url });
if (taskInfo.output.task_status !== "SUCCEEDED") {
throw new BailianError(
`Task is not complete (status: ${taskInfo.output.task_status}).`,
ExitCode.GENERAL,
"Wait for the task to complete before downloading.",
);
}
const downloadUrl =
taskInfo.output.video_url || (taskInfo.output.results && taskInfo.output.results[0]?.url);
if (!downloadUrl) {
throw new BailianError("No download URL available for this task.", ExitCode.GENERAL);
}
const { size } = await downloadFile(downloadUrl, outPath, { quiet: config.quiet });
if (config.quiet) {
emitBare(outPath);
return;
}
emitResult({ saved: outPath, size: formatBytes(size) }, format);
},
});
-246
View File
@@ -1,246 +0,0 @@
import {
defineCommand,
requestJson,
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoEditRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
isInteractive,
resolveOutputDir,
resolveFileUrl,
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "video edit",
description:
"Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.)",
usage: "bl video edit --video <url> --prompt <text> [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: happyhorse-1.0-video-edit)" },
{
flag: "--video <url>",
description: "Input video URL or local file (mp4/mov, 2-10s)",
required: true,
},
{ flag: "--prompt <text>", description: 'Edit instruction (e.g. "将画面转换为黏土风格")' },
{ flag: "--ref-image <url>", description: "Reference image URL (up to 4, comma-separated)" },
{
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{ flag: "--resolution <res>", description: "Resolution: 720P or 1080P (default: 1080P)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4)" },
{
flag: "--duration <seconds>",
description: "Output video duration in seconds (2-10)",
type: "number",
},
{
flag: "--audio-setting <mode>",
description: "Audio: auto (default) or origin (keep original)",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 15)",
type: "number",
},
],
examples: [
'bl video edit --video https://example.com/input.mp4 --prompt "将整个画面转换为黏土风格"',
'bl video edit --video https://example.com/input.mp4 --prompt "替换衣服为图片中的款式" --ref-image https://example.com/clothes.png',
'bl video edit --video https://example.com/input.mp4 --prompt "Convert to anime style" --resolution 720P --download output.mp4',
'bl video edit --video https://example.com/input.mp4 --prompt "给视频里的小猫穿上衣服" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// --- Validate video URL ---
let videoUrl = flags.video as string | undefined;
if (!videoUrl) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter the video URL to edit:" });
if (!hint) {
process.stderr.write("Video editing cancelled.\n");
process.exit(1);
}
videoUrl = hint;
} else {
failIfMissing("video", "bl video edit --video <url> --prompt <text>");
}
}
// --- Prompt ---
let prompt = flags.prompt as string | undefined;
if (!prompt) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter your edit instruction:" });
if (!hint) {
process.stderr.write("Video editing cancelled.\n");
process.exit(1);
}
prompt = hint;
}
// prompt is optional for video edit per API spec
}
const model = (flags.model as string) || "happyhorse-1.0-video-edit";
const format = detectOutputFormat(config.output);
// Auto-upload local files
const credential = await resolveCredential(config);
const resolvedVideoUrl = await resolveFileUrl(videoUrl!, credential.token, model);
// --- Build media array ---
const media: DashScopeVideoEditRequest["input"]["media"] = [
{ type: "video", url: resolvedVideoUrl },
];
// Support comma-separated reference images
const refImageArg = flags.refImage as string | undefined;
if (refImageArg) {
const images = refImageArg
.split(",")
.map((s) => s.trim())
.filter(Boolean);
for (const imgUrl of images) {
const resolved = await resolveFileUrl(imgUrl, credential.token, model);
media.push({ type: "reference_image", url: resolved });
}
}
// --- Build request body ---
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeVideoEditRequest = {
model,
input: {
prompt: prompt || undefined,
negative_prompt: (flags.negativePrompt as string) || undefined,
media,
},
parameters: {
resolution: (flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
audio_setting: (flags.audioSetting as "auto" | "origin") || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
},
};
if (config.dryRun) {
emitResult({ request: body }, format);
return;
}
// --- Submit async task ---
const url = videoGenerateEndpoint(config.baseUrl);
const response = await requestJson<DashScopeAsyncResponse>(config, {
url,
method: "POST",
body,
async: true,
});
const taskId = response.output.task_id;
if (!config.quiet) {
process.stderr.write(`[Model: ${model}]\n`);
process.stderr.write("Note: Video editing typically takes 5-8 minutes. Please be patient.\n");
}
// --no-wait or --async: return task ID immediately
if (flags.noWait || config.async) {
emitResult({ task_id: taskId }, format);
return;
}
// --- Poll until completion ---
// Video editing is compute-intensive; default timeout = 600s (10 min)
const pollInterval = (flags.pollInterval as number) ?? 15;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const editTimeout = Math.max(config.timeout, 600);
const result = await poll<DashScopeTaskResponse>(config, {
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: editTimeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
},
});
const resultVideoUrl =
result.output.video_url || (result.output.results && result.output.results[0]?.url);
if (!resultVideoUrl) {
throw new BailianError("Task completed but no video URL returned.", ExitCode.GENERAL);
}
// --download: save to file
if (flags.download) {
const destPath = flags.download as string;
const { size } = await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
emitBare(destPath);
} else {
emitResult(
{
task_id: taskId,
video_url: resultVideoUrl,
status: "SUCCEEDED",
saved: destPath,
size: formatBytes(size),
},
format,
);
}
return;
}
// Default: auto-download to output directory
const path = await import("path");
const destDir = resolveOutputDir(config, { subDir: "videos" });
const destPath = path.join(destDir, `${taskId}.mp4`);
await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
emitResult({ task_id: taskId, video_url: resultVideoUrl, saved: destPath }, format);
},
});
-260
View File
@@ -1,260 +0,0 @@
import {
defineCommand,
requestJson,
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
isInteractive,
resolveOutputDir,
resolveFileUrl,
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { runConcurrent, getConcurrency } from "../../utils/concurrent.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
// Normalize shorthand resolution (720P, 1080P) to pixel format for video generation models
const RESOLUTION_SHORTCUTS: Record<string, string> = {
"720p": "1280*720",
"1080p": "1920*1080",
"480p": "832*480",
};
function normalizeResolution(res: string | undefined): string | undefined {
if (!res) return undefined;
return RESOLUTION_SHORTCUTS[res.toLowerCase()] || res;
}
export default defineCommand({
name: "video generate",
description:
"Generate a video from text or image (happyhorse-1.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v)",
usage: "bl video generate --prompt <text> [--image <url>] [flags]",
options: [
{
flag: "--model <model>",
description: "Model ID (default: happyhorse-1.0-t2v, or happyhorse-1.0-i2v with --image)",
},
{ flag: "--prompt <text>", description: "Video description", required: true },
{ flag: "--image <url>", description: "Input image URL for image-to-video generation" },
{
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{ flag: "--resolution <res>", description: "Resolution (e.g. 1280*720, 960*960)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (e.g. 16:9, 1:1)" },
{
flag: "--duration <seconds>",
description: "Video duration in seconds (default: 5)",
type: "number",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 5)",
type: "number",
},
],
examples: [
'bl video generate --prompt "一个人在读书,静态镜头"',
'bl video generate --prompt "Ocean waves at sunset." --download sunset.mp4',
'bl video generate --image https://example.com/cat.png --prompt "让画面中的猫动起来"',
'bl video generate --prompt "Mountain landscape" --resolution 1280*720 --duration 5',
'bl video generate --prompt "A cat playing with a ball" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
let prompt = flags.prompt as string | undefined;
if (!prompt) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter your video prompt:" });
if (!hint) {
process.stderr.write("Video generation cancelled.\n");
process.exit(1);
}
prompt = hint;
} else {
failIfMissing("prompt", "bl video generate --prompt <text>");
}
}
const model =
(flags.model as string) ||
config.defaultVideoModel ||
((flags.image as string) ? "happyhorse-1.0-i2v" : "happyhorse-1.0-t2v");
const format = detectOutputFormat(config.output);
const imageUrl = flags.image as string | undefined;
// Auto-upload local image file for i2v
let resolvedImageUrl: string | undefined;
if (imageUrl) {
const credential = await resolveCredential(config);
resolvedImageUrl = await resolveFileUrl(imageUrl, credential.token, model);
}
const watermark = resolveWatermark(flags.watermark);
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const body: DashScopeVideoRequest = {
model,
input: {
prompt: prompt!,
negative_prompt: (flags.negativePrompt as string) || undefined,
// i2v models (happyhorse-1.0-i2v) require input.media with type 'first_frame'
...(resolvedImageUrl
? { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
},
parameters: {
resolution: normalizeResolution(flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
},
};
if (config.dryRun) {
emitResult({ request: body }, format);
return;
}
// Submit async task(s) — supports --concurrent for parallel generation
const concurrent = getConcurrency(flags);
const url = videoGenerateEndpoint(config.baseUrl);
const responses = await runConcurrent(
concurrent,
config,
() =>
requestJson<DashScopeAsyncResponse>(config, {
url,
method: "POST",
body,
async: true,
}),
"tasks",
);
const taskIds = responses.map((r) => r.output.task_id);
if (!config.quiet) {
process.stderr.write(`[Model: ${model}]\n`);
}
// --no-wait or --async: return task ID(s) immediately
if (flags.noWait || config.async) {
emitResult(taskIds.length === 1 ? { task_id: taskIds[0] } : { task_ids: taskIds }, format);
return;
}
// Poll all tasks concurrently
const pollInterval = (flags.pollInterval as number) ?? 5;
const pollPromises = taskIds.map((taskId) => {
const pollUrl = taskEndpoint(config.baseUrl, taskId);
return poll<DashScopeTaskResponse>(config, {
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: config.timeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
},
});
});
const results = await Promise.all(pollPromises);
// Collect video URLs from all results
const videos: Array<{ taskId: string; videoUrl: string }> = [];
for (let i = 0; i < results.length; i++) {
const result = results[i]!;
const videoUrl =
result.output.video_url || (result.output.results && result.output.results[0]?.url);
if (videoUrl) {
videos.push({ taskId: taskIds[i]!, videoUrl });
}
}
if (videos.length === 0) {
throw new BailianError("All tasks completed but no video URLs returned.", ExitCode.GENERAL);
}
// --download: save to file (first video only for explicit path)
if (flags.download) {
const destPath = flags.download as string;
const { size } = await downloadFile(videos[0]!.videoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
emitBare(destPath);
} else {
emitResult(
{
task_id: videos[0]!.taskId,
video_url: videos[0]!.videoUrl,
status: "SUCCEEDED",
saved: destPath,
size: formatBytes(size),
},
format,
);
}
return;
}
// Default: auto-download all to output directory
const destDir = resolveOutputDir(config, { subDir: "videos" });
// eslint-disable-next-line @typescript-eslint/unbound-method
const { join } = await import("path");
const saved: Array<{ task_id: string; video_url: string; saved: string }> = [];
await Promise.all(
videos.map(async ({ taskId, videoUrl }) => {
const destPath = join(destDir, `${taskId}.mp4`);
await downloadFile(videoUrl, destPath, { quiet: config.quiet });
saved.push({ task_id: taskId, video_url: videoUrl, saved: destPath });
}),
);
if (saved.length === 1) {
emitResult(saved[0]!, format);
} else {
emitResult({ videos: saved, total: saved.length }, format);
}
},
});
-276
View File
@@ -1,276 +0,0 @@
import {
defineCommand,
requestJson,
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoRefRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
isInteractive,
resolveOutputDir,
resolveFileUrl,
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "video ref",
description:
"Reference-to-video generation (happyhorse-1.0-r2v / wan2.6-r2v): multi-subject, multi-shot with voice",
usage: "bl video ref --prompt <text> --image <url>... [--ref-video <url>...] [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: happyhorse-1.0-r2v)" },
{
flag: "--prompt <text>",
description: "Video description with reference markers (图1, 视频1, etc.)",
required: true,
},
{
flag: "--image <url>",
description: "Reference image URL or local file (repeatable for multiple subjects)",
type: "array",
},
{
flag: "--ref-video <url>",
description: "Reference video URL or local file (repeatable)",
type: "array",
},
{
flag: "--image-voice <url>",
description: "Voice URL for corresponding image (pairs by position)",
type: "array",
},
{
flag: "--video-voice <url>",
description: "Voice URL for corresponding ref-video (pairs by position)",
type: "array",
},
{ flag: "--resolution <res>", description: "Resolution: 720P or 1080P (default: 720P)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (16:9, 9:16, 1:1)" },
{
flag: "--duration <seconds>",
description: "Video duration in seconds (2-10, default: 5)",
type: "number",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 15)",
type: "number",
},
],
examples: [
'bl video ref --prompt "图1在草地上奔跑" --image person.jpg',
'bl video ref --prompt "视频1在弹吉他图1走过来" --ref-video scene.mp4 --image person.jpg',
'bl video ref --prompt "图1说话" --image person.jpg --image-voice voice.mp3 --resolution 1080P',
'bl video ref --prompt "图1和图2在对话" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3',
'bl video ref --prompt "图1在喝水" --image person.jpg --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// --- Validate prompt ---
let prompt = flags.prompt as string | undefined;
if (!prompt) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({
message: "Enter your video prompt (use 图1, 视频1 to reference inputs):",
});
if (!hint) {
process.stderr.write("Video generation cancelled.\n");
process.exit(1);
}
prompt = hint;
} else {
failIfMissing("prompt", "bl video ref --prompt <text> --image <url>");
}
}
const images = (flags.image as string[] | undefined) || [];
const refVideos = (flags.refVideo as string[] | undefined) || [];
if (images.length === 0 && refVideos.length === 0) {
throw new BailianError(
"At least one --image or --ref-video is required.",
ExitCode.USAGE,
'bl video ref --prompt "描述" --image person.jpg',
);
}
const imageVoices = (flags.imageVoice as string[] | undefined) || [];
const videoVoices = (flags.videoVoice as string[] | undefined) || [];
const model = (flags.model as string) || "happyhorse-1.0-r2v";
const format = detectOutputFormat(config.output);
// --- Resolve file URLs (auto-upload local files) ---
const credential = await resolveCredential(config);
const media: DashScopeVideoRefRequest["input"]["media"] = [];
// Add reference images
for (let i = 0; i < images.length; i++) {
const resolved = await resolveFileUrl(images[i]!, credential.token, model);
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
type: "reference_image",
url: resolved,
};
// Pair voice by position
if (imageVoices[i]) {
const resolvedVoice = await resolveFileUrl(imageVoices[i]!, credential.token, model);
entry.reference_voice = resolvedVoice;
}
media.push(entry);
}
// Add reference videos
for (let i = 0; i < refVideos.length; i++) {
const resolved = await resolveFileUrl(refVideos[i]!, credential.token, model);
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
type: "reference_video",
url: resolved,
};
// Pair voice by position
if (videoVoices[i]) {
const resolvedVoice = await resolveFileUrl(videoVoices[i]!, credential.token, model);
entry.reference_voice = resolvedVoice;
}
media.push(entry);
}
// --- Build request body ---
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeVideoRefRequest = {
model,
input: {
prompt: prompt!,
media,
},
parameters: {
resolution: (flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
},
};
if (config.dryRun) {
emitResult({ request: body }, format);
return;
}
// --- Submit async task ---
const url = videoGenerateEndpoint(config.baseUrl);
const response = await requestJson<DashScopeAsyncResponse>(config, {
url,
method: "POST",
body,
async: true,
});
const taskId = response.output.task_id;
if (!config.quiet) {
process.stderr.write(`[Model: ${model}]\n`);
process.stderr.write(
`Note: Reference-to-video typically takes 5-10 minutes. Please be patient.\n`,
);
}
// --no-wait or --async: return task ID immediately
if (flags.noWait || config.async) {
emitResult({ task_id: taskId }, format);
return;
}
// --- Poll until completion ---
const pollInterval = (flags.pollInterval as number) ?? 15;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const refTimeout = Math.max(config.timeout, 600);
const result = await poll<DashScopeTaskResponse>(config, {
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: refTimeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
},
});
const resultVideoUrl =
result.output.video_url || (result.output.results && result.output.results[0]?.url);
if (!resultVideoUrl) {
throw new BailianError("Task completed but no video URL returned.", ExitCode.GENERAL);
}
// --download: save to file
if (flags.download) {
const destPath = flags.download as string;
const { size } = await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
emitBare(destPath);
} else {
emitResult(
{
task_id: taskId,
video_url: resultVideoUrl,
status: "SUCCEEDED",
saved: destPath,
size: formatBytes(size),
},
format,
);
}
return;
}
// Default: auto-download to output directory
// eslint-disable-next-line @typescript-eslint/unbound-method
const { join } = await import("path");
const destDir = resolveOutputDir(config, { subDir: "videos" });
const destPath = join(destDir, `${taskId}.mp4`);
await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
emitResult({ task_id: taskId, video_url: resultVideoUrl, saved: destPath }, format);
},
});
@@ -1,53 +0,0 @@
import {
defineCommand,
requestJson,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeTaskResponse,
} from "bailian-cli-core";
import { failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
export default defineCommand({
name: "video task get",
description: "Query async task status",
usage: "bl video task get --task-id <id>",
options: [{ flag: "--task-id <id>", description: "Async task ID" }],
examples: [
"bl video task get --task-id 3b256896-3e70-xxxx-xxxx-xxxxxxxxxxxx",
"bl video task get --task-id 3b256896-3e70-xxxx --output json",
],
async run(config: Config, flags: GlobalFlags) {
const taskId = flags.taskId as string | undefined;
if (!taskId) failIfMissing("task-id", "bl video task get --task-id <id>");
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult({ task_id: taskId }, format);
return;
}
const url = taskEndpoint(config.baseUrl, taskId);
const response = await requestJson<DashScopeTaskResponse>(config, { url });
if (config.quiet) {
emitBare(response.output.task_status);
return;
}
emitResult(
{
task_id: response.output.task_id,
task_status: response.output.task_status,
video_url: response.output.video_url,
results: response.output.results,
submit_time: response.output.submit_time,
end_time: response.output.end_time,
},
format,
);
},
});
@@ -1,196 +0,0 @@
import {
defineCommand,
requestJson,
chatEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type ChatRequest,
type ChatResponse,
type ChatMessageContent,
isInteractive,
resolveFileUrl,
resolveCredential,
BailianError,
ExitCode,
isLocalFile,
} from "bailian-cli-core";
import { promptText } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { readFileSync, existsSync } from "fs";
import { extname } from "path";
const IMAGE_MIME_TYPES: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
};
const VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"]);
function isVideoInput(input: string): boolean {
// Check by extension
const ext = extname(input).toLowerCase().split("?")[0]!;
if (VIDEO_EXTENSIONS.has(ext)) return true;
// URL heuristic: contains common video extensions
if (/\.(mp4|mov|avi|mkv|webm|flv|wmv)(\?|$)/i.test(input)) return true;
return false;
}
async function toImageUrl(image: string): Promise<string> {
if (image.startsWith("data:")) return image;
if (image.startsWith("http://") || image.startsWith("https://")) return image;
if (image.startsWith("oss://")) return image;
// Local file → data URI (for small files < 10MB, fallback)
if (!existsSync(image)) throw new BailianError(`File not found: ${image}`, ExitCode.USAGE);
const ext = extname(image).toLowerCase();
const mime = IMAGE_MIME_TYPES[ext];
if (!mime)
throw new BailianError(
`Unsupported image format "${ext}". Supported: jpg, jpeg, png, webp`,
ExitCode.USAGE,
);
const buf = readFileSync(image);
return `data:${mime};base64,${buf.toString("base64")}`;
}
export default defineCommand({
name: "vision describe",
description: "Describe an image or video using Qwen-VL",
usage: "bl vision describe --image <path-or-url> [--video <url>] [--prompt <text>]",
options: [
{ flag: "--image <path-or-url>", description: "Local image path or URL" },
{
flag: "--video <url>",
description: "Video file URL or local path (mp4/mov/avi/mkv/webm)",
type: "array",
},
{ flag: "--prompt <text>", description: "Question about the content (default: auto-detected)" },
{ flag: "--model <model>", description: "Vision model (default: qwen3-vl-plus)" },
],
examples: [
"bl vision describe --image photo.jpg",
'bl vision describe --image https://example.com/photo.jpg --prompt "这只狗是什么品种?"',
'bl vision describe --video https://example.com/video.mp4 --prompt "总结视频内容"',
"bl vision describe --video ./local-video.mp4",
'bl vision describe --image photo.png --prompt "Extract the text" --model qwen-vl-plus',
],
async run(config: Config, flags: GlobalFlags) {
let image = (flags.image ?? (flags._positional as string[] | undefined)?.[0]) as
| string
| undefined;
const videoInputs = (flags.video as string[] | undefined) ?? [];
const model = (flags.model as string) || "qwen3-vl-plus";
// Auto-detect: if --image was given a video file, treat it as --video
if (image && isVideoInput(image)) {
videoInputs.push(image);
image = undefined;
}
const hasVideo = videoInputs.length > 0;
const defaultPrompt = hasVideo ? "Describe the video." : "Describe the image.";
const prompt = (flags.prompt as string) || defaultPrompt;
if (!image && !hasVideo) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({
message: "Enter image/video path or URL:",
});
if (!hint) {
process.stderr.write("Vision describe cancelled.\n");
process.exit(1);
}
// Detect if user entered a video
if (isVideoInput(hint)) {
videoInputs.push(hint);
} else {
image = hint;
}
} else {
throw new BailianError(
"Missing required argument --image or --video.",
ExitCode.USAGE,
"bl vision describe --image <path-or-url>\nbl vision describe --video <url-or-path>",
);
}
}
const format = detectOutputFormat(config.output);
if (config.dryRun) {
emitResult(
{ request: { prompt, image, video: videoInputs.length ? videoInputs : undefined, model } },
format,
);
return;
}
const contentArray: ChatMessageContent[] = [];
// ---- Handle video inputs ----
if (videoInputs.length > 0) {
for (const videoInput of videoInputs) {
let videoUrl = videoInput;
// Local video file → upload to OSS
if (isLocalFile(videoInput)) {
if (!existsSync(videoInput)) {
throw new BailianError(`Video file not found: ${videoInput}`, ExitCode.USAGE);
}
const credential = await resolveCredential(config);
videoUrl = await resolveFileUrl(videoInput, credential.token, model);
}
contentArray.push({ type: "video_url", video_url: { url: videoUrl } });
}
}
// ---- Handle image input ----
if (image) {
const imageUrl = await toImageUrl(image);
let finalImageUrl = imageUrl;
if (isLocalFile(image) && imageUrl.startsWith("data:")) {
const { statSync } = await import("fs");
const fileSize = statSync(image).size;
if (fileSize > 5 * 1024 * 1024) {
const credential = await resolveCredential(config);
finalImageUrl = await resolveFileUrl(image, credential.token, model);
}
}
contentArray.push({ type: "image_url", image_url: { url: finalImageUrl } });
}
// ---- Text prompt ----
contentArray.push({ type: "text", text: prompt });
const body: ChatRequest = {
model,
messages: [
{
role: "user",
content: contentArray,
},
],
};
const url = chatEndpoint(config.baseUrl);
const response = await requestJson<ChatResponse>(config, {
url,
method: "POST",
body,
});
const content = response.choices?.[0]?.message?.content;
if (format !== "text") {
emitResult(response, format);
return;
}
emitBare((content || "") as string);
},
});
-137
View File
@@ -1,137 +0,0 @@
import {
defineCommand,
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "../../output/output.ts";
import { displayWidth, padEnd } from "../../output/cjk-width.ts";
const LIST_WORKSPACES_API = "zeldaEasy.bailian-dash-workspace.space.listWorkspaces";
interface WorkspaceInfo {
workspaceId: string;
agentName: string;
defaultAgent: boolean;
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const val = obj[key];
if (val && typeof val === "object" && !Array.isArray(val)) return val as Record<string, unknown>;
return undefined;
}
function extractResponseData(result: Record<string, unknown>): Record<string, unknown> {
const data = getNestedRecord(result, "data");
if (!data) return result;
const dataV2 = getNestedRecord(data, "DataV2");
if (dataV2) {
const inner = getNestedRecord(dataV2, "data");
const innerData = inner ? getNestedRecord(inner, "data") : undefined;
return innerData ?? inner ?? dataV2;
}
const direct = getNestedRecord(data, "data");
return direct ?? data;
}
function printTable(workspaces: WorkspaceInfo[], noColor: boolean): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const green = noColor ? (text: string) => text : (text: string) => `\x1b[32m${text}\x1b[0m`;
const headersCn = ["空间名称", "Workspace ID", "默认空间"];
const headersEn = ["Name", "", "Default"];
const rows = workspaces.map((ws) => [
ws.agentName,
ws.workspaceId,
ws.defaultAgent ? "Yes" : "-",
]);
const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...rows.map((row) => displayWidth(row[col])),
),
);
const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
const cells = row.map((cell, col) => {
if (col === 2 && cell === "Yes") return green(padEnd(cell, widths[col]));
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(
dim(`\n共 ${workspaces.length} 个空间 (Total: ${workspaces.length})`) + "\n",
);
}
export default defineCommand({
name: "workspace list",
description: "List all workspaces",
usage: "bl workspace list [flags]",
options: [
{
flag: "--list <n>",
description: "Limit number of results",
},
{
flag: "--region <region>",
description: "API region (default: cn-beijing)",
},
],
examples: ["bl workspace list", "bl workspace list --list 5", "bl workspace list --output json"],
async run(config: Config, flags: GlobalFlags) {
const region = (flags.region as string) || "cn-beijing";
const limit = Number(flags.list) || 0;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
if (config.dryRun) {
emitResult({ api: LIST_WORKSPACES_API, data: {}, region }, format);
return;
}
const result = await callConsoleGateway(config, credential.token, {
api: LIST_WORKSPACES_API,
data: {},
region,
});
if (format === "json") {
emitResult(result, format);
return;
}
const resp = extractResponseData(result as Record<string, unknown>);
const dataArr = resp.data as Record<string, unknown>[] | undefined;
if (!Array.isArray(dataArr) || dataArr.length === 0) {
process.stdout.write("No workspace found.\n");
return;
}
let workspaces = dataArr as unknown as WorkspaceInfo[];
if (limit > 0) workspaces = workspaces.slice(0, limit);
printTable(workspaces, config.noColor);
},
});
+21 -176
View File
@@ -1,178 +1,23 @@
import { scanCommandPath, parseFlags } from "./args.ts";
import { registry } from "./registry.ts";
import {
GLOBAL_OPTIONS,
loadConfig,
resolveCredential,
trackCommandExecution,
flushTelemetry,
} from "bailian-cli-core";
import { ensureApiKey } from "./utils/ensure-key.ts";
import { setupProxyFromEnv } from "./proxy.ts";
import { handleError } from "./error-handler.ts";
import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts";
import { maybeShowStatusBar } from "./output/status-bar.ts";
import { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
import { CLI_VERSION } from "./version.ts";
import {
printCurrentCommandHelp,
registerCommandHelpPrinter,
setExecutingCommandPath,
} from "./utils/command-help.ts";
import { createCli } from "bailian-cli-runtime";
import { commandAliases, commands } from "./commands.ts";
import { commandPackPolicy } from "./command-pack-policy.ts";
import pkg from "../package.json" with { type: "json" };
// 必须在任何 fetch 发起前安装(含 update-checker / telemetry
try {
setupProxyFromEnv();
} catch (err) {
handleError(err);
}
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",
] as const;
registerCommandHelpPrinter((commandPath, out) => {
registry.printHelp(commandPath, out);
});
// 优雅处理 Ctrl+C
// 退出前尝试 best-effort 刷出埋点,让去抖队列中 / 在途的 fetch 请求有机会
// 落网络flush 与较短超时 race保证 SIGINT 仍然响应及时。
process.on("SIGINT", () => {
process.stderr.write("\nInterrupted. Exiting.\n");
void flushTelemetry(500).finally(() => process.exit(130));
});
// 优雅处理 stdout EPIPE例如管道到提前退出的 `mpv`
process.stdout.on("error", (e: NodeJS.ErrnoException) => {
if (e.code === "EPIPE") process.exit(0);
else throw e;
});
// 自己接管鉴权 或 根本不需要 API key 的命令
const NO_AUTH_SETUP = [
["auth", "login"],
["auth", "logout"],
["config", "show"],
["config", "set"],
["config", "export-schema"],
["update"],
["knowledge", "retrieve"],
["pipeline", "run"],
["pipeline", "validate"],
["model", "list"],
["app", "list"],
["console", "call"],
["usage", "free"],
["usage", "freetier"],
["usage", "stats"],
["mcp", "list"],
["mcp", "tools"],
["mcp", "call"],
["workspace", "list"],
["quota", "list"],
["quota", "request"],
["quota", "history"],
["quota", "check"],
];
async function main() {
let argv = process.argv.slice(2);
if (argv[0] === "--") argv = argv.slice(1);
if (argv.includes("--version") || argv.includes("-v")) {
process.stdout.write(`bl ${CLI_VERSION}\n`);
process.exit(0);
}
const commandPath = scanCommandPath(argv, GLOBAL_OPTIONS);
if (argv.includes("--help") || argv.includes("-h")) {
registry.printHelp(commandPath, process.stderr);
process.exit(0);
}
// 未传任何命令:展示帮助信息与登录引导
if (commandPath.length === 0) {
registry.printHelp([], process.stderr);
const flags = parseFlags(argv, GLOBAL_OPTIONS);
const config = loadConfig(flags);
config.clientName = "bailian-cli";
config.clientVersion = CLI_VERSION;
const hasKey = !!(
config.apiKey ||
config.fileApiKey ||
config.fileAccessToken ||
config.accessTokenEnv
);
if (hasKey) printQuickStart();
else printWelcomeBanner();
process.exit(0);
}
// 组路径(例如 `bl speech` 未接子命令):展示帮助后干净退出
if (registry.isGroupPath(commandPath)) {
registry.printHelp(commandPath, process.stderr);
process.exit(0);
}
const { command, extra } = registry.resolve(commandPath);
const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...(command.options ?? [])]);
if (extra.length > 0) (flags as Record<string, unknown>)._positional = extra;
const config = loadConfig(flags);
config.clientName = "bailian-cli";
config.clientVersion = CLI_VERSION;
const needsAuthSetup = !NO_AUTH_SETUP.some((cmd) => cmd.every((c, i) => commandPath[i] === c));
if (needsAuthSetup) {
await ensureApiKey(config);
try {
const credential = await resolveCredential(config);
maybeShowStatusBar(config, credential.token, credential);
} catch {
/* 没有凭证,不展示状态栏 */
}
}
const updateCheckPromise = checkForUpdate(CLI_VERSION).catch(() => {});
setExecutingCommandPath(commandPath);
if (
commandPath[0] === "auth" &&
commandPath[1] === "login" &&
!flags.console &&
!String((flags.apiKey as string | undefined) ?? "").trim() &&
!String(config.apiKey ?? "").trim() &&
!process.env.DASHSCOPE_API_KEY?.trim()
) {
printCurrentCommandHelp(process.stderr);
process.exit(0);
}
await trackCommandExecution(config, commandPath, flags, () => command.execute(config, flags));
await updateCheckPromise;
const isUpdateCommand = commandPath.length === 1 && commandPath[0] === "update";
const newVersion = getPendingUpdateNotification();
if (newVersion && !config.quiet && !isUpdateCommand) {
const isTTY = process.stderr.isTTY;
const yellow = isTTY ? "\x1b[33m" : "";
const cyan = isTTY ? "\x1b[36m" : "";
const reset = isTTY ? "\x1b[0m" : "";
process.stderr.write(`\n ${yellow}Update available: ${CLI_VERSION}${newVersion}${reset}\n`);
process.stderr.write(` Run ${cyan}bl update${reset} to upgrade\n\n`);
}
// 进程退出前尽力等待在途的埋点完成。
// 使用较短超时兜底,避免慢网拖慢用户感知。
await flushTelemetry(1000);
}
main().catch((err) => {
// 在 handleError() 调用 process.exit() 之前刷出在途埋点。
// 命令抛出的错误已被 trackCommandExecution 的 finally 块记录,
// 但底层 tracker 有 ~500ms 的发送去抖。不主动 flush 的话,
// 错误事件会随进程退出丢掉。
void flushTelemetry(1000).finally(() => handleError(err));
});
void createCli(
{ ...commands, ...commandAliases },
{
binName: "bl",
version: pkg.version,
clientName: "bailian-cli",
npmPackage: "bailian-cli",
quickStartTasks,
commandPacks: commandPackPolicy,
},
).run();
-34
View File
@@ -1,34 +0,0 @@
import { API_KEY_PAGE } from "../urls.ts";
const QUICK_START_TASKS = [
"帮我生成一套鸭舌帽的亚马逊电商主图(白底 + 场景图 + 模特上身图)",
"帮我生成一段 3 分钟的幽默相声音频",
"帮我生成一套小红帽故事绘本 PDF(含插图)",
"帮我分析这个视频的内容并写一篇小红书文案",
];
function colors() {
const isTTY = process.stderr.isTTY;
return {
purple: isTTY ? "\x1b[38;2;147;51;234m" : "",
dim: isTTY ? "\x1b[2m" : "",
reset: isTTY ? "\x1b[0m" : "",
};
}
export function printWelcomeBanner(): void {
const { purple, reset } = colors();
process.stderr.write(`\n Welcome to ${purple}Bailian${reset} CLI!\n\n`);
process.stderr.write(" Get started in 2 steps:\n");
process.stderr.write(` 1. Get your API Key: ${API_KEY_PAGE}\n`);
process.stderr.write(" 2. Login: bl auth login --api-key <your-key>\n\n");
}
export function printQuickStart(): void {
const { dim, reset } = colors();
process.stderr.write("\n🎯 Try these with your AI coding assistant:\n\n");
QUICK_START_TASKS.forEach((task, i) => {
process.stderr.write(`${dim}${i + 1}${reset} ${task}\n`);
});
process.stderr.write("\n");
}
-23
View File
@@ -1,23 +0,0 @@
import { formatOutput, type OutputFormat } from "bailian-cli-core";
/**
* Emit the primary result of a command.
*
* Design principle:
* stdout → structured data only (JSON when piped, text when TTY)
* stderr → human info (progress, logs, tips) — handled elsewhere
*
* This ensures `bl cmd ... | jq .` always receives clean JSON,
* while interactive users see human-readable text.
*/
export function emitResult(data: unknown, format: OutputFormat): void {
process.stdout.write(formatOutput(data, format) + "\n");
}
/**
* Emit a bare value (file path, plain text) to stdout.
* Used in --quiet mode or when the result is a single scalar.
*/
export function emitBare(value: string): void {
process.stdout.write(value + "\n");
}
-114
View File
@@ -1,114 +0,0 @@
/**
* Interactive prompt utilities.
*
* Wraps @clack/prompts with environment-awareness:
* - In interactive mode: shows prompts and lets users input values.
* - In non-interactive / CI / Agent mode: fails fast with a clear error.
*
* All functions here are no-ops (return undefined) when non-interactive,
* so callers must check isInteractive() first or handle the missing-value
* case explicitly.
*/
import { BailianError, ExitCode, isInteractive } from "bailian-cli-core";
import { printCurrentCommandHelp, getExecutingCommandPath } from "../utils/command-help.ts";
// Dynamic import to avoid loading @clack/prompts in non-interactive envs unnecessarily
// (though for CLI tools the startup cost is usually acceptable)
/**
* Prompt the user for a text value.
* Only call this when isInteractive() is true; otherwise the function returns
* undefined immediately so the caller can fail fast.
*/
export async function promptText(options: {
message: string;
defaultValue?: string;
}): Promise<string | undefined> {
if (!isInteractive()) return undefined;
const { defaultValue, message } = options;
const inquirer = (await import("@clack/prompts")) as {
text: (opts: {
message: string;
default?: string;
placeholder?: string;
}) => Promise<string | symbol>;
};
const val = await inquirer.text({
message,
default: defaultValue,
placeholder: defaultValue,
});
// @clack/prompts returns a Symbol.cancel when the user presses Ctrl+C
if (typeof val === "symbol") return undefined;
return val as string;
}
/**
* Like promptText but confirms with y/N before proceeding.
*/
export async function promptConfirm(options: {
message: string;
initialValue?: boolean;
}): Promise<boolean | undefined> {
if (!isInteractive()) return undefined;
const { message, initialValue } = options;
const inquirer = (await import("@clack/prompts")) as {
confirm: (opts: { message: string; initialValue?: boolean }) => Promise<boolean | symbol>;
};
const val = await inquirer.confirm({ message, initialValue });
if (typeof val === "symbol") return undefined;
return val as boolean;
}
/**
* Prompt the user to select one value from a list.
* Only call this when isInteractive() is true; otherwise the function returns
* undefined immediately so the caller can fail fast.
*/
export async function promptSelect(options: {
message: string;
choices: Array<{ value: string; label: string; hint?: string }>;
defaultValue?: string;
}): Promise<string | undefined> {
if (!isInteractive()) return undefined;
const { message, choices, defaultValue } = options;
const clack = (await import("@clack/prompts")) as {
select: (opts: {
message: string;
initialValue?: string;
options: Array<{ value: string; label: string; hint?: string }>;
}) => Promise<string | symbol>;
};
const val = await clack.select({
message,
initialValue: defaultValue,
options: choices,
});
if (typeof val === "symbol") return undefined;
return val as string;
}
/**
* Fail fast with a user-friendly error when a required option is missing
* in non-interactive (agent / CI) mode.
*/
export function failIfMissing(flagName: string, context: string): never {
if (getExecutingCommandPath().length > 0) {
printCurrentCommandHelp(process.stderr);
process.exit(0);
}
throw new BailianError(
`Missing required argument: --${flagName}\n` +
`Hint: In non-interactive (CI / agent) environments all required flags must be provided.\n` +
` In an interactive terminal, run without --${flagName} and the CLI will prompt for it.`,
ExitCode.USAGE,
context,
);
}
-39
View File
@@ -1,39 +0,0 @@
import { homedir } from "os";
import { maskToken, type Config, type ResolvedCredential } from "bailian-cli-core";
const reset = "\x1b[0m";
const dim = "\x1b[2m";
const bold = "\x1b[1m";
const mmBlue = "\x1b[38;2;43;82;255m";
const mmCyan = "\x1b[38;2;6;184;212m";
const mmPink = "\x1b[38;2;236;72;153m";
function tildePath(p: string): string {
return p.startsWith(homedir()) ? p.replace(homedir(), "~") : p;
}
export function maybeShowStatusBar(
config: Config,
token: string,
resolved?: ResolvedCredential,
): void {
if (config.quiet || !process.stderr.isTTY) return;
const filePath = config.configPath ? tildePath(config.configPath) : "~/.bailian/config.json";
const regionSrc = config.fileRegion ? `${config.fileRegion} (file)` : "cn (default)";
const authTag = resolved
? `${resolved.source} · ${resolved.method}`
: config.apiKey
? "flag · api-key"
: "config";
const maskedKey = maskToken(token);
process.stderr.write(
`${bold}${mmBlue}BAILIAN${reset} ` +
`${dim}${filePath}${reset} ` +
`${dim}|${reset} ` +
`${dim}Region:${reset} ${mmCyan}${regionSrc}${reset} ` +
`${dim}|${reset} ` +
`${dim}Auth:${reset} ${mmPink}${maskedKey}${reset} ${dim}${authTag}${reset}\n`,
);
}
-24
View File
@@ -1,24 +0,0 @@
import { loadConfig, type Config, type GlobalFlags } from "bailian-cli-core";
const PIPELINE_FLAGS: GlobalFlags = {
output: "json",
nonInteractive: true,
noColor: true,
quiet: true,
verbose: false,
yes: false,
dryRun: false,
help: false,
async: false,
};
/**
* Build a Config suitable for in-process API calls from within pipeline steps.
* Uses the same config resolution (env vars, config file) as the CLI itself,
* but forces JSON output + non-interactive + quiet mode.
*/
export function buildPipelineConfig(): Config {
const config = loadConfig(PIPELINE_FLAGS);
config.clientName = "bailian-cli";
return config;
}
-275
View File
@@ -1,275 +0,0 @@
import type { Command } from "bailian-cli-core";
import { BailianError } from "bailian-cli-core";
import { ExitCode } from "bailian-cli-core";
import { GLOBAL_OPTIONS } from "bailian-cli-core";
import { commands } from "./commands/catalog.ts";
export type { Command, OptionDef } from "bailian-cli-core";
interface CommandNode {
command?: Command;
children: Map<string, CommandNode>;
}
class CommandRegistry {
private root: CommandNode = { children: new Map() };
constructor(commands: Record<string, Command>) {
for (const [path, cmd] of Object.entries(commands)) {
this.register(path, cmd);
}
}
private register(path: string, command: Command): void {
const parts = path.split(" ");
let node = this.root;
for (const part of parts) {
if (!node.children.has(part)) {
node.children.set(part, { children: new Map() });
}
node = node.children.get(part)!;
}
node.command = command;
}
getAllCommands(): Command[] {
const commands: Command[] = [];
const traverse = (node: CommandNode) => {
if (node.command) commands.push(node.command);
for (const child of node.children.values()) {
traverse(child);
}
};
traverse(this.root);
return commands;
}
isGroupPath(commandPath: string[]): boolean {
let node = this.root;
for (const part of commandPath) {
const child = node.children.get(part);
if (!child) return false;
node = child;
}
return !node.command && node.children.size > 0;
}
resolve(commandPath: string[]): { command: Command; extra: string[] } {
let node = this.root;
const matched: string[] = [];
for (const part of commandPath) {
const child = node.children.get(part);
if (!child) break;
node = child;
matched.push(part);
}
if (node.command) {
return { command: node.command, extra: commandPath.slice(matched.length) };
}
// Single child: auto-forward (e.g. `bl config` → `bl config show`)
if (matched.length > 0 && node.children.size === 1) {
const [, child] = node.children.entries().next().value as [string, CommandNode];
if (child.command) {
return { command: child.command, extra: commandPath.slice(matched.length) };
}
}
// If we matched some path but no command, show help for that group
if (matched.length > 0 && node.children.size > 0) {
const subcommands = Array.from(node.children.entries())
.map(([name, n]) => {
if (n.command) return ` ${matched.join(" ")} ${name} ${n.command.description}`;
const subs = Array.from(n.children.keys()).join(", ");
return ` ${matched.join(" ")} ${name} [${subs}]`;
})
.join("\n");
throw new BailianError(
`Unknown command: bl ${commandPath.join(" ")}\n\nAvailable commands:\n${subcommands}`,
ExitCode.USAGE,
`bl ${matched.join(" ")} --help`,
);
}
throw new BailianError(
`Unknown command: bl ${commandPath.join(" ")}`,
ExitCode.USAGE,
"bl --help",
);
}
private buildResourceLines(a: (s: string) => string, d: (s: string) => string): string {
const entries: Array<{ path: string; desc: string }> = [];
const collect = (node: CommandNode, prefix: string) => {
for (const [name, child] of node.children) {
const fullPath = prefix ? `${prefix} ${name}` : name;
if (child.command) {
entries.push({ path: fullPath, desc: child.command.description });
}
if (child.children.size > 0) {
collect(child, fullPath);
}
}
};
collect(this.root, "");
const maxLen = Math.max(...entries.map((e) => e.path.length));
return entries.map((e) => ` ${a(e.path.padEnd(maxLen + 2))} ${d(e.desc)}`).join("\n");
}
private buildGlobalFlagLines(a: (s: string) => string, d: (s: string) => string): string {
const maxLen = Math.max(...GLOBAL_OPTIONS.map((o) => o.flag.length));
return GLOBAL_OPTIONS.map((o) => ` ${a(o.flag.padEnd(maxLen + 2))} ${d(o.description)}`).join(
"\n",
);
}
// Color helpers — no-ops when output is not a TTY
private bold = (s: string, out: NodeJS.WriteStream) => (out.isTTY ? `\x1b[1m${s}\x1b[0m` : s);
private accent = (s: string, out: NodeJS.WriteStream) =>
out.isTTY ? `\x1b[38;2;59;130;246m${s}\x1b[0m` : s;
private dim = (s: string, out: NodeJS.WriteStream) => (out.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
printHelp(commandPath: string[], out: NodeJS.WriteStream = process.stdout): void {
if (commandPath.length === 0) {
this.printRootHelp(out);
return;
}
let node = this.root;
for (const part of commandPath) {
const child = node.children.get(part);
if (!child) {
this.printRootHelp(out);
return;
}
node = child;
}
if (node.command) {
this.printCommandHelp(node.command, out);
return;
}
// Group help (e.g. `bl auth --help`)
const prefix = commandPath.join(" ");
out.write(`\n${this.bold("Usage:", out)} bl ${prefix} <command> [flags]\n\n`);
out.write(`${this.bold("Commands:", out)}\n`);
this.printChildren(node, prefix, out);
if (prefix === "pipeline") {
this.printPipelineQuickStart(out);
}
out.write("\n");
}
private printPipelineQuickStart(out: NodeJS.WriteStream): void {
const b = (s: string) => this.bold(s, out);
const d = (s: string) => this.dim(s, out);
out.write(`
${b("Minimal workflow.yaml:")}
${d(" version: workflow/v1")}
${d(" steps:")}
${d(" - id: chat")}
${d(" type: text/chat")}
${d(" input:")}
${d(' message: "Who are you?"')}
${d(' system: "You are a concise assistant."')}
${b("Try it:")}
${d(" bl pipeline validate workflow.yaml")}
${d(" bl pipeline run workflow.yaml --dry-run --output json")}
`);
}
private printRootHelp(out: NodeJS.WriteStream): void {
// Bailian brand color: #615ced → RGB(97, 92, 237)
const LOGO = [
"██████╗ █████╗ ██╗██╗ ██╗ █████╗ ███╗ ██╗",
"██╔══██╗██╔══██╗██║██║ ██║██╔══██╗████╗ ██║",
"██████╔╝███████║██║██║ ██║███████║██╔██╗ ██║",
"██╔══██╗██╔══██║██║██║ ██║██╔══██║██║╚██╗██║",
"██████╔╝██║ ██║██║███████╗██║██║ ██║██║ ╚████║",
"╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
];
const PURPLE = "\x1b[38;2;97;92;237m";
const RESET = "\x1b[0m";
out.write("\n");
for (const line of LOGO) {
if (out.isTTY) {
out.write(`${PURPLE}${line}${RESET}\n`);
} else {
out.write(line + "\n");
}
}
const b = (s: string) => this.bold(s, out);
const a = (s: string) => this.accent(s, out);
const d = (s: string) => this.dim(s, out);
const commandLines = this.buildResourceLines(a, d);
const globalFlagLines = this.buildGlobalFlagLines(a, d);
out.write(`
${b("Usage:")} bl <resource> <command> [flags]
${b("Commands:")}
${commandLines}
${b("Global Flags:")}
${globalFlagLines}
${b("Getting Help:")}
${d("Add --help after any command to see its full list of options, defaults,")}
${d("and usage examples. For example:")} bl text chat --help
`);
}
private printCommandHelp(cmd: Command, out: NodeJS.WriteStream): void {
const b = (s: string) => this.bold(s, out);
const a = (s: string) => this.accent(s, out);
const d = (s: string) => this.dim(s, out);
out.write(`\n${cmd.description}\n`);
if (cmd.usage) out.write(`${b("Usage:")} ${cmd.usage}\n`);
if (cmd.options && cmd.options.length > 0) {
const maxLen = Math.max(...cmd.options.map((o) => o.flag.length));
out.write(`\n${b("Options:")}\n`);
for (const opt of cmd.options) {
out.write(` ${a(opt.flag.padEnd(maxLen + 2))} ${d(opt.description)}\n`);
}
}
if (cmd.examples && cmd.examples.length > 0) {
out.write(`\n${b("Examples:")}\n`);
for (const ex of cmd.examples) {
out.write(` ${d(ex)}\n`);
}
}
out.write(
`\n${d("Global flags (--api-key, --output, --quiet, etc.) are always available.")}\n`,
);
out.write(`${d("Run")} bl --help ${d("for the full list.")}\n`);
}
private printChildren(node: CommandNode, prefix: string, out: NodeJS.WriteStream): void {
const entries: Array<{ fullName: string; description: string }> = [];
const collect = (n: CommandNode, p: string) => {
for (const [name, child] of n.children) {
if (child.command)
entries.push({ fullName: `${p} ${name}`, description: child.command.description });
if (child.children.size > 0) collect(child, `${p} ${name}`);
}
};
collect(node, prefix);
const maxLen = Math.max(...entries.map((e) => e.fullName.length));
for (const { fullName, description } of entries) {
out.write(` ${this.accent(fullName.padEnd(maxLen), out)} ${this.dim(description, out)}\n`);
}
}
}
export const registry = new CommandRegistry(commands);
-16
View File
@@ -1,16 +0,0 @@
/**
* User-facing Aliyun Bailian / Model Studio console URLs.
*
* Single source of truth for all `bailian.console.aliyun.com/*` references
* across cli source. Currently pinned to cn-beijing — expand to a region map
* if/when overseas console support is added.
*/
/** Root entry — generic console landing, region picker. */
export const BAILIAN_CONSOLE_ROOT = "https://bailian.console.aliyun.com";
/** Region-pinned console base. Add a region map if/when overseas is supported. */
export const BAILIAN_CONSOLE = `${BAILIAN_CONSOLE_ROOT}/cn-beijing`;
/** Direct deep link to API key management page. */
export const API_KEY_PAGE = `${BAILIAN_CONSOLE}/?tab=app#/api-key`;
-25
View File
@@ -1,25 +0,0 @@
/** Current command path (e.g. `["auth","login"]`) for help-on-missing; set by `main` before `execute`. */
let executingCommandPath: string[] = [];
let printCommandHelpImpl: ((commandPath: string[], out: NodeJS.WriteStream) => void) | null = null;
export function setExecutingCommandPath(path: string[]): void {
executingCommandPath = path;
}
export function getExecutingCommandPath(): string[] {
return executingCommandPath;
}
export function registerCommandHelpPrinter(
fn: (commandPath: string[], out: NodeJS.WriteStream) => void,
): void {
printCommandHelpImpl = fn;
}
/** Print help for the command currently being executed (must call `setExecutingCommandPath` first). */
export function printCurrentCommandHelp(out: NodeJS.WriteStream = process.stderr): void {
if (printCommandHelpImpl && executingCommandPath.length > 0) {
printCommandHelpImpl(executingCommandPath, out);
}
}
-51
View File
@@ -1,51 +0,0 @@
import {
BailianError,
ExitCode,
isInteractive,
maskToken,
readConfigFile,
writeConfigFile,
type Config,
} from "bailian-cli-core";
import { promptText, promptConfirm } from "../output/prompt.ts";
export async function ensureApiKey(config: Config): Promise<void> {
if (config.apiKey || config.fileApiKey || config.accessTokenEnv || config.fileAccessToken) return;
const envKey = process.env.DASHSCOPE_API_KEY;
let key: string | undefined;
if (envKey) {
if (!isInteractive({ nonInteractive: config.nonInteractive })) {
key = envKey;
} else {
const use = await promptConfirm({
message: `Found DASHSCOPE_API_KEY in environment (${maskToken(envKey)}). Save it to config file?`,
});
if (use) key = envKey;
}
}
if (!key) {
if (!isInteractive({ nonInteractive: config.nonInteractive })) {
throw new BailianError(
"No API key found.",
ExitCode.AUTH,
"Set DASHSCOPE_API_KEY environment variable, pass --api-key, or run interactively to be prompted.",
);
}
const input = await promptText({ message: "Enter your DashScope API key:" });
if (!input) throw new BailianError("API key is required.", ExitCode.AUTH);
key = input;
}
const data: Record<string, unknown> = {
...(readConfigFile() as Record<string, unknown>),
api_key: key,
};
await writeConfigFile(data);
config.fileApiKey = key;
const path = config.configPath ?? "~/.bailian/config.json";
process.stderr.write(`API key saved to ${path}\n`);
}
-39
View File
@@ -1,39 +0,0 @@
/**
* Resolve image `size` flag for image generate/edit.
*
* Users may pass either a ratio (e.g. "1:1", "3:4", "16:9") or a pixel size
* (e.g. "2048*2048"). The DashScope API only accepts the pixel format, so we
* map known ratios to the recommended pixel size for each model family.
*
* Sync models (qwen-image-2.0 / qwen-image-max / qwen-image-edit-2.0):
* higher-resolution presets.
*
* Async models (wanx2.x): smaller presets.
*
* Pixel-format input is passed through unchanged.
*/
export const SYNC_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
"1:1": "2048*2048",
"4:3": "2368*1728",
"3:4": "1728*2368",
};
export const ASYNC_RATIO_MAP: Record<string, string> = {
"16:9": "1664*928",
"4:3": "1472*1104",
"1:1": "1328*1328",
"3:4": "1104*1472",
"9:16": "928*1664",
};
/** Resolve `--size` value: accept ratio (3:4) or pixel (W*H) format. */
export function resolveImageSize(input: string, useSync: boolean): string;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined {
if (!input) return undefined;
const map = useSync ? SYNC_RATIO_MAP : ASYNC_RATIO_MAP;
return map[input] ?? input;
}
-97
View File
@@ -1,97 +0,0 @@
import { join } from "path";
import { readFileSync, writeFileSync } from "fs";
import { getConfigDir, trackingHeaders } from "bailian-cli-core";
export const NPM_REGISTRY = "https://registry.npmjs.org";
export const NPM_PACKAGE = "bailian-cli";
const STATE_FILE = () => join(getConfigDir(), "update-state.json");
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4h
const FETCH_TIMEOUT_MS = 3000;
/**
* Simple semver comparison: returns true if a > b.
* Supports standard x.y.z format.
*/
function isNewerVersion(a: string, b: string): boolean {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
}
return false; // equal
}
interface UpdateState {
lastChecked: number;
latestVersion: string;
}
function readState(): UpdateState | null {
try {
const raw = readFileSync(STATE_FILE(), "utf-8");
return JSON.parse(raw) as UpdateState;
} catch {
return null;
}
}
function writeState(state: UpdateState): void {
try {
writeFileSync(STATE_FILE(), JSON.stringify(state));
} catch {
/* ignore */
}
}
export async function fetchLatestVersion(
timeoutMs: number = FETCH_TIMEOUT_MS,
): Promise<string | null> {
try {
const encoded = NPM_PACKAGE.replace("/", "%2f");
const res = await fetch(`${NPM_REGISTRY}/${encoded}/latest`, {
headers: {
Accept: "application/json",
...trackingHeaders(),
},
signal: AbortSignal.timeout(timeoutMs),
});
if (!res.ok) return null;
const data = (await res.json()) as { version?: string };
return data.version ?? null;
} catch {
return null;
}
}
let pendingNotification: string | null = null;
export function getPendingUpdateNotification(): string | null {
return pendingNotification;
}
export async function checkForUpdate(currentVersion: string): Promise<void> {
// Skip in CI / non-TTY environments
if (process.env.CI || !process.stderr.isTTY) return;
const state = readState();
const now = Date.now();
// Throttle: skip if checked within the last 4 hours
if (state && now - state.lastChecked < CHECK_INTERVAL_MS) {
if (state.latestVersion && isNewerVersion(state.latestVersion, currentVersion)) {
pendingNotification = state.latestVersion;
}
return;
}
const latest = await fetchLatestVersion();
if (!latest) return;
writeState({ lastChecked: now, latestVersion: latest });
if (latest && isNewerVersion(latest, currentVersion)) {
pendingNotification = latest;
}
}
-95
View File
@@ -1,95 +0,0 @@
import { expect, test } from "vite-plus/test";
import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core";
import { parseFlags } from "../src/args.ts";
import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts";
const IMAGE_GENERATE_OPTIONS = [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID" },
{ flag: "--watermark <bool>", description: BOOL_FLAG_WATERMARK },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
];
test("parseFlags rejects unknown long flags", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining('Unknown flag "--xxxx"'),
}),
);
});
test("parseFlags rejects unknown flags with = syntax", () => {
expect(() =>
parseFlags(
["--prompt=cat", "--unknown-flag=yes"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
),
).toThrow(/Unknown flag "--unknown-flag"/);
});
test("parseFlags accepts defined command and global flags", () => {
const flags = parseFlags(
["--quiet", "--prompt", "cat", "--watermark", "false"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
);
expect(flags.quiet).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags rejects value flag when next token is another flag", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
for (const argv of [
["--watermark", "--prompt", "cat"],
["--watermark", "-h"],
["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"],
]) {
expect(() => parseFlags(argv, opts)).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
}
});
test("parseFlags rejects trailing value flag without value", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
});
test("parseFlags allows boolean flags without values adjacent to other flags", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
const flags = parseFlags(
["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"],
opts,
);
expect(flags.quiet).toBe(true);
expect(flags.dryRun).toBe(true);
expect(flags.noWait).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags does not treat the next flag as a boolean flag value", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --prompt requires a value"),
}),
);
// --dry-run is boolean: no value check; parsing continues to --prompt.
const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts);
expect(flags.dryRun).toBe(true);
expect(flags.prompt).toBe("cat");
});
@@ -1,170 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts";
describe("e2e: advisor recommend", () => {
test("advisor 分组展示子命令帮助且成功退出", async () => {
const { stdout, stderr, exitCode } = await runCli(["advisor"]);
expect(exitCode, stderr).toBe(0);
expect(`${stdout}\n${stderr}`).toMatch(/advisor|recommend/i);
});
test("advisor recommend --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["advisor", "recommend", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/recommend|--message|dry-run/i);
});
});
describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommendDashScope", () => {
test("advisor recommend 缺少 --message 时打印帮助并退出 (0)", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--non-interactive",
]);
expect(exitCode).toBe(0);
expect(`${stdout}\n${stderr}`).toMatch(/--message|Usage:/i);
});
test("advisor recommend --dry-run 输出意图分析和候选列表", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--dry-run",
"--message",
"我想做一个能理解图片的客服机器人",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
userInput?: string;
intent?: { requiredCapabilities?: string[]; inputModality?: string[] };
candidateCount?: number;
candidates?: Array<{ model?: string; score?: number }>;
}>(stdout);
expect(data.userInput).toBe("我想做一个能理解图片的客服机器人");
expect(data.intent?.requiredCapabilities).toContain("VU");
expect(data.intent?.inputModality).toContain("Image");
expect(data.candidateCount).toBeGreaterThan(0);
expect(data.candidates?.[0]?.model).toBeDefined();
expect(data.candidates?.[0]?.score).toBeGreaterThan(0);
}, 60_000);
test("advisor recommend 完整推荐流程返回结果", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--message",
"低成本高并发的在线客服",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
type?: string;
recommendations?: Array<{
model?: string;
name?: string;
reason?: string;
}>;
}>(stdout);
expect(data.type).toBe("single");
expect(data.recommendations?.length).toBeGreaterThan(0);
expect(data.recommendations?.[0]?.model).toBeDefined();
expect(data.recommendations?.[0]?.reason).toBeDefined();
}, 120_000);
// ---- 模型偏好:正例 ----
test("scoped 偏好 — 限定系列时 intent 含 modelPreference.mode=scoped", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--dry-run",
"--message",
"deepseek系列中哪个模型最适合用来进行快速推理",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
intent?: { modelPreference?: { mode?: string; targets?: string[] } };
}>(stdout);
expect(data.intent?.modelPreference?.mode).toBe("scoped");
expect(data.intent?.modelPreference?.targets?.length).toBeGreaterThan(0);
expect(
data.intent?.modelPreference?.targets?.some((target) =>
target.toLowerCase().includes("deepseek"),
),
).toBe(true);
}, 60_000);
test("comparison 偏好 — 对比模型时 intent 含 modelPreference.mode=comparison", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--dry-run",
"--message",
"qwen-max和deepseek-v3哪个更适合做代码生成",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
intent?: { modelPreference?: { mode?: string; targets?: string[] } };
}>(stdout);
expect(data.intent?.modelPreference?.mode).toBe("comparison");
expect(data.intent?.modelPreference?.targets?.length).toBeGreaterThanOrEqual(2);
}, 60_000);
test("excludes 偏好 — 排除模型时 intent 识别出 modelPreference", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--dry-run",
"--message",
"不要qwen推荐一个适合文本生成的模型",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
intent?: {
modelPreference?: { mode?: string; excludes?: string[]; targets?: string[] };
};
}>(stdout);
const pref = data.intent?.modelPreference;
expect(pref).toBeDefined();
const hasExcludes =
(pref?.excludes?.length ?? 0) > 0 ||
(pref?.mode !== "unconstrained" && pref?.mode !== undefined);
expect(hasExcludes).toBe(true);
}, 60_000);
// ---- 模型偏好:反例 ----
test("无偏好 — 普通需求查询时 intent 不含 modelPreference 或 mode=unconstrained", async () => {
const { stdout, stderr, exitCode } = await runCli([
"advisor",
"recommend",
"--dry-run",
"--message",
"我要做一个能理解图片的客服机器人",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
intent?: { modelPreference?: { mode?: string } };
}>(stdout);
const mode = data.intent?.modelPreference?.mode;
expect(mode === undefined || mode === "unconstrained").toBe(true);
}, 60_000);
});
-178
View File
@@ -1,178 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts";
/**
* Auth 相关 E2E只验证 CLI 进程能正常解析参数并退出。
*/
describe("e2e: auth", () => {
test("auth 分组展示子命令帮助且退出码为 0", async () => {
const { stdout, stderr, exitCode } = await runCli(["auth"]);
expect(exitCode, stderr).toBe(0);
const out = `${stdout}\n${stderr}`;
expect(out).toMatch(/auth|Authentication|login|logout|status/i);
});
test("auth login --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["auth", "login", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/login|api-key/i);
});
test("auth logout --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["auth", "logout", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/logout|dry-run|yes/i);
});
test("auth status --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["auth", "status", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/status|output/i);
});
test("auth login 缺少 --api-key 时打印子命令帮助并退出 (0)", async () => {
const { stderr, exitCode } = await runCli(["auth", "login", "--non-interactive"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--api-key|Usage:/i);
});
test("auth login --dry-run --api-key 不发起校验与落盘", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"login",
"--dry-run",
"--api-key",
"sk-e2e-dry-run-placeholder",
"--non-interactive",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would validate and save API key.");
});
test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"login",
"--dry-run",
"--api-key",
"sk-e2e-dry-run-placeholder",
"--non-interactive",
"--output",
"json",
"--timeout",
"120",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would validate and save API key.");
});
test("auth login 缺少密钥且 --output json 时仍打印子命令帮助 (0)", async () => {
const { stderr, exitCode } = await runCli([
"auth",
"login",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode).toBe(0);
expect(stderr).toMatch(/--api-key|Usage:/i);
});
test("auth logout --dry-run 不写入配置", async () => {
const { stdout, stderr, exitCode } = await runCli(["auth", "logout", "--dry-run"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
expect(stderr).not.toContain("Cleared api_key");
});
test("auth logout --dry-run --yes --non-interactive", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"logout",
"--dry-run",
"--yes",
"--non-interactive",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
expect(stderr).not.toContain("Cleared api_key");
});
test("auth logout --dry-run --quiet --no-color", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"logout",
"--dry-run",
"--quiet",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
});
test("auth logout --dry-run --output json不清除密钥", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"logout",
"--dry-run",
"--output",
"json",
"--non-interactive",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
expect(stderr).not.toContain("Cleared api_key");
});
test.skipIf(!isDashScopeE2EReady())("auth status 文本输出", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"status",
"--non-interactive",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(
/Authentication Status|API key:|Console token:|DashScope API:|Console gateway:/,
);
});
test.skipIf(!isDashScopeE2EReady())("auth status --output json", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"status",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
authenticated?: boolean;
api_key?: { configured?: boolean };
dashscope_commands?: { method?: string };
}>(stdout);
expect(data.authenticated).toBe(true);
expect(data.api_key?.configured).toBe(true);
expect(data.dashscope_commands?.method).toBeDefined();
});
test.skipIf(!isDashScopeE2EReady())("auth status --output json --quiet --region cn", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"status",
"--non-interactive",
"--output",
"json",
"--quiet",
"--region",
"cn",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ authenticated?: boolean; dashscope_commands?: unknown }>(stdout);
expect(data.authenticated).toBe(true);
expect(data.dashscope_commands).toBeDefined();
});
});
@@ -0,0 +1,134 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures", "command-pack");
let configDir: string;
function env(): NodeJS.ProcessEnv {
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
}
describe("e2e: Command Pack", () => {
beforeAll(async () => {
configDir = await mkdtemp(join(tmpdir(), "bl-command-pack-e2e-"));
});
afterAll(async () => {
await rm(configDir, { recursive: true, force: true });
});
test("plugin 分组和管理命令 help 正常", async () => {
const group = await runCli(["plugin"], env());
expect(group.exitCode, group.stderr).toBe(0);
expect(group.stderr).toContain("plugin install");
expect(group.stderr).toContain("plugin link");
expect(group.stderr).toContain("plugin list");
expect(group.stderr).toContain("plugin remove");
const install = await runCli(["plugin", "install", "--help"], env());
expect(install.exitCode, install.stderr).toBe(0);
expect(install.stderr).toContain("--package");
});
test("未 link 时插件命令不存在", async () => {
const result = await runCli(["agent", "ping", "--message", "before"], env());
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Unknown command/i);
});
test("link 后命令进入原生 registry/help/执行链路", async () => {
const linked = await runCli(
["plugin", "link", "--path", fixtureRoot, "--output", "json"],
env(),
);
expect(linked.exitCode, linked.stderr).toBe(0);
const linkedJson = parseStdoutJson<{ linked: { name: string; commands: string[] } }>(
linked.stdout,
);
expect(linkedJson.linked.name).toBe("@ali/bailian-plugin-agent");
expect(linkedJson.linked.commands).toEqual([
"agent credential",
"agent credential-denied",
"agent fail",
"agent output",
"agent ping",
]);
const rootHelp = await runCli(["--help"], env());
expect(rootHelp.exitCode, rootHelp.stderr).toBe(0);
expect(rootHelp.stderr).toContain("agent ping");
const commandHelp = await runCli(["agent", "ping", "--help"], env());
expect(commandHelp.exitCode, commandHelp.stderr).toBe(0);
expect(commandHelp.stderr).toContain("Ping the Command Pack fixture");
expect(commandHelp.stderr).toContain("--message");
const executed = await runCli(["agent", "ping", "--message", "hello"], env());
expect(executed.exitCode, executed.stderr).toBe(0);
expect(executed.stdout).toContain("command-pack:hello");
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
expect(credential.exitCode, credential.stderr).toBe(0);
expect(credential.stdout).toContain("credential-source:flag");
const denied = await runCli(["agent", "credential-denied"], {
...env(),
DASHSCOPE_API_KEY: "fixture-key",
});
expect(denied.exitCode).toBe(1);
expect(denied.stderr).toContain('must declare auth="apiKey"');
const outputText = await runCli(["agent", "output"], env());
expect(outputText.exitCode, outputText.stderr).toBe(0);
expect(outputText.stdout).toBe("command-pack-output\n");
const outputJson = await runCli(["agent", "output", "--output", "json"], env());
expect(outputJson.exitCode, outputJson.stderr).toBe(0);
expect(parseStdoutJson(outputJson.stdout)).toEqual({ source: "command-pack", ok: true });
const failed = await runCli(["agent", "fail", "--output", "text"], env());
expect(failed.exitCode).toBe(2);
expect(failed.stderr).toContain("Command Pack fixture usage error.");
expect(failed.stderr).toContain("Use agent fail only in tests.");
});
test("plugin list 输出加载状态", async () => {
const result = await runCli(["plugin", "list", "--output", "json"], env());
expect(result.exitCode, result.stderr).toBe(0);
const json = parseStdoutJson<{
command_packs: Array<{ name: string; status: string; commands: string[] }>;
}>(result.stdout);
expect(json.command_packs).toEqual([
expect.objectContaining({
name: "@ali/bailian-plugin-agent",
status: "loaded",
commands: [
"agent credential",
"agent credential-denied",
"agent fail",
"agent output",
"agent ping",
],
}),
]);
});
test("remove 后命令从下一进程消失", async () => {
const removed = await runCli(
["plugin", "remove", "--name", "@ali/bailian-plugin-agent", "--output", "json"],
env(),
);
expect(removed.exitCode, removed.stderr).toBe(0);
expect(parseStdoutJson<{ removed: string }>(removed.stdout).removed).toBe(
"@ali/bailian-plugin-agent",
);
const result = await runCli(["agent", "ping", "--message", "after"], env());
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/Unknown command/i);
});
});
@@ -0,0 +1,164 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
function withTempConfigDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
const dir = mkdtempSync(join(tmpdir(), "bl-config-profile-"));
return fn(dir).finally(() => {
rmSync(dir, { recursive: true, force: true });
});
}
function writeConfig(dir: string, data: Record<string, unknown>): void {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "config.json"), JSON.stringify(data, null, 2) + "\n");
}
describe("e2e: named config", () => {
test("根帮助展示 --config 全局标志", async () => {
const { stderr, exitCode } = await runCli(["--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--config <name>/);
});
test("config set --config 写入命名 block 且不影响默认配置", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, { output: "text", api_key: "sk-default" });
const setResult = await runCli(
[
"config",
"set",
"--config",
"dev",
"--key",
"output",
"--value",
"json",
"--output",
"json",
],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(setResult.exitCode, setResult.stderr).toBe(0);
const setData = parseStdoutJson<{
output?: string;
config?: string;
config_file?: string;
}>(setResult.stdout);
expect(setData.output).toBe("json");
expect(setData.config).toBe("dev");
expect(setData.config_file).toBe(join(dir, "config.json"));
const raw = JSON.parse(readFileSync(join(dir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(raw.output).toBe("text");
expect((raw.dev as Record<string, unknown>).output).toBe("json");
});
});
test("config show --config 只展示命名 block", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, {
output: "text",
api_key: "sk-default",
dev: { output: "json", access_token: "tok-dev" },
});
const { stdout, stderr, exitCode } = await runCli(
["config", "show", "--config", "dev", "--output", "json"],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<Record<string, unknown>>(stdout);
expect(data.config).toBe("dev");
expect(data.config_file).toBe(join(dir, "config.json"));
expect(data.output).toBe("json");
expect(data.access_token).toBeDefined();
expect(data.api_key).toBeUndefined();
});
});
test("auth status --config 不继承默认凭证", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, { api_key: "sk-default", dev: { output: "json" } });
const devStatus = await runCli(["auth", "status", "--config", "dev", "--output", "json"], {
BAILIAN_CONFIG_DIR: dir,
DASHSCOPE_API_KEY: "",
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
});
expect(devStatus.exitCode, devStatus.stderr).toBe(0);
const devData = parseStdoutJson<Record<string, unknown>>(devStatus.stdout);
expect(devData.authenticated).toBe(false);
expect(devData.config).toBe("dev");
const defaultStatus = await runCli(["auth", "status", "--output", "json"], {
BAILIAN_CONFIG_DIR: dir,
DASHSCOPE_API_KEY: "",
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
});
expect(defaultStatus.exitCode, defaultStatus.stderr).toBe(0);
const defaultData = parseStdoutJson<Record<string, unknown>>(defaultStatus.stdout);
expect(defaultData.authenticated).toBe(true);
expect(defaultData.config).toBe("default");
});
});
test("--config default 等价默认配置", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, {
active_config: "token-plan",
output: "json",
api_key: "sk-default",
"token-plan": { output: "text", api_key: "sk-token" },
});
const { stdout, stderr, exitCode } = await runCli(
["config", "show", "--config", "default", "--output", "json"],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<Record<string, unknown>>(stdout);
expect(data.config).toBe("default");
expect(data.active).toBeUndefined();
expect(data.api_key).toBeDefined();
const raw = JSON.parse(readFileSync(join(dir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(raw.active_config).toBe("token-plan");
});
});
test("非法 --config 名称报 usage error", async () => {
const { stderr, exitCode } = await runCli(["auth", "status", "--config", "../evil"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid config name/);
});
test("auth status 文本输出分行展示选中 Config 和配置文件", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, {
active_config: "token-plan",
"token-plan": { api_key: "sk-token" },
});
const result = await runCli(["auth", "status", "--output", "text"], {
BAILIAN_CONFIG_DIR: dir,
DASHSCOPE_API_KEY: "",
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
});
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toContain("Config: token-plan\n");
expect(result.stdout).toContain(`Config file: ${join(dir, "config.json")}\n`);
expect(result.stdout).not.toContain("Active config:");
});
});
});
-207
View File
@@ -1,207 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
/**
* Config 相关 E2E
*/
describe("e2e: config", () => {
test("config 分组展示子命令帮助且成功退出", async () => {
const { stdout, stderr, exitCode } = await runCli(["config"]);
expect(exitCode, stderr).toBe(0);
const out = `${stdout}\n${stderr}`;
expect(out).toMatch(/config|show|set|export-schema/i);
});
test("config show --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["config", "show", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/show|config/i);
});
test("config set --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["config", "set", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/set|--key|--value/i);
});
test("config export-schema --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["config", "export-schema", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/export-schema|--command/i);
});
test("config show --output json", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"show",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
region?: string;
config_file?: string;
base_url?: string;
timeout?: number;
}>(stdout);
expect(data.region).toBeDefined();
expect(data.config_file).toBeDefined();
expect(data.base_url).toBeDefined();
expect(data.timeout).toBeDefined();
});
test("config show --output text --no-color", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"show",
"--non-interactive",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(/region|config_file|timeout|base_url/i);
});
test("config set 缺少 --key / --value 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli(["config", "set", "--non-interactive"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--key|--value|required/i);
});
test("config set 非法 key 时退出为用法错误", async () => {
const { stderr, exitCode } = await runCli([
"config",
"set",
"--non-interactive",
"--key",
"not-a-real-key",
"--value",
"x",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid config key|not-a-real-key/i);
});
test("config set 非法 region", async () => {
const { stderr, exitCode } = await runCli([
"config",
"set",
"--non-interactive",
"--key",
"region",
"--value",
"invalid-region",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid region|cn, us, intl/i);
});
test("config set 非法 output", async () => {
const { stderr, exitCode } = await runCli([
"config",
"set",
"--non-interactive",
"--key",
"output",
"--value",
"yaml",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid output|text, json/i);
});
test("config set 非法 timeout", async () => {
const { stderr, exitCode } = await runCli([
"config",
"set",
"--non-interactive",
"--key",
"timeout",
"--value",
"0",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid timeout|positive/i);
});
test("config set --dry-run 不落盘(仅输出 would_set", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"set",
"--dry-run",
"--non-interactive",
"--key",
"output",
"--value",
"json",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_set?: { output?: string } }>(stdout);
expect(data.would_set?.output).toBe("json");
});
test("config set --dry-run 支持连字符别名 key", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"set",
"--dry-run",
"--non-interactive",
"--key",
"default-text-model",
"--value",
"qwen3.7-max",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_set?: { default_text_model?: string } }>(stdout);
expect(data.would_set?.default_text_model).toBe("qwen3.7-max");
});
test("config export-schema --command 导出单条工具 JSON", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"export-schema",
"--command",
"text chat",
"--non-interactive",
]);
expect(exitCode, stderr).toBe(0);
const schema = parseStdoutJson<{ name?: string; input_schema?: { type?: string } }>(stdout);
expect(schema.name).toMatch(/bailian_text_chat/);
expect(schema.input_schema?.type).toBe("object");
});
test("config export-schema 不存在的子命令时报错", async () => {
const { stderr, exitCode } = await runCli([
"config",
"export-schema",
"--command",
"this-command-does-not-exist-xyz",
"--non-interactive",
"--output",
"json",
]);
expect(exitCode).toBe(2);
const err = JSON.parse(stderr.trim()) as { error?: { message?: string } };
expect(err.error?.message).toMatch(/not found/i);
});
test("config export-schema 导出全部为 JSON 数组", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"export-schema",
"--non-interactive",
]);
expect(exitCode, stderr).toBe(0);
const arr = parseStdoutJson<Array<{ name?: string }>>(stdout);
expect(Array.isArray(arr)).toBe(true);
expect(arr.length).toBeGreaterThan(0);
expect(arr[0]?.name).toMatch(/^bailian_/);
});
});

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