mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# 鉴权扩展
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 增加新的鉴权方式(OAuth、SSO、控制台回调登录)
|
||||
- 增加新的 token 来源(env / config / flag / 文件)
|
||||
- 调整凭证解析优先级
|
||||
- 改 `bl auth login` 流程
|
||||
|
||||
## 鉴权链路
|
||||
|
||||
```
|
||||
flag 优先 ─→ config 文件 ─→ env var
|
||||
│ │ │
|
||||
└──── resolveCredential() (core) ───┐
|
||||
│
|
||||
▼
|
||||
cli/utils/ensure-key.ts (启动时拦)
|
||||
命令注入 Authorization 头
|
||||
```
|
||||
|
||||
凭证类型(`AuthMethod`):
|
||||
|
||||
- `api-key` — DashScope SK(`sk-...`),走 Bearer 头
|
||||
- `access-token` — 控制台 OAuth 回调拿到的临时 token,走 Bearer + 不同 endpoint
|
||||
- `ak/sk` — Alibaba Cloud 标准 AK/SK,走 ROA 签名(只用于知识库)
|
||||
|
||||
### 双凭证并存(API Key + Console)
|
||||
|
||||
`~/.bailian/config.json` 可同时保存 `api_key` 与 `access_token`。**登录任一种方式不得删除另一种**(`bl auth login --api-key` / `--console` 只更新对应字段)。
|
||||
|
||||
解析分工:
|
||||
|
||||
- `resolveCredential()` — DashScope API 命令(`text chat`、`file upload` 等);config 里两者都有时 **优先 `api_key`**
|
||||
- `resolveConsoleGatewayCredential()` — 控制台网关(`app list`、`usage free`、`console call`);**只用** env/file 的 `access_token`,忽略 `api_key`
|
||||
|
||||
必改调用点: 凡 `callConsoleGateway` 必须用 `resolveConsoleGatewayCredential`,不能误用 `resolveCredential`(否则 config 仅有 api_key 时会拿 sk- 打网关)。
|
||||
|
||||
`bl auth logout --console` 只清 `access_token`;全量 `bl auth logout` 清两者。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. core 层(类型 + 解析)
|
||||
|
||||
- [ ] `packages/core/src/auth/types.ts`:
|
||||
- 新增 `AuthMethod` 字面量
|
||||
- 新增 `ResolvedCredential` 字段(如 token 类型 / 过期时间)
|
||||
- [ ] `packages/core/src/auth/resolver.ts`:
|
||||
- `resolveCredential()` 增加新分支
|
||||
- 控制台网关命令用 `resolveConsoleGatewayCredential()`(与 DashScope 解析分离)
|
||||
- 优先级注释保持清晰(数字标号)
|
||||
- [ ] `packages/core/src/auth/credentials.ts`:
|
||||
- 如果新方式需要持久化,加 `save*` / `load*` / `clear*`
|
||||
- [ ] `packages/core/src/config/schema.ts`:
|
||||
- `Config` 接口加新字段(如 `fileAccessToken`、`accessTokenEnv`)
|
||||
- `ConfigFile` 接口加对应 disk 字段(snake_case)
|
||||
- [ ] `packages/core/src/config/loader.ts`:
|
||||
- `loadConfig()` 把 env / 文件读到 Config 上
|
||||
|
||||
### B. core 客户端
|
||||
|
||||
- [ ] `packages/core/src/client/http.ts`:
|
||||
- 不同 `credential.method` 走不同分支(参考已有 `access-token` 分支走 console gateway)
|
||||
- Authorization 头注入正确
|
||||
|
||||
### C. cli 层
|
||||
|
||||
- [ ] `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
|
||||
|
||||
### 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. 用户面文档
|
||||
|
||||
- [ ] `README.md` / `README_CN.md` "Authentication" 段落
|
||||
|
||||
### G. 测试
|
||||
|
||||
- [ ] `packages/cli/tests/e2e/auth.e2e.test.ts` 增加新方式的 happy / failure 路径
|
||||
- [ ] mask token 的输出格式不变(避免泄漏)
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 各种凭证组合
|
||||
unset DASHSCOPE_API_KEY DASHSCOPE_ACCESS_TOKEN
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts auth status
|
||||
|
||||
# flag 注入
|
||||
node packages/cli/src/main.ts auth status --api-key sk-xxx
|
||||
|
||||
# env 注入
|
||||
DASHSCOPE_ACCESS_TOKEN=xxx node packages/cli/src/main.ts auth status
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 加了新 token 来源但忘了改 `resolveCredential` 优先级,实际不生效
|
||||
- ✗ `Config` 加字段但 `loadConfig` 没读 → 字段永远 undefined
|
||||
- ✗ `bl auth login` 写成功但 `bl auth status` 不识别(两边走的 storage path 不一致)
|
||||
- ✗ token mask 显示完整 token,日志泄漏
|
||||
@@ -0,0 +1,104 @@
|
||||
# 分支合并 Review
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 评估某分支(feature / pipeline / 重构分支)能否合到 `main`
|
||||
- 评估合并后对原有功能的侵入性影响
|
||||
- 用户问"X 分支可以合 Y 吗 / 有什么影响"
|
||||
|
||||
## 目标
|
||||
|
||||
- **不破坏原功能**:共享文件的运行时行为、公共类型、构建配置不能静默变化
|
||||
- **新功能可发现**:用户可见的新命令/新 flag 必须有文档和示例
|
||||
|
||||
## 步骤(按顺序)
|
||||
|
||||
### ① 看分歧
|
||||
|
||||
```sh
|
||||
git fetch origin <base>
|
||||
git log --oneline <base>..<head> # head 比 base 多的提交
|
||||
git log --oneline <head>..<base> # base 比 head 多的提交(双向都看,base 已大幅领先时尤其重要)
|
||||
```
|
||||
|
||||
### ② 干跑合并,先确认有无冲突
|
||||
|
||||
```sh
|
||||
git merge-tree $(git merge-base <base> <head>) <base> <head> > /tmp/merge.txt
|
||||
echo "exit=$?"
|
||||
grep -E "^(<<<<<<<|>>>>>>>|CONFLICT)" /tmp/merge.txt | head -20
|
||||
```
|
||||
|
||||
- exit=0 且无 `<<<<<<<` → 机器可合,继续 ③
|
||||
- 有冲突 → 先列冲突文件,把方案讲清楚再动手
|
||||
|
||||
### ③ 拆 diff:共享文件 vs 新增文件
|
||||
|
||||
```sh
|
||||
git diff --stat <base>...<head>
|
||||
git diff --name-only <base>...<head>
|
||||
```
|
||||
|
||||
- **新增文件**(对方分支没有)→ 侵入性 = 0,只看是否需要文档透出(跳到清单 B)
|
||||
- **共享文件**(两边都有)→ 重点看,逐个跑 `git diff <base>...<head> -- <file>`,过清单 A
|
||||
|
||||
## 清单 A:侵入性(共享文件必看)
|
||||
|
||||
- [ ] **运行时行为没静默变化**:默认值、错误码 / `ExitCode`、retry 次数、并发度、超时
|
||||
- [ ] **公共类型 / 导出签名向后兼容**:新增可选字段 OK;改必填、删字段、改返回类型 → 不行(参考 [packages/core/src/types/](packages/core/src/types/))
|
||||
- [ ] **`pnpm-workspace.yaml` 没收窄通配**:`packages/*` 改成显式列表会漏掉目标分支新增的子包(本次 pipeline → main 踩过这个坑,漏了 `packages/skills`)
|
||||
- [ ] **`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` 三个全局表新增项不和现有项冲突
|
||||
|
||||
## 清单 B:用户透出(用户可见的新东西必看)
|
||||
|
||||
- [ ] **新命令 / 新 flag** 已同步到用户面文档:
|
||||
- [README.md](README.md) + [README_CN.md](README_CN.md)(中英文都要,常漏 `_CN`)
|
||||
- (SKILL.md 已迁出本仓库,由 `npx add skills` 机制独立维护,不在本仓库 review 范围)
|
||||
- [ ] **`bl <cmd> --help`** 文案完整:`description` / `examples` / `apiDocs` 都填了
|
||||
- [ ] **demo / quickstart**:用户可调用的新命令至少有一个示例(参考 [packages/cli/scene/](packages/cli/scene/) 的组织方式)
|
||||
- [ ] **行为变化的老命令**:在 commit message / CHANGELOG 注明用户感知的差异
|
||||
- [ ] **错误信息 / 提示文案**:面向用户的字符串通顺、双语(项目主体是中文场景)
|
||||
|
||||
## 清单 C:容易漏的(每条一行扫一眼)
|
||||
|
||||
- [ ] **改了文件但没补测试**:`git diff --stat <base>...<head> -- '*test*' '*spec*'` 与改动文件清单对照
|
||||
- [ ] **新功能埋点同步**:遥测事件名 + 参数 allowlist(参考 main 上的 `feat(telemetry): track console gateway api name in params allowlist` commit)
|
||||
- [ ] **环境变量**:新增 / 重命名的 env var 进 README,旧的有没有兼容
|
||||
- [ ] **i18n**:`README.md` 改了,`README_CN.md` 同步了吗
|
||||
|
||||
## 输出报告(照模板填)
|
||||
|
||||
```
|
||||
冲突: 无 / 有 → <文件列表>
|
||||
必须修(合并前在 head 分支上 commit 掉):
|
||||
- <清单项> + <文件:行号> + <一句话原因>
|
||||
↑ 只放真正"head 分支自己写错了"的项,例如 pnpm-workspace.yaml 收窄、version 倒退、
|
||||
删了不该删的字段等。这些 fix 应该作为 head 分支上的新 commit,而不是合并解冲突时顺手处理。
|
||||
解冲突要点(merge 时不要漏):
|
||||
- <冲突文件> + <字段/段落> + <怎么取舍>
|
||||
↑ 放"合并那一刻才会出现"的细节,例如 package.json 的 files/scripts/devDependencies 各取并集、
|
||||
NO_AUTH_SETUP 这种全局表两边都加项时不要丢一侧、pnpm-lock.yaml 直接 rm 后 pnpm install 重生等。
|
||||
建议修(可后置):
|
||||
- ...
|
||||
仅信息(无需动作,告知即可):
|
||||
- ...
|
||||
合并姿势:
|
||||
1. 在 head 分支上修上面"必须修"的项,提 commit
|
||||
2. 合并 main,按"解冲突要点"逐项处理冲突
|
||||
3. <pnpm install / 测试 / 构建命令>
|
||||
4. 提 MR 合 main
|
||||
```
|
||||
|
||||
## 常见漏点(基于历史踩坑)
|
||||
|
||||
| 漏点 | 后果 |
|
||||
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| `pnpm-workspace.yaml` 把 `packages/*` 收窄成显式列表 | 合并后目标分支的新子包不再被 workspace 识别,`pnpm install` 看似正常但子包失联 |
|
||||
| 源分支 version 比目标分支低,直接 merge 覆盖 | npm 上版本号回退,latest tag 错乱 |
|
||||
| `registry.ts` 注册新命令但忘了 [README](README.md) / [README_CN](README_CN.md) | 用户完全感知不到新功能 |
|
||||
| 共享 util 重构(抽公共函数)只改了一处调用方 | 其它调用方静默走旧分支,行为分裂 |
|
||||
| `NO_AUTH_SETUP` 加了不该免登录的命令 | 安全风险,用户没登录也能调付费 API |
|
||||
| `NO_AUTH_SETUP` / `registry.ts` 这类全局表两边都加项,解冲突时被合掉一侧 | 某个命令突然要求登录 / 某个新命令注册丢失,编译能过、回归不易察觉 |
|
||||
@@ -0,0 +1,186 @@
|
||||
# 撰写 Change Log
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 发布新版本(beta / rc / stable)后,需要为这个版本写一份 release notes
|
||||
- 补写历史版本遗漏的 changelog
|
||||
|
||||
## 发布位置
|
||||
|
||||
更新仓库内的两份文件,英文优先,中文同步:
|
||||
|
||||
- [`CHANGELOG.md`](../../CHANGELOG.md)
|
||||
- [`CHANGELOG_CN.md`](../../CHANGELOG_CN.md)
|
||||
|
||||
新版本条目插在文件顶部"## [X.Y.Z] - YYYY-MM-DD"位置,旧版本依次向下保留。两份文件保持一一对应——任何条目只在一份里出现,另一份漏写,视为错误。
|
||||
|
||||
## 输出格式:Keep a Changelog
|
||||
|
||||
本项目采用 [Keep a Changelog](https://keepachangelog.com/) 规范,标准 6 段(按出现顺序):
|
||||
|
||||
| 段名 | 含义 | 示例 |
|
||||
| ------------ | ----------------------------- | --------------------------------- |
|
||||
| `Added` | 新增功能 / 命令 / 参数 | 新增 `bl app list` |
|
||||
| `Changed` | 已有功能行为/形式变化(非破坏) | `--page-num` 改名 `--page` |
|
||||
| `Deprecated` | 即将移除但当前仍可用 | `--legacy-flag` 弃用,下个版本移除 |
|
||||
| `Removed` | 本版本删除的能力 | 移除 `--instructions` |
|
||||
| `Fixed` | Bug 修复 | 修复视频生成模型错误 |
|
||||
| `Security` | 安全相关修复 | 修复凭据明文落盘 |
|
||||
|
||||
项目扩展段:`Internal`(放研发体系 / 工具链 / 测试基建,与用户无关但值得记录的工程改动)。
|
||||
|
||||
某段没内容就**直接省略**,不要写"无"。
|
||||
|
||||
## 撰写步骤
|
||||
|
||||
### 1. 确定版本边界
|
||||
|
||||
通过 `package.json` 的版本号变更找出两个版本的 commit 边界:
|
||||
|
||||
```sh
|
||||
# 找出 packages/cli/package.json 历次版本变更
|
||||
git log --all --oneline --pretty=format:'%h %ad %s' --date=short \
|
||||
-G '"version"' -- packages/cli/package.json | head
|
||||
```
|
||||
|
||||
记下:
|
||||
|
||||
- `prevBumpCommit` — 上一个版本号 bump commit(如 `1.0.0` 的 commit)
|
||||
- `currBumpCommit` — 当前版本号 bump commit(如 `1.0.1` 的 commit)
|
||||
|
||||
本版本内容 = `git log prevBumpCommit..currBumpCommit`。
|
||||
|
||||
### 2. 列出本版本所有 commit
|
||||
|
||||
```sh
|
||||
git log <prevBumpCommit>..<currBumpCommit> --no-merges \
|
||||
--pretty=format:'%h %ad %s' --date=short
|
||||
```
|
||||
|
||||
> 注意:用 `--no-merges` 过滤 merge commit,避免重复条目。
|
||||
|
||||
### 3. 逐条核对 commit 是否真的进了本版本
|
||||
|
||||
merge 顺序复杂时,commit 标题在但内容未必合入。用 `git merge-base --is-ancestor` 严格验证:
|
||||
|
||||
```sh
|
||||
git merge-base --is-ancestor <featureCommit> <releaseCommit> \
|
||||
&& echo "IN" || echo "NOT IN"
|
||||
```
|
||||
|
||||
例:验证 `agent chat`(commit `12f2b1b`)是否在 `1.0.0`(commit `3fc54ae`)里:
|
||||
|
||||
```sh
|
||||
git merge-base --is-ancestor 12f2b1b 3fc54ae && echo "IN" || echo "NOT IN"
|
||||
```
|
||||
|
||||
**不要凭 commit 标题猜**——分支模型常导致一个功能开发完成但未合入当前 release。
|
||||
|
||||
### 4. 在 release commit 上抽样校验代码真实存在
|
||||
|
||||
光看 commit 还不够,要确认目标功能的代码 / 文件在 release commit 上真的存在:
|
||||
|
||||
```sh
|
||||
# 列出 release commit 下某目录的文件
|
||||
git ls-tree -r <releaseCommit> --name-only -- packages/cli/src/commands/
|
||||
|
||||
# 看 release commit 下某文件的内容
|
||||
git show <releaseCommit>:packages/cli/src/commands/console/call.ts | head
|
||||
```
|
||||
|
||||
特别注意被一行带过的"杂项" commit。本仓库历史踩过坑:`feat(cli): enhance output options and add new commands` 这种标题里藏了**新命令** + **新输出格式** + **logout 增强**三件事,粗看会全部漏掉。
|
||||
|
||||
```sh
|
||||
# 看整个 commit 改了哪些文件、新增了多少行
|
||||
git show <commit> --stat
|
||||
```
|
||||
|
||||
只要 `--stat` 里出现新文件或大块新增,就值得展开看。
|
||||
|
||||
### 5. 区分 Added vs Changed
|
||||
|
||||
- **Added**:新文件、新命令、新参数、新输出格式 → 用户能"用上一个新东西"
|
||||
- **Changed**:已有功能改名、改默认值、改交互文案、性能优化、参数命名统一 → 用户"原来就在用的东西变样了"
|
||||
|
||||
判断方法:在 `prevBumpCommit` 上 `git show <prevBumpCommit>:<file>` 看这个文件 / 函数原来在不在。
|
||||
|
||||
### 6. 排除"不该写进去"的内容
|
||||
|
||||
| 不要写 | 原因 |
|
||||
| --------------------------------------------------- | ---------------------------------- |
|
||||
| 未发布 / 仅在分支上的功能 | release commit 不包含 → 用户拿不到 |
|
||||
| 仅在文档 / 设计稿层面的能力 | 没代码就没"功能",不能写进 Added |
|
||||
| 内部包 / 私有工具的开发过程 commit | 用户不可见 |
|
||||
| 仓库内 file path / 模块路径(如 `core/telemetry/`) | 用户感知不到内部结构 |
|
||||
| 临时调试 commit(remove console / 补 TODO 等) | 噪音 |
|
||||
|
||||
### 7. 标记 Breaking Change
|
||||
|
||||
- 单条:在条目末尾加 `**(BREAKING)**` 或行内提示
|
||||
- 多条:版本头单独开 `### Breaking Changes` 段(major 升级才允许)
|
||||
|
||||
判断标准:用户**已有的命令 / 脚本 / 集成会因为升级而坏掉**。常见来源:
|
||||
|
||||
- 命令重命名 / 删除
|
||||
- 参数重命名 / 删除
|
||||
- 默认值变化导致输出格式变化
|
||||
- 包结构 / import 路径变化(monorepo 拆包)
|
||||
- 配置文件 schema 不兼容
|
||||
|
||||
### 8. 写完后给用户过一遍再写入文件
|
||||
|
||||
**不要直接编辑 `CHANGELOG.md` / `CHANGELOG_CN.md`**。先把中英两份草稿都贴回对话里,让用户:
|
||||
|
||||
- 增删条目
|
||||
- 调整措辞(中英、术语)
|
||||
- 确认 BREAKING 标记
|
||||
- 确认版本边界假设是否成立
|
||||
|
||||
用户确认后,把新版本块插入两个文件顶部(在 H1 标题与上一个版本块之间)。
|
||||
|
||||
## 模板
|
||||
|
||||
```markdown
|
||||
## [X.Y.Z] - YYYY-MM-DD
|
||||
|
||||
> 一句话概括本版本主线(可省略,大版本/含 breaking 时建议加)
|
||||
|
||||
### Added
|
||||
|
||||
- **<能力名>**:一句话描述用户能做什么
|
||||
- 子项 1
|
||||
- 子项 2
|
||||
|
||||
### Changed
|
||||
|
||||
- **<点名>(BREAKING)**:变化前 → 变化后,影响范围
|
||||
|
||||
### Fixed
|
||||
|
||||
- 修复 X 在 Y 场景下的问题
|
||||
|
||||
### Internal
|
||||
|
||||
- 工程类改动(不影响用户行为)
|
||||
```
|
||||
|
||||
## 常见漏点(基于真实踩坑)
|
||||
|
||||
| 漏点 | 后果 |
|
||||
| ------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 只看 commit 标题不看 `--stat` | "enhance output options" 这种笼统标题里藏的新命令被漏掉 |
|
||||
| 凭 commit 标题判断是否进了 release | 分支没合入,标题在但代码不在 |
|
||||
| 把分支上 WIP 当作已发布功能 | 用户升级后找不到对应能力,被投诉 |
|
||||
| 把内部文件路径写进 changelog | 用户看不懂,且暴露内部结构 |
|
||||
| 优化类改动错放到 Added | 用户以为是新功能去找,找不到入口 |
|
||||
| 版本号 bump commit 自身的 README 改动算进上个版本 | 重复 / 错位 |
|
||||
| 中英两份不同步 | 文档可信度直接崩,等同于撒谎 |
|
||||
|
||||
## 与 release.md 的边界
|
||||
|
||||
| 文档 | 管什么 |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| [release.md](release.md) | 发版前自检:版本号 / 包内容 / 安全扫描 / publish 流程 |
|
||||
| 本文档 | 发版后写说明:面向用户的 release notes |
|
||||
|
||||
两者顺序:`release.md` → npm publish → 本文档(更新 `CHANGELOG.md` + `CHANGELOG_CN.md`)→ 推到 GitHub。
|
||||
@@ -0,0 +1,100 @@
|
||||
# CLI E2E 测试规范
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增/修改 `packages/cli/src` 下的 command(`commands/catalog.ts` 登记、`defineCommand` 实现、options/usage)
|
||||
- 新建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例
|
||||
- 为命令补 help / 缺参 / dry-run / 真实集成测试
|
||||
|
||||
以上情况必须同步维护 `packages/cli/tests/e2e/<topic>.e2e.test.ts`。跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。
|
||||
|
||||
## 文件与工具
|
||||
|
||||
- 路径:`packages/cli/tests/e2e/<kebab-topic>.e2e.test.ts`
|
||||
- 框架:`vite-plus/test`;子进程跑 CLI:`runCli` from `./helpers.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:分组 + --help,无密钥、无真实 API
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<group> 分组展示子命令帮助且成功退出", ...);
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成;原有集成用例放最后、勿改逻辑
|
||||
describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
test("缺少 --<flag> 时退出为用法错误 (2)", ...);
|
||||
test("<cmd> --dry-run ...", ...); // 若适用
|
||||
test("【model】真实流程", ..., LONG_TIMEOUT);
|
||||
});
|
||||
```
|
||||
|
||||
## skip 条件(helpers.ts)
|
||||
|
||||
| 场景 | 条件 |
|
||||
| ------------------- | ----------------------------------------------------- |
|
||||
| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` |
|
||||
| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` |
|
||||
| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` |
|
||||
| 知识库 | `isKnowledgeE2EReady()` |
|
||||
| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_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 块**末尾**
|
||||
|
||||
## 安全与例外
|
||||
|
||||
- **禁止真实破坏性操作**:`auth logout` 只用 `--dry-run`;`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` 更新 `tools/generated/reference/`(本仓库 gitignore)
|
||||
- [ ] 顶层:分组 help + 子命令 `--help`(多子命令则各一条 help)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `pnpm test packages/cli/tests/e2e/<file>` 通过
|
||||
|
||||
## 示例片段
|
||||
|
||||
```ts
|
||||
test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["foo", "bar", "--non-interactive"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--prompt|Missing required argument/i);
|
||||
});
|
||||
|
||||
test("foo bar --dry-run 仅输出计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"foo",
|
||||
"bar",
|
||||
"--dry-run",
|
||||
"--prompt",
|
||||
"x",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ request?: unknown }>(stdout);
|
||||
expect(data.request).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
## 与批量压测的关系
|
||||
|
||||
- **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)。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 命令增删改
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 增加新的 `bl xxx` 命令
|
||||
- 删除已有命令
|
||||
- 重命名命令(包括从单级 `bl x` 改成 `bl x y` 或反向)
|
||||
|
||||
## 命令路径与文件路径的对应规则
|
||||
|
||||
```
|
||||
单级命令(无 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 时合理(否则拍平到两级)
|
||||
```
|
||||
|
||||
文件路径与命令路径必须 1:1 对齐。
|
||||
|
||||
## CLI 命令注册架构(必读)
|
||||
|
||||
命令元数据以 **`catalog.ts` 为单一登记处**;`registry.ts` 只负责解析与打印 help,不再内嵌命令表或手写 Resources 列表。
|
||||
|
||||
```
|
||||
commands/<...>.ts defineCommand({ name, description, usage, options, examples, apiDocs?, run })
|
||||
↓
|
||||
commands/catalog.ts export const commands: Record<string, Command>
|
||||
↓
|
||||
┌────┴────┬──────────────────────┬─────────────────────┐
|
||||
↓ ↓ ↓ ↓
|
||||
registry.ts main.ts tools/generate-reference.ts export-schema.ts
|
||||
(解析/help) (入口) → tools/generated/reference/index.md + <group>.md
|
||||
```
|
||||
|
||||
- **`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`**: build 前读 `catalog.ts`,写 `tools/generated/reference/index.md`(索引) + `tools/generated/reference/<一级命令>.md`(详情,勿手改)。该目录被 gitignore,产物供未来的 `npx add skills` 安装机制消费
|
||||
|
||||
已删除、勿再引用:`commands/help.ts`、`registry.ts` 内联 `new CommandRegistry({...})`、`printRootHelp` 手写命令行。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### 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 读取)
|
||||
- [ ] 命令需在 `bl help` / `reference/` 展示 API 文档链接时,在 `defineCommand` 里设 `apiDocs`(相对路径);help 与 reference 均从此字段生成
|
||||
- [ ] 如果命令需要鉴权之外的特殊路径,看 `packages/cli/src/main.ts` 的 `NO_AUTH_SETUP`
|
||||
- [ ] **`config/export-schema.ts`**: 若新命令不适合作为 agent tool,评估是否加入 `SKIP_PREFIXES`;该文件在 `run()` 内 `import("../catalog.ts")`,勿顶层 import catalog 以免循环依赖
|
||||
|
||||
### B. 文档层
|
||||
|
||||
- [ ] 运行 `pnpm --filter bailian-cli run generate:reference`(或 `build`),刷新 `tools/generated/reference/` 下生成文件(本仓库 gitignore,仅供本地校验和未来 skill 安装机制消费)
|
||||
- [ ] `README.md` / `README_CN.md`: Quick Start、命令一览(用户向,与 help 对齐即可)
|
||||
- [ ] SKILL.md 已搬出本仓库(由 `npx add skills` 机制分发),本仓库不再维护
|
||||
|
||||
### C. 测试层
|
||||
|
||||
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/cli/tests/e2e/<topic>.e2e.test.ts`
|
||||
- [ ] 删除命令时一并删对应 e2e
|
||||
|
||||
### D. 重命名特殊处理
|
||||
|
||||
- [ ] 全仓 grep **旧命令名字符串**,确保以下位置全部更新:
|
||||
- `catalog.ts` 的 key
|
||||
- error hints(cli 层)
|
||||
- `tools/generated/reference/`(重建后检查;本仓库 gitignore)
|
||||
- README 示例
|
||||
- 测试断言
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
pnpm --filter bailian-cli run generate:reference # reference/ 与 catalog 一致
|
||||
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
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 只改了命令文件,忘了 **`catalog.ts`** → 命令不存在或 help 里没有
|
||||
- ✗ 手改 **`tools/generated/reference/*.md`** → 下次 build 被覆盖;应改 `defineCommand` 后重新 generate
|
||||
- ✗ 在 `export-schema.ts` 顶层 `import catalog` → 可能与 registry 循环依赖
|
||||
- ✗ 单 action 的子组是反模式,新增时优先拍平为两级
|
||||
@@ -0,0 +1,56 @@
|
||||
# 命令选项变更
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 给已有命令新增 `--flag <value>`
|
||||
- 改 flag 默认值
|
||||
- 删除 / 重命名已有 flag
|
||||
- 把 flag 从可选变成必填(或反向)
|
||||
|
||||
## 必查清单
|
||||
|
||||
### 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
|
||||
|
||||
### 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`
|
||||
|
||||
### C. 文档层
|
||||
|
||||
- [ ] `README.md` / `README_CN.md` 如果在示例里展示了相关命令,补充新 flag
|
||||
- [ ] 跑 `pnpm --filter bailian-cli run generate:reference`,让 `tools/generated/reference/` 与命令一致(本仓库 gitignore,勿手改;SKILL.md 已迁出本仓库)
|
||||
|
||||
### D. 测试层
|
||||
|
||||
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 在 `packages/cli/tests/e2e/<command>.e2e.test.ts` 增加新 flag 的断言(含缺参、`--help`、dry-run 若适用)
|
||||
- [ ] 删除 flag 时,清掉相关测试用例
|
||||
|
||||
### E. 重命名特殊处理
|
||||
|
||||
- [ ] 全仓 grep 旧 flag 名(包括 `--old-name`、`oldName`、`old_name` 三种形态,因为 args.ts 会做 kebab→camel 转换)
|
||||
- [ ] 必要时保留**deprecated alias**(老 flag 仍可用,但 stderr 警告 → 下版本删)
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
node packages/cli/src/main.ts <command> --help # 看新 flag 出现在 Options
|
||||
node packages/cli/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 的命令文件作为示例)
|
||||
@@ -0,0 +1,84 @@
|
||||
# 配置项扩展
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增 env var(如 `DASHSCOPE_*` / `BAILIAN_*` / `NO_COLOR`)
|
||||
- 给 `~/.bailian/config.json` 加字段
|
||||
- 给全局 flag 加新选项(`--xxx`)
|
||||
- 改 config 字段优先级
|
||||
|
||||
## 配置三层来源
|
||||
|
||||
```
|
||||
flag (--xxx) ─┐
|
||||
├─ loadConfig() 合并 ─→ Config(运行时单一对象)
|
||||
env (XXX=yyy) ─┤
|
||||
│
|
||||
config 文件 ─┘
|
||||
~/.bailian/config.json
|
||||
```
|
||||
|
||||
优先级一般是 **flag > env > config 文件 > 默认值**,具体见 `core/config/loader.ts`。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 类型定义
|
||||
|
||||
- [ ] `packages/core/src/config/schema.ts`:
|
||||
- `Config`(运行时形状)加新字段
|
||||
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
|
||||
- `parseConfigFile()` 解析新字段
|
||||
- 如果是 enum 字段,加校验
|
||||
|
||||
### B. 加载逻辑
|
||||
|
||||
- [ ] `packages/core/src/config/loader.ts:loadConfig()`:
|
||||
- 加新字段的合并逻辑(`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 正确解析
|
||||
- [ ] 改完全局 flag 后跑 `pnpm --filter bailian-cli run generate:reference`
|
||||
|
||||
### D. 命令使用方
|
||||
|
||||
- [ ] 用到新字段的命令文件直接读 `config.xxx`,不要重复解析
|
||||
- [ ] 配置展示 / 修改命令同步:
|
||||
- `packages/cli/src/commands/config/show.ts` 显示新字段
|
||||
- `packages/cli/src/commands/config/set.ts` 允许 set
|
||||
- `packages/cli/src/commands/config/export-schema.ts` 在 schema 输出里
|
||||
|
||||
### E. 文档
|
||||
|
||||
- [ ] `README.md` / `README_CN.md` 的 env var 表格
|
||||
|
||||
### F. 测试
|
||||
|
||||
- [ ] 单测覆盖优先级:flag > env > file
|
||||
- [ ] 校验失败抛错(非法值)
|
||||
- [ ] 默认值正确
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```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>
|
||||
|
||||
# 写到文件
|
||||
node packages/cli/src/main.ts config set --key <key> --value <value>
|
||||
cat ~/.bailian/config.json
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ `Config` 接口加字段但 `loadConfig` 没填,运行时永远 undefined
|
||||
- ✗ `ConfigFile` 用 camelCase 字段名(disk schema 应该是 snake_case)
|
||||
- ✗ 全局 flag 没标 `type: "boolean"`,被当成需要值的 `--xxx <value>`
|
||||
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
|
||||
- ✗ `config show` 不显示新字段,用户改了无法回查
|
||||
@@ -0,0 +1,125 @@
|
||||
# 错误文案变更
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 修改 `BailianError` 的 message 或 hint
|
||||
- 调整 cli 的 hint 增强逻辑(`enhanceHint`)
|
||||
- 改 ensure-key 的 setup 流程文案
|
||||
- 改任何抛错位置的分类(exitCode)
|
||||
|
||||
> 注意:`mapApiError` **不再做错误分类**(参见下方"边界原则")。如果你想给某种 HTTP 错误码加白名单分类,请先回到本文档读完"边界原则"再说。
|
||||
|
||||
## 边界原则:CLI 不翻译服务端错误
|
||||
|
||||
**CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的错误原样透传。**
|
||||
|
||||
| 错误来源 | 归类 | 处理方式 |
|
||||
| ---------------------------------------------------- | -------- | ----------------------------------------------------------- |
|
||||
| 命令解析、缺 flag、参数校验 | **内部** | `BailianError(USAGE)` |
|
||||
| 文件 I/O(ENOENT/EACCES/...) | **内部** | `BailianError(GENERAL)` + errno-specific hint |
|
||||
| 本地 credentials 缺失(resolver/ensure-key/AK-SK 等) | **内部** | `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 原样透传**,不分类、不替换 |
|
||||
|
||||
**判断标准**:错误信息的"权威来源"在哪一侧 —— 来自服务端响应 → 服务错误,透传;来自本地(OS / Node / CLI 自己)→ 内部错误,包装。
|
||||
|
||||
### 反面 case(为什么这条原则是必要的)
|
||||
|
||||
历史上的几个错误处理 bug 都是因为越过了这条边界:
|
||||
|
||||
1. **`mapApiError` 白名单未覆盖 OpenAI 兼容错误码** —— `qwen3.7` 不存在时服务端返回 `invalid_request_error`,白名单只认 `ModelNotFound`,fall through 到 GENERAL,该是 USAGE 信号被丢
|
||||
2. **`bl auth login` 用 try/catch 替换错误** —— key 有效但缺模型权限时,显示"API key validation failed"撒谎,误导用户去换 key
|
||||
3. **`error-handler.ts` 用 message 关键词归并网络错误** —— ENOTFOUND / ECONNREFUSED / TLS 错误全归并成"Network request failed.",错误根源完全丢失
|
||||
|
||||
共同根因:**我们试图在没有"权威信息"的位置代理服务端做分类**。修复方案是统一退回到透传 + 让本地错误的真实诊断浮上来。
|
||||
|
||||
## 错误流的分层架构
|
||||
|
||||
```
|
||||
core 抛出 BailianError(message, exitCode, hint, cause?)
|
||||
↓ 沿调用栈冒泡
|
||||
cli/main.ts: main().catch(handleError)
|
||||
↓
|
||||
cli/error-handler.ts:
|
||||
- 服务端错误(BailianError(GENERAL)) → text 直接打 message
|
||||
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
|
||||
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
|
||||
- Node fs errno → 翻成 GENERAL + errno hint
|
||||
- 其它 Error → 默认走 cause 链
|
||||
↓
|
||||
process.exit(err.exitCode)
|
||||
```
|
||||
|
||||
## 不变量(必须遵守)
|
||||
|
||||
### 1. 不替换服务端错误的 message
|
||||
|
||||
- ❌ `try { call() } catch { throw new BailianError("xxx failed", FIXED_CODE) }`
|
||||
- ✅ `try { call() } catch (err) { /* 加上下文不替换 */ throw err; }` 或直接不 catch
|
||||
|
||||
### 2. 不在 `mapApiError` 里加 status / apiCode 白名单分支
|
||||
|
||||
- ❌ 不要回退到"401 → AUTH、429 → QUOTA"那套白名单
|
||||
- ✅ message 把 status / apiCode / request_id 拼进去就够,exit 统一 GENERAL
|
||||
- 例外:CLI **自己**因为本地状态产生的 BailianError(resolver、ensure-key 等)可以用语义化 exitCode
|
||||
|
||||
### 3. core 的 hint 必须不含 cli 关切
|
||||
|
||||
- ❌ 不写 `bl xxx` 命令名
|
||||
- ❌ 不写控制台 URL 或 region
|
||||
- ❌ 不写渠道追踪参数(`source_channel=xxx`)
|
||||
- ✅ 只描述抽象做法(如 `"Set DASHSCOPE_API_KEY environment variable, or pass --api-key."`)
|
||||
|
||||
### 4. cli 端可以自由使用 cli 命令名 + URL
|
||||
|
||||
- 命令文件、`error-handler.ts`、`utils/ensure-key.ts` 是 cli 层,内部可以写 `bl xxx`
|
||||
- URL 必须从 `packages/cli/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:同上
|
||||
|
||||
### B. cli 增强(`enhanceHint`)
|
||||
|
||||
- [ ] `packages/cli/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为只有 resolver/ensure-key 等内部位置会发 AUTH)
|
||||
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`
|
||||
|
||||
### C. cli 直接抛错(`ensure-key`、命令文件)
|
||||
|
||||
- [ ] cli 层抛 BailianError 时,hint 里可以放 cli 命令名,但 **URL 一律走 `urls.ts` import**
|
||||
- [ ] 抛错位置如果**已经在调用服务端**,catch 时不要替换 message——重新评估是否需要 catch
|
||||
|
||||
### D. 文案一致性
|
||||
|
||||
- [ ] 服务端错误的 message:必含 `HTTP <status>` 字段;有 apiCode/request_id 也拼上
|
||||
- [ ] 网络层错误的 message:必含 `err.cause.code`(如 ENOTFOUND)
|
||||
- [ ] 中英文混用慎重 —— 当前主要是英文文案
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 触发对应错误,看 text 输出
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive
|
||||
|
||||
# 看 JSON 输出(应包含 cause 字段当 cause 存在时)
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message "x" --non-interactive --output json
|
||||
|
||||
# 模拟网络层错误,验证 errno 透传
|
||||
DASHSCOPE_BASE_URL=https://nonexistent-host.invalid \
|
||||
node packages/cli/src/main.ts text chat --message hi
|
||||
# 预期:"Network request failed: ENOTFOUND ..." + Caused by 链
|
||||
```
|
||||
|
||||
text 模式应看到准确诊断,JSON 模式应能解析出 cause.code(用于 agent 决策)。
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 在 core 的 hint 里"顺便"写了 `bl auth login` —— 违反第 3 条不变量
|
||||
- ✗ 给 `mapApiError` 加了"401 → AUTH"等白名单分支 —— 违反边界原则
|
||||
- ✗ 用 `try/catch` 包住服务调用,catch 里 throw 了一个新的 BailianError 替换原错误 —— 违反第 1 条不变量
|
||||
- ✗ 加新 ExitCode 但没想清楚谁产生它 —— 服务端永远不应产生新 ExitCode,内部错误才需要新分类
|
||||
@@ -0,0 +1,65 @@
|
||||
# 工具链调整
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 升级 Vite+ / TypeScript / Node 版本
|
||||
- 调整 `vite.config.ts`(根 / 各包)
|
||||
- 改 lint 规则(Oxlint / Oxfmt / typescript-eslint)
|
||||
- 升级或替换依赖
|
||||
- 修改 `.vite-hooks/` 或 git hooks
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 版本一致性
|
||||
|
||||
- [ ] `package.json` 的 `engines.node` 与 README 的 Node.js 徽章一致
|
||||
- [ ] `pnpm-lock.yaml` 同步生成(运行 `pnpm install`)
|
||||
- [ ] 三处 `tsconfig.json`(根 + cli + core)的 target / module 设置一致
|
||||
|
||||
### B. lint / format 规则改动
|
||||
|
||||
- [ ] 全仓跑 `vp check --fix`,看是否产生大量自动 reformat
|
||||
- [ ] 如果产生 mass diff,**单独提一个 commit**(代码语义改动和 lint reformat 不要混)
|
||||
- [ ] 已有 warning 的处理:
|
||||
- 如果新规则消除了某些旧 warning,确认是否合理
|
||||
- 如果新规则产生了新 warning,评估是否要修
|
||||
|
||||
### 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` 会断言)
|
||||
|
||||
### D. 依赖升级
|
||||
|
||||
- [ ] 检查 `bailian-cli-core` 在 cli 的 `dependencies` 里仍是 `"workspace:*"`(不要变成实际版本号 — `tools/release.mjs` 会拦)
|
||||
- [ ] 升级后跑 `vp check && vp test`
|
||||
- [ ] 升级 `@types/node` 时注意 Node API 变化(如 fs.existsSync 行为)
|
||||
|
||||
### E. git hooks / pre-commit
|
||||
|
||||
- [ ] `.vite-hooks/pre-commit` 改动后,`pnpm install` 重新软链(走 `prepare: vp config`)
|
||||
- [ ] 增加 hook 时,确认在干净 clone 后能自动激活
|
||||
|
||||
### F. CI / 发版工具
|
||||
|
||||
- [ ] `tools/release.mjs` 中如有版本/规则相关的硬编码,同步更新
|
||||
- [ ] 比如 `secretPatterns` 添加新的敏感值识别
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 完整冒烟
|
||||
pnpm install --frozen-lockfile
|
||||
vp check
|
||||
vp test
|
||||
node tools/release.mjs check
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 升级 Node engines 但忘了 README 徽章
|
||||
- ✗ 改 lint 规则后没全仓 `--fix`,新人 PR 报红一片
|
||||
- ✗ 改 cli 的 vite config 把 core 不小心打成 inline,bundle 体积暴涨
|
||||
- ✗ Oxlint 配置改了但 IDE 缓存还是旧的(IDE 可能要重启 ts server)
|
||||
- ✗ 升级依赖一并升 lockfile,改动量大但没拆 commit
|
||||
@@ -0,0 +1,130 @@
|
||||
# 维护本指南
|
||||
|
||||
这份"AI 维护指南"(`AGENTS.md` + `docs/agents/*`)本身也是项目资产,需要随项目演化。
|
||||
|
||||
## 谁来维护
|
||||
|
||||
两类人/agent 会触发更新:
|
||||
|
||||
1. **AI agent 完成改动后**:回顾"这次改动是不是某个场景的常见漏点?清单是不是漏了什么?"
|
||||
2. **人(开发者)发现新场景**:某次维护用不上 `docs/agents/` 现有任何场景文档,新增一份
|
||||
|
||||
## 何时更新
|
||||
|
||||
任何一种情况发生,这份指南就该改:
|
||||
|
||||
- ✅ 完成改动后发现"清单里没说,但实际要做"的步骤 → 补到对应场景的**必查清单**
|
||||
- ✅ AI 漏了某个文件,review 时被指出 → 补到对应场景的**常见漏点**
|
||||
- ✅ 一类改动反复出现且不属于任何已有场景 → 新建场景
|
||||
- ✅ 项目结构调整(新增 `packages/xxx`、移动主要目录)→ 改 `AGENTS.md` 的**项目地图**
|
||||
- ✅ 全局原则调整(如开始支持海外 region、加新的 core/cli 边界规则)→ 改 `AGENTS.md` 的**通用约定**
|
||||
|
||||
## 新增一份场景文档
|
||||
|
||||
### 何时新增 vs 扩展现有
|
||||
|
||||
- 已有场景 + 新清单项 → 直接加到对应 `.md`,**不要新建**
|
||||
- 一类改动有自己**独立的触发条件 + 不重叠的清单** → 新建
|
||||
|
||||
判断:如果两个场景的清单重叠 ≥ 80%,说明是同一场景;新增清单项即可。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 在 `docs/agents/` 下新建 `<scenario>.md`,文件名 kebab-case 描述场景
|
||||
2. 用下方模板填充
|
||||
3. 在 `AGENTS.md` 的"业务场景索引"表格里加一行(按场景频率从高到低排序)
|
||||
4. 提 PR 时附 1-2 个真实改动 commit 链接,说明这个场景已经发生过
|
||||
|
||||
### 文件模板
|
||||
|
||||
```markdown
|
||||
# <场景中文标题>
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 何时进入这份文档(2-4 条具体情况)
|
||||
|
||||
## 概念图(可选)
|
||||
|
||||
若场景涉及多文件协作,画一张简单的层次/数据流图
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. <分组名>
|
||||
|
||||
- [ ] 具体到文件路径的 action
|
||||
- [ ] ...
|
||||
|
||||
### B. <分组名>
|
||||
|
||||
- [ ] ...
|
||||
|
||||
## 完成后自查
|
||||
|
||||
1-3 条可执行的验证命令
|
||||
|
||||
## 常见漏点
|
||||
|
||||
基于真实踩坑(初版可空,随实际场景生长)
|
||||
```
|
||||
|
||||
### 命名约定
|
||||
|
||||
- **文件名**:`<topic>-<verb>.md`(`command-add-remove.md`、`url-change.md`、`config-add.md`)
|
||||
- **场景标题**:3-6 字中文短语(命令增删改、URL / 渠道变更)
|
||||
- **必查清单分组**:用 `### A. xxx` `### B. xxx` 字母编号,方便引用
|
||||
|
||||
### 跨场景引用
|
||||
|
||||
两份文档有共同规则时,**一处定义、其他引用**:
|
||||
|
||||
```markdown
|
||||
<!-- error-hint-change.md 是定义方 -->
|
||||
|
||||
## 不变量
|
||||
|
||||
### 1. core 的 hint 必须不含 cli 关切
|
||||
|
||||
<!-- url-change.md 是引用方 -->
|
||||
|
||||
- ✗ 在 core 的 hint 里写 URL(违反 [error-hint-change.md](error-hint-change.md) 不变量 1)
|
||||
```
|
||||
|
||||
避免同一规则在 N 份文档里复制粘贴,改一处全跟。
|
||||
|
||||
## 修改 AGENTS.md(主入口)
|
||||
|
||||
`AGENTS.md` 修改频率应该**远低于**场景文件,只在三种情况变动:
|
||||
|
||||
| 触发 | 改的段落 |
|
||||
| ------------- | -------------- |
|
||||
| 加/删一个场景 | 业务场景索引表 |
|
||||
| 项目结构变化 | 项目地图 |
|
||||
| 全局原则变化 | 通用约定 |
|
||||
|
||||
**不要把场景特定的清单往 AGENTS.md 塞** —— 它的设计目标是 AI 加载到上下文里 ~60 行就够,详细内容按需读 `docs/agents/`。
|
||||
|
||||
## 文档应该是什么样
|
||||
|
||||
### Do
|
||||
|
||||
- 写清晰的 **must / must-not / 必查**,不写"建议"性语气
|
||||
- 用 file path + 具体 action 的句式(`packages/cli/src/commands/catalog.ts:增加 import 与 commands 条目`)
|
||||
- 在每份场景末尾留**常见漏点**段,持续累积真实经验
|
||||
- 在跨场景的不变量上互相引用,不复制
|
||||
|
||||
### Don't
|
||||
|
||||
- ❌ 不写前置知识(假设读者会 TS / 会读代码)
|
||||
- ❌ 不堆砌实现细节(代码改了文档也要改 — 不可持续;清单是"做什么",代码是"怎么做",后者交给代码)
|
||||
- ❌ 不重复 `README.md` 的内容(README 面向用户,本指南面向维护者)
|
||||
- ❌ 不写反向索引(文件 → 场景)— 双向维护成本高,AI 不需要
|
||||
- ❌ 不要在场景文件里塞通用约定 — 通用的放 AGENTS.md
|
||||
|
||||
## 文档生长的节奏
|
||||
|
||||
这套文档**不是一次写完**,是随真实工作沉淀的:
|
||||
|
||||
- 初版只有触发条件 + 骨架清单 + 空"常见漏点"
|
||||
- 每完成一次相关改动,补一两条清单或漏点
|
||||
- 长期未触发的场景文件可以归并或删除(避免文档腐化)
|
||||
@@ -0,0 +1,53 @@
|
||||
# 模型上下架
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 上线新的 Qwen / Wan / CosyVoice / 等模型
|
||||
- 切换某命令的默认模型(如 `bl text chat` 默认从 qwen3.7-max 切到 qwen3.7-plus)
|
||||
- 废弃旧模型
|
||||
|
||||
模型本身是阿里云后端在管,本仓库要做的是**让 CLI 能正确调用 + 文档/AI 入口准确反映可用模型清单**。
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. 命令实现
|
||||
|
||||
- [ ] `packages/cli/src/commands/<group>/<action>.ts`:
|
||||
- `--model` flag 的 description 里"default:"反映新默认值
|
||||
- 命令内部 `const model = (flags.model as string) || "<default>"` 的 fallback 字符串
|
||||
- 如果命令维护一个 supported-models 列表(如 `speech/synthesize.ts:MODEL_VOICES`),增删条目
|
||||
- 如果不同模型有不同 endpoint / 请求体形状,确保 `if (model.startsWith("xxx"))` 分支覆盖
|
||||
- [ ] 模型如有特殊 endpoint,看 `packages/core/src/client/endpoints.ts`
|
||||
|
||||
### B. 类型层
|
||||
|
||||
- [ ] `packages/core/src/types/api.ts` 的 request/response 类型如果跟模型相关,同步字段
|
||||
|
||||
### C. 命令手册
|
||||
|
||||
- [ ] 若 `--model` 的 description 含 default,改命令后跑 `pnpm --filter bailian-cli run generate:reference` 更新 `tools/generated/reference/<group>.md`(本仓库 gitignore;SKILL.md 由独立的 `npx add skills` 仓库维护,本仓库不再含)
|
||||
|
||||
### D. 用户面文档
|
||||
|
||||
- [ ] `README.md` / `README_CN.md`:
|
||||
- Quick Start 示例如使用了具体型号,确认仍可用
|
||||
- 顶部 introduction 段落如提到"Qwen-Omni"等品牌名,无需变(模型代号变化不算品牌变)
|
||||
|
||||
### E. 测试层
|
||||
|
||||
- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 维护 e2e:断言不硬编码废弃模型 ID;新模型至少一条 happy-path 集成
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 默认模型走通
|
||||
node packages/cli/src/main.ts <command> --message "test"
|
||||
# 显式指定新模型
|
||||
node packages/cli/src/main.ts <command> --model <new-model> --message "test"
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 改了命令默认模型,但 SKILL.md frontmatter 仍写老型号 → AI agent 调用时仍按老型号宣传
|
||||
- ✗ 废弃模型时只删了代码,e2e 测试还在跑,CI 红
|
||||
- ✗ 新模型 endpoint 不一致,但只改了 default,没加 endpoint 分支判断
|
||||
@@ -0,0 +1,94 @@
|
||||
# 发版前自检
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 准备发布 beta / rc / 正式版到 npm
|
||||
- 准备打 git tag
|
||||
|
||||
## 主入口:`tools/release.mjs`
|
||||
|
||||
发版流程**必须**走这两个命令,不要手动跑 `pnpm publish`:
|
||||
|
||||
```sh
|
||||
node tools/release.mjs check # 全套自检,不发布
|
||||
node tools/release.mjs publish # 自检 + 交互确认 + 发布
|
||||
```
|
||||
|
||||
### `check` 已自动覆盖(无需手动重复)
|
||||
|
||||
| 检查项 | 实现 |
|
||||
| -------------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| cli/core 版本号一致 | `validatePackages()` |
|
||||
| `publishConfig.registry` 指向公共 npm | `assertPublishConfig()` |
|
||||
| frozen lockfile install | `pnpm install --frozen-lockfile` |
|
||||
| format + lint + type check | `pnpm run check` |
|
||||
| 构建 core + cli | `pnpm --filter ... run build` |
|
||||
| **黑名单文件**(`.env` / `.npmrc` / `*.pem` / `*.key` / `*.crt` / SSH keys / debug log) | `denyPathPatterns` in `scanPackageContents` |
|
||||
| **敏感字符串**(DashScope `sk-xxxxx`、Alibaba `LTAI...`、access key secret) | `secretPatterns` in `scanPackageContents` |
|
||||
| tarball 里 cli 依赖 `bailian-cli-core@<exact version>`,无 `workspace:*` 泄漏 | `assertCliPackage()` |
|
||||
| cli bin 含 `#!/usr/bin/env node` shebang | 同上 |
|
||||
| cli bundle 把 core 当外部依赖 | 同上 |
|
||||
| core tarball 含 `dist/index.mjs` + `dist/index.d.mts` | `assertCorePackage()` |
|
||||
|
||||
如果新增了"应该自动检查"的项目级规则,优先加到 `tools/release.mjs`,不要单独靠人工或 AI 记。
|
||||
|
||||
## `release.mjs` 不覆盖的(手动确认)
|
||||
|
||||
### 版本号目标
|
||||
|
||||
- [ ] `packages/cli/package.json` 和 `packages/core/package.json` 已升到目标版本
|
||||
- [ ] pre-release 格式正确(`1.0.0-beta.0` / `1.0.0-rc.1`,**不要直接用 `1.0.0` 当 beta**)
|
||||
|
||||
### 用户面文档
|
||||
|
||||
- [ ] `README.md` / `README_CN.md` 的 Quick Start 命令仍能跑通
|
||||
- [ ] README 的 Node.js 徽章版本与 `cli/package.json.engines.node` 一致
|
||||
- [ ] README 宣传的 bin 名称在 `cli/package.json.bin` 都真的注册(常见漏点)
|
||||
- [ ] `LICENSE` 文件存在(根 + cli + core 各一份)
|
||||
|
||||
### AI 入口资产
|
||||
|
||||
- [ ] `pnpm --filter bailian-cli run build` 已执行(`generate:reference` 会刷新 `tools/generated/reference/`,仅本地校验用,不随 npm 包发布)
|
||||
- [ ] SKILL.md 与命令手册的分发已迁出本仓库,改由独立的 `npx add skills` 机制安装;本仓库的 `cli/package.json.files` 不再包含 `skill` 与 `scripts/postinstall.js`
|
||||
|
||||
## 发布
|
||||
|
||||
### beta / rc(不进 `latest` dist-tag)
|
||||
|
||||
`tools/release.mjs publish` 当前不接受 `--tag` 参数(见末尾 TODO)。pre-release 版本 npm 默认行为不会污染 `latest`,但稳妥起见,**直接用 pnpm 命令显式带 tag**:
|
||||
|
||||
```sh
|
||||
# 先跑一次 check 确保通过
|
||||
node tools/release.mjs check
|
||||
|
||||
# 然后显式发布到 beta dist-tag
|
||||
pnpm --filter bailian-cli-core publish --tag beta --no-git-checks
|
||||
pnpm --filter bailian-cli publish --tag beta --no-git-checks
|
||||
```
|
||||
|
||||
### 正式版(默认进 latest)
|
||||
|
||||
```sh
|
||||
node tools/release.mjs publish
|
||||
```
|
||||
|
||||
## 完成后
|
||||
|
||||
- [ ] 推 git tag(如 `v1.0.0-beta.0`)
|
||||
- [ ] 验证 npm 上能装:`npm view bailian-cli@beta version`
|
||||
- [ ] 试装一次:`npm i -g bailian-cli@beta && bl --version`
|
||||
|
||||
## TODO(给 release.mjs 维护者)
|
||||
|
||||
- [ ] `releasePublish` 接受 `--tag <name>` 参数,beta/rc 不再绕开脚本
|
||||
- [ ] `check` 增加 SKILL.md 与 `catalog.ts` / `reference/index.md` 一致性断言(如命令数、关键子命令名)
|
||||
|
||||
## 常见漏点(基于历史踩坑)
|
||||
|
||||
| 漏点 | 后果 |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------------- |
|
||||
| 改了源码忘 `vp pack`,直接 publish | npm 上是旧代码 — `tools/release.mjs publish` 会自动重建,**不要绕过它** |
|
||||
| cli 升版号但 core 没升 | release.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 警告或直接失败 |
|
||||
@@ -0,0 +1,223 @@
|
||||
# 批量压测脚本(多能力统一入口)
|
||||
|
||||
**使用者文档**(方案说明、命令示例、fixtures、默认值表):[../stress-testing.md](../stress-testing.md)
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增、修改或排查 `packages/cli/tests/stress/**/*.mjs` 批量压测代码
|
||||
- 用户要求并发压测 `bl` 各能力(文本 / 语音 / 图像 / 视频等)、生成 Markdown/HTML 报告
|
||||
- 修改对应命令的 JSON 输出字段后,需同步 `packages/cli/tests/stress/lib/parsers.mjs` 与各 `targets/*.mjs` 的成功判定
|
||||
- 在 monorepo 根 `package.json` 调整 `test:stress` 入口
|
||||
|
||||
**不要**为压测去改 CLI 产品行为(除非用户明确要求修 CLI bug);压测侧通过子进程参数与解析逻辑适配现有命令。
|
||||
|
||||
## 与 E2E / `vp test` 的关系
|
||||
|
||||
| 维度 | E2E(Vitest) | 批量压测(`.mjs`) |
|
||||
| ---- | ---------------------------------------- | ------------------------------------------------------------------ |
|
||||
| 路径 | `packages/cli/tests/e2e/*.e2e.test.ts` | `packages/cli/tests/stress/run.mjs` + `targets/*.mjs`、`lib/*.mjs` |
|
||||
| 运行 | `vp test`(真实 API 需 `BAILIAN_E2E=1`) | **仅手动** `pnpm run test:stress -- <target> -- ...` |
|
||||
| 目的 | 回归:help、缺参、dry-run、单条集成 | 并发、限流、耗时统计、批量报告、前置资源 fixtures |
|
||||
| CI | 可纳入 `vp test`(skip 块默认跳过) | **禁止**默认 CI / `vp test` 自动执行 |
|
||||
|
||||
`vp test` 只收集 `*.test.ts` / `*.spec.ts` 等,**不会**执行 `tests/stress/*.mjs`。勿将压测逻辑写成 Vitest 用例并放进默认测试流。
|
||||
|
||||
E2E 规范见 [cli-e2e-tests.md](cli-e2e-tests.md)。
|
||||
|
||||
## 文件与入口
|
||||
|
||||
```
|
||||
packages/cli/tests/stress/
|
||||
├── run.mjs # 路由入口(解析 target、全局 flags、`--`)
|
||||
├── lib/
|
||||
│ ├── argv-parse.mjs # 共用 --count/-n、-c、-m、--voice、--report-dir 等
|
||||
│ ├── stress-config.mjs # count/concurrency 配置加载与解析
|
||||
│ ├── cli-runner.mjs # spawn main.ts、限流重试、线程池
|
||||
│ ├── fixtures.mjs # prerequisites.json + 前置音频/图/视频生成
|
||||
│ ├── parsers.mjs # 各命令 stdout / 文件解析
|
||||
│ ├── paths.mjs
|
||||
│ ├── rate-limit.mjs
|
||||
│ ├── finish-run.mjs # 写报告;套件模式返回摘要而不 exit
|
||||
│ ├── run-suite.mjs # 全量套件编排
|
||||
│ ├── suite-catalog.mjs # 用例顺序与中文名
|
||||
│ ├── suite-report.mjs # SUITE_REPORT.md / .html
|
||||
│ ├── trace-ids.mjs # requestId / taskId 提取
|
||||
│ └── report.mjs # REPORT.md / REPORT.html / results.json
|
||||
└── targets/
|
||||
├── text-chat.mjs
|
||||
├── speech-synthesize.mjs
|
||||
├── speech-recognize.mjs
|
||||
├── image-generate.mjs
|
||||
├── image-edit.mjs
|
||||
├── video-t2v.mjs
|
||||
├── video-i2v.mjs
|
||||
├── video-ref.mjs
|
||||
└── video-edit.mjs
|
||||
```
|
||||
|
||||
monorepo 根 `package.json`:
|
||||
|
||||
```json
|
||||
"test:stress": "node packages/cli/tests/stress/run.mjs"
|
||||
```
|
||||
|
||||
`pnpm` 会在子进程 argv 中插入 `--`;入口会**跳过**孤立的 `--`,因此 `pnpm run test:stress -- list` 与 `pnpm run test:stress list` 均可。
|
||||
|
||||
## 如何运行
|
||||
|
||||
在 **monorepo 根目录**执行(需已配置 `DASHSCOPE_API_KEY` 或 `~/.bailian/config.json`):
|
||||
|
||||
```sh
|
||||
# 顺序执行全部 9 个用例,并生成套件总报告 SUITE_REPORT.md(耗时长、会打真实 API)
|
||||
pnpm run test:stress
|
||||
pnpm run test:stress -- all -- --count 5 -c 2
|
||||
|
||||
# 列出全部 target
|
||||
pnpm run test:stress -- list
|
||||
|
||||
# 文本对话
|
||||
pnpm run test:stress -- text -- --count 20 -c 5
|
||||
|
||||
# 语音合成(音色可用 --voice 或环境变量 STRESS_TTS_VOICE)
|
||||
pnpm run test:stress -- speech-tts -- --count 10
|
||||
|
||||
# 语音识别(先在同批次目录下生成 fixtures/setup-audio.mp3 等)
|
||||
pnpm run test:stress -- speech-asr -- --count 5
|
||||
|
||||
# 生图 / 修图 / 视频类
|
||||
pnpm run test:stress -- image-generate -- --count 50 -c 2
|
||||
pnpm run test:stress -- image-edit --reuse-fixtures -- --count 10
|
||||
pnpm run test:stress -- video-t2v -- --count 3 -c 1
|
||||
pnpm run test:stress -- video-i2v --setup-only # 仅生成前置图 + manifest
|
||||
pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3
|
||||
```
|
||||
|
||||
**环境变量**:可写在 `pnpm` 前,例如 `COUNT=5 pnpm run test:stress -- video-t2v`。
|
||||
|
||||
**命令行参数**:`--` 之后传给具体 target(与旧 `stress:* -- --count` 语义一致)。
|
||||
|
||||
配置优先级:
|
||||
|
||||
- **任务数 / 并发**:命令行 `--count` / `-n`、`--concurrency` / `-c` > `packages/cli/tests/stress/stress.defaults.json`(或 `--stress-config` / `STRESS_CONFIG`)> `lib/stress-config.mjs` 中 `CODE_DEFAULTS`
|
||||
- **其它参数**(如 `MODEL`):命令行 > 环境变量 > target 内默认值
|
||||
|
||||
### 全局选项(由 `run.mjs` 解析,可从 argv 任意位置剥离)
|
||||
|
||||
| 选项 | 含义 |
|
||||
| ----------------------- | ------------------------------------------------------------------------------- |
|
||||
| `--reuse-fixtures` | 若当前批次的 `fixtures/prerequisites.json` 已存在且校验通过,则不再跑前置 CLI |
|
||||
| `--setup-only` | 仅生成前置资源并写入 manifest,不进入压测循环(适用于带 fixtures 的 target) |
|
||||
| `--fixtures-dir <path>` | 使用**已有**目录下的 `prerequisites.json`(不拷贝),满足本 target 所需字段即可 |
|
||||
|
||||
前置 manifest 写入路径:`{REPORT_DIR}/fixtures/prerequisites.json`(默认 REPORT_DIR 含时间戳)。
|
||||
|
||||
## 脚本架构(不可随意破坏的约束)
|
||||
|
||||
### 子进程调用方式
|
||||
|
||||
- **实际执行**:`node packages/cli/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`;须阻塞到任务完成(及下载路径正确时落盘)。
|
||||
|
||||
### 成功 / 失败判定(概要)
|
||||
|
||||
| Target / 能力 | 成功条件 |
|
||||
| ------------- | ------------------------------------------------------------------------------------------ |
|
||||
| text | JSON 含有效 `choices[0].message.content` 或流式汇总后的 `content` |
|
||||
| speech-tts | JSON 含 `audio_url` / `audio_urls` 与 `saved` |
|
||||
| speech-asr | 进程 exit 0,且 `--out` JSON 可被解析出 `transcripts` 文本(或 stdout 有非空正文作为兜底) |
|
||||
| image | JSON 含 `urls` 和/或 `saved`(不可仅凭 `task_id` 判失败) |
|
||||
| video | JSON 含 `video_url` 和/或 `saved`(同上) |
|
||||
|
||||
详见 `lib/parsers.mjs`。
|
||||
|
||||
### 墙钟耗时
|
||||
|
||||
报告中的 **墙钟总耗时** = `finishedAt - startedAt`(整批真实经过时间),不是各任务 `durationMs` 之和。
|
||||
|
||||
## 默认参数(摘要)
|
||||
|
||||
各 target 默认值见对应 `targets/*.mjs` 文件头与常量;常见约定:
|
||||
|
||||
- **count / concurrency**:见仓库内 `packages/cli/tests/stress/stress.defaults.json`,可按 target 修改
|
||||
- **video-t2v / i2v / ref / edit**:默认视频类较低 `COUNT`、并发多为 1,`TIMEOUT_MS` 可达 1 小时;前置 `video` fixtures 使用文生视频生成约 5s 样片
|
||||
- **speech-asr**:默认 `POLL_INTERVAL=2`;输入音频由 `speech synthesize` 写入 fixtures
|
||||
|
||||
## 报告产物
|
||||
|
||||
### 单用例
|
||||
|
||||
每次运行在 `REPORT_DIR`(默认 `test/output/<target>-batch-<时间戳>/`)生成:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| ----------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `REPORT.md` | 汇总与明细表(含 **Request ID**、**Task ID** 列,便于排查) |
|
||||
| `REPORT.html` | 同上 |
|
||||
| `results.json` | 精简后的原始结果(含 `requestId` / `taskId` 字段) |
|
||||
| `fixtures/prerequisites.json` | 需前置资源时:记录 `audio` / `image` / `video` 的 http URL、`saved` 本地路径与可复制命令 |
|
||||
|
||||
`requestId` / `taskId` 提取逻辑见 `lib/trace-ids.mjs`(**仅压测脚本**):
|
||||
|
||||
- 成功时 CLI stdout 通常不含 `request_id`;压测会默认加 `--verbose`,从 stderr 的 `request_id:` / JSON 块解析
|
||||
- 有 `task_id` 仍缺 `request_id` 时,压测会额外调用 DashScope `GET /tasks/{id}` 补齐(`lib/fetch-request-id.mjs`)
|
||||
- 关闭方式:`STRESS_VERBOSE_REQUEST_ID=0`(不注入 `--verbose`)、`STRESS_FETCH_REQUEST_ID=0`(不查任务 API)
|
||||
|
||||
### 全量套件
|
||||
|
||||
`pnpm run test:stress`(无 target 或 `all`)在 `test/output/stress-suite-<时间戳>/` 下为每个用例建子目录,并生成:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| ----------------------- | ------------------------------------------------------------------- |
|
||||
| `SUITE_REPORT.md` | 各用例中文名、墙钟耗时、任务数、并发、成功/失败、成功率、子报告路径 |
|
||||
| `SUITE_REPORT.html` | 同上 |
|
||||
| `suite-results.json` | 结构化汇总 |
|
||||
| `<canonical>/REPORT.md` | 该用例明细报告 |
|
||||
|
||||
进程退出码:存在失败任务 → `1`,全部成功 → `0`。
|
||||
|
||||
根目录 `.gitignore` 已忽略 `test/`,压测产物勿提交。
|
||||
|
||||
## Agent 修改清单
|
||||
|
||||
### 只改压测脚本时
|
||||
|
||||
- [ ] `lib/paths.mjs` 解析的 `CLI_PACKAGE` / `MONOREPO_ROOT` 仍正确
|
||||
- [ ] 子进程仍为 `node` + `src/main.ts`,未改回裸 `pnpm run dev` 执行任务
|
||||
- [ ] 未对子进程加 `--quiet`,视频未加 `--no-wait`
|
||||
- [ ] `parsers.mjs` 与文档中的成功判定一致
|
||||
- [ ] 根 `package.json` 仅保留 `test:stress` 入口指向 `run.mjs`
|
||||
- [ ] `node --check` 对相关 `.mjs` 通过,`pnpm run test:stress -- list` 可运行
|
||||
|
||||
### 改了某命令 JSON 输出时
|
||||
|
||||
- [ ] 同步 `lib/parsers.mjs` 及受影响 `targets/*.mjs`
|
||||
- [ ] 有 API Key 时跑小规模:`pnpm run test:stress -- <target> -- --count 2 -c 1`
|
||||
|
||||
### 禁止(除非用户明确要求)
|
||||
|
||||
- [ ] 为压测专门改 CLI `--quiet` + `--output json` 行为
|
||||
- [ ] 压测写进默认 `vp test` 路径且未 skip
|
||||
- [ ] CI / `ready` 自动调用 `test:stress`
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- 环境变量写在 `pnpm run test:stress` **之后** → 未注入进程
|
||||
- **未知 target `"--"`**:已在新入口中跳过孤立 `--`;若仍报错请检查是否多空格或 shell 引用
|
||||
- 高并发无节流 → DashScope Rate limit(exit 4);应依赖脚本内限流或降低 `-c`
|
||||
- `image-generate` 大批量开启 `REPORT_THUMBNAILS=1` → HTML 变慢
|
||||
- 改压测却未同步本文档与 `AGENTS.md`/`CLAUDE.md` 索引
|
||||
|
||||
## 与 E2E 的分工
|
||||
|
||||
- **改命令选项、help、缺参、单条真实调用** → `tests/e2e/*.e2e.test.ts`
|
||||
- **并发、配额、fixtures、批量报告** → `tests/stress/` + **手动** `pnpm run test:stress -- <target>`
|
||||
@@ -0,0 +1,74 @@
|
||||
# URL / 渠道变更
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 控制台域名变更(如 `bailian.console.aliyun.com` → 新域名)
|
||||
- API endpoint 迁移
|
||||
- 文档站迁移
|
||||
- 调整 / 删除 / 新增渠道追踪参数(`source_channel` 等)
|
||||
|
||||
## URL 的分层架构
|
||||
|
||||
```
|
||||
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)
|
||||
|
||||
cli/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
|
||||
|
||||
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
|
||||
```
|
||||
|
||||
## 必查清单
|
||||
|
||||
### A. TS 源码(必须 import,不准硬编码)
|
||||
|
||||
- [ ] `packages/core/src/config/schema.ts` 是所有 API/docs 基址的源头
|
||||
- [ ] `packages/cli/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
|
||||
|
||||
# API endpoint — 应只在 schema.ts 和 upload.ts 出现
|
||||
grep -rnE "https://dashscope[a-z-]*\.aliyuncs\.com" packages/ --include="*.ts" \
|
||||
| grep -v "node_modules" | grep -v "/dist/"
|
||||
# 期望:只匹配 schema.ts(REGIONS)、upload.ts(派生)、tests
|
||||
```
|
||||
|
||||
### B. 非 TS 文件(只能人工同步,无法 import)
|
||||
|
||||
- [ ] `tools/generated/reference/` 各 `<group>.md` 中 API/控制台 URL(`generate:reference` 重建后核对;本仓库 gitignore)
|
||||
- [ ] `README.md` / `README_CN.md` 中所有 URL
|
||||
|
||||
### C. 渠道追踪参数
|
||||
|
||||
- [ ] **当前现状**:全仓不带 `source_channel=aliway` 等追踪参数
|
||||
- [ ] 如未来要恢复以收集分析数据,**统一评估再加回**(不要单点恢复造成不一致)
|
||||
- [ ] 全仓 grep `source_channel=`,确认无残留
|
||||
|
||||
## 完成后自查
|
||||
|
||||
```sh
|
||||
# 验证错误 hint 不再泄漏旧 URL
|
||||
HOME=/tmp/empty node packages/cli/src/main.ts text chat --message x --non-interactive
|
||||
# 看输出的 Get API Key URL 是否走新值
|
||||
|
||||
# 验证 banner / help
|
||||
node packages/cli/src/main.ts # banner
|
||||
node packages/cli/src/main.ts help # help 命令
|
||||
```
|
||||
|
||||
## 常见漏点
|
||||
|
||||
- ✗ 改了 `urls.ts` 但忘记同步 README(用户最先看到)
|
||||
- ✗ 在 cli 命令文件里 inline `https://bailian.console.aliyun.com/...` 而不是 `${API_KEY_PAGE}`
|
||||
- ✗ 在 core 的 hint 里写 URL(违反 [error-hint-change.md](error-hint-change.md) 不变量 1)
|
||||
Reference in New Issue
Block a user