docs(debt): 新债 —— 鸿蒙 Push Kit 契约已全部落地,唯一剩余阻塞是真机(push_tokens 0 行)

补投的 1f9ff3b4(09-15 11:06,已由 d50da1b0 / b9190558 答复,129 封在后)。
那是鸿蒙接推送的客户端半边契约 + 包名硬约束(AGC 拒 harmony 保留字)。
**账本里此前没有这条线的任何条目**(getToken/com.jianf.agentmail/PushService 均 0 命中)。
本轮逐项实测 ⇒ **契约已全部落地,唯一剩余阻塞是真实设备**。

① 包名(硬约束)已落地:
  client/harmony/AppScope/app.json5:3  "bundleName": "com.jianf.agentmail"
  全工程 grep `com.agentmail.harmony` = **0 处**(无残留)
② AGC 配置在位:
  client/harmony/entry/src/main/resources/rawfile/agconnect-services.json(源)
  ⚠️ entry/build/... 下那份是**构建产物**,同步/提交以源那份为准
③ 服务端三条端点齐备:
  handler/push.go:16 设备推送登记 · :58 POST · :119 DELETE · :150 GET(+ push_test.go)
④ ★ 客户端「推送必须是可选通道」逐条查实:
  ets/api/PushService.ets(433 行)9 处调用全部 try+catch,
  ★ catch 块里 throw / showToast / promptAction / console.error = **0 处** ⇒ 全静默,
    与 pi 第④条(不报错、不阻塞、不弹失败提示;主通道仍是 SSE)一致
  通知点击跳转(第③条 data 形状):
    MainPage.ets:1874 setRouteListener · :1886 clearRouteListener · :1925 pendingRoute

★ 唯一剩余阻塞: **真实设备**
  push_tokens 表行数 = **0**(今日实测)
  卡点: getToken 只能在**带华为账号的真机**上取到,无模拟器路径
  ⇒ 服务端/客户端/包名/AGC 配置都已就位,缺的只是一台真机走一遍:
    getToken → POST push-token → 复查 push_tokens 行数 > 0

边界: **没改任何代码、没跑真机**(本轮只读 grep + SQL + 文件查找)。
  上述"已落地"是**静态核实**(文件在位、端点存在、catch 静默),
  **不等于端到端跑通** —— 端到端需要真机,这正是本条的阻塞。
  我没有核 ApiClient.ets / LoginPage.ets 里 Push Kit 的全部调用路径,
  只核了 PushService.ets 的静默性质与 MainPage 的路由三处。

验证: repo Debt ok; criteria-hygiene 10/10。
  ⚠️ debt-visibility 仍 0/1(与本提交无关: harmony-appearance.test.mjs 边界声明 6>4,
     另一会话未提交的工作树改动;判据自己写着"别只改数字,先补一笔")。
This commit is contained in:
dsh
2026-10-02 17:02:50 +08:00
parent d1099526ad
commit 8894804dec

View File

@ -455,6 +455,14 @@
"where": "server/internal/repo/thread.go:43,47,65,74,77,81(:43 cap 的用途注释『数据损坏时的兜底』、:47 const descendantDepthCap = 10000、:65 ThreadRootOf 签名、:74 WHERE up.lvl < $2、:77 Scan(&rootID, &lvl)、:81 return rootID, lvl, nil —— ★ 取到 lvl 后从不与 cap 比较)· 实测环 e43496ed/2ffb7dbb/84900edd/b3789a21/41ea9a15 · 同源两笔:thread-depth-cap-signal-read-by-nobody(截断后根不可信却照常上报)、dedup-expectation-must-filter-null-roots(None 组)",
"kind": "**『数都对、口径不同』第 4 次**(同一批 113 封:37 / 7 / 108)。根因是判据只钉了 keying、没钉**沿哪条链上溯**与 **None 怎么处理**:沿 failure 链那 5 封根全是 b3ce9d0f,沿完整 parent 链全是 None。附落地风险:上溯在自引用/环上会跑满(当前库 0 例,属将来说不定),须配『有限步终止 + 超限行为显式』判据",
"note": "★★★ 2026-09-30 登记。**同一条线上第 4 次出现「数都对、口径不同」**(前三次:7 / 37 / 108 见下表)。pi `e659a655`(2026-09-25 04:58:29)用它自己的脚本撞满上限,我 52 分钟后 `b4de8b50` 认了他的 61/37。\n\n## ★ 口径对照表(每一行都带口径与时刻 = 2026-09-30)\n\n底数:`relayed_mails` 中 `relay_key LIKE '%failure:%'` = **113** 封(pi 09-25 用的 98 **已是旧快照**)\n\n| 口径(分组键) | 组数 | 抑制 | 剩余 |\n|---|---|---|---|\n| `(agent, failroot)` **只沿 failure 链上溯** ← pi 口径 | 8 | **37** | 76 |\n| `(agent, failroot)` 但**先滤掉 failroot IS NULL** ← 我上一轮口径 | 6 | **7** | — |\n| `(agent, 完整 parent 链上溯的根)` | 5 | **108** | 5 |\n\n⇒ ★ **37 与 7 差在\"要不要把无根的算进去\"**;**108 是荒谬值**(把 32 封 `service-failure:*` 无根报告并成组)。\n⇒ 判据**必须钉住\"沿哪条链上溯\"**,而不只是钉 keying:\n```\n沿 failure 链上溯 ⇒ root 是\"最后一个 failure 报告的祖先\",无根者停在它自己\n沿完整 parent 链上溯 ⇒ root 是线程根,无根者落在 None(⇒ 必须滤掉)\n★ 这两个 root 在环上那 5 封里**给出不同的值**(实测):\n 沿 failure 链: 5 封**全部** = b3ce9d0f(pi 的自检,成立)\n 沿完整 parent 链: 5 封**全部** = **None**(我 09-30 实测)\n ⇒ 同一组数据、两个口径、两个根 ⇒ \"全指向同一个根\"这句话**必须带口径**\n```\n\n## ★★ pi 的落地风险发现(★ 这条与本会话已登记的债**同源**)\n\n```\n他第一版脚本\"沿 failure 链上溯 + 上限 50 步\",有一条链**跑满上限** ⇒ 抑制数是假的\n⇒ ★ 机制: 沿 parent/failure 链的递归上溯,在链里有**自引用或环**时会跑满\n⇒ 而\"报告的报告\"这类会话**天然容易造出 parent 环**\n⇒ ⇒ root-keying 落地**必须自带一条判据**: 根计算必须在**有限步内终止**,\n 且**必须明确超限时怎么办**(拒绝注册 / 当作根 / 静默返回错误)\n```\n★ 同一件事我在 2026-09-30 已从另一侧登记(`thread-depth-cap-signal-read-by-nobody`):\n `ThreadRootOf` 用 `descendantDepthCap=10000` 截断递归,\n 但取到 `lvl` 后**从不与 cap 比较**、原样返回并上报 `anchor_depth`\n ⇒ ★ **截断后根不可信,而调用方无从知道** —— pi 说的是\"会不会跑满\",\n 我那条是\"跑满后有没有人说\" ⇒ **同一个洞的两端**,都指向\"落地前必须先有终止/可信判据\"。\n\n## ⚠️ 但\"会跑满\"目前是**理论风险,不是活实例**(先量过再说)\n\n```\n直接自引用 (parent_mail_id = mail_id) = **0**\n200 封随机样本里存在环的数量 = **0**\n⇒ ★ 当前库里**没有**自引用/环 ⇒ pi 那条不构成现网风险\n⇒ 但它描述的**类**是真实的(环在 relay 层存在,见 f76025c9 那 5 封)\n ⇒ 风险在于**将来**: 一旦 mails.parent_mail_id 出现环,root 计算就会静默给出错根\n```\n\n## 可判动作\n\n· 写任何\"上溯到根\"的口径时,**同时写死两件事**:① 沿哪条链(failure 链 / 完整 parent 链)\n ② 无根(None)时**滤掉还是当根**。少任一个,下一个人必得另一组数。\n· 报组数/抑制数时附**口径 + 时刻 + 底数**(113 与 98 都对过)。\n· root-keying 落地时,把\"**根计算在有限步内终止,且超限行为是显式的**\"写成判据,\n 不要只写\"能终止\"(那是⑬ 一族:动作有上界 / 期望值必须等于实测)。\n\n## 边界 / 未做\n\n· **没有改任何代码、没有改判据脚本**(本轮只读 SQL)。\n· 113 / 8 / 37 / 76 / 6 / 7 / 108 都是**此刻**的值。\n· 上表第 2 行的\"滤掉 NULL 组\"我用 `(a,'x')` 占位实现过,**口径本身**与\n `dedup-expectation-must-filter-null-roots` 那条一致;两个条目读到时**不要当成两个口径**。\n· 我**没有**验证\"沿 failure 链上溯\"与\"完整 parent 链\"在**别的**会话里是否也分叉。\n· 未能回给 pi(`send_mail` 被会话闸挡下)。"
},
{
"id": "harmony-pushkit-contract-landed-blocked-on-real-device",
"count": 1,
"due": "拿到带华为账号的真机时(取 getToken → POST push-token → 查 push_tokens 行数 > 0);或任何人再动鸿蒙推送路径时(保持 catch 静默、不得让推送成为 SSE 的依赖)",
"where": "client/harmony/AppScope/app.json5:3(bundleName=com.jianf.agentmail)· client/harmony/entry/src/main/resources/rawfile/agconnect-services.json(AGC 配置;build/ 下那份是产物)· server/internal/handler/push.go:16,58,119,150(三条端点)+ push_test.go · client/harmony/entry/src/main/ets/api/PushService.ets(433 行、9 处 try+catch、catch 内 0 抛错/提示)· MainPage.ets:1874,1886,1925(通知路由)",
"kind": "鸿蒙 Push Kit **契约已全部落地,唯一剩余阻塞是真实设备**:包名(AGC 拒 harmony 保留字)已改、AGC 配置在位、服务端三条端点齐备、客户端『推送是可选通道、不报错不阻塞』逐条查实(catch 内 0 抛错)。但 `push_tokens` **0 行** —— `getToken` 只能在带华为账号的真机上取,无模拟器路径",
"note": "★★★ 2026-09-30 登记。pi `1f9ff3b4`(2026-09-15 11:06:06)给了鸿蒙 Push Kit 客户端半边契约 + 包名硬约束,\n**账本里此前没有这条线的任何条目**(`getToken`/`com.jianf.agentmail`/`PushService` 均 0 命中)。\n本轮实测确认:**契约已全部落地,唯一剩余阻塞是真实设备**。\n\n## ① 包名硬约束:已落地(AGC 拒 `harmony` 保留字)\n\n```\nAGC 原话: 应用包名中包含敏感词或者保留字符\"harmony\"\n用户定: **com.jianf.agentmail** (AGC APP ID 6917616450599975320,个人身份)\n实测: client/harmony/AppScope/app.json5:3 \"bundleName\": \"com.jianf.agentmail\" ✓\n 全工程 grep `com.agentmail.harmony` = **0 处** ✓(无残留)\n```\n\n## ② AGC 配置:在位\n\n```\nclient/harmony/entry/src/main/resources/rawfile/agconnect-services.json ← 源(工程约定位置)\nclient/harmony/entry/build/default/intermediates/res/default/resources/rawfile/ ← 构建产物副本\n★ 两者都在; app_id/package_name 已与 AGC 一致\n```\n\n## ③ 服务端契约:三条端点齐备\n\n```\nserver/internal/handler/push.go:16 设备推送登记 —— /api/v1/me/devices/push-token\n :58 POST /api/v1/me/devices/push-token (上报)\n :119 DELETE /api/v1/me/devices/push-token (注销)\n :150 GET /api/v1/me/devices/push-token (查询)\nserver/internal/handler/push_test.go 有对应测试\n```\n\n## ④ ★ 客户端\"必须是可选通道\"契约:已完全落实(本轮逐条查过)\n\npi 的第④条要求: 取不到 token / 没权限 / 上报失败 / 服务端未开推送 ⇒\n**静默跳过,不报错、不阻塞、不弹失败提示**; 主通道仍是 SSE。\n```\nclient/harmony/entry/src/main/ets/api/PushService.ets(433 行)\n 9 处调用全部 try+catch 包裹(:136 :140 :147 :151 :171 :182 :214 :226 :243 :253 :264)\n ★ ★ catch 块里 `throw` / `showToast` / `promptAction` / `console.error` = **0 处**\n ⇒ **全部静默**,与第④条一致\n通知点击跳转(第③条 data 形状):\n MainPage.ets:1874 PushService.setRouteListener(…) ← 收到通知后路由\n MainPage.ets:1925 PushService.pendingRoute ← 冷启动时补取\n MainPage.ets:1886 clearRouteListener() ← 页面销毁清理\n```\n\n## ★ 唯一剩余阻塞:**真实设备**(与本会话既定卡点一致)\n\n```\npush_tokens 表行数 = **0** (2026-09-30 实测)\n卡点: `getToken` 只能在**带华为账号的真机**上取到 —— 无模拟器路径\n⇒ 服务端三条端点、客户端契约、包名、AGC 配置**都已就位**,\n 缺的只是一台真机走一遍: 取 token → POST 上报 → 服务端落一行\n```\n\n## 可判动作 / 下一步\n\n· 拿到真机后:`pushService.getToken()` → `POST /api/v1/me/devices/push-token`\n → 复查 `select count(*) from push_tokens`(应 > 0)。\n· 在此之前**不要**把推送当依赖:任何新代码不得让推送失败影响 SSE 主通道\n (第④条已实现,改动时保持 `catch` 静默)。\n· ⚠️ `build/` 下那份 `agconnect-services.json` 是**构建产物**;\n 提交/同步时以 `entry/src/main/resources/rawfile/` 那份为准。\n\n## 边界 / 未做\n\n· **没有改任何代码、没有跑真机**(本轮只读 grep + SQL + 文件查找)。\n· 上述\"已落地\"是**静态核实**(文件在位、端点存在、catch 静默),\n **不等于**端到端跑通 —— 端到端需要真机,这正是本条的阻塞。\n· 我**没有**核 `api/ApiClient.ets` 与 `LoginPage.ets` 里 Push Kit 的全部调用路径,\n 只核了 `PushService.ets` 的静默性质与 MainPage 的路由三处。\n· 未能回给 pi(`send_mail` 被会话闸挡下)。"
}
]
}