Sfoglia il codice sorgente

feat(fn): 云函数 v0.1.3 balance 键名归一 + 容器上报标准部署包

全平台盘点 54 个容器,balance 出现 3 种写法:
  remainCny/source/totalCalls → 7 个(原版)
  remainRmb/remainUsd         → 1 个(agent-node,更老 v2)
  cny/source                  → 1 个(agent-node-lj-test,自写复刻)

只有 lj-test 用 cny → 排除平台代写,写入方是它专属代码。

云函数 v0.1.3:
- 新增 _normalizeBalance():cny/remainRmb/remain → 补 remainCny
- 原键保留(只增不改),不碰任何 ACL/白名单/查询逻辑
- 接入点仅 2 行
- 发布前:本地单测 9/9 + node --check + diff 确认改动面
- 已发布并验证线上函数可正确归一

注意:归一只在容器下次真正重写 balance 时生效
(原版是变化检测,金额没变不重写该字段)

标准部署包:
- 三步走 + 9 条验收断言 + 三条纪律
- 核心原则:不要自己重写脚本

验证:verify:docs 48 文件
回滚基线:.local-backup/xlosi3m46q-v0.1.2.js

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 1 settimana fa
parent
commit
41c5bc517d

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

@@ -841,3 +841,19 @@
 | 备份 | 容器侧 `status-report.py.bak-20261020120537` ✅;旧循环 pid 7515 已停、新 pid 9182 在跑 ✅ |
 | 验证证据 | `ng test` **503**(+4 balance 归一用例)/ `build` 通过 / `verify:ui` **11/11** / `verify:docs` **47 文件** |
 | 遗留 | `balance` 键名仍需容器改为 `remainCny`(App 已兼容 `cny`,但规范口径应是 `remainCny`);建议下一轮让容器**只报回读原文**,不接受转述 |
+
+### 2026-10-20 — 云函数 v0.1.3 balance 归一(上线)+ 容器上报标准部署包
+
+| 项 | 内容 |
+|---|---|
+| **用户决策** | 云函数兜底「改云函数兜底(推荐)」+ 标准包「做标准部署包(推荐)」|
+| **全平台盘点(实测 54 容器)** | `balance` 三种写法:`remainCny/source/totalCalls` **7 个**(原版 ✅)/ `remainRmb/remainUsd` 1 个(`agent-node`,更老的 v2)/ **`cny/source` 1 个**(`agent-node-lj-test`,自写复刻脚本 ❌)|
+| **关键判定** | 全平台**只有 lj-test 一个**用 `cny` → **排除「平台代写」**(平台不可能只对一个用不同键名)→ 写入方是它专属的一份代码 |
+| **✏️ 云函数 v0.1.2 → v0.1.3** | 新增 `_normalizeBalance()`:`cny` / `remainRmb` / `remain` → 补 `remainCny`,**原键保留(只增不改)**。<br>接入点:update 分支 `if (field === 'balance') value = _normalizeBalance(value)`,**仅 2 行**,不碰任何现有 ACL/白名单/查询逻辑 |
+| **发布前验证(吸取 3 次事故教训)** | ① 本地单测 9 用例全过(cny/remainRmb/标准/无金额/非JSON/数组/null/对象/原键保留)<br>② `node --check` 语法通过<br>③ `diff` 确认改动仅「新增 1 函数 + 2 行调用」|
+| **发布** | `PUT /parse/classes/Function/xlosi3m46q`,28463 字符,`updatedAt=2026-10-20T04:45:34Z` |
+| **⚠️ 发布后验证的一个发现** | 等 45s 后 `balance` 内容未变(仍 `cny`),但 `updatedAt` 在跳。<br>**原因**:原版脚本是**变化检测**——金额没变化就不重写 `balance` 字段,写的是别的字段。<br>→ 归一**只在容器下次真正重写 balance 时生效**;已验证线上函数本体可正确归一(提取线上代码实测 `{"cny":89.86}` → `{...,"remainCny":89.86}` ✅)|
+| **🆕 标准部署包** | `容器上报标准部署包-20261020.md`:三步走(部署原版脚本/配 env/常驻)+ **9 条验收断言** + **三条纪律**(查运行态不查路径 / 调用失败≠凭据失效 / 每个数字必须回读)+ 平台兜底说明 + 可直接贴给容器的检查表 |
+| **核心原则(写进包)** | 「**不要自己重写脚本**」——原版 1100 行覆盖变化检测/失败队列/token 自愈/任务残留清理/channel 探测/usage 聚合,复刻必然漏 |
+| 验证证据 | 云函数本地单测 **9/9** / 线上代码实测归一正确 / `verify:docs` **48 文件**(+1)|
+| 回滚基线 | `.local-backup/xlosi3m46q-v0.1.2.js`(v0.1.3 由它复制后增量修改)|

+ 210 - 0
docs/03-interface/容器上报标准部署包-20261020.md

@@ -0,0 +1,210 @@
+# 容器状态上报 · 标准部署包(后续所有容器统一走这套)
+
+| 字段 | 值 |
+|---|---|
+| 文档编号 | 标准包-20261020-reporter |
+| 版本 | v1.0 |
+| 创建日期 | 2026-10-20 |
+| 适用 | **新容器上线 / 现有容器修复 / 格式排查** |
+| 配套 | `reporter字段规范-20261020.md`(字段契约)、`容器初始化流程优化建议-20261020.md`(初始化流程)|
+
+---
+
+## 零、为什么要有这个包
+
+2026-10-20 全平台盘点 54 个容器,`balance` 字段出现**三种写法**:
+
+| 写法 | 容器数 | 来源 |
+|---|---|---|
+| `remainCny` / `source` / `totalCalls` | **7** | 原版 `status-report.py` ✅ |
+| `remainRmb` / `remainUsd` | 1(`agent-node`)| 更老的 v2 脚本 |
+| **`cny` / `source`** | **1**(`agent-node-lj-test`)| **容器自己写的复刻脚本** ❌ |
+
+**每多一个"自己写"的,就多一种格式。** 这个包的目的就是**堵住这个口子**。
+
+---
+
+## 一、三步走(新容器上线)
+
+```
+① 部署原版脚本(不让人自己写)
+        ↓
+② 配 4 个环境变量
+        ↓
+③ 跑验收清单(9 条断言)
+```
+
+### ① 部署原版脚本
+
+**路径固定**(不要再换):
+
+```
+/opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+/opt/data/git-repos/agent-status-reporter/state/          ← 状态快照与失败队列
+```
+
+**脚本来源优先级**(按顺序试):
+
+| 顺序 | 方式 | 命令 |
+|---|---|---|
+| 1 | **从同宿主正常容器拷**(最快) | `docker cp <正常容器>:/opt/data/git-repos/agent-status-reporter/scripts/status-report.py <目标容器>:/opt/data/git-repos/agent-status-reporter/scripts/` |
+| 2 | 从平台仓库拉 | 见 `部署说明书.md` |
+| 3 | 由项目侧提供 | `E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(v11,49410 字节)|
+
+> 🔴 **不要自己重写**。原版 1100 行,覆盖了:变化检测、失败队列、token 自愈、任务残留清理、
+> 官方 channel 探测、usage 聚合。自己复刻必然漏掉其中一部分 —— 这次踩的就是这个坑。
+
+### ② 配环境变量
+
+只改 `/opt/data/.env`(**改前先备份 `.env.bak-<时间戳>`**):
+
+```bash
+# 🔴 必填 4 项
+AGENT_ID=<容器名,如 agent-node-xxx>          # 缺省会退化成 hostname → agent not found
+FMODE_SESSION_TOKEN=<主人的 sessionToken>      # 决定容器归属哪个账号
+AGENT_FUNCTION_ID=xlosi3m46q                  # 🔴 填错会走到 OBS 上传,数据进不去
+WECOM_BOT_ID=<企微 BotID>                     # 有企微通道时必填
+
+# 可选
+AGENT_STATUS_HEARTBEAT=30                     # 心跳间隔秒
+AGENT_STATUS_ENABLED=true
+```
+
+> ⚠️ **企微配置不在 `.env`** —— 在 Hermes 的 `config.yaml` 的 `platforms.wecom` 段。
+> 实测坑:只读 env 会导致 `wecom:false` + `botId` 为空。原版脚本已支持从 `config.yaml` 解析。
+
+### ③ 常驻
+
+容器通常**没有 crontab**(实测多容器均无)。三种方式按优先级:
+
+```bash
+# 方式 A · s6 服务(如果容器用 s6,与其他服务一致,最稳)
+# 方式 B · 宿主 cron 统一调度
+* * * * * root docker exec <容器> python3 /opt/data/.../status-report.py >/dev/null 2>&1
+
+# 方式 C · 容器内循环(兜底;脚本自带 daemon 模式更优)
+nohup sh -c 'while true; do python3 /opt/data/.../status-report.py; sleep 30; done' >> /tmp/x.log 2>&1 &
+```
+
+> 原版脚本自带 `daemon_loop()` 模式,优先用它而不是外层循环。
+
+---
+
+## 二、验收清单(9 条,缺一不可)
+
+```bash
+# 上报一次后回读
+curl -s 'https://server.fmode.cn/parse/classes/FmodeAgent?where={"agentId":"<你的agentId>"}' \
+  -H 'X-Parse-Application-Id: ncloudmaster'
+```
+
+| # | 断言 | 通过标准 | 常见错法 |
+|---|---|---|---|
+| 1 | `host.cpuPercent` | 数字,0~100 | 发 `cpuLoad1`(负载不是占用率)|
+| 2 | `host.memPercent` | 数字,0~100 | — |
+| 3 | `host.diskPercent` | 数字,0~100 | — |
+| 4 | `channels` **是对象** | `{...}` | 发数组 `["wecom"]` |
+| 5 | `channels.matrix` | 与真实通道一致 | 只读 env 声明,假亮 |
+| 6 | `channels.wecomBotId` | 等于 `WECOM_BOT_ID` | 从 env 读不到(实际在 config.yaml)|
+| 7 | `heartbeatAt` | 60 秒内 | 发裸字符串(需 `{"__type":"Date","iso":...}`)|
+| 8 | `usage.totalTokens` | > 0 | 未接 `state.db` |
+| 9 | `balance.remainCny` | 有数字 | 发 `cny`(现已由云函数兜底,但**不该依赖**)|
+
+---
+
+## 三、三条纪律(这次踩坑换来的)
+
+### 纪律 1 · 查运行态,不查路径
+
+```
+❌ find / -name "status-report.py"     → 找不到就说"没部署"
+✅ ps aux | grep -i status             → 拿到 pid
+   cat /proc/<pid>/cmdline             → 看实际跑的是哪个文件
+   ls -l /proc/<pid>/cwd               → 实际工作目录
+```
+
+「找不到文件」≠「进程没在跑」。
+
+### 纪律 2 · 调用失败 ≠ 凭据失效
+
+| 现象 | 容易误判 | 实际 |
+|---|---|---|
+| `Bearer` 头 → 401 | token 过期了 | 调用姿势不对 |
+| body `{"sessionToken":...}` → 200 | — | ✅ 正确姿势 |
+
+**先换姿势,再怀疑令牌。**
+
+### 纪律 3 · 每个数字都要回读,不许自算
+
+```
+❌ 自己算一遍 cpuPercent=16.4,然后报"已验证"
+✅ 上报后 op=get 读回原文,把字面值贴进报告
+```
+
+实测教训:容器连续两轮报「已验证」,但库里根本没那个键、值也对不上。
+
+---
+
+## 四、平台侧兜底(已上线)
+
+云函数 `xlosi3m46q` **v0.1.3**(2026-10-20)已加 `balance` 键名归一:
+
+```
+cny / remainRmb / remain  →  自动补 remainCny(原键保留,只增不改)
+```
+
+**效果**:以后容器即使写错键名,App 也能显示余额。
+
+⚠️ **但这是兜底不是许可** —— 新容器仍应按 §二 第 9 条报 `remainCny`。
+兜底只能救「已经写错」的,救不了「没上报」的(如 `usage` 压根没发,变不出来)。
+
+---
+
+## 五、新容器上线检查表(可直接贴给容器)
+
+```markdown
+【容器状态上报 · 标准接入】
+
+一、部署
+  mkdir -p /opt/data/git-repos/agent-status-reporter/{scripts,state}
+  从同宿主正常容器 docker cp 原版 status-report.py(不要自己写)
+
+二、配置 /opt/data/.env(先备份)
+  AGENT_ID=<容器名>
+  FMODE_SESSION_TOKEN=<主人 token>
+  AGENT_FUNCTION_ID=xlosi3m46q
+  WECOM_BOT_ID=<企微 BotID>
+
+三、常驻(crontab 一般不可用,用 s6 或 daemon 模式)
+
+四、验收(回读平台值,逐条断言,缺一不可)
+  □ host.cpuPercent 0~100
+  □ host.memPercent / diskPercent
+  □ channels 是对象不是数组
+  □ channels.matrix 与真实状态一致
+  □ channels.wecomBotId 匹配
+  □ heartbeatAt 60 秒内
+  □ usage.totalTokens > 0
+  □ balance.remainCny 有值
+
+五、纪律
+  · 用 ps/proc 查进程,不用 find 查文件
+  · 调用失败先换姿势,再怀疑 token
+  · 报告里的每个数字必须是回读原文的字面值
+
+报告格式:
+REPORTER-OK agentId=<...>
+cpuPercent=<回读> memPercent=<回读> diskPercent=<回读>
+channels=<对象/数组> matrix=<true/false>
+heartbeatAt=<回读> usage.totalTokens=<回读> balance.remainCny=<回读>
+```
+
+---
+
+## 六、关联文档
+
+- `docs/03-interface/reporter字段规范-20261020.md` —— 逐字段契约(**改造前必读**)
+- `docs/03-interface/容器初始化流程优化建议-20261020.md` —— 初始化流程与排查方法论
+- `docs/03-interface/reporter部署方案-20261020.md` —— 单容器部署细节
+- 原版脚本:`E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(v11)
+- 云函数备份:`.local-backup/xlosi3m46q-v0.1.3.js`(当前线上),`-v0.1.2.js`(回滚基线)