---
name: miloco-miot-identity-register
description: 家庭成员身份注册主流程。**用户表达「想让系统认识/记住/录入某个人」的意图就触发本 SKILL,不要求出现"注册/登记"字面词**——包括上传图/视频指明某人是谁、回应 `[感知引擎]` 陌生人推送、说"建档案/录入家人/记下样子"、指认摄像头里某人身份、从近期陌生人记录里挑人建档、撤销注册、查看某人样本。覆盖"上传图/视频"与"从摄像头挑人"两条通路,所有路径需用户确认才入库。仅改 person 行(改名/删人,不涉及样本)走 miloco-miot-identity。
metadata:
  author: miloco
  version: "1.4"
  date: "2026-05-20"
  openclaw:
    requires:
      bins: ["miloco-cli"]
---

# miloco-miot-identity-register

## 何时激活

按 **"附件 + 来源 hint"** 两个维度分入口形态:

| 用户意图 / 触发源                                  | 用户说了类似…                                                                                                | → 路由                                                                  |
|----------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| **附件主动注册**                                   | "这是张三"(附图/视频)/ "帮我登记"(附图/视频)                                                                | 上传通路(第三步 3.1 / 3.2)                                            |
| **无附件主动注册 · 无来源 hint**                   | "帮我登记张三" / "建个档案" / "注册家人"(无附件, 也没说从哪来)                                              | 双入口话术(第二步 2.3)后让用户分流                                    |
| **无附件主动注册 · 指明摄像头**                    | "X 摄像头下的那位是 yyy 帮我注册" / "刚走进门口的是张三, 登记一下" / "玄关摄像头看到的那个" + 姓名/家庭角色   | 跳过双入口话术,直接锁定该摄像头候选(第三步 3.3 · 摄像头作用域变体)    |
| **无附件主动注册 · 想直接挑**                      | "从看到的人里挑" / "看看现在摄像头下都有哪些陌生人" / "你帮我挑一个" / "最近有谁来过"                         | 跳过双入口话术,直接跨摄像头取候选(第三步 3.4)                         |
| **推送陌生人响应**                                 | `[感知引擎]` 推送"陌生人在 X 活动" + 用户回"这是 XXX"(或仅"对" "登记一下"等隐式确认)                       | 锁定推送 hint 的候选(第三步 3.3 · 推送响应变体)                       |
| **撤销刚才的注册**                                 | "刚才挑错了,撤销" / "重选"                                                                                  | 附加操作 · 撤销                                                         |
| **查看某人的样本**                                 | "看看张三的样本" / "登记成什么样了"                                                                          | 附加操作 · 看样本                                                       |
| 纯改 name / role / 删 person 行                    | "把张三的家庭角色改成爸爸" / "删除张三"                                                                      | → miloco-miot-identity                                                  |

**判断规则**:
- 涉及"样本 / 照片 / 视频 / 录入 / 注册 / 登记 / 让系统认识这人" → **本 SKILL**
- 只是改 DB 行(纯 name / role / 删 person) → **miloco-miot-identity**

**边界判断**:
- 用户既上传图又说"删掉张三" → 拆为两个意图,删走 identity, 注册走本 SKILL
- 用户的话同时含"指明摄像头" + "想直接挑"(如"看看玄关下有谁") → 取**指明摄像头**优先,带 `--cam` 单 cam 取候选

## 总原则 · 引导用户回到设计流程

本 SKILL 只支持上表列出的入口和动作。当用户的表达**偏离这些入口**时(例如纯文字描述"他长这样"、想用录音当样本、要从历史录像里指定时段抽取等),**不要尝试用其他工具组合"模拟"出一条非标流程**,而是用一句话告知不支持 + 指明能走的标准入口。

- 用户的请求只是"换个说法说同一件事" → 直接照标准入口跑
- 用户的请求需要 SKILL 没有的能力 → 一句话引导回到"上传素材"或"从摄像头记录里挑"两条入口之一

理由:注册流是固定设计,任何"擦边"操作都会绕过用户确认环节(约束 1)或导致样本质量失控。引导用户回到标准入口是对系统行为可控性的最低要求。

## 核心工作流

**5 步流水线**:

1. **解析触发源** — 识别 6 种入口形态(详见第一步)
2. **通路判定** — 上传通路 / 陌生人候选通路; 无附件无 hint 时先走 2.3 双入口话术
3. **首轮候选获取** — 按通路调命令 + 发拼图 + 文字
4. **等待用户回复** — 本轮不发任何中间消息
5. **次轮入库** — 确认 → `commit` / `from-cluster`; 取消 → 不入库, 清退会话

### 推荐路径

- **上传通路优先推荐视频**(≥ 15 秒, 至少 5 秒正脸): 多角度 + 姿态变化覆盖更广, 单次拿到的高质量样本数远多于多张静态图
- **单张图也接受**, 但提示用户"补几张不同角度或拍段 15 秒视频效果更好"——不强制重发
- **无附件 + 无 hint**: 走 2.3 双入口话术让用户选, 不替用户做主
- **fetch 出 0 组 / 样本过少时**, 发现场采集引导后等用户回复, 不主动重复 fetch:

  > 让目标人**站到任一摄像头前**, 在画面中间停留至少 15 秒, 期间**原地慢慢转一圈身体 + 转转头**(正脸 + 侧脸 + 后背几个角度), 然后回我"拍好了"我重新拉记录。

## 第一步 · 解析触发源

按消息来源 + 是否附素材区分 6 种入口:

| 入口形态                                       | 附件         | 候选标识  | 走哪个分支                                                |
|-----------------------------------------------|--------------|-----------|-----------------------------------------------------------|
| 用户消息附图 ≥ 1 张(无视频)                   | ✓ 图        | —         | 第三步 3.1 · 上传通路 · 图片分支                          |
| 用户消息附视频 1 个                            | ✓ 视频      | —         | 第三步 3.2 · 上传通路 · 视频分支                          |
| 用户消息附图 + 视频混合(罕见)                | ✓ 混合      | —         | 优先走视频分支,提示用户图后续可单独再注册一次             |
| `[感知引擎]` 推送陌生人响应 + 用户回"这是 XXX" | ✗            | ✓ cam+轨迹 | 第三步 3.3 · 陌生人候选通路 · 锁定候选                    |
| 用户无附件主动说"帮我登记 XXX"                 | ✗            | ✗         | 第二步走双入口话术后再分流                                |
| 用户回"撤销" / "重选"                          | —            | —         | 附加操作 § 撤销                                           |

## 第二步 · 通路判定

### 2.1 决策树

```
收到用户 / 系统消息:
  if 来源 == [感知引擎] 推送陌生人响应:
      → 3.3 推送响应变体: pool fetch --cam X --track Y  (跳过双入口话术)

  elif 含附件:
      if 视频:
          → 3.2 视频分支
          (混合图+视频时优先视频; 图片可在视频登记完成后单独再注册一次, 本次不混合处理)
      elif 图: → 3.1 图片分支

  else:  # 无附件
      if 出现摄像头名 / 位置(玄关/门口/客厅摄像头):
          → 3.3 摄像头作用域变体: pool fetch --cam X --window <按 2.4 抽取>  (跳过话术)
      elif "想直接挑"语义(看看现在谁在 / 你帮我挑 / 从看到的人里挑):
          → 3.4 跨摄像头: pool fetch --window <按 2.4 抽取>  (跳过话术)
      else:  # 既无附件也无来源 hint
          → 走 2.3 双入口话术, 等待用户回复, 本轮结束
            用户回 "1"/"我发图" → 简短确认"好,等你发素材", 等下一条附素材后重进第一步
            用户回 "2"/"从看到的人里挑" → 3.4 跨摄像头
            用户附素材(合并发送) → 直接进 3.1 或 3.2
            无明确意图 → 继续等
```

### 2.2 姓名 / 家庭角色解析(所有分支共用)

用户在注册指令里**可能会同时提供姓名 + 家庭角色**, agent 要从一句话里抽字段: 真名 `name`(必填)写入 `--member-name`(或 `--name`); 家庭角色 `role`(可选, 如爸爸/妈妈)抽到就写入 `--member-role`(或 `--role`), 抽不到就**省略**。`name` 和 `role` 完全独立: role 留空就是"没有家庭角色", 绝不会用 name 顶替, name 也不会被 role 覆盖。

**抽取速查**:

| 用户原话                    | name      | role     |
|-----------------------------|-----------|----------|
| "这是我爸爸李建国, 帮我注册" | 李建国    | 爸爸     |
| "登记一下张三"              | 张三      | (空, 省略 `--role`, role 留空不影响 name) |
| "这是我妈"(只有家庭角色)    | (空, **追问真名**) | 妈妈     |

完整规则 / 边界情况 / CLI 命令 `--member-name` vs `--name` 前缀差异详见 [references/name-parsing.md](./references/name-parsing.md)。

### 2.3 双入口话术(约束 4 · 仅无附件无 hint 时用)

话术按用户是否给姓名分两版,**模板保持精简, 不要改写或加细节**(免得用户读两屏才看到选项)。两条入口里, **视频前置 + 视频参数显式**, 这是 SKILL 推荐用户走的最佳路径。

**无姓名情况**:

```
用户: 注册家人   /   帮我登记一下

Agent: 好的,要建家庭成员档案。两条路径任选,告诉我走哪条:

       1. **你来发素材**(优先视频; 也可发照片 + 姓名)
          - 视频 ≥ 15 秒(至少 5 秒正脸), 或 2~5 张不同角度照片
          - 跟"这是 XXX"的文字**同一条消息**发我

       2. **从摄像头看到的人里挑**(不用再发图)
          - 摄像头过去一段时间看到的陌生人记录,可以从里面挑
          - 我把这些拼成一张图并标好编号发你,你回编号 + 给名字
```

**有姓名情况**:

```
用户: 帮我登记张三

Agent: 好的,要给「张三」建档案。两条路径任选:

       1. **你来发素材**(优先视频; 也可发照片)
          - 视频 ≥ 15 秒(至少 5 秒正脸), 或 2~5 张不同角度照片
          - 把张三的视频 / 照片跟这条指令**同一条消息**重发

       2. **从摄像头看到的人里挑**(不用再发图)
          - 摄像头过去一段时间看到的陌生人记录,可以从里面挑
          - 我把这些拼成一张图并标好编号发给你, 你回编号确认哪位是张三
```

发完话术后**本轮终止等待用户回复**, 绝不擅自走任一条。

### 2.4 时间 hint 抽取(陌生人候选通路用 · 3.3 / 3.4 共用)

用户消息里可能含时间范围, agent 把它转成 `pool fetch --window <秒数>` 并在话术里用 `{时间表述}` 占位(取本表第 3 列对应行的值)。后端 cluster pool TTL 是 72 小时(259200 秒), 是 `--window` 的硬上限——传更大也只能拿到 72h 内的记录。

**抽取速查**:

| 用户时间 hint                                | --window (秒)                | `{时间表述}` 填什么     |
|----------------------------------------------|------------------------------|-------------------------|
| (没说) / "刚才" / "最近几分钟"               | 300(默认,可省略 `--window`) | "近 5 分钟"             |
| "半小时" / "一小时" / "今天上午" / "今天"    | 按粒度换算, ≤ 86400          | 按用户原话, 如"今天上午" |
| "昨天" / "前天" / "这两天" / "几天前"        | 按粒度换算, ≤ 259200(72h)   | 按用户原话, 如"昨天"     |

**抽不出 / 模糊 ("最近" / "前阵子")** → 用默认 300 (即 `{时间表述}` = "近 5 分钟"), 首轮文字末尾追加:"如果没看到目标人, 告诉我大概什么时候来过, 我拉更长范围。"

**by-id 注册不受 window 限制**: `register from-cluster` 服务端按 cluster_id 直查, 只要 cluster 还在池内(≤ 72h TTL)就能注册——所以"用户在 `pool fetch` 看到的候选, 一定能注册"这条 UX 承诺成立。`--window` 只影响 list 模式那一次 fetch 拉到哪些候选给用户挑。

## 第三步 · 首轮候选获取

按通路分四个子分支。**所有子分支的共同终态**:发一张候选拼图给用户 + 一段文字,然后进第四步等待回复。

**本步硬约束**(违反会导致样本错乱、字段丢失、图发不出去或首轮体验降级):
- ❌ **每个 turn 内 `register preview` / `pool fetch` 恰好调一次**, 不要"刷新看看 / 再查一下 / 重试一次"。即使第一次结果不理想(候选少 / weak_diversity / 4 组比预期 5 组少), 也直接按结果发拼图 + 文字让用户决定。用户对结果不满意, 会主动回"更多"/"重新发素材", 那时再走对应分支(翻页 / 重发素材), 不要 agent 自己代用户决定"再试一次"
- ❌ 不要给 CLI 加 `| python3 -c` / `| jq` / `| grep` 等管道后处理 —— 会丢 `multi_track / tracks / montage_kind / clusters_total` 等关键字段
- ❌ 不要把后端拼好的拼图拆成多张消息发给用户 —— 拼图就一张 jpg, 发这一张即可
- ❌ 不要在 tool 调用前 / 后到本步最终 reply 之间发"收到视频,正在分析..."等中间状态消息 —— tool 调用几秒到几十秒的延迟可接受, 用户等就是了。整步只该有一条 assistant 消息(就是末尾的拼图 + 文字)
- 🚨 **发图必须用 `message` 工具显式上传, 不要用 `MEDIA:<path>` 内联标记**: 飞书 DM 会话 `capabilities=none`, MEDIA: 标记不渲染, 用户看不到图。正确调用: `message action=send media=<拼图本地路径>`(再单独 reply 发文字, 或 message 工具自带 text 字段一起发)。**任何子分支的发图都走这条路径**, 不要在 reply 文本里嵌入 `MEDIA:` / `![](path)` / `<img>` 等内联引用

**用户没给姓名的处理**:任意子分支拿到 ok 候选时,如果用户消息里**没出现要登记的人的姓名/家庭角色**,首轮文字末尾追问名字。例如把"要登记到「{name}」?"改成"要登记到哪位家人?请告诉我 TA 叫什么"。用户在次轮回复时把"确认/数字" + "姓名"一起说,如"2,张三"。

---

### 3.1 上传通路 · 图片分支

附件数 = 1:

```bash
miloco-cli identity register preview \
    --images /tmp/<uuid>_1.jpg \
    --topk 5 --save-montage /tmp/<uuid>_preview.jpg --pretty
```

附件数 ≥ 2 (约束 3,**单次批量**):

```bash
miloco-cli identity register preview \
    --images /tmp/<uuid>_1.jpg \
    --images /tmp/<uuid>_2.jpg \
    --images /tmp/<uuid>_3.jpg \
    --topk 5 --save-montage /tmp/<uuid>_preview.jpg --pretty
```

返回字段:
- `register_session_id_pending` —— 候选会话 id, 第五步 commit 用
- `auto_selected_indices` —— 后端挑出的候选索引数组, 第五步 commit 用
- `auto_selected_body_count` / `auto_selected_face_count` —— 拼图里人体 / 人脸数
- `multi_track` —— 图片分支永远是 `false`(图片不做跨图轨迹关联,即使多人合影也走单候选路径)
- `status_preview` —— `ok` / `weak_diversity` / `no_valid_subject`
- `montage_saved_to` —— 拼图本地路径,本轮发给用户的就是这张

首轮文字(按 `status_preview` 选,见 §异常处理表)。

---

### 3.2 上传通路 · 视频分支

```bash
miloco-cli identity register preview \
    --video /tmp/<uuid>.mp4 \
    --topk 5 --save-montage /tmp/<uuid>_preview.jpg --pretty
```

看顶层 **`multi_track`** 字段决定走单候选还是号码图:

| `multi_track` | 含义 | 首轮文字(有姓名 / 无姓名时句尾追加"叫什么名字") | 第五步选号字段 |
|---|---|---|---|
| `false` | 视频只 1 人, 自动选样拼图(body 256h + face 128h) | "找到 N 张身体 + M 张人脸样本, 要登记到「{name}」?回'确认'" | 顶层 `auto_selected_indices` |
| `true` | 视频 ≥ 2 人, 编号拼图(每人 1 张代表样本, 左上角 `[1] [2] [3]`) | "视频里识别出 N 个人(图中编号), 要登记的是哪一位?回数字 1/2/..." | `tracks[N-1].auto_selected_indices_global` |

`multi_track=true` 时 `tracks` 元素结构: `{label, track_id, auto_selected_indices_global, body_count, face_count, auto_status}`。其中 **`auto_selected_indices_global`** 是第五步 commit 必须用的字段; `auto_status` ∈ `ok` / `weak_diversity` / `no_valid_subject`。

⚠️ **视频路径 `tracks` 不含 `cluster_id`**——commit **必走 `register commit` + `--indices`**, 禁止走 `register from-cluster`(`from-cluster` 是陌生人候选通路(3.3 / 3.4)专用, 那里的 `tracks` 才含 `cluster_id`)。

⚠️ **多人视频 commit 绝不能用顶层 `auto_selected_indices`**——那是跨人物平铺, 会把不同人的样本混入同一档案。**必须用 `tracks[N-1].auto_selected_indices_global`**。

⚠️ 多人视频**禁止让用户重发"只含目标人的视频"**——编号拼图已分人, 用户回数字即可。

**号码图封面**: 后端自动优先挑带 face 的候选作 `[N]` 代表样本(同 face 状态下按打分降序), 整 track 都无 face 时退化为纯打分。agent 直接发拼图即可。

---

### 3.3 陌生人候选通路 · 单 cam 作用域

> 两个变体共用本节命令, 差别只在 `pool fetch` 给不给 `--track`。

**变体 A · 推送响应触发**(消息来自 `[感知引擎]`, 自带摄像头 + 轨迹标识):

```bash
miloco-cli identity pool fetch \
    --cam <X> --track <Y> \
    --save-montage /tmp/<uuid>_pool.jpg --pretty
```

由于推送把单一轨迹也锁定了, 通常返回**单候选**(只有 1 组)。

**变体 B · 用户指明摄像头**(用户的话里出现摄像头名 / 位置, 但没指明是哪条轨迹):

🚨 **必须先做 cam_id 映射**: 后端 `--cam` 参数走**严格字符串相等**过滤(`e.cam_id == "<X>"`)。用户口语"C700"/"玄关"/"门口" 通常和后端真实 `cam_id` (如 `MI_C700_<MAC>` / `cam_001` / `c700_kitchen`)对不上, 直接透传会查到 0 候选(且没有报错, 静默返空, 极易误判"摄像头下没有陌生人")。

**正确步骤**:

1. 先调一次 `miloco-cli device list --category camera --pretty` 拿全量摄像头列表 + 真实 `cam_id`
2. 用 LLM 语义匹配把用户口语映射到列表里的真实 cam_id, **要兼容**: 别称("C700" / "客厅 C700" / "客厅摄像头"等指同一台)、错别字("C7000" 漏 0)、位置描述("玄关" / "门口" 映射到对应位置的摄像头)
3. **匹配结果 1 个** → 用真实 cam_id 调 fetch
4. **匹配结果 ≥ 2 个或匹配不到** → 列候选追问用户("家里有 N 台摄像头: X / Y / Z, 你说的是哪台?")

```bash
# Step 1: 拉真实 cam_id 列表
miloco-cli device list --category camera --pretty

# Step 2: agent LLM 语义匹配 (e.g. 用户说"C700" → 命中 cam_id="MI_C700_AABBCC")

# Step 3: 用真实 cam_id 调 fetch(window 按 2.4 抽取, 默认 300 可省略)
miloco-cli identity pool fetch \
    --cam <真实 cam_id, 不是用户口语字符串> \
    --window <按 2.4 抽取的秒数, 默认 300> \
    --save-montage /tmp/<uuid>_pool.jpg --pretty
```

该摄像头在指定时间窗口内的全部候选都会返回(无时间 hint 时默认近 5 分钟), 可能 1 组也可能多组——后续走第四步用户选号或确认。

⚠️ **不要把用户口语字符串直接透传给 `--cam`**, 即使口语和真实 cam_id 看起来字面接近(如用户说"C700", 后端可能是"C700_KITCHEN" — 多了后缀, 字符串相等会失败)。**任何时候 `--cam` 后跟的必须是从 `device list` 拿到的字面 cam_id**。

**两变体共同返回字段** 同 3.4 节 `tracks` 表格(见下), 不再赘述。

首轮文字模板(有姓名时):
- **单候选**(变体 A 常见): "锁定到 1 组候选(N 张样本,跨 X 摄像头), 要给「{name}」创建档案?回'确认'入库。"
- **多候选**(变体 B 常见): "{摄像头名}{时间表述}看到 N 组陌生人(图中编号), 要给「{name}」选哪一组?回数字 1/2/..."

无姓名时,按第二步 2.2 抽取规则尝试补;实在抽不到时追问"叫什么名字"(家庭角色抽不到则省略, 不必追问)。

---

### 3.4 陌生人候选通路 · 跨摄像头

来源:走完双入口话术后选"2",或决策树识别出"想直接挑"语义直接路由(细则见 2.1)。

```bash
# window 按 2.4 抽取的秒数(默认 300 可省略 --window), 上限 259200 (72h TTL)
miloco-cli identity pool fetch \
    --window <按 2.4 抽取的秒数, 默认 300> \
    --save-montage /tmp/<uuid>_pool.jpg --pretty
```

返回指定时间窗口内(无 hint 时默认近 5 分钟)跨摄像头的 top-N 候选(后端最多展 6 个,质量分排序;**带正脸的候选会被排到前面**)。多候选时让用户回数字选号。

返回字段:
- `clusters_total` —— 实际返回的候选总数
- `clusters_displayed` —— 拼图里展示的候选数(最多 6)
- `offset` —— 本页起始位置(回显入参)
- `next_offset` —— 下一页起始位置;`null` 表示已到末页
- `has_more` —— 是否还有下一页
- `tracks` —— 候选元数据数组(下表展开)
- `montage_saved_to` —— 拼图本地路径

`tracks` 数组每个元素结构(与 3.2 视频分支 tracks 字段**不同**,这里没有 `auto_selected_indices_global`):

| 字段              | 含义                                                                |
|-------------------|---------------------------------------------------------------------|
| `label`           | 候选组编号("1"/"2"/"3" ...) , 跟拼图里左上角 `[1]` `[2]` 对齐       |
| **`cluster_id`**  | **第五步 commit 必须用的字段**, 用户选号 N 后取 `tracks[N-1].cluster_id` |
| `total_crops`     | 该候选组里的样本数                                                  |
| `span_cam_count`  | 该候选组跨了几个摄像头(≥ 2 时拼图会带视觉复核 hint)               |
| `rep_sharpness`   | 该候选组代表样本的清晰度                                            |
| `earliest_ts` / `latest_ts` | 该候选组首次 / 末次出现时间                              |

首轮文字模板(无姓名时句尾追加"叫什么名字"):
- **单候选**:"{时间表述}看到 1 组陌生人(N 张样本), 要给「{name}」创建档案?回'确认'入库。"
- **多候选**:"{时间表述}看到 N 组陌生人(图中编号), 要给「{name}」选哪一组?回数字 1/2/..."
- **`has_more=true`** 时文字末尾加"还有 M 组没展示, 可回'更多'看下一页"

**翻页用法**(用户回"更多"时):

```bash
# 用上次响应的 next_offset 重发 fetch(window 保持和首次一致):
miloco-cli identity pool fetch --window <与首次 fetch 相同的秒数> \
    --offset <next_offset> \
    --save-montage /tmp/<uuid>_pool_p2.jpg --pretty
```

注意:
- 翻页号码图编号仍从 `[1]` 开始(本页内编号, 不延续上页), 用户回数字 N 时取**新返回 `tracks` 的 `[N-1]`**
- 旧的 `register_session_id_pending` / `tracks` 数组**作废**, 必须用新返回的
- `next_offset = null` 时不要再翻; 回用户"没有更多了, 要不重新发素材?"

---

## 第四步 · 等待用户回复

第三步发完拼图 + 文字后,**本步是纯等待**——既不发任何主动消息, 也不主动调任何 tool, 直到用户在下一轮回复消息。

**本步硬约束**(违反等同违反约束 1):
- ❌ 用户没回复时主动催 / 重发拼图 —— 候选 session 10 分钟内有效, 等就是了
- ❌ 收到用户回复后, 先发一段"准备入库, 稍等"再调入库命令 —— 直接进第五步调命令即可
- ❌ 重复 / 复述用户已经看到的拼图描述("刚才那张图是…") —— 直接进入第五步的入库或取消分支

> 第三步和第五步各自的命令调用约束分别归在那两步, 本步只管"等待 + 收到回复后的最简处理"。

**用户可能的回复**:
| 用户回复                                       | 解析                                          | 下一步           |
|------------------------------------------------|-----------------------------------------------|------------------|
| "确认" / "OK" / "好" / "yes"                    | 单候选场景默认 N=1, 多候选需追问                | 第五步 5.1 / 5.2 |
| 数字 N / "[N]" / "N 号" / "第 N 个"             | 选号                                          | 第五步 5.1 / 5.2 |
| "取消" / "重选" / "不要"                       | 用户放弃                                      | 第五步 5.3       |
| "更多"(仅多候选 + has_more)                  | 翻页:用上次响应的 `next_offset` 重发 `pool fetch --offset <X>`(详见 3.4) | 重做第三步       |
| 用户回数字但没给姓名(首轮也没给)              | 追问名字                                      | 不入库,反问     |
| 完全无回应                                     | 候选 session 在后端内存中保留 10 分钟,超时需重发素材 | 不主动催        |

## 第五步 · 次轮入库 / 处理取消

按上一步用户的回复分三个分支。

### 5.1 上传通路 → `register commit`

**单候选**(图片分支 / 单人视频):

```bash
miloco-cli identity register commit \
    --pending-id <第三步记下的 register_session_id_pending> \
    --indices <auto_selected_indices 逗号拼接,如 "0,3,7"> \
    --member-name <name> [--member-role <role>] --pretty
```

**多人视频**(用户回数字 N):

```bash
miloco-cli identity register commit \
    --pending-id <第三步记下的 register_session_id_pending> \
    --indices <tracks[N-1].auto_selected_indices_global 逗号拼接> \
    --member-name <name> [--member-role <role>] --pretty
```

⚠️ **`--member-role` 抽到了才传, 抽不到时省略**(规则见 2.2 节)。`name` 和 `role` 完全独立: role 留空就是"没有家庭角色", **绝不会**用 name 顶替, name 也不会被 role 覆盖。抽到家庭角色却漏传 `--member-role` 属于漏传错误(本应是"爸爸"的角色变成空)。

成功后回复:"已为「{name}」入库 N 张样本"。

### 5.2 陌生人候选通路 → `register from-cluster`

```bash
miloco-cli identity register from-cluster \
    --name <name> [--role <role>] \
    --cluster-id <tracks[N-1].cluster_id, 单候选时 N=1> \
    --pretty
```

⚠️ **`--role` 同 5.1 规则**: 抽到了才传, 抽不到时省略; role 留空就是"没有家庭角色", 不会用 name 顶替。

成功后回复:"已为「{name}」入库 N 张样本"。

### 5.3 用户回"取消" / "重选"

**不调任何入库命令**,直接回复:"已取消,样本未入库。要重新发素材吗?"。

候选 session 在后端内存中 10 分钟内无操作会自动过期, 不需要手动清理。

## 用户可见输出(硬约束)

### 术语黑名单

回复用户时**禁止**出现以下内部技术术语,改用自然语言:

| ❌ 禁用                                                  | ✅ 改用                                                                            |
|----------------------------------------------------------|------------------------------------------------------------------------------------|
| "陌生人池" / "池子" / "候选池" / "识别池"                | "摄像头最近的陌生人记录" / "近 X 分钟看到 N 组陌生人" / "从最近看到的陌生人里挑"     |
| "track" / "cluster" / "cluster_id" / "auto_selected_indices" | 改用"组" / "位" / "候选"                                                          |
| "preview" / "commit" / "fetch"                           | 改用自然语言:"准备登记什么" / "入库" / "拉一下记录"                                |
| 任何内部 id (pending_id / session_id / job_id)           | 用户不需要知道,完全不出现                                                          |

下游 SKILL 流程描述里(给技术人员看的部分)继续用这些术语没问题; 只看 agent 实际发给用户的 reply 文本,这部分要清理干净。

### 视频路径失败时的拍摄指引(必须逐字复制,不要改写——这些参数是后端模型硬要求,改写易丢关键数字)

视频通路 `status_preview ∈ {weak_diversity, no_valid_subject}` 时, 首轮文字末尾**必须**附:

```
重新拍摄建议:
1. 视频画面包含目标的腿膝盖以上,时长建议不小于 15 秒
2. 视频至少保证 5 秒的正脸露出
```

## 关键约束

> 4 条约束适用于所有路径, 任何场景都不得违反。第三步已展开各路径具体怎么走, 本节集中给出**为什么这么走 + 不这么走的反例**。

### 约束 1 · 候选展示与入库必须分阶段, 中间等待用户明确回复

"候选获取"与"入库"是**两个独立阶段**, 中间**必须有用户明确回复**才能推进:
- 上传通路: `register preview` → 等待 → `register commit`
- 陌生人候选通路: `pool fetch` → 等待 → `register from-cluster`
- CLI 的 `register from-media` 是开发调试用的,**agent 不得调用**

> ⚠️ **"两个阶段"≠"总共 2 轮对话"**: 上传 + 给姓名 → 候选拼图 → 确认 = **2 轮**(场景 1/2/4); 无附件 → 双入口话术 → 选 2 → 候选拼图 → 确认 = **3 轮**(场景 5)。关键判定在"获取与入库中间是否有明确回复", 不在总轮数。

理由: 用户预期"系统给我看一眼准备登记什么, 我说 OK 才入库"。视频 / 多人合影场景跳过确认容易登记错人再回滚。

> 📌 **本约束是对 v4 设计文档 §3 "最少交互原则"的有意偏离**(v4 主张挑组即视为确认, 错了用 rollback 兜底)。SKILL 选保守路线 — 用 +1 轮明确"确认"换"预防误录"。这是 deliberate 决策, 不要按 v4 §3 改回单轮直接 commit。

### 约束 2 · 视频附件必须以原文件提交, 禁止抽帧后当图片处理

视频文件(.mp4 / .mov / .webm / .avi / .mkv 等)必须直接 `--video <path>`, **不要** ffmpeg 抽帧后走 `--image`。

`❌ ffmpeg ... xxx.mp4.png && register preview --image xxx.mp4.png`
`✅ register preview --video xxx.mp4 --topk 5 --pretty`

理由:
- 抽帧只产生孤立的几张图, **丢失视频里的多人物轨迹关联**(分不出几个人)
- 单帧的人体特征不足以代表一个人, **样本多样性差** → 后续识别误识率高
- 后端 `--video` 通路已含完整的多人物追踪 + 跨帧关联 + 单帧选样, agent 端重做无意义

CLI 已加防呆: `--image` 收到视频文件或 `xxx.mp4.png` 衍生帧直接 exit 1。OpenClaw 飞书插件下载视频默认存 `~/.openclaw/media/inbound/<原文件名>.mp4`。

### 约束 3 · 同一条消息内 ≥ 2 张图必须单次批量, 禁止拆调

用户一条消息里附 N 张图(N ≥ 2)是"同一个人的 N 份样本", 必须一次 `--images` 批量调用:

```bash
miloco-cli identity register preview \
    --images /tmp/<uuid>_1.jpg --images /tmp/<uuid>_2.jpg \
    --images /tmp/<uuid>_3.jpg --images /tmp/<uuid>_4.jpg \
    --topk 5 --save-montage /tmp/<uuid>_preview.jpg --pretty
```

禁止: 拆 N 次 `--image` 循环 (后端建 N 个 session, 只发第 1 张拼图后 N-1 张全丢) / 每张图各发 1 条 reply / 逐张描述"图 1: ..."。

理由:
- 单次批量让后端**一次性跨图联合去重**(像素相似度 + 人体特征 + 时间间隔三维), 把所有候选拼到一张图
- 拆 N 次循环既慢, 又没办法在拼图里展示全部候选

**判定**: ≥ 2 张走 `--images` 批量; 1 张可走 `--image` 或 `--images` 都行(后端兼容)。

### 约束 4 · 未附素材 + 未指明来源时, 必须列出两条入口供选

用户说"帮我登记 XX" / "建个档案"但没附图 / 视频也没说来源时, **不论是否给姓名**, agent 必须列"你来发素材" + "从摄像头看到的人里挑"两条让用户选, **不要默认只引导其中一条**。

理由: 摄像头持续累积陌生人记录, 默认走附件会吞掉这条选项。

**两类例外**(不走双入口话术, 详见何时激活表):
- 消息来源已是 `[感知引擎]` 推送响应(自带摄像头 + 轨迹) → 直接锁定该候选
- 用户**自己明确了来源**(指明摄像头 或 表达"想直接挑") → 跳过话术直接跑对应分支

### 其他通用规则

- **入库前必须用户确认**, 不调 `register from-media`(已在约束 1 强调)
- **重名后端自动追加同 person**, agent 不需要自己查重
- **附件下载是 agent 框架职责**, 本 SKILL 假设附件已存到本地路径(飞书默认 `~/.openclaw/media/inbound/`)
- **CLI 非零 exit code 时用人话告知用户**, 不直接抛 stack

## 异常处理

| 异常 / 状态字段值              | 处理方式                                  | 回复话术                                                                                          |
|-------------------------------|-------------------------------------------|---------------------------------------------------------------------------------------------------|
| `status_preview = ok`         | 正常走第三步问询                          | "找到 N 张可用样本(含人脸 M 张),要登记到「{name}」?回'确认'入库。"                                |
| `status_preview = weak_diversity` | 告知挑出 K 张,询问继续或重发; 视频分支附拍摄指引 | "只挑出 K 张不太一样的样本(N 张里大多太相似),要先用这些登记,还是发新的素材?"                       |
| `status_preview = no_valid_subject` | 提示重拍; 视频分支附拍摄指引                | "这个素材里没看清楚人(光线 / 角度 / 距离原因),让 TA 再拍清晰一点。"                                |
| 上传素材里完全没人体 / 只有动物 / 纯风景 | 后端会返 `no_valid_subject`, 但话术再具体一点 | "这张图 / 视频里没看到人, 是不是发错了? 帮我重发一张含目标人的素材。"                          |
| 陌生人候选通路 fetch 返回 0 候选 | 引导现场采集; 话术问时间 hint, 用户下一轮触发加宽 window | "{时间表述}摄像头没看到陌生人, 要不让 TA 站到摄像头前 15 秒, 在画面中间停留, 原地慢慢转身 + 转头, 然后回我'拍好了'我重新拉?(或者告诉我 TA 大概什么时候来过, 我拉更长范围)" |
| 多人视频用户选号对应的 track `auto_status` 不是 `ok` | 提示该位质量不足, 给用户选择   | "你选的这一位样本不太够 / 太相似, 入库后识别可能不太稳。要先用这些登记, 还是让 TA 再到摄像头前拍久一点?" |
| 用户姓名 / 家庭角色都没给(空消息)   | 追问                                      | "登记给谁? 请告诉我 TA 叫什么 / 是家里什么人。"                                                  |
| 用户只给家庭角色没给真名(如"这是我妈") | 追问真名                                  | "妈妈叫什么名字呢? 系统需要一个正式姓名作为标识。"                                                |
| 用户回数字超出候选范围         | 不入库,追问                              | "图里只有 N 个编号,请回 1~N"                                                                       |
| 用户回数字 + 多个人(如"2 是张三, 3 是李四") | 告知一次只能登记一个                 | "一次只能登记一位, 这轮先登记哪一位? 另一位等这次完成后再来一次。"                                |
| HTTP 4xx / 5xx / CLI 连不上服务 | 不重试,告知服务不可用                    | "服务暂时不可用,稍后再试。"                                                                       |
| CLI 非零 exit code            | 不抛 stack,用人话告知                    | "刚才登记没成功,稍后再试一次"                                                                     |
| `--image` 误传视频文件         | CLI 已 exit 1 防呆,按提示改 `--video`     | (CLI 自带提示)                                                                                  |
| 候选 session 已过期(> 10 min)| 让用户重发素材                            | "刚才的样本预览已过期,重新发一下素材吧"                                                            |
| 用户上传单张图,样本数不足      | 不强制重发,提示更优做法                  | "找到 K 张样本,已入库。要识别更稳,后面可以补几张不同角度,或直接拍段 15 秒视频。"                   |
| 用户上传**非图非视频**附件(PDF / 文档 / 音频 / 压缩包) | 不支持,引导改发图 / 视频         | "目前只能用图片或视频登记。帮我重发一张照片或一段视频?"                                            |
| 用户在双入口话术后**回了不相关内容**(继续聊别的) | 不退出, 继续等待用户对话术回 1 / 2 / 发素材 | (不主动 reply,继续等)                                                                            |
| 同名人物已存在(后端自动追加到该 person) | 告知用户已并入已有档案                  | "「{name}」档案已存在, 本次入库的 N 张样本已合并到原档案。"                                       |
| 用户撤销但说不出哪次 / 没指定 session  | 列最近批次让用户挑                        | "你最近有 N 次登记: 1. ... 2. ...。要撤销哪一次?"                                                |
| 用户请求 SKILL 不支持的能力     | 按总原则,引导回标准入口                  | "我这边目前只支持上传素材或者从摄像头看到的人里挑两种登记方式,要不咱们走其中一条?"                |

## 示例与反例

5 个端到端场景 + 11 条 LLM 容易犯的反例集中在 [references/examples.md](./references/examples.md)。**遇到第一次跑某通路、或者拿不准是否同一轮 commit 时, 去读这个文件确认**。覆盖:
- 场景 1: 多图批量(约束 3)
- 场景 2: 视频单人
- 场景 3: 视频多人(无姓名 + 选号补名)
- 场景 4: 推送响应锁定单候选
- 场景 5: 无附件主动 + 双入口话术 + 3 轮对话

## 附加操作

低频操作(撤销 / 看某人样本 / 拆开跨摄像头合并的候选组 / 飞书纯文本降级话术)集中在 [references/extras.md](./references/extras.md)。用户表达对应意图时去读。

## 边界

- ❌ 不做 person DB 行 CRUD(改名 / 删人)(→ miloco-miot-identity)
- ❌ 不接陌生人候选记录的内部清理 / 容量管理动作(主流程后端自动)
- ❌ 不控制设备(→ miloco-devices)
- ❌ 不做实时环境感知(→ miloco-perception)
- ❌ 不主动触发识别 / 不主动拉摄像头记录(除非用户明确要求或推送响应触发)
- ⚠️ 候选 session 在后端内存中存活 10 分钟,超时需用户重发素材
- ✅ 翻页("更多")通过 `pool fetch --offset <next_offset>` 实现, 每页 6 个
- ⚠️ 多人**图片** / 合影目前没接跨图轨迹关联,仍走单候选路径(多人**视频**才有号码图)
- ✅ 重名自动追加到同 person, 不查重
- ✅ 视频走 `--video` 一气拿完整人物追踪 + 跨帧关联 + 单帧选样
- ✅ 多图走 `--images` 单次批量, 后端跨图联合去重
- ✅ rollback 按 register_session_id 反查精确删
- ✅ 号码图封面优先选**带正脸的样本**, 用户挑人时更易辨认(后端 1.2 起生效)
