# 完整示例

一次典型的「本地代码 → RAP」，`$RAP` 代表 `<skill目录>/scripts/rap.mjs` 的绝对路径。

## 场景一：新增接口（代码是未追踪的新文件）

### 0.0 自检

```bash
node "$RAP" doctor
node "$RAP" status
```

```json
{ "ok": false, "loggedIn": false, "sessionFile": "/tmp/xiaoma-generate-rap-interface/session.json" }
```

### 0.1 ⛔ 门禁 A：先把 6 项摆出来

登录之前，先把卡片需要的值查齐（这些都是只读操作，不碰 RAP）：

```bash
git -C D:/code/fc-payment rev-parse --git-dir            # 确认是 git 仓库
git -C D:/code/fc-payment rev-parse --abbrev-ref HEAD    # feature/quota
git -C D:/code/fc-payment remote get-url origin          # 看 remote 形态（SSH 还是 HTTPS），认证时按它开口
GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND='ssh -o BatchMode=yes' git -C D:/code/fc-payment fetch origin --prune
git -C D:/code/fc-payment status --porcelain
```

```
?? src/main/java/com/example/cust/controller/QuotaController.java
?? src/main/java/com/example/cust/dto/QuotaQueryDTO.java
?? src/main/java/com/example/cust/vo/QuotaVO.java
 M pom.xml
```

三个未追踪的 Java 文件 → 取材基准就是「工作区未提交改动」，不用问用户要 commit。

然后回读卡片，**停下来等**：

```
同步接口到 RAP 前请确认以下信息：

  1. RAP 地址   : http://rap.corp.yljr.com:8080          ← 默认值
  2. RAP 账号   : zhangsan　密码：待你提供
  3. 代码仓库   : D:\code\fc-payment
  4. 开发分支   : feature/quota                          ← 仓库当前分支
  5. 对比分支   : origin/master                          ← 默认值，已 fetch 刷新
  6. 取材基准   : 工作区未提交改动（3 个 Java 文件）
       ?? QuotaController.java
       ?? QuotaQueryDTO.java
       ?? QuotaVO.java
       （pom.xml 的改动不影响接口，不计入）
  7. 需求文档   : docs\需求文档.md（仅作辅助，不作为字段来源）

  本次只会创建和更新接口，不会删除任何 RAP 接口。

以上确认无误请回复「确认」，我再登录并开始取材。
```

用户回「确认，密码是 xxx」之后才登录：

```bash
RAP_BASE_URL='http://rap.corp.yljr.com:8080' RAP_USERNAME='zhangsan' RAP_PASSWORD='***' \
  node "$RAP" login
```

```json
{ "ok": true, "matchedAttempt": "json-email-password", "username": "zhangsan", "sessionFile": "/tmp/xiaoma-generate-rap-interface/session.json" }
```

### 1. 读接口代码

未追踪文件没有 diff，读整个文件即可。

```java
@RestController
@RequestMapping("/api/cust")
public class QuotaController {

    /** 按客户号查询可用额度 */
    @PostMapping("/quota/query")
    public R<QuotaVO> queryQuota(@RequestBody QuotaQueryDTO dto) { ... }

    /** 冻结客户额度 */
    @PostMapping("/quota/freeze")
    public R<Void> freezeQuota(@RequestBody QuotaFreezeDTO dto) { ... }
}
```

需求文档 `D:\code\fc-payment\docs\需求文档.md` 这时只用来核对语义、补字段说明——字段本身以 DTO 为准。

先出改动清单表让用户核对：

```
本次改动 → 待同步接口（请核对，多同步少同步都在这一步纠正）

取材基准：工作区未提交改动（分支 feature/quota，基线 origin/master 已刷新）

┌──────────────────────────┬──────┬─────────────────────────────┬───────────────────┐
│ 改动文件                  │ 状态 │ 接口                         │ 依据              │
├──────────────────────────┼──────┼─────────────────────────────┼───────────────────┤
│ QuotaController.java     │ ??   │ POST /api/cust/quota/query  │ 新增 @PostMapping │
│ QuotaController.java     │ ??   │ POST /api/cust/quota/freeze │ 新增 @PostMapping │
├──────────────────────────┼──────┼─────────────────────────────┼───────────────────┤
│ 不同步：pom.xml（与接口契约无关）                                                    │
└──────────────────────────┴──────┴─────────────────────────────┴───────────────────┘
```

### 1.5 写草案

写到 `D:\code\fc-payment\src\main\java\...\interface-docs\2026-08-12-16-40-05\review.md`（格式见 [reference.md](reference.md#reviewmd-格式)），每条都带上代码来源。

摘要发给用户：

> 从工作区未提交的代码提取到 2 个接口，草案已写到 `interface-docs/2026-08-12-16-40-05/review.md`：
> 1. 查询客户额度 — **POST** `/api/cust/quota/query` — `QuotaController.java:9`（工作区未提交）
> 2. 冻结客户额度 — **POST** `/api/cust/quota/freeze` — `QuotaController.java:14`（工作区未提交）
>
> 是新增还是更新要等选定模块后按路径比对确认。请先从下面选仓库。

### 2. 用户选仓库和模块

```bash
node "$RAP" repos
```

```json
{
  "ok": true,
  "count": 3,
  "repositories": [
    { "id": 108, "name": "供应链金融", "description": "核心交易", "source": "owned" },
    { "id": 233, "name": "支付中心", "description": "", "source": "joined" },
    { "id": 251, "name": "风控平台", "description": "", "source": "joined" }
  ]
}
```

把这三个列给用户，**等用户回复 id**。用户说 108 之后：

```bash
node "$RAP" repo 108
```

```json
{
  "ok": true,
  "repositoryId": 108,
  "repositoryName": "供应链金融",
  "ownerId": 42,
  "moduleCount": 2,
  "modules": [
    { "id": 771, "name": "客户中心", "interfaceCount": 12, "interfaces": [ { "id": 34901, "name": "查询客户信息", "method": "POST", "url": "/api/cust/info", "locker": null } ] },
    { "id": 772, "name": "额度中心", "interfaceCount": 5, "interfaces": [] }
  ]
}
```

再等用户回复 moduleId。用户说 772。

### 2.5 定稿变更类型

模块 772「额度中心」下 `interfaces` 是空的，两条路径 `/api/cust/quota/query`、`/api/cust/quota/freeze` 都匹配不到 → 两条都是**新增**，不需要跑 `itf`。

（如果这里匹配到了，哪怕代码文件是未追踪的新文件，也按**更新**处理，并把这个反常情况告诉用户。）

### 3. 属性转换

按 DTO 结构把入出参写成示例 JSON，存到 `/tmp/quota-query.json`：

```json
{
  "request": { "custNo": "C0001" },
  "response": { "msg": "success", "status": 200, "data": { "quota": 100000 } }
}
```

```bash
node "$RAP" props-from-json /tmp/quota-query.json
```

输出的 `properties` 直接粘进 plan，再按 DTO 补 `description`（`@ApiModelProperty` 或 javadoc）、按校验注解修正 `required`——`props-from-json` 一律给 `required: true`，不能照单全收。

### 3.5 ⛔ 门禁 B：写入前回读

```
⛔ 即将写入 RAP，请确认：

  RAP 地址   : http://rap.corp.yljr.com:8080
  登录账号   : zhangsan
  目标仓库   : 108 供应链金融        ← 你选定的
  目标模块   : 772 额度中心          ← 你选定的
  取材基准   : 工作区未提交改动（分支 feature/quota）
  草案文件   : D:\code\fc-payment\...\interface-docs\2026-08-12-16-40-05\review.md

  本次将写入 2 个接口：

   1. [新增] 查询客户额度   POST /api/cust/quota/query
        入参 1 个 / 出参 4 个　　来源 QuotaController.java:9
   2. [新增] 冻结客户额度   POST /api/cust/quota/freeze
        入参 3 个 / 出参 3 个　　来源 QuotaController.java:14

  两条在模块「额度中心」下都未匹配到同 method + 路径的接口，故判定为新增。
  本次只创建和更新，不会删除任何接口。

以上确认无误请回复「确认」，我再执行同步。
```

用户没回「确认」之前不许往下走。用户说「第 2 条先不传」→ 去掉后**重新回读整张卡片**再等确认。

### 4. 写 plan 并同步

用户回复「确认」之后，写 `interface-docs/2026-08-12-16-40-05/plan.json`（`userConfirmed: true` 就是这次「确认」的凭证）：

```json
{
  "repositoryId": 108,
  "moduleId": 772,
  "userConfirmed": true,
  "interfaces": [
    {
      "changeType": "新增",
      "name": "查询客户额度",
      "method": "POST",
      "url": "/api/cust/quota/query",
      "description": "按客户号查询可用额度",
      "bodyOption": "JSON",
      "properties": [
        { "id": "memory-1", "scope": "request", "name": "custNo", "type": "String", "parentId": -1, "required": true, "description": "客户号" },
        { "id": "memory-2", "scope": "response", "name": "msg", "type": "String", "parentId": -1, "description": "提示信息" },
        { "id": "memory-3", "scope": "response", "name": "status", "type": "Number", "parentId": -1, "description": "状态码" },
        { "id": "memory-4", "scope": "response", "name": "data", "type": "Object", "parentId": -1, "description": "业务数据" },
        { "id": "memory-5", "scope": "response", "name": "quota", "type": "Number", "parentId": "memory-4", "description": "可用额度" }
      ]
    }
  ]
}
```

```bash
node "$RAP" sync "D:\code\fc-payment\src\main\java\com\example\cust\interface-docs\2026-08-12-16-40-05\plan.json"
```

```json
{
  "ok": true,
  "repositoryId": 108,
  "repositoryName": "供应链金融",
  "moduleId": 772,
  "moduleName": "额度中心",
  "total": 1,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "ok": true,
      "name": "查询客户额度",
      "action": "create",
      "interfaceId": 34936,
      "unlocked": true,
      "metaUpdated": true,
      "properties": { "sent": 5, "assignedMemoryIds": [], "warnings": [] },
      "editorUrl": "http://rap.corp.yljr.com/repository/editor?id=108&mod=772&itf=34936"
    }
  ]
}
```

### 5. 回报

> 1 个接口已同步到「供应链金融 → 额度中心」，0 失败。
> 查询客户额度（新增，接口 id 34936）：<http://rap.corp.yljr.com/repository/editor?id=108&mod=772&itf=34936>

## 场景二：更新已有接口（代码是已修改文件）

取材路径和场景一完全一样，差别只在 git 状态是 ` M` 而不是 `??`，以及**已有字段必须原样带回**——漏带的字段等于删除。

### 2.1 找接口代码

```bash
git -C D:/code/fc-payment status --porcelain
```

```
 M src/main/java/com/example/cust/controller/CustController.java
 M src/main/java/com/example/cust/dto/CustInfoQueryDTO.java
?? src/main/java/com/example/cust/dto/CustQuotaVO.java
   pom.xml
```

有未提交的 Java 改动 → **直接用，不问用户**。看清这次改了什么：

```bash
git -C D:/code/fc-payment diff -- '*.java'
```

```diff
+    /** 是否带回额度 */
+    private Boolean withQuota;
```

如果 `status --porcelain` 是空的，或者只有 `pom.xml`、`.md` 这类非接口文件，才走另一条路——把候选提交列给用户。**候选清单用「本分支自己的提交」，不是裸 `git log -20`**，否则会把 master 上别人的提交摆进来让用户误选：

```bash
git -C D:/code/fc-payment log --no-merges --format='%h %an %ad %s' --date=short feature/quota --not origin/master
git -C D:/code/fc-payment log --no-merges --shortstat feature/quota --not origin/master | tail -3
```

```
a1b2c3d 张三 2026-08-11 客户信息接口增加额度返回
9f8e7d6 李四 2026-08-09 修复额度精度问题
 3 files changed, 47 insertions(+), 6 deletions(-)
```

> 当前分支 `feature/quota` 相对 `origin/master` 有 2 个自己的提交（共 3 个文件，+47/-6）。工作区没有未提交的接口改动，请指定用哪次提交的代码，或者说「两条都要」。

（这条命令**输出为空** → 分支相对基线没有自己的提交，停下来问用户：分支给错了？代码没推？）

用户回 `a1b2c3d` 之后，按**该提交时点**的内容读文件（不是工作区）：

```bash
git -C D:/code/fc-payment show --stat a1b2c3d
git -C D:/code/fc-payment show a1b2c3d:src/main/java/com/example/cust/controller/CustController.java
```

### 2.2 读代码提取定义

```java
@RestController
@RequestMapping("/api/cust")
public class CustController {

    /** 按客户号查询客户基本信息 */
    @PostMapping("/info")
    public R<CustInfoVO> queryInfo(@RequestBody CustInfoQueryDTO dto) { ... }
}
```

```java
public class CustInfoQueryDTO extends BaseReqDTO {   // ← 父类字段别漏
    /** 客户号 */
    @NotBlank
    private String custNo;
    /** 是否带回额度 */
    private Boolean withQuota;
}
```

得到 `POST /api/cust/info`，入参 `custNo`(String, required) + `withQuota`(Boolean) + `BaseReqDTO` 里的公共字段，出参 `R<CustInfoVO>` 拆成 `msg` / `status` / `data`，`data` 下面展开 `CustInfoVO`。详见 [code-extraction.md](code-extraction.md)。

### 2.3 定稿变更类型并与平台现状比对

用户选定仓库 108、模块 771 后，`repo 108` 返回的模块 771 里有 `{ "id": 34901, "method": "POST", "url": "/api/cust/info" }` —— 和代码提取的 `POST /api/cust/info` 匹配上了，所以是**更新**，`interfaceId` 就是 34901。

```bash
node "$RAP" itf 34901
```

```json
{
  "ok": true,
  "interface": { "id": 34901, "name": "查询客户信息", "method": "POST", "url": "/api/cust/info", "repositoryId": 108, "moduleId": 771, "locker": null },
  "properties": [
    { "id": 880101, "scope": "request", "name": "custNo", "type": "String", "parentId": -1, "priority": 1, "pos": 3, "required": true, "value": "", "description": "客户号" },
    { "id": 880103, "scope": "request", "name": "legacyFlag", "type": "String", "parentId": -1, "priority": 3, "pos": 3, "required": null, "value": "", "description": "旧标识" },
    { "id": 880102, "scope": "response", "name": "status", "type": "Number", "parentId": -1, "priority": 2, "pos": 2, "required": null, "value": "", "description": "状态码" }
  ]
}
```

`legacyFlag` 平台上有、代码里没有 —— **不许静默丢弃，先问**：

> 平台上的入参 `legacyFlag`（旧标识）在当前代码的 `CustInfoQueryDTO` 里找不到。是代码里已经删掉了（RAP 也该删），还是我漏提取了？

用户回「保留，那个字段还有老系统在用」。**没拿到这个答复之前不许进门禁 B** —— 卡片里带着「待你回答」的项就不算一张可确认的卡片。

### 2.4 ⛔ 门禁 B：写入前回读

```
⛔ 即将写入 RAP，请确认：

  RAP 地址   : http://rap.corp.yljr.com:8080
  登录账号   : zhangsan
  目标仓库   : 108 供应链金融
  目标模块   : 771 客户中心
  取材基准   : 提交 a1b2c3d「客户信息接口增加额度返回」（分支 feature/quota）
  草案文件   : D:\code\fc-payment\...\interface-docs\2026-08-12-16-40-05\review.md

  本次将写入 1 个接口：

   1. [更新] 查询客户信息   POST /api/cust/info  (interfaceId=34901)
        + 新增入参 withQuota (Boolean, 选填, 是否带回额度)
        = 保留 custNo / status
        = 保留 legacyFlag（代码里已无，按你的答复保留）
        来源 CustController.java:18 @ a1b2c3d

  判定为更新的依据：模块「客户中心」下匹配到同 method + 路径的接口 34901。
  本次只创建和更新，不会删除任何接口。

以上确认无误请回复「确认」，我再执行同步。
```

### 2.5 写 plan 并同步

平台已有的两条**连 `id` 和 `priority` 一起抄回去**，代码里新加的字段不写 `id`（脚本会分配 `memory-N`）：

```json
{
  "repositoryId": 108,
  "moduleId": 771,
  "userConfirmed": true,
  "interfaces": [
    {
      "changeType": "更新",
      "interfaceId": 34901,
      "name": "查询客户信息",
      "method": "POST",
      "url": "/api/cust/info",
      "description": "按客户号查询客户基本信息",
      "bodyOption": "JSON",
      "properties": [
        { "id": 880101, "scope": "request", "name": "custNo", "type": "String", "parentId": -1, "priority": 1, "required": true, "description": "客户号" },
        { "id": 880103, "scope": "request", "name": "legacyFlag", "type": "String", "parentId": -1, "priority": 3, "description": "旧标识" },
        { "scope": "request", "name": "withQuota", "type": "Boolean", "parentId": -1, "required": false, "description": "是否带回额度" },
        { "id": 880102, "scope": "response", "name": "status", "type": "Number", "parentId": -1, "priority": 2, "description": "状态码" }
      ]
    }
  ]
}
```

- `custNo` / `legacyFlag` / `status` 是平台已有字段，`id` 和 `priority` 原样抄回——`legacyFlag` 就是靠这一条才没被删掉。
- `withQuota` 是新字段，不写 `id`，脚本自动分配 `memory-1` 并给 `priority`，结果里以 `assignedMemoryIds` 报出来。

## 场景三：部分失败

```json
{
  "ok": false,
  "total": 3,
  "succeeded": 2,
  "failed": 1,
  "results": [
    { "ok": true, "name": "查询客户额度", "action": "create", "interfaceId": 34936, "editorUrl": "..." },
    { "ok": true, "name": "冻结客户额度", "action": "create", "interfaceId": 34937, "editorUrl": "..." },
    { "ok": false, "name": "解冻客户额度", "method": "POST", "url": "/api/cust/quota/unfreeze", "error": "变更类型为「更新」，但在所选模块下未匹配到接口；请补 interfaceId 或核对 method + 路径。" }
  ]
}
```

如实报给用户：成功 2 条给链接，失败 1 条给原因和修法（去 `repo 108` 里找这个接口的真实 id 补进 plan，或把变更类型改成「新增」）。**不要**只报成功的那部分。
