# sdlc-cli 配置 schema 与写入参考

配置文件：`~/.sdlc/config.yaml`（测试可用环境变量 `SDLC_CONFIG` 覆盖路径）。
schema 为 **strict**（出现未知子键即报错）。[配置全貌](#配置全貌)覆盖全部 11 个顶层键及其来源与默认值；
下面的分维度章节详述本 skill 主要负责的 workspace / products / repos、databases/archery/gapi.apps 与 feishu.folder_token。

## 目录

- [配置全貌](#配置全貌)
  - [段来源：哪些安装即有、哪些要手工配](#段来源哪些安装即有哪些要手工配)
  - [完整样例](#完整样例)
- [维度一：workspace.root](#维度一workspaceroot)
- [维度二：products.<产品名>](#维度二products产品名)
- [维度三：repos.<逻辑id>](#维度三repos逻辑id)
- [数据库与 GAPI 应用](#数据库与-gapi-应用)
- [飞书发布落点：feishu.folder_token](#飞书发布落点feishufolder_token)
- [写入命令速查](#写入命令速查)
- [缺失项问话术](#缺失项问话术)
- [常见校验错误](#常见校验错误)

---

## 配置全貌

顶层键共 11 个，**全部 optional**：`workspace` / `repos` / `products` / `kb` / `session_collector` / `cp` / `feishu` / `gapi` / `apollo` / `databases` / `archery`。
所以「必填」在这里只有一种含义：**某条命令要用它时缺了就报错**，而不是「配置文件缺了它就不合法」。空文件也能过校验。

### 段来源：哪些安装即有、哪些要手工配

三类来源，判断口径是「`sdlc-cli install` 会不会替你写进文件」：

| 标记 | 含义 |
|---|---|
| **① 新建落盘** | `install`（配置文件不存在时）与 `config init` 写入 `DEFAULT_CONFIG_TEMPLATE`，装完打开文件就能看到 |
| **② 升级补键** | 配置文件已存在时 `install` 走升级合并，**只补整键缺失**、用户已有值一律不覆盖、token/AK 永不写 |
| **③ 手工配** | 安装永不写，`config set` 或手改；缺了就是缺了 |

| 段 | 来源 | 用到它的命令缺了会怎样 |
|---|---|---|
| `workspace.root` | ① | 不报错，代码回落 `~/sdlc-workspaces` |
| `workspace.products_root` | ③（模板里是注释行） | 不报错，回落 `<workspace.root>/products` |
| `kb.collector.*` | ① | 不报错，每项各有代码级默认（与模板同值） |
| `session_collector.*` | ①，且整段缺失时 ② 补 5 个键 | 不报错，每项各有代码级默认 |
| `archery.base_url` | ①（`https://archery.chinawayltd.com`），缺失时 ② 补 | **有 AK 而无地址 = 配置错误 exit 1**；无运行时回落 |
| `archery.access_key` | ①（空串占位），**② 永不补写** | `db` 走 archery 通道时返回 `ARCHERY_KEY_MISSING` |
| `gapi.base_url` | ③ 新建时无，②（`http://devcloud.chinawayltd.com`） | 不报错，代码内置默认同值 |
| `gapi.*_token` / `gapi.apps.*` | ③ | `gapi` 检索报 `GAPI_TOKEN_MISSING` |
| `apollo.server_urls` | ③ 新建时无，②（四环境地址） | 不报错，四环境地址内置默认 |
| `products.*` | ①（空 `{}` + 注释示例） | 装配要产品时无从匹配；`--product` 指向未配产品直接报错 |
| `repos.*` | ①（空 `{}` + 注释示例） | 装配该逻辑仓时找不到地址 |
| `cp.service.base_url` | ③ | `cp` 命令报 `cp_service_base_url_missing`，**无默认** |
| `cp.jira.*` | ③ | schema 收，但当前 CLI 无代码读取（`jira` 组的站点/账号走 `JIRA_SERVER` 等环境变量，不走配置） |
| `feishu.folder_token` | ③ | 不报错，发布落调用者飞书个人空间根目录 |
| `databases.*` | ③ | `db` 命令报 `DB_NOT_FOUND`；**新建必须一次写完整条目** |

> **新建 ≠ 升级后**：模板把 `gapi` / `apollo` / `cp` / `feishu` 整段注释掉，所以刚 `install` 完的文件里**没有** `gapi` 和 `apollo` 段；等下一次 `install`（此时文件已存在，走升级路径）才会把 `gapi.base_url` 与 `apollo.server_urls` 实际写进去。两种状态都正常——这两项本来就有代码内置默认，写不写进文件不影响行为。

### 完整样例

下方是一份**可直接落盘的完整配置**（每个顶层键只出现一次，整体过 strict 校验），行尾标注来源与默认值：
①② 两类照抄即可（就是安装后的实际内容），③ 类按自己环境替换。

```yaml
workspace:
  root: ~/sdlc-workspaces            # ① 装配产物 / 远程 mirror（<root>/.mirrors）的根；缺省回落 ~/sdlc-workspaces
  products_root: ~/products          # ③ 模板里是注释行；缺省回落 <workspace.root>/products

kb:                                  # ① 知识库事件采集；整段与每一项都可省
  collector:
    server_url: http://sdlc-knowledge-base.g7in.com/   # 缺省同值内置默认
    server_token: ""                 # 空 = 不发 Authorization 头
    timeout_ms: 1000                 # 默认 1000
    flush_limit: 50                  # 默认 50
    worker_interval_seconds: 60      # 默认 60
    worker_max_seconds: 900          # 默认 900

session_collector:                   # ① transcript / 反馈采集；整段与每一项都可省
  enabled: true                      # 默认 true（仅显式 false 才停）
  include: []                        # 默认 []，空 = 全采；非空则为白名单
  exclude:                           # 默认同此 5 条，优先于 include
    - "~/.claude/**"
    - "~/.codex/**"
    - "~/.ssh/**"
    - "~/.gnupg/**"
    - "**/node_modules/**"
  collect_subagents: true            # 默认 true
  floor_bytes:                       # 去抖阈值；默认 claude 1MB / codex 2MB
    claude: 1048576
    codex: 2097152
  skip_files_larger_than: 0          # 默认 0 = 不限
  feedback_flush_limit: 20           # 默认 20，0 = 不限；与 transcript 的 --limit 刻意分开
  server_url: ""                     # 空 = 回落内置 https://devlake.chinawayltd.com
  server_token: ""                   # 配了才发 Bearer；不配不发

archery:
  base_url: https://archery.chinawayltd.com  # ① 仅安装写入，运行时不补默认；缺失时 ② 补
  access_key: ""                     # ① 写空串占位；② 升级永不补写。用前自己填，输出自动打码

gapi:                                # ② 首次 install 后没有这段，第二次 install 才写入
  base_url: http://devcloud.chinawayltd.com  # ② 缺省同值内置默认
  app_token: ""                      # ③ 三个维度 token 按需配；缺失 → GAPI_TOKEN_MISSING
  product_token: ""                  # ③
  user_token: ""                     # ③
  apps:                              # ③ 按应用覆盖 app_token；映射缺失/空串则回落全局 app_token
    checkout: { app_token: "" }

apollo:                              # ② 同上，升级路径才写入
  server_urls:                       # ② 四环境均有内置默认；只覆盖要改的那个环境即可
    dev:  http://apollo-api.dev.chinawayltd.com:8080
    test: http://apollo-api.test.chinawayltd.com:8080
    demo: http://apollo-api.demo.chinawayltd.com:8080
    pro:  http://apollo.merak.chinawayltd.com:8000

cp:                                  # ③ 安装永不写
  service:
    base_url: https://devlake.example.com  # ③ cp 命令必需，无默认；缺失 → cp_service_base_url_missing
  jira:                              # ③ schema 收，当前 CLI 无代码读取
    base_url: https://jira.example.com
    read_token: "<jira-token>"       # ③ 注意 min(1)：空串过不了校验，要么给真值要么整个键省掉

feishu:                              # ③ 安装永不写
  folder_token: fldcnXXXXXXXXXXXXXX  # ③ 飞书目标文件夹标识；不配则落个人空间根目录。非凭证，config get 回真值不打码

products:                            # ① 模板写空 map `{}`；下面的具体产品属 ③
  checkout:                          # ③ key 由用户给定，不可臆造
    context_repo:   git@git.g7e6.com:sdlc/checkout-ctx.git  # 单个 git 地址
    knowledge_base:                  # 单个字符串 或 字符串数组
      - git@git.g7e6.com:kb/checkout-kb.git
      - git@git.g7e6.com:kb/common-kb.git
    jira_prefixes:  [PROJ, CHK]      # 字符串数组，按 Jira id 前缀匹配产品
    description:    "结账支付系统"     # 展示用
    keywords:       [结账, 支付, checkout]  # 供语义匹配
    epic:           CHK-100          # 本产品默认 epic/迭代 id（装配兜底）
    repos:          [backend, frontend]    # 本产品默认逻辑仓 id（对应下方 repos 表 key）
    databases:      [orders, cache]  # 仅供 db list/search 过滤，不限制直接查询

repos:                               # ① 模板写空 map `{}`；下面的具体仓属 ③
  backend:                           # ③ key 由用户给定；决定工作区落位 repos/<逻辑id>/
    remote: git@git.g7e6.com:checkout/backend.git  # git 地址，装配时按需拉 mirror
    local:  ~/code/backend           # 存在则装配直接用该目录、不拉 mirror
    base:   develop                  # 新工作区默认基准分支，不带 origin/
  sdlc/backend:                      # ③ 不同 group 同名项目：逻辑 id 写 <group>/<project>
    remote: git@git.g7e6.com:sdlc/backend.git

databases:                           # ③ 安装永不写；新建必须一次写完整条目（type + 至少一个完整 env）
  orders:
    type: mysql                      # 必填 mysql / redis
    note: 订单主库                    # 可选
    envs:
      dev:  { host: localhost, user: reader, password: "", database: orders }  # channel 缺省 direct，port 默认 3306
      prod: { channel: archery, instance: mysql-prod-01, database: orders }    # archery 通道不接受 host/user/password
  cache:
    type: redis
    envs:
      dev: { host: localhost, db: 0 }  # port 默认 6379，db 默认 0；redis 不支持 archery 通道
```

> `products` 与 `repos` 的顶层 key（产品名 / 逻辑 id）**必须由用户给定**，不可臆造。

---

## 维度一：workspace.root

| 字段 | dot-key | 值格式 | 必填 |
|---|---|---|---|
| 工作区根目录 | `workspace.root` | 路径字符串，`~` 会被展开（如 `~/sdlc-workspaces`） | 否（安装即写入 `~/sdlc-workspaces`；未配时代码同样回落该路径） |

单值，一次 `config set` 即可。配置里删掉它不会让任何命令报错——只会让工作区落回默认路径，而用户可能以为自己改的那份配置生效了。

### workspace.products_root（可选）

产品工作区根目录。`ws init` 将产品工作区装配到 `<products_root>/<产品名>-product/` 下（目录名带 `-product` 后缀，与任务工作区的产品层级 `<workspace.root>/<产品名>/` 区分）。

| 字段 | key | 类型 | 必需 |
|---|---|---|---|
| 产品工作区根 | `workspace.products_root` | 路径字符串，`~` 展开 | 否（默认 `<workspace.root>/products`） |

**示例**：
```yaml
workspace:
  root: ~/sdlc-workspaces
  products_root: ~/products  # 可与 workspace.root 完全分离
```

**与 workspace.root 的关系**：
- `products_root` 未配置时 fallback 到 `<workspace.root>/products`（**不是** `workspace.root` 本身——那会让产品工作区与任务工作区的产品层级挤在同一个 `ls` 里）
- `ws init --root <path>` 可显式覆盖
- 任务工作区（`<workspace.root>/<产品>/<主id>/`）与产品工作区结构同形，CLI 靠产品工作区根的 `.sdlc-product.yaml` 标记区分；`ws which` 的候选**只取 `products_root` 的直接子目录**，所以改这一项会直接影响 `ws which` 能不能找到产品工作区

---

## 维度二：products.<产品名>

产品是**分组**：每个子字段是独立 dot-key，一次 `config set` 写一个。

| 字段 | dot-key | 值格式 | 必填 |
|---|---|---|---|
| context-repo | `products.<name>.context_repo` | 单个 git 地址（字符串） | 建议有 |
| 知识库 | `products.<name>.knowledge_base` | 单个字符串，或数组 `'[a, b]'` | 可选 |
| jira 前缀 | `products.<name>.jira_prefixes` | 字符串数组 `'[PROJ, CHK]'` | 可选 |
| 产品描述 | `products.<name>.description` | 字符串（展示用） | 可选 |
| 关键词 | `products.<name>.keywords` | 字符串数组 `'[结账, 支付]'` | 可选 |
| 默认 epic | `products.<name>.epic` | 单个 epic/迭代 id（字符串） | 可选 |
| 默认代码仓 | `products.<name>.repos` | 逻辑 id 数组 `'[backend, frontend]'` | 可选 |
| 数据库归属 | `products.<name>.databases` | 数据库 key 数组 `'[orders, cache]'`，仅用于 db list/search 过滤，不限制直接查询 | 可选 |

**不允许**出现以上之外的子键（strict schema）。若用户想配一个不在表内的属性，先说明当前 schema 不支持。

> `epic` / `repos` 供 `assembling-workspace` 首次装配兜底：命中产品后无需再敲 `--epic`、`--repo`。
> 优先级 `命令行 > 工作区缓存 > 产品预设`；`repos` 命令行为整体替换，增量增删走 `ws mount/unmount`。

关键词 / jira 前缀的作用：`assembling-workspace` 靠 `keywords` 做语义匹配、靠 `jira_prefixes`
按 Jira id 前缀便利匹配产品，命中后自动带出 context-repo / 知识库地址。配好这些能减少后续装配的追问。

---

## 维度三：repos.<逻辑id>

| 字段 | dot-key | 值格式 | 说明 |
|---|---|---|---|
| 远程地址 | `repos.<id>.remote` | git 地址字符串 | 装配时按需拉 mirror |
| 本地路径 | `repos.<id>.local` | 本地路径字符串 | **存在则装配直接用该目录、不拉 mirror** |
| 默认基准分支 | `repos.<id>.base` | 分支名字符串 | 新工作区未显式指定 base 时使用 |

**remote / local 判定**：用户给的若是本地已存在的目录路径 → 记 `local`；若是 git 地址（`git@…` / `http(s)://…`）→ 记 `remote`。两者可并存（有 local 优先用 local、缺失回退 remote）。拿不准就问用户「这是本地路径还是远程 git 地址」。

`base` 只保存分支名，如 `main` 或 `develop`，不带 `origin/`。装配优先级为
`--repo-base > workspace cache > repos.<id>.base > origin/main > origin/master`。配置时不连接远程仓，装配时验证分支是否存在。

**逻辑 id 怎么取**：自取的短名，格式不限（可含 `/`），装配时决定工作区里的落位目录 `repos/<逻辑id>/`。

- 默认用项目名即可（`backend`）。
- **不同 group / 命名空间下有同名项目**时（`sdlc/backend` 与 `tms/backend`），写成 `<group>/<project>`——工作区里会按层级落位成 `repos/sdlc/backend/`、`repos/tms/backend/`，一眼能区分是哪个仓。
- 用户若已有一个 `backend` 又要加另一个 group 的同名仓，提示他把两个都改成带 group 前缀的形式（只改配置 key 与 `--repo` 参数，不影响已装配的工作区目录）。
- 无需担心取错名会取到错的仓：共享 mirror 按**仓库 URL** 命名，同名逻辑 id 或改过 `remote` 地址都不会撞车。

---

## 数据库与 GAPI 应用

这些段在 schema 中均可省略，旧配置兼容；Archery 默认配置由安装/初始化落盘，databases 与 gapi.apps 按需配置。连接与凭证只取 config，db 命令仅传 key/env。

```yaml
databases:
  orders:
    type: mysql
    note: 订单主库
    envs:
      dev: { host: localhost, user: reader, password: "", database: orders }
      prod: { channel: archery, instance: mysql-prod-01, database: orders }
  cache:
    type: redis
    envs:
      dev: { host: localhost, db: 0 }
archery:
  base_url: https://archery.chinawayltd.com
  access_key: ""
gapi:
  app_token: ""
  apps:
    checkout: { app_token: "" }
products:
  checkout:
    databases: [orders, cache]
```

| 路径/组合 | 合法字段与默认值 |
|---|---|
| `databases.<key>` | 必填 `type: mysql/redis`、至少一个完整 `envs.<env>`；可选 `note` |
| mysql direct env | `channel` 缺省 direct；非空 host/user，password 必须为字符串（允许 `""`）；可选 database，port 默认 3306 |
| redis direct env | 非空 host；可选 username/password；port 默认 6379，db 默认 0 且为非负整数 |
| mysql archery env | `channel: archery`，非空 instance/database；不接受 host/user/password；redis 不支持此通道 |
| `archery` | schema 允许省略 base_url（存在时须为合法 URL）和 access_key；运行时无地址默认回落。AK 缺省/空串时返回 `ARCHERY_KEY_MISSING`；AK 已有而 base_url 缺失时为配置错误（exit 1） |
| `gapi.apps.<app>` | strict 对象，仅允许可选 app_token；映射不存在/token 缺省或空串时回落全局 `gapi.app_token` |

所有 port 均为 1..65535 的整数。未知 type/channel、缺必填字段、空 envs 均为配置错误（exit 1），不能作为逐字段草稿保存。mysql 无 database 可 query/check，schema 前需补全。数据库账号权限由下游返回，不需要配置权限探测字段。

`install` / `config init` 共用 `DEFAULT_CONFIG_TEMPLATE`：新建配置写入 `archery.base_url: https://archery.chinawayltd.com` 与 `access_key: ""` 占位。`install` 升级已有配置时只从模板补缺失的 base_url，保留自定义地址和任何 access_key，不补写 AK；`config init` 对已有文件不覆盖。已有 AK 但缺地址时，可执行 `sdlc-cli install` 或 `sdlc-cli config set archery.base_url <地址>`。`db list/search` 的 missing 会分别列出缺失的 `archery.base_url` / `archery.access_key`，两项齐备时才 configured/ready；不表示实际连通或权限已验证。

新建用完整条目一次写入（示例为空密码本地库）：

```bash
sdlc-cli config set databases.orders '{type: mysql, envs: {dev: {host: localhost, user: reader, password: "", database: orders}}}' --json
sdlc-cli config set products.checkout.databases '[orders]' --json
sdlc-cli config set gapi.apps.checkout '{app_token: ""}' --json
sdlc-cli config set archery.access_key '""' --json
```

已有数据库可按子键更新，但每次写入仍校验整体配置。空字符串值须传 YAML 字符串 `'""'`，不能用空参数代替。config get/set/list 对 password/access_key/app_token 及父对象内的凭证递归打码；db/config get/set/list 配置读取及 config set 值的 YAML 语法错误仅保留错误码与行列位置，不回显源文本；不要把 `[REDACTED]` 当成原值写回。`gapi search --app checkout` 仅可与 app scope 组合，最终 token 仍为空沿用 `GAPI_TOKEN_MISSING`。

## 飞书发布落点：feishu.folder_token

`sdlc-lite` 把方案 / 计划 / 用例 / 测试报告推成飞书文档时的**目标文件夹**。

| 字段 | dot-key | 值格式 | 必填 |
|---|---|---|---|
| 飞书落点文件夹 | `feishu.folder_token` | 文件夹 token 字符串（如 `fldcnXXXXXXXXXXXXXX`） | 否（不配则落调用者飞书个人空间根目录） |

**它不是凭证**，是文件夹标识 —— 就写在飞书文件夹 URL 里（`https://…/drive/folder/<folder_token>`），本身不授权，鉴权走 `lark-cli` 的 OAuth。因此：

- 在 `config-redact.ts` 里有一条**全路径窄豁免**，`config get feishu.folder_token` 回真值而非 `[REDACTED]`。这条豁免是必要的：打码后 skill 会把字面量 `[REDACTED]` 当 token 传给 `lark-cli`，只能拿到一个「文件夹不存在」的错 —— 那是**读到假值**，比读不到更难排查。
- 用户问「这个 token 要不要保密」时可以如实说不用，它等价于一个目录链接。

**缺省不是故障**：未配时发布落到调用者飞书个人空间根目录（`lark-cli` 省略 `--folder-token` 即根目录），而不是跳过发布。所以用户没提落点时**不要追问** —— 回落行为是设计选择，不是缺配。

> 取值怎么来：让用户打开目标飞书文件夹，从地址栏 URL 末段复制。CLI 不校验该 token 是否存在或可写，配错要等发布时才报。

---

## 写入命令速查

所有命令带 `--json`。值经 YAML 解析，故数组用 `[a, b]` 语法。

```bash
# workspace 根
sdlc-cli config set workspace.root '~/sdlc-workspaces' --json

# 产品：逐子键写
sdlc-cli config set products.checkout.context_repo 'git@git.g7e6.com:sdlc/checkout-ctx.git' --json
sdlc-cli config set products.checkout.knowledge_base '[git@git.g7e6.com:kb/a.git, git@git.g7e6.com:kb/b.git]' --json
sdlc-cli config set products.checkout.jira_prefixes '[PROJ, CHK]' --json
sdlc-cli config set products.checkout.description '结账支付系统' --json
sdlc-cli config set products.checkout.keywords '[结账, 支付, checkout]' --json
sdlc-cli config set products.checkout.epic 'CHK-100' --json
sdlc-cli config set products.checkout.repos '[backend, frontend]' --json

# 代码仓
sdlc-cli config set repos.backend.remote 'git@git.g7e6.com:checkout/backend.git' --json
sdlc-cli config set repos.backend.local '~/code/backend' --json
sdlc-cli config set repos.backend.base 'develop' --json

# 代码仓：不同 group 的同名项目，逻辑 id 用 <group>/<project> 区分
sdlc-cli config set 'repos.sdlc/backend.remote' 'git@git.g7e6.com:sdlc/backend.git' --json
sdlc-cli config set 'repos.tms/backend.remote' 'git@git.g7e6.com:tms/backend.git' --json

# 飞书发布落点（sdlc-lite 定稿发布用）
sdlc-cli config set feishu.folder_token 'fldcnXXXXXXXXXXXXXX' --json
sdlc-cli config get feishu.folder_token --json               # 非凭证，回真值不打码

# 读 / 列 / 初始化
sdlc-cli config get products.checkout --json      # 覆盖前确认用；未设置返回 value:null
sdlc-cli config list                              # YAML 全貌，供核对
sdlc-cli config init                              # 仅在用户要「初始化空模板」时用；set 会自动建文件
```

**引号规则**：整体套 shell 单引号。若值本身以 `[` `{` `-` 开头或含 `: `（冒号加空格），在单引号内再套一层双引号当纯字符串，例如 `'"- 带横线开头"'`。git 地址（含 `@` 和无空格 `:`）、`~/path` 可裸写。

---

## 缺失项问话术

只问缺的、拿不准的，一次问一件，别一口气盘问全部可选字段。

| 缺什么 | 问法 |
|---|---|
| 产品名 / 逻辑 id | 「这个产品/代码仓叫什么名字？（会作为配置里的标识 key）」 |
| context-repo 地址 | 「<产品> 的 context-repo git 地址是多少？」 |
| 知识库：一个还是多个 | 「知识库是一个还是多个地址？多个请都发给我。」 |
| jira 前缀 | 「<产品> 的 Jira id 前缀有哪些？（如 PROJ、CHK，可多个）」 |
| repos 地址是 local 还是 remote | 「这个是本地已 clone 的目录路径，还是远程 git 地址？」 |
| 代码仓 base | 「`<代码仓>` 的默认基准分支是什么？（如 `main`、`develop`）」 |
| workspace 根 | 「工作区根目录想设到哪个路径？」 |
| 飞书落点文件夹 | 「想让 sdlc-lite 的文档推到哪个飞书文件夹？把文件夹链接发我，我取里面的 token。」（用户没提落点时不问——缺省落个人空间根目录是设计行为） |

---

## 常见校验错误

CLI 写入前对整个配置做 schema 校验，失败返回 `{"error":{"code":1,"message":"配置不合法: <path>: <原因>"}}`。

| message 片段 | 原因 | 处理 |
|---|---|---|
| `Unrecognized key(s) in object: 'xxx'` | 子键不在合法字段表内（strict） | 对照本文件字段表纠正 key 名，或告知该属性暂不支持 |
| `Expected string, received …` / 类型不符 | 值类型错（如该给字符串却传了数组） | 按字段值格式重新给值 |
| 未识别的顶层键或数据库字段 | key 拼错或超出 schema | 对照对应字段表纠正；databases/archery/gapi.apps 为有效配置 |

校验失败**不要静默重试或改动用户意图**——如实转述 message，纠正 key/值格式后确认再重试。
