瀏覽代碼

fix(chat): 连接状态跟随 SDK sync + 产出容器初始化优化建议

## 缺陷:一直显示「正在连接」
chat.service.ts 原实现在 openReal() 末尾只读一次 mx.syncState(),
而 /sync 是异步的 —— 那一刻必然是 connecting,之后无任何代码更新,
UI 永久停在「正在连接」(实测:SDK 已 SYNCING,页面仍显示连接中)。

修复:构造函数加 effect 订阅 mx.syncState() signal,
状态变化(connected / connecting / disconnected)自动同步到 _connection。

## 容器上报问题诊断(平台侧实测)
用容器 token 手动调 xlosi3m46q 确证通道正常,问题在容器侧:
- channels 上报 ["wecom"](数组),契约要求对象 → App 判 matrixReady=false
- host 缺 cpuPercent(上报的是 cpuLoad1/cpuLoad5)→ CPU 不显示
- usage / heartbeatAt / progress 为空
- 调用了 AlP56LCKFm(OBS 上传)而非 xlosi3m46q(状态上报)
- reporter 从未部署(status-report.py 不存在、AGENT_ID 空、无 crontab)

## 归属修正(用户裁决)
agent-node-lj-test: user sr2WiPsDyQ → xhVRjTMwQH(19170119836)
                   company rpnXZC1loU → E4KpGvTEto(飞码AI)
ACL 同步更新。改前已备份(.local-backup/db/,gitignored)。

## 新增文档
- 容器初始化流程优化建议-20261020.md
  (基于 workshop-avatar 实操 + lj-test 实测,5 个问题 + 5 项优化建议)
- reporter部署方案-20261020.md

## 验证
ng test 484 / build 通过 / verify:ui 11/11 / verify:docs 46

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 6 小時之前
父節點
當前提交
747a1fa660

+ 15 - 0
docs/00-tracking/操作追踪.md

@@ -763,3 +763,18 @@
 | 根因 1 的处置 | `localStorage` 残留是**测试副作用**(冒烟脚本 `fixMode()` 写入)。已确认**代码无缺陷**(偏好优先是设计)。用户侧清理方式:DevTools → Application → localStorage → 删 `fmode_ui_mode_override`;或 App 内「我的 → 恢复自动识别」|
 | 根因 1 的处置 | `localStorage` 残留是**测试副作用**(冒烟脚本 `fixMode()` 写入)。已确认**代码无缺陷**(偏好优先是设计)。用户侧清理方式:DevTools → Application → localStorage → 删 `fmode_ui_mode_override`;或 App 内「我的 → 恢复自动识别」|
 | 验证证据 | `ng test` **484** / `smoke` **28/0** / `verify:ui` **11/11** / `verify:docs` **44** / `build` 通过 |
 | 验证证据 | `ng test` **484** / `smoke` **28/0** / `verify:ui` **11/11** / `verify:docs` **44** / `build` 通过 |
 | 未做(用户未选) | ② 冒烟脚本清理 localStorage 残留(用户未勾选)|
 | 未做(用户未选) | ② 冒烟脚本清理 localStorage 残留(用户未勾选)|
+
+### 2026-10-20 — 修复"一直正在连接" + 容器上报问题诊断 + 初始化流程优化文档
+
+| 项 | 内容 |
+|---|---|
+| **缺陷 1:一直显示"正在连接"** | 🔴 `chat.service.ts:217` 只在 `openReal()` 末尾**读一次** `mx.syncState()`。而 `/sync` 是**异步**的——那一刻必然是 `connecting`,之后无任何代码更新 → UI 永久停在「正在连接」(实测:SDK 已 SYNCING,页面仍显示连接中)<br>**修复**:构造函数加 `effect(() => {...})` 订阅 `mx.syncState()` signal,状态变化自动同步 |
+| **缺陷 2:刷新后显示"未接入"** | 🔴 容器上报 `channels = ["wecom"]`(**数组**),App 判 `channels.matrix` → `undefined` → `matrixReady=false` → 显示未接入<br>**根因在容器**(字段格式不对),App 逻辑本身正确 |
+| **缺陷 3:只同步了内存** | 🔴 容器 `host` 上报的是 `{"cpuLoad1":24.09,"cpuLoad5":13.68,"memPercent":54.9,"diskPercent":33.3}`——**缺 `cpuPercent`**(平台契约字段名)。内存/磁盘因名字对上才显示 |
+| **缺陷 4:容器调错云函数** | 🔴 容器上报返回 `{"key":"user/.../log/...json"}` = OBS 上传(`AlP56LCKFm`),**不是**状态上报(`xlosi3m46q`)。数据进存储桶、没进 FmodeAgent 表 |
+| **平台侧手动验证(确证通道正常)** | 用容器 token 手动调 `xlosi3m46q` 上报 host/channels/botName → 全部 `{"code":200,"field":"..."}` 且回读确认落库(cpuPercent=12.5、含 wecomBotId、lastField=botName)|
+| 归属修正(用户裁决) | `agent-node-lj-test` 的 `user` 由 `sr2WiPsDyQ`(刘雨飏)改为 `xhVRjTMwQH`(用户9836/19170119836)、`company` 由 `rpnXZC1loU`(脑控科技)改为 `E4KpGvTEto`(飞码AI),ACL 同步。<br>改前已备份至 `.local-backup/db/`(gitignored)。改后实测:你的 token `scope=mine` 可见该容器 |
+| **🆕 输出文档** | 《容器初始化流程优化建议-20261020.md》:基于 workshop-avatar 实操 + lj-test 实测,列出 **5 个问题**(reporter 未部署 / 字段格式不符 / 调错云函数 / 归属三方不一致 / 重复绑定)+ **5 项优化建议** + 完整部署提示词 + 字段对照表 |
+| 澄清(非缺陷) | TURN 404(`/voip/turnServer`)是 matrix-js-sdk 自动探测语音中继,SPEC FR-11 已裁决语音不做 → **无影响** |
+| 验证证据 | `ng test` **484** / `build` 通过 / `verify:ui` **11/11** / `verify:docs` **46 文件** |
+| 待容器处理 | ① 部署 reporter(用原版 v10)② 改用 `xlosi3m46q` ③ 字段对齐(`channels` 对象、`host.cpuPercent`、`usage`/`heartbeatAt`/`progress`)|

+ 215 - 0
docs/03-interface/reporter部署方案-20261020.md

@@ -0,0 +1,215 @@
+# reporter 部署方案(给容器执行)
+
+> 目标:让容器具备状态上报能力,使 App 能看到它
+> 适用:`agent-node-lj-test`(或任何缺 reporter 的容器)
+> 依据:`E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(**v10,1100 行**)
+
+---
+
+## 一、容器缺什么(实测确认)
+
+| 项 | 现状 | 需要 |
+|---|---|---|
+| `status-report.py` | ❌ 全盘不存在 | ✅ 部署 |
+| `/opt/data/git-repos/` | ❌ 目录都不存在 | ✅ 创建 |
+| `AGENT_ID` | ❌ 空 → 退化成 hostname hash | ✅ 设置为容器名 |
+| `FMODE_SESSION_TOKEN` | ✅ 有(`~/.fmode/config/user.json`)| ✅ 沿用 |
+| crontab | ❌ 未安装 | ⚠️ 用 **systemd/s6 或 循环脚本** 替代 |
+
+---
+
+## 二、reporter 的配置契约(源码确证)
+
+```python
+# status-report.py:91
+AGENT_ID = os.environ.get("AGENT_ID", socket.gethostname())
+#   🔴 不配就用 hostname → 云函数判 agent not found → 数据不上报
+
+# status-report.py:88
+BACKEND = os.environ.get("AGENT_STATUS_BACKEND",
+                         "https://server.fmode.cn/api/fmodeagent/status")
+#   ✅ 旧 REST 地址(服务端已转发到云函数 xlosi3m46q)
+
+# status-report.py:139
+token = os.environ.get("FMODE_SESSION_TOKEN", "")
+#   🔴 上报鉴权必需
+
+# status-report.py:354
+wecom_bot_id = os.environ.get("WECOM_BOT_ID", "")
+#   ✅ 你已配(aibF…J3kV)→ 会带进 channels 上报
+
+# status-report.py:89
+HEARTBEAT_SECS = int(os.environ.get("AGENT_STATUS_HEARTBEAT", "30"))
+```
+
+**必配的只有 2 个**:`AGENT_ID`、`FMODE_SESSION_TOKEN`。
+
+---
+
+## 三、部署步骤
+
+### 步骤 1 · 建目录 + 落脚本
+
+```bash
+# 在容器内执行
+mkdir -p /opt/data/git-repos/agent-status-reporter/scripts
+mkdir -p /opt/data/git-repos/agent-status-reporter/state
+```
+
+**把 `status-report.py` 从宿主拷进去**(宿主侧已有 v10):
+
+```bash
+# 在宿主机执行(假设 agent-node-lj-test 是容器名)
+docker cp <宿主路径>/status-report.py agent-node-lj-test:/opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+docker exec agent-node-lj-test chmod +x /opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+```
+
+> 宿主路径参考:`/opt/fmode-containers/agent-node/data/git-repos/agent-status-reporter/`(其他容器已有,可从那拷)
+
+### 步骤 2 · 配 env
+
+**编辑 `/opt/data/.env`**(或容器启动参数),加:
+
+```bash
+# 🔴 必配:agentId 用容器名(云函数会按它自动建记录)
+AGENT_ID=agent-node-lj-test
+
+# 🔴 必配:上报鉴权 token
+#   来源:~/.fmode/config/user.json 里的 sessionToken
+FMODE_SESSION_TOKEN=r:dd15e8993d0b382d099d1faff6acd20c
+
+# 可选(已有则保持)
+# WECOM_BOT_ID=aibF7SNq4qV2dNwHypwp-eGUNPelYqiJ3kV
+# AGENT_BOT_NAME=lj-test
+# AGENT_STATUS_HEARTBEAT=30
+```
+
+> ⚠️ **`FMODE_SESSION_TOKEN` 用谁的?** 决定容器归属哪个账号:
+> - 用**你个人**的 `r:dd15...`(19170119836)→ 容器归到你个人账号
+> - 用**容器主人**的 token(刘雨飏 18691770343)→ 归到主人账号
+> **这一点需你确认后填**(见下方「待确认」)
+
+### 步骤 3 · 重启或重载 env
+
+```bash
+# 方式 A:容器重启(env 生效)
+docker restart agent-node-lj-test
+
+# 方式 B:进程内不重启,直接 export 后手动跑(仅测试)
+```
+
+### 步骤 4 · 手动验证(关键)
+
+```bash
+docker exec agent-node-lj-test sh -c '
+  export AGENT_ID=agent-node-lj-test
+  export FMODE_SESSION_TOKEN=<你的 token>
+  python3 /opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+'
+```
+
+**期望输出**:`{"code":200,...}` 或类似成功信息。
+
+**如果报错**:
+
+| 错误 | 原因 | 处理 |
+|---|---|---|
+| `agent not found` | `AGENT_ID` 没生效 | 检查 env 是否真的注入 |
+| `401` / `SessionToken 无效` | token 失效 | 重新登录取新 token |
+| `No such file` | 脚本没落到位 | 检查路径 |
+| `ModuleNotFoundError` | 缺依赖 | `pip3 install requests`(若需要)|
+
+### 步骤 5 · 确认入库
+
+**手动跑成功后**,我会从平台侧查 `FmodeAgent` 表,确认 `agent-node-lj-test` 的:
+- `host` 有值
+- `channels.wecomBotId` = `aibF7SNq4qV2dNwHypwp-eGUNPelYqiJ3kV`
+- `botName` 正确
+
+---
+
+## 四、常驻运行(crontab 不可用时的替代)
+
+容器**没装 crontab**,三种替代:
+
+### 方式 A · 无限循环(最简单)
+
+```bash
+# 启动一个常驻进程
+nohup sh -c '
+  while true; do
+    AGENT_ID=agent-node-lj-test \
+    FMODE_SESSION_TOKEN=<token> \
+    python3 /opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+    sleep 30
+  done
+' >> /tmp/fmodeagent-status.log 2>&1 &
+```
+
+### 方式 B · Hermes gateway 的 s6 服务(如果容器用 s6)
+
+```bash
+# 参照其他容器的 s6 配置
+ls /etc/s6-overlay/s6-rc.d/ | grep -i report
+```
+
+### 方式 C · 宿主 cron 统一调度(推荐,与其他容器一致)
+
+```bash
+# 宿主 /etc/cron.d/fmodeagent-status
+* * * * * root for c in agent-node-lj-test; do
+  docker exec $c python3 /opt/data/git-repos/agent-status-reporter/scripts/status-report.py >/dev/null 2>&1
+done
+```
+
+---
+
+## 五、待确认(阻塞项)
+
+| # | 问题 | 影响 |
+|---|---|---|
+| **1** | 🔴 **`FMODE_SESSION_TOKEN` 用谁的?**<br>· 你个人(19170119836)→ 容器归你<br>· 容器主人(刘雨飏 18691770343)→ 归主人 | 决定容器归属 |
+| **2** | 容器脚本从哪拷?<br>· 从同宿主其他容器 `/opt/fmode-containers/agent-node/data/git-repos/...`<br>· 还是从本地 `E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py` 上传 | 部署方式 |
+
+---
+
+## 六、附:给容器的完整执行提示词
+
+```
+【任务】部署 reporter(状态上报),让 App 能看到你
+
+== 背景 ==
+你缺 status-report.py(全盘不存在)、AGENT_ID 为空、无 crontab。
+导致从未上报,App 里看不到你。
+
+== 你要做的 ==
+
+1. 建目录
+   mkdir -p /opt/data/git-repos/agent-status-reporter/scripts
+   mkdir -p /opt/data/git-repos/agent-status-reporter/state
+
+2. 从同宿主其他容器拷脚本(agent-node 有完整的)
+   在宿主机执行:
+   docker cp /opt/fmode-containers/agent-node/data/git-repos/agent-status-reporter/scripts/status-report.py \
+     <你的容器名>:/opt/data/git-repos/agent-status-reporter/scripts/
+   (若宿主机路径不同,请 find / -name "status-report.py" 定位)
+
+3. 配 env(写 /opt/data/.env,先备份)
+   AGENT_ID=<你的 agentId,用容器名>
+   FMODE_SESSION_TOKEN=<待用户确认用哪个>
+   保留已有的 WECOM_BOT_ID 等
+
+4. 手动跑一次验证
+   python3 /opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+   → 把返回的 JSON 原文发回来
+
+5. 配常驻(crontab 不可用,用循环或 s6)
+   参照同宿主正常容器的做法
+
+== 输出 ==
+REPORTER-DEPLOY 脚本=<已落位/失败> env=<已配/失败> 手动上报=<返回码+JSON> 常驻=<方式>
+
+== 纪律 ==
+· 改 .env 前先备份
+· token 不明文输出
+```

+ 344 - 0
docs/03-interface/容器初始化流程优化建议-20261020.md

@@ -0,0 +1,344 @@
+# 容器初始化流程优化建议(基于 workshop-avatar 实操)
+
+| 字段 | 值 |
+|---|---|
+| 文档编号 | 建议-20261020-容器初始化优化 |
+| 版本 | v1.0 |
+| 创建日期 | 2026-10-20 |
+| 来源 | `https://fmode.cn/aipm-lesson/workshops/avatar-camp/workshop-avatar.html`(数字分身实战营)+ **本次 lj-test 实测踩坑** |
+| 目的 | 按实操中发现的问题,给出**初始化提示词/流程**的具体优化点 |
+
+---
+
+## 一、现有流程(workshop-avatar 实际做了什么)
+
+### 1.1 五章结构
+
+| 章节 | 内容 | 谁做 |
+|---|---|---|
+| **CH1** | Harness 部署:初始化工程架构 + 材料整理 + 数字分身初始化 + cc 无人驾驶 | 工具为主 |
+| **CH2** | 数据本体 · 诊断分析 | 人为主(回答)|
+| **CH3** | 数据本体 · 架构设计 | 人为主(拍板)|
+| **CH4** | 数据本体 · RSI 架构 | 数字分身为主 |
+| **CH5** | 数据本体 · 复盘报告 | 数字分身为主 |
+
+### 1.2 "绑定数字员工"流程(实测页面逻辑)
+
+```
+① 平台账号登录(手机号 + 验证码)
+        ↓  POST /api/fmode/mobile?mobile=&code=
+② 绑定数字员工(点「绑定数字员工」)
+        ↓  选择通道
+   ┌────────────┴────────────┐
+   企业微信                  飞书(中国版)
+   ↓                        ↓
+   扫码绑定 / 手动输入         扫码 / 手动
+   ↓                        ↓
+   填 BotID + Secret          填 AppID + AppSecret
+   ↓                        ↓
+   保存并绑定                 保存并绑定
+   ↓                        ↓
+   POST /api/fmode/hermes/wecom-cloud/manual-bind
+   body: { sessionToken, botId, secret }
+        ↓
+   返回「数字员工已绑定 / 机器人信息已保存到当前账号」
+```
+
+**页面上的三条 CH1 提示词**:
+
+| 步骤 | 提示词作用 |
+|---|---|
+| **Harness 01** | 初始化工程架构(接平台云资源 + 装核心技能)|
+| **Harness 02** | 材料整理到 `./storage` |
+| **Harness 03** | **数字分身初始化**(装 Loop 技能 + 平台登录 + **领取企业资源**)|
+| **Harness 04** | 唤醒数字分身(环境自检)|
+| **Harness 05** | 数字 DNA 初始化(建仓建档)|
+
+---
+
+## 二、🔴 本次实测发现的 5 个问题
+
+> 全部来自 `agent-node-lj-test` 容器的真实排查
+
+### 问题 1 · **reporter 根本没部署**(最严重)
+
+**现象**:
+```
+/opt/data/git-repos/          → ❌ 目录不存在
+status-report.py              → ❌ 全盘找不到
+crontab                       → ❌ 容器没装
+AGENT_ID                      → ❌ 空
+```
+
+**结果**:容器从不向 `FmodeAgent` 表写记录 → **App 完全看不到它**
+
+**根因**:workshop 的 Harness 流程**没包含 reporter 部署步骤**
+
+---
+
+### 问题 2 · **容器上报的字段格式与平台契约不符**
+
+**实测数据**(容器上报 vs 平台要求):
+
+| 字段 | 容器上报 | 平台要求 | 后果 |
+|---|---|---|---|
+| **`channels`** | `["wecom"]`(**数组**)| `{"matrix":true,"matrixConfig":{...},"wecom":true,...}`(**对象**)| 🔴 App 判 `channels.matrix` = `undefined` → **显示"未接入"** |
+| **`host`** | `{"cpuLoad1":24.09, "cpuLoad5":13.68, "memPercent":54.9, ...}` | `{"cpuPercent":N,"memPercent":N,"diskPercent":N}` | 🔴 缺 `cpuPercent` → **CPU 显示不出来**(内存/磁盘能显示,因为名字对上了)|
+| **`usage`** | 空 | `{"totalTokens":N,"promptTokens":N,...}` | 🔴 Token 用量不显示 |
+| **`heartbeatAt`** | 空 | ISO 时间戳 | 🔴 心跳不更新 |
+| **`progress`** | 空 | 0-100 | 任务进度不显示 |
+
+**根因**:容器用的是**复刻版**上报脚本,字段命名自己定的,**没对齐平台契约**
+
+---
+
+### 问题 3 · **上报调错了云函数**
+
+**现象**:容器上报返回
+```json
+{"code":200,"ok":true,"key":"user/xhVRjTMwQH/log/20261020/1004.json","publicUrl":"https://s3.fmode.cn/..."}
+```
+
+**这是「上传日志文件到 OBS」**,不是「更新 FmodeAgent」
+
+| 云函数 | 用途 | 容器用了 |
+|---|---|---|
+| `xlosi3m46q` | ✅ **状态上报**(fmodeagent-status)| ❌ |
+| `AlP56LCKFm` | OBS 上传签名 | ✅ **误用** |
+
+**后果**:数据进了存储桶,**没进 FmodeAgent 表** → App 看不到
+
+---
+
+### 问题 4 · **Agent 与用户的归属三方不一致**
+
+**实测**:
+```
+FmodeAgent.user     → sr2WiPsDyQ(刘雨飏 / 18691770343)
+FmodeAgent.company  → rpnXZC1loU(脑控科技)
+容器里的 token      → r.dd15...(用户9836 / 19170119836)
+容器名              → agent-node-lj-test
+```
+
+**三个地方三个答案**。
+
+**根因**:容器初始化时**没校验**「容器 → 用户 → 公司」的一致性
+
+---
+
+### 问题 5 · **同一 Agent 出现两条绑定**
+
+**实测**:`FmodeMatrixBinding` 里 `agentId=agent-node-lj-test` 有 **2 条**:
+- 第 1 条:`userPointer=sr2WiPsDyQ`(刘雨飏),room `!OIm7iWR6...`
+- 第 2 条:`userPointer=xhVRjTMwQH`(你),room `!miyB1DxRE...`
+
+**两条都 `active`**。
+
+**后果**:不同用户进同一 Agent 会拿到不同房间 → 数据割裂
+
+---
+
+## 三、优化建议(按优先级)
+
+### P0 · 在 Harness 流程中**新增「reporter 部署」步骤**
+
+**新增一步(建议放在 Harness 03 之后)**:
+
+```markdown
+## Harness 03.5 · 状态上报(reporter)部署
+
+在容器内执行(不需要你手动操作,交给数字分身):
+
+1. 建目录
+   mkdir -p /opt/data/git-repos/agent-status-reporter/{scripts,state}
+
+2. 拷脚本(原版 v10,1100 行)
+   · 从平台仓库下载:https://repos.fmode.cn/.../agent-status-reporter/status-report.py
+   · 或从同宿主其他正常容器 docker cp
+
+3. 配 env(写 /opt/data/.env,先备份)
+   AGENT_ID=<FmodeAgent.agentId>
+   FMODE_SESSION_TOKEN=<你的 sessionToken>
+
+4. 常驻(crontab 不可用时的替代)
+   · s6 服务 或 无限循环 nohup
+
+5. 验证
+   手动跑一次 → 返回 {"code":200,"field":"host",...}
+   然后去 App 工作台看是否有 CPU/内存数字
+```
+
+---
+
+### P0 · 上报走**正确云函数** + **标准字段格式**
+
+**明确写进提示词**:
+
+```markdown
+## 状态上报接口(唯一正确用法)
+
+POST https://server.fmode.cn/api/functions
+Body: {
+  "id": "xlosi3m46q",          ← 🔴 必须用这个
+  "token": "<FMODE_SESSION_TOKEN>",
+  "params": {
+    "op": "update",
+    "agentId": "<你的 agentId>",
+    "field": "<字段名>",
+    "value": "<值>"
+  }
+}
+
+⚠️ 不要用 AlP56LCKFm(那是 OBS 上传签名)
+
+### 字段格式(必须严格对齐)
+
+host(JSON 字符串):
+  {"cpuPercent": 12.5, "memPercent": 38.2, "diskPercent": 35}
+  🔴 用 cpuPercent(不是 cpuLoad1/cpuLoad5)
+  🔴 cpuPercent = cpuLoad1 / cpu核数 * 100(或读 /proc/stat)
+
+channels(JSON 字符串,🔴 必须是对象不是数组):
+  {
+    "homeserver": "https://matrix.fmode.cn",
+    "matrix": true,
+    "matrixConfig": {"userId": "@<mxid>", "channelState": "connected", "listenerOk": true},
+    "wecom": true,
+    "wecomBotId": "<BotID>",
+    "wecomConfig": {"botId": "<BotID>", "connected": true}
+  }
+
+balance(JSON 字符串):
+  {"remainCny": 17842.31, "source": "session"}
+
+usage(JSON 字符串):
+  {"totalTokens": 123456, "promptTokens": 100000, "completionTokens": 23456}
+
+heartbeatAt:ISO 时间戳(每次上报必须更新)
+progress:0-100
+```
+
+---
+
+### P1 · 初始化时**校验归属一致性**
+
+**新增自检步骤**:
+
+```markdown
+## 初始化自检:身份三方对齐
+
+登录后、绑定机器人前,确认以下三项**指向同一个账号**:
+
+1. 你的 sessionToken 解析出的 userid
+   GET https://server.fmode.cn/parse/users/me
+   → objectId / mobile / company
+
+2. FmodeAgent 记录(如有)
+   where={agentId:<你的agentId>}
+   → user 指针 / company 指针
+
+3. 容器内 env
+   AGENT_ID / FMODE_SESSION_TOKEN / WECOM_BOT_ID
+
+✅ 正确:user.objectId === token 解析出的 objectId
+❌ 不一致:立即停止,向主人报告「归属不一致:<详情>」,不要强行绑定
+```
+
+---
+
+### P1 · 绑定数字员工后**自动创建/更新 FmodeAgent**
+
+**当前流程**:扫码/手填 → `manual-bind` → 只保存到"当前账号"
+**缺失**:**没同步到 `FmodeAgent` 表**
+
+**建议**:`manual-bind` 成功后**自动**:
+1. 按 `agentId` 查/建 `FmodeAgent` 记录
+2. 写入 `user` 指针(= 当前登录用户)
+3. 写入 `company`(= 用户所属公司)
+4. 写入 `channels.wecomBotId` + `channels.wecom = true`
+5. 清掉该 agentId 的**旧绑定**(避免问题 5 的重复绑定)
+
+---
+
+### P2 · 提示词补充「验证清单」
+
+**在每个 Harness 步骤末尾加验收标准**:
+
+```markdown
+## 验收(做完必须逐条确认)
+
+□ reporter 进程在跑(ps aux | grep status-report)
+□ 手动上报返回 {"code":200,"field":"host"}
+□ App 工作台能看到 CPU/内存/余额
+□ App 会话列表状态徽标显示「在线」
+□ channels 是对象不是数组
+□ host 里有 cpuPercent 字段
+```
+
+---
+
+## 四、给容器的**完整部署提示词**(可直接用)
+
+```markdown
+【任务】部署状态上报(reporter),让 App 能看到你
+
+== 背景 ==
+平台侧实测:你缺 status-report.py、AGENT_ID 为空、无 crontab,
+从未向 FmodeAgent 表上报过 → App 里看不到你。
+
+== 你要做的 ==
+
+1. 建目录
+   mkdir -p /opt/data/git-repos/agent-status-reporter/{scripts,state}
+
+2. 拿原版脚本(1100 行 v10)
+   优先从同宿主其他正常容器拷贝:
+     find /opt/fmode-containers -name "status-report.py" -path "*agent-status-reporter*"
+   或从平台仓库下载
+
+3. 配 env(/opt/data/.env,先备份 .env.bak-<时间戳>)
+   AGENT_ID=agent-node-lj-test
+   FMODE_SESSION_TOKEN=r:dd15e8993d0b382d099d1faff6acd20c
+
+4. 手动验证(关键)
+   python3 /opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+   ✅ 期望返回:{"code":200,"agentId":"...","field":"host","objectId":"..."}
+   ❌ 若返回 {"key":"user/.../log/....json"} → 你调了 AlP56LCKFm(错),应调 xlosi3m46q
+
+5. 常驻(crontab 不可用)
+   nohup sh -c 'while true; do python3 .../status-report.py; sleep 30; done' &
+
+6. 字段自查(对照平台契约)
+   host.channels 必须是对象:{"matrix":true,"matrixConfig":{...},"wecom":true,...}
+   host 必须有 cpuPercent(不是 cpuLoad1)
+   heartbeatAt 每次更新
+
+== 输出 ==
+REPORTER-OK 云函数=<xlosi3m46q> host=<cpuPercent有> channels=<对象> 落库=<lastField有值>
+```
+
+---
+
+## 五、本次实测的**字段对照表**(给开发/容器参考)
+
+| 字段 | 容器当前上报 | 平台契约 | 状态 |
+|---|---|---|---|
+| `host.cpuLoad1` | ✅ 有 | ❌ 不认 | 🔴 需改名 `cpuPercent` |
+| `host.cpuLoad5` | ✅ 有 | ❌ 不认 | ⚪ 可保留(额外字段)|
+| `host.memPercent` | ✅ 有 | ✅ 认 | ✅ 正常 |
+| `host.diskPercent` | ✅ 有 | ✅ 认 | ✅ 正常 |
+| `channels` | `["wecom"]` | `{...}` | 🔴 需改对象 |
+| `currentTask` | ✅ 有 | ✅ 认 | ✅ 正常 |
+| `usage` | ❌ 空 | ✅ 需要 | 🔴 需补 |
+| `heartbeatAt` | ❌ 空 | ✅ 需要 | 🔴 需补 |
+| `progress` | ❌ 空 | ✅ 需要 | 🔴 需补 |
+| `balance` | `{"source":"none"}` | `{"remainCny":N}` | 🔴 需取真实余额 |
+
+---
+
+## 六、关联文档
+
+- `docs/03-interface/reporter部署方案-20261020.md`(部署细节)
+- `docs/02-design/architecture/账号与账套体系设计.md`(归属模型)
+- `docs/02-design/plans/2026-10-19-数据层治理与新容器接入.md`(数据治理)
+- 云函数 `xlosi3m46q`(状态上报,`Function` 表 v0.1.2)

+ 22 - 1
src/app/core/chat.service.ts

@@ -1,4 +1,4 @@
-import { computed, inject, Injectable, signal } from '@angular/core';
+import { computed, effect, inject, Injectable, signal } from '@angular/core';
 import { FmodeApi } from './api/fmode-api';
 import { FmodeApi } from './api/fmode-api';
 import {
 import {
   Agent,
   Agent,
@@ -106,6 +106,27 @@ export class ChatService {
     return this.provision;
     return this.provision;
   }
   }
 
 
+  constructor() {
+    /**
+     * 🔴 连接状态跟随 SDK 的 sync 状态(FR-04 AC-04.9)。
+     *
+     * Bug 背景:原实现只在 `openReal()` 末尾**读一次** `mx.syncState()`:
+     *   `this._connection.set(this.mx.syncState() === 'connected' ? ...)`
+     * 但 `/sync` 是**异步**的 —— 那一刻必然还是 `connecting`,之后没有任何代码更新,
+     * 于是 UI **永久停在「正在连接」**(实测:SDK 已 SYNCING,页面仍显示连接中)。
+     *
+     * 修复:用 `effect` 订阅 `mx.syncState()` signal,状态变化时自动同步。
+     * 仅在已打开会话时生效(未打开会话不干预 `close()` 后的复位)。
+     */
+    effect(() => {
+      const s = this.mx.syncState();
+      if (!this._agentId()) return;
+      this._connection.set(
+        s === 'connected' ? 'connected' : s === 'connecting' ? 'connecting' : 'disconnected',
+      );
+    });
+  }
+
   /** 打开会话(路由进入时调用) */
   /** 打开会话(路由进入时调用) */
   async open(agentId: string): Promise<void> {
   async open(agentId: string): Promise<void> {
     if (this._agentId() === agentId && this._state() === 'ready') return;
     if (this._agentId() === agentId && this._state() === 'ready') return;