Initial commit

This commit is contained in:
若麒
2026-05-28 18:37:07 +08:00
commit 1533e2013e
220 changed files with 26860 additions and 0 deletions
+118
View File
@@ -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,日志泄漏
+104
View File
@@ -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` 这类全局表两边都加项,解冲突时被合掉一侧 | 某个命令突然要求登录 / 某个新命令注册丢失,编译能过、回归不易察觉 |
+186
View File
@@ -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。
+100
View File
@@ -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)。
+94
View File
@@ -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 的子组是反模式,新增时优先拍平为两级
+56
View File
@@ -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 的命令文件作为示例)
+84
View File
@@ -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` 不显示新字段,用户改了无法回查
+125
View File
@@ -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,内部错误才需要新分类
+65
View File
@@ -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
+130
View File
@@ -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
## 文档生长的节奏
这套文档**不是一次写完**,是随真实工作沉淀的:
- 初版只有触发条件 + 骨架清单 + 空"常见漏点"
- 每完成一次相关改动,补一两条清单或漏点
- 长期未触发的场景文件可以归并或删除(避免文档腐化)
+53
View File
@@ -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 分支判断
+94
View File
@@ -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 警告或直接失败 |
+223
View File
@@ -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>`
+74
View File
@@ -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)