Commit Graph
253 Commits
Author SHA1 Message Date
shanqin d03fed97d2 进度暂存:L2 启动活动按钮接功能 —— 参数抽屉(LaunchDrawer)按规格渲染,占位参数表每活动一份,提交落「待执行」演示行;需求文档记 2026-08-27 决策 2026-08-27 10:31:50 +08:00
xcsandClaude Opus 5 df18882322 脚本 e2e 逼出的八条:闸看错了表 · 拒回喂指针 · 零行骗过闸与哨兵 · 早收不该算撞墙
凭证到位后 `w05` 五案例连跑六轮,逼出八条**浏览器走查逮不到**的缺陷 ——
脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候永远不会去试
「空着交上去会怎样」。末轮:跑到的四例全部 released,三例被拒 0
(第五例停在大模型账户余额不足,不是代码问题)。

平台侧(六条):
- L 数据工单的提交闸没牙 —— 卡没绑表就回落到必交付物,而数据工单要人补的是
  `parts.*` 主数据,不在交付物名单里:闸盯着一堆无关的表、一格不缺,空手交卷一路放行。
  契约新增 `ExecutionWorkOrder.gateLedgers`(这张卡该拦哪几张表,发卡的人说了算),
  `gateAddressesOf` 变三级取一:绑的表 > 卡自报 > 交付物。
- M `finish` 撞 doneWhen 为假的拒回只谈「行与列」,而缺的是一次**工具调用**;
  现在摆出判据原文 + 那几个名字此刻的读数,`tool_*_calls` 读到 0 的单独点名(`unmetWhy`)。
- N 人点了决策卡,模型每拍再发一张,连拒五拍熔断。把「他选的是 approve」写进拒回**没用** ——
  下一拍它又从头翻一次会滚走的窗口。改结构:`answeredCardFact` 把
  「这张卡已答过 → 他选的是什么」做成每拍都在的事实行。
- O 模型传 `requirement: null`,工具 `null.trim()` 当场崩(边界上一律经 `text()` 归一)。
- P 拒回里的 `count=(没写过)` 是 S-FEEL 函数名被当成台账名 —— 假线索模型会去追。
- Q/S 零行 = 零缺,闸与哨兵一起放过空手交卷的数据工单(`emptyGate`:卡自报的表一行都没有
  = 没完成);「早收」头一次只落观察、第二次起才算被拒(与「重发已答过的卡」同一把尺)。

活动侧(1.2.7 → 1.2.9):三张执行工单各自声明 `gateLedgers`;说明书③补「发卡前骨架必须先有行」、
⑤补这张卡的前提(一颗 absent 都不剩);哨兵 `parts_profile_rows >= created` 逐颗对账;
S0_bind 写明发录入卡那一拍别抢写需求行。

e2e 脚本加 `JCAI_E2E_BEARER` 直供旁路(服务账号 client_credentials 仍是正路)。
门禁 typecheck / lint / depcruise / test(2091)全绿;api:check 的差异仍只在 org/* 与 api/*
(工作树里先前那批鉴权改动),card.d.ts 是本轮的纯新增字段。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 09:31:35 +08:00
xcsandClaude Opus 5 c6b02dfd39 ACT-WORK-05:标准化参数集改子表 parts.parameter,与目标件页面结构对齐
用户核对实跑页面后指出:P-08 数据工单做出来的页面结构必须与目标件
《数据工单-01-芯片主数据创建》一致——目标件本就是两张表(表 1 主记录 /
表 2 标准化参数子表),而实现把参数集塞成了表 1 的一个文本列,违反
《SS02-SS03-SS04_Demo台账字段细化与统一方案》§六「重复明细用子表承载,
不在单一文本字段堆叠」。

五处联动改齐:
- 平台基线新增 parts.parameter 主数据表(内部料号+参数名复合主键),
  两颗样例料的参数集从 normalized_parameter_set 文本列拆成 7 行子表数据
  (数值原样搬,一个字未改);parts.profile 物理列保留、不再赋值。
- 活动清单:parts.profile 撤掉该字段声明,新增 parts.parameter 声明
  (internal_pn 引用墙、param_value/constraint_type 必填);版本号
  1.1.1 → 1.2.0。
- 案例模型:S0d_p08_master/S1_input_confirm/S2_normalize 的读写面与
  guide 全部改指向新子表;顺带修正候选计数措辞歧义(实测样例0一张该发
  的决策卡被跳过——"候选absent且候选数≥2"被模型读成"不在库的候选≥2颗",
  改成先分别数"候选总数"与"不在库的候选数"两个数、显式说清楚判据认哪个)。
- caseFileModel 补 parts_parameter_missing 读数,两条哨兵(s_p08_created /
  s_pns_resolved)一并要求这一列不缺必填。
- 报告模版补齐表 2(逐块搬自目标件),主键列挂 data-col,约束类型走
  data-class-of 徽标。
- e2e 套件加性支持"一张卡填两张表"(stage.extraTables),buildEdits
  的 rowKey 泛化支持复合主键数组形态(不破坏既有八套件)。

验证:vitest 413 passed(活动包+平台种子测试全绿)、typecheck/lint/
depcruise 三绿、activities:install 零静态审发现、SQL 核对
parts.parameter 主键与行数(两颗料各7行)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 02:19:39 +08:00
xcsandClaude Fable 5 e1b04f01aa feat(w05): 差异评估活动登记进构建与安装通道;装包时顺带注册纯工具
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:13:26 +08:00
xcsandClaude Fable 5 f52f0168df feat(w05): 差异评估两枚纯工具 —— 逐参数四态比对与推荐定级(R-2026.07)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:17:43 +08:00
xcsandClaude Fable 5 63034f5ab2 feat(w05): 主数据基线加料件/需求/替代/验证四域六表(P-08/P-10/P-01/P-11/P-06)与样例料号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:10:08 +08:00
shanqin 92b7796601 沙箱占位页清成空白页(2026-08-26 用户定):内容(含骨架)全清,沙箱功能保留,等接口按活动定义返回 HTML 2026-08-26 21:26:18 +08:00
shanqin a8797b8cc8 L2 沙箱占位页清空演示数据(2026-08-26 用户定):只留骨架当结构占位,空值口径 -/暂无数据 2026-08-26 21:20:25 +08:00
shanqin fce60b1570 确认气泡挪到触发钮左侧(2026-08-26 用户定:在按钮旁边):垂直对到钮中线,小三角从右缘指回;L2/L3 各自的 right 让位值随钮数定 2026-08-26 20:49:18 +08:00
shanqin 8139948c28 活动模块二次确认瘦身成轻气泡(2026-08-26 用户定):只问一句+确认/取消,不摆影响三必答;本地 PopAsk 组件替代共享 AskBand,L2 禁用/激活与 L3 终止同用 2026-08-26 20:41:47 +08:00
shanqin 237286e908 活动模块二次确认改气泡弹层(2026-08-26 用户定):锚定页头右下+小三角指回触发钮,三必答收竖排;L2 禁用/激活与 L3 终止同生效;页头容器补正 z-index 破动画层叠困局 2026-08-26 20:15:13 +08:00
shanqin 023d3157a0 实例不可删除、不可编辑(2026-08-26 用户定):L3 撤掉编辑/删除入口,只留进行中的终止活动;需求稿同步关闭两条待确认 2026-08-26 19:59:46 +08:00
shanqin 0c029b694d 工作台撤顶栏:身份菜单收进左轨底部(悬停从轨右侧弹出);姓名展示改姓在前(token claims 与名册同步两处拼装同尺) 2026-08-26 19:42:31 +08:00
shanqin 99f021f964 活动模块 L3 实例详情页:过程时间线为主角的主从布局;终止/编辑/删除收在最深层;L2 查看详情接入 2026-08-26 15:39:19 +08:00
xcsandClaude Fable 5 a681ffe9f7 合并 feat/keycloak-auth:统一鉴权中心后端(ADR-012)+ 邮件收发模块(ADR-013)+ CI/CD 入库
CI / ci (push) Failing after 10s
主支这边(f01286a)是前端接 Keycloak(keycloak-js / PKCE / Bearer / 60 秒票据)、
活动模块运营态模版、数据域与工作台改版、/me 个人中心;分支这边 66 条:后端资源服务器
与角色→端、邮件收发(IMAP/SMTP · 企业微信邮件 API)、CI/CD 四件与两份文档、
pnpm-lock 补 mail 模块 importer。

冲突两处(packages/contracts/src/org.ts · api/workbench.ts):前端对着后端文档先行补的
principalSchema / meRoute / authTicketRoute(注释里自己写着「apps/api 仍是旧后端,契约暂留」)
缺 RouteContract 必填的 access,SURFACES/surfaceSchema 也已在 api/http.ts —— 取分支(后端)
这一份;前端代码对它原样编译通过(staff.email / roles 本就在 staffSchema 里)。
pnpm-lock 自动合并,--frozen-lockfile 核对一致。

五道门禁绿:1945 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项);
合并树与先前验证过的那棵树(9e9ba38)逐字节一致。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01712YeSo5fTfkCSPZHBiniq
2026-08-26 14:34:19 +08:00
shanqin f01286a89d 接入统一鉴权中心(Keycloak,ADR-012);活动模块 L1/L2 运营态模版(历史活动列表+检索+沙箱);数据域与工作台界面改版;个人中心 /me 2026-08-26 13:29:36 +08:00
xcsandClaude Fable 5 f70de44338 终审残余两处:换 address 就是换信箱;错误话的出口也剥 NUL
复审的两条范围外观察,都落在这一轮刚动过的那两段里,一并收掉:

① 收侧判据看不见 address —— `imapSettingsOf` 用的是 `login ?? address`,而 login **常常空着**,
   这时候真正决定「登到哪个信箱」的就是 address。上一刀把发信三键排除掉之后,
   换 address 判据为假:旧游标的 UID 说的是原来那个箱子,而 UID 是按信箱编号的。
   判据改认**实际登录名**:login 填了就比 login(这时 address 只是发信落款,换它不动收信),
   空着就比 address。wecom 不参与 —— 它的 address 由探测回填,比它会在回填那一刻误清游标。

② 治毒信的那道门自己会被同一个字符毒死 —— 降级存档把解析失败的原因写进正文与
   `last_poll_error`,而那串原因常常来自那封信自己的字节。`errorText` 出口一律剥 NUL:
   所有落库的错误话都从这一个出口出去,分散到每个调用点去剥,总会漏掉下一个新调用点。

两条测试都验过 RED→GREEN(拿掉修复各自变红,旁边的既有用例不受影响)。

顺带:`mail/CONTRACT.md` 两处点名的 `createFakeMailProvider` 全仓不存在(未定义、未导出、无调用方)——
契约文档摆一个不存在的出口,照它接线的人会去 import 一个没有的东西;改成实情。
debt 补 ⑳-㉔(IMAP 每轮重登录与腾讯频率限制 / wecom -1 无退避 / pollDue 每 tick 刷日志 /
notes 不设上限 / claimForResend 推 created_at 使该列不再是创建时刻)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 04:07:08 +08:00
xcsandClaude Fable 5 58b1111e7e 终审修复轮补测:翻页上限与 lowestUid 各钉一条(P1-1 / P1-2 原本只改了实现没有测试)
- wecom 翻页上限:服务端一直回 has_more:1 时不再死循环 —— 到顶交出手上这些、more=true、游标原样退回。
  单请求 30s 超时护不住它:每一页都是成功返回,超时永远不触发。
- imap 回填锚点的取最小抽成纯函数 `lowestUid`(原先内联在 withImap 里,测不着):
  测里拿二十万个 UID 反证 `Math.min(...uids)` 真的抛 RangeError,而 reduce 那一版稳。
- 顺手清掉 mail-service-poll.test.ts 里最后一处指向「修复报告」的注释(语境会丢,P1-6 同款)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 03:49:57 +08:00
xcsandClaude Fable 5 816389a185 邮件收发终审修复轮:token 按凭证指纹、毒信有退路、只改发信设置不丢信、重发先抢、列表不搬原件、save 不盖运行列、重锚出声、脏游标不炸页、线程口径归一
全分支终审(跨任务视角)逮出的十条 P0 + 八条 P1,一次修完。每条都配了测试。

P0-1 企业微信 token 缓存改按**凭证指纹**(sha256(corpId,corpSecret),secret 不进 Map 的键):
  按 account.id 存有两条真实串号路径 —— 草稿探测共用 'draft'(探完 A 公司再探 B 公司会命中 A 的
  token,回的是 A 的应用邮箱地址,凭证填错的 B 也显示成功);已存账号换了凭证 id 不变,
  下一轮轮询继续拿旧 token 拉旧 corp 的收件箱,分发给新账号绑的通道(窗口 ≤ 7200s)。
P0-2 一封「毒信」不许卡死整条路:①domain 的 buildInboundRecord 把落 text/jsonb 的字段一律剥 NUL
  (PG 拒收它,MIME 解出二进制垃圾时真会出现);②单封失败先落**降级骨架**(原件原样存、
  主题从原始字节扒、正文写明失败原因)→ 落成了就推进游标并把「第 N 封(providerRef)解析失败,
  已降级存档」写进 last_poll_error;骨架也落不下才停在这一封,那时 error **点名是哪一封**。
  降级行不自动分发(正文是失败原因,不是客户的话)。
P0-3 cursorReset 只比**收侧**键(imapHost/imapPort/imapTls/login/folder;corpId/apiBase):
  从前比全部 settings,把 465 改成 587 也会清游标 → IMAP 重锚 → 上次游标到重锚之间的来信永远不拉。
P0-4 复用旧行重发**先抢**(OutboundStore.claimForResend,一条条件更新;真 store 与内存件同款):
  两个后端连同一个库,两台同时重放同一次放行都判「超时可复用」→ 两封真邮件,破掉「同键不发两封」。
P0-5 列表与只要元信息的读**不取 raw**(一封上限 16MB × 200 行);rawAvailable 由 `raw is not null`
  投影,不靠取回字节再判;只有「下载原件」走全列的 get()。InboundRecord 新增 rawAvailable。
P0-6 账号 save() 只写**配置列**:运行列(cursor/last_polled_at/last_poll_error/last_inbound_at/
  polling_until)只经 savePoll/claimPolling/releasePolling 改 —— 整行 upsert 会把另一台正持着的租约
  与刚推进的游标盖回读那一刻的快照(08-24 loop_state 整份覆盖同款指纹)。清游标改为显式单写那一列。
P0-7 UIDVALIDITY 变化不再静默:provider 回 note,用例落进 last_poll_error(「已重锚在当下;
  期间来信未拉取」)—— 丢了一段信而账号页一片绿是界面撒谎。
P0-8 parseCursor 连取值范围一起查(非负安全整数 + 界内:UID ≤ 0xFFFFFFFF、秒 ≤ Date 上限)——
  1e13 正是安全整数,乘 1000 出界,一行脏游标能让 listAccounts 整页 500。debt ⑨ 划线销账。
P0-9 线程口径归一:threadIdOf 改成**取根**(references[0] ?? inReplyTo,ADR-013 决策 3),
  mail-wiring 调它、不再内联一份。原测试改断言,不删。
P0-10 文档说老实话:mail.send 工具路发出的信**不登记 outbound_ref**(recordOutbound 的唯一调用方
  channel.sendMail 在 src 里零调用方)—— API 文档 §4 与 ADR-013 决策 6 各补一段,根治记 debt。

P1:imap anchor 的 Math.min(...uids) 改 reduce(十万级 UID 会栈溢出)· wecom 翻页加页数上限
(服务端一直回 has_more:1 会死循环)· MAIL_TOOL 导出并对**真契约**断言(原 v1/v2 是手抄夹具、
自证,且 v1 版本号写错)· reset-runs 清运行数据时把邮箱账号的游标与拉取状态一并清(留着游标而信没了,
wecom 十分钟重叠窗会把最近的信当新信再投一次)· due.test 的 leaseMsFor(30) 断言改写死 300_000(原为自证)·
cursor.test 删掉指向评审报告的注释 · 设计稿稿头标注三处已被 ADR-013 取代 · ADR-013 补记 wecom
compose_send 不带 inReplyTo/references(它发出的信在对方那里不串线程)。

契约变更(§12.4):port.ts 的 InboundStore 加 getMeta、OutboundStore 加 claimForResend、
savePoll 的 cursor 允许显式 null、MailProvider.pull 回参可带 note —— 实现方只有本模块 adapter 与
__tests__/fakes.ts 的内存件(两边语义一致,已同改),CONTRACT.md 已同步,api-report 已刷新。

测试:1922 → 1942(mail 模块 102 → 113,含真 PG 的 save 不盖运行列 / 不搬原件 / 两条真连接抢同一行)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 03:47:48 +08:00
xcsandClaude Fable 5 e862a5279b 文档:ADR-013 邮件收发、API 文档与前端对接说明(两形账号 / 四个动作 / 投递状态 / 腾讯侧前置)、总表 15 条、欠账十五笔、CLAUDE 快照
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:38:12 +08:00
xcsandClaude Fable 5 e0fc16db33 mail.send 工具契约 2.0.0(to 数组 / html / 附件 / inReplyTo / dryRun;保留 v1 的 body 与 queued);契约快照刷新,五道门禁绿
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:23:24 +08:00
xcsandClaude Fable 5 b6ec37a231 mail 接线两处收紧:入站线程取根不取最近一跳(否则跑一个来回①级归属就落空);附件没给 uploadRef 当场判 invalid,不静默丢掉
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:20:08 +08:00
xcsandClaude Fable 5 2d1558b99e 邮件路由 15 条:账号 CRUD / 探测 / 立即拉取 / 试发 / 入站出站 / 附件原件 / 重新投递;领域错误翻状态码,厂商失败在 body 里说
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:13:16 +08:00
xcsandClaude Fable 5 7b79d542a8 修复轮 1(wecom pull):游标推进与 more 改由同一判据决定,治单页 has_more:0 也被 limit 截断时丢信
评审逐字节比对确认 wecom-provider.ts 与 brief 转录一致——问题在计划本身:`exhausted` 只说明
页翻完了,而 LIST_PAGE(100)与 opts.limit 无关,最后一页可能同时 has_more:0 且新信多过本轮预算,
此时按 exhausted 推进游标会把 wanted 截掉的那几封永久丢弃(重叠窗只兜 10 分钟)。

改法:先算 more = !exhausted || fresh.length > wanted.length,再据同一个 more 决定 cursor 是否
推进到窗口上沿——游标与 more 从此不会分裂成两个判据。补两条测试:①单页 has_more:0 但新信
多过 limit 时游标原样退回(改前 RED:游标被错误推到 endTime,改后 GREEN);②-1 连续繁忙到
边界时 get_email_alias 恰被调 4 次(首次 + BUSY_RETRIES 3 次),此前已隐性通过,补断言钉死。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:09:06 +08:00
xcsandClaude Fable 5 d64f0b5ffd 修复轮 1(评审):mail 用例测试补钉三处伪覆盖 —— 附件不重存/中途失败/分发不因单个失败断链
评审判 Needs fixes:三条断言此前恒真,改坏实现照样绿。①重复拉到里撞 providerRef 的
那封改带附件,才验得出「守卫在存附件之前生效」;②拉取抛错改为两轮 pollNow ——
第一轮先用 afterCalls 让 pull() 真正成功推进游标,第二轮才炸,不再是「零封被处理,
游标没动只因为它压根没机会动」;③分发失败的订阅方 boom 从最后一个挪到中间(ok →
boom → skip),证「继续往下投」而不是恰好排在末尾。send 测试补 references 断言。
fakes.ts 的 failPullWith 加 afterCalls 支持第 N 次调用起报错。逐条用临时注入的
实现缺陷验证测试确实会红(验证后已还原,mail-service.ts 净差异为零)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:06:42 +08:00
xcsandClaude Fable 5 0c6e8e2c1b app 装配 mail:channel 作首个订阅方、出站经 mail.send、mail.send 工具真发信(dryRun 只校验)、轮询工作者;上传件存取抽出共用
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:05:50 +08:00
xcsandClaude Fable 5 5bd92f2e93 mail adapter:企业微信邮件 API provider(token 缓存与刷新 / 翻页 / 已知 mail_id 不再读 / EML 编码兜底 / compose_send)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:50:11 +08:00
xcsandClaude Fable 5 7afc40987b mail adapter:IMAP/SMTP provider(只读打开 / UID 游标 / n:* 过滤 / UIDVALIDITY 重锚 / nodemailer 发信)+ env 门控真信箱 IT
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:47:24 +08:00
xcsandClaude Fable 5 40e592ae94 mail 用例测试:发信留痕 / 厂商失败不抛 / 幂等键 / 附件来源 / from 对账号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:39:06 +08:00
xcsandClaude Fable 5 fba82c71dc mail 用例测试:拉取 / 去重 / 逐封游标 / 失败停位 / 分发三态 / 重新投递 / 附件原件
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:38:01 +08:00
xcsandClaude Fable 5 c5cb779390 修复轮 1:邮件 GET 改归 ops,不撑大两端共用读路由名单
全仓测试 route-access.test.ts 钉着一份有意枚举、随 Keycloak spec 审过的名单:哪些读路由被两端共用。
之前把邮件 7 条 GET 标成 design|ops,名单从八条长到十四条。裁决改路由、不改名单:邮箱那一屏落在
运营中心,设计端没有消费方;入站正文装的是客户来往内容,ADR-012 已把「设计端能读客户主数据」记为
一处有意承担的代价,不再无故加宽。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:36:12 +08:00
xcsandClaude Fable 5 78e36ab73c 修复轮1:凭证解不开时明文经 SyntaxError 泄进 last_poll_error;同键在飞的那封会被当失败行重发;发成功不清上一次的错误话;卡死的 dispatching 给人手一条路
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:29:24 +08:00
xcsandClaude Fable 5 79fd25e1c1 mail 测试夹具:内存 store 排序与真 store 严格同序,补两处差异注释
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:11:48 +08:00
xcsandClaude Fable 5 1328aeb142 mail 用例:账号建改删与探测(草稿探测可借库里密文;wecom 探到地址回填);收发用例同份落地待测
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:08:48 +08:00
xcsandClaude Fable 5 b9a4af9a2a mail adapter:postal-mime 解析器(GBK 字头 / 群组地址 / References 拆串 / 附件字节)+ 测试夹具
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:59:40 +08:00
xcsandClaude Fable 5 d65579c0d8 修复轮1:mail 账号列表排序键还原为 name(与 channel/llm 惯例一致),测试改断言集合而非顺序
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:53:28 +08:00
xcsandClaude Fable 5 5f1e64b4d4 迁移 0033:邮箱账号 / 入站 / 投递 / 出站四张表;mail store 与跨进程租约(两连接对抢 20 轮每轮一个赢)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:49:24 +08:00
xcsandClaude Fable 5 8d7e56aa92 mail domain:发信校验与选账号(from 不静默回落)、分发状态机、账号记录与视图(密文进不了视图)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:28:02 +08:00
xcsandClaude Fable 5 6748ef01b7 mail domain:解析结果 → 入站记录 / 视图,附件与原件按上限分存留注,厂商原话脱敏三形
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:26:08 +08:00
xcsandClaude Fable 5 83f65b2785 mail 模块骨架:契约 / 端口 / 领域错误 / 游标两形 / 到期与租约(ADR-013)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:11:14 +08:00
xcsandClaude Fable 5 7c6e399b57 邮件路由契约 15 条:账号 CRUD / 探测 / 立即拉取 / 试发 / 入站出站列表 / 附件与原件 / 重新投递
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:59:57 +08:00
xcsandClaude Fable 5 26f07f3487 mail 模块登记为 L1;批准 imapflow / nodemailer / postal-mime 三个依赖(设计稿 §2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:34:17 +08:00
xcsandClaude Opus 5 bb22545bdf 路由表字面重复也掀桌子;死信没收件人时出声;票据横向范围与「权限只到端」写进 ADR
**I1:字面路由重复注册仍是静默覆盖 —— 与参数侧的掀桌子不对称。**
两条**字面**契约同方法同路径撞上时,后写覆盖先写,命中谁全看 `Object.values(contracts)`
的顺序。参数模版撞了会抛(理由是「命中谁只看声明顺序 = 静默判错权限」),而字面这一半
没设防:同一个病、同一个后果。把守卫从 59 处收成 1 处之后,注册表是**唯一**的授权
事实源,它的自检不该有半边缺口。`size` 一并从 `routes.length` 改成
`literal.size + patterns.length`(重复被拒之后两者相等,但语义上它说的是「表里有几条」)。
`lookup` 上补一句:它只认 HTTP_METHODS 那五个,**归一是调用方的职责**;测里钉了
`lookup('HEAD', …)` 仍是 undefined —— 将来有人把中间件那行归一挪走,这条分工还在。

**顺手两条:**
- 死信通知的收件人为空时不出声(`deadLetters.report`),而 container 的 governanceStaff
  对同一件事会往 stderr 喊一句 —— 后者的理由(把「出事了」无声丢掉最糟)在前者同样成立。
- ADR-010 状态行补「已被 ADR-012 取代」:ADR-012 说了取代,但先翻到 ADR-010 的人看不到。

**两条不修,写进文档:**
- 票据的**横向范围**进 ADR-012 与 debt:票据里没有用途,60 秒内它等于持票人的
  **全部读权限** —— 凭一枚为下载附件取的票据,同一分钟里读得了整张客户主数据。
  不是提权(那些读权限他本来就有),风险在留痕面(URL 走浏览器历史 / 反代日志 / Referer)。
  收敛做法记进 debt:签发时把用途绑进票据(scope ∈ {stream, file}),或中间件对认票据的
  路径设白名单。
- 「权限只到端」的影响面进 ADR-012 的「后果」:design 现在同时意味着装活动、跑测试台、
  传文件、探大模型;design|ops 意味着 `GET /api/gov/data/:domain/:object` 能翻整张客户
  主数据(**从前要 data:read 才行**)。半年后有人问「为什么设计的人看得到客户手机号」,
  ADR 里得有这句话在。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:42:48 +08:00
xcsandClaude Opus 5 5facf6b745 名册同步补三道下限保护;同名让位;停用判定走在 JIT 写之前
**C2:一次同步能把所有人停用,而停用之后没有界面能救回来。**
「本地有、远端无 → 停」是有意的,但没有下限:①`listUsers()` 因任何原因回了空数组
(realm 配错但存在、代理 200 空体、view-users 被改窄)→ 全表 disabled;②更可能的是
**Admin 的 `GET /users` 不返回服务账号用户**(手册 §14.5 为此单列一条接口)——
e2e 与试跑的 AI 账号是首登 JIT 建的,本地在册、远端查无此人,**第一次点「同步名册」
就把它停了,再同步多少次都回不来**。而恢复动作 `POST /api/gov/staff/sync` 要 ops,
ops 要一个 active 的 principal:全员停用之后这条路自己把自己锁死,只剩去库里手改 state。

三道闸(两个触发面分别被后两条挡住,第一条是最后的网):
- `diffRoster`:`remote` 为空时不产出「缺席即停」—— 远端一个人都没有,更像取数出了问题;
- `diffRoster`:`testAccounts` 豁免「缺席即停」(新入参,必填 —— 只有装配层知道哪几个是
  服务账号),判据用既有的 `isTestAccount`(不分大小写、忽略空白);
- `syncRoster`:落 setState 之前用**同步后的活跃员工角色集**(新纯函数 `activeRolesAfter`)
  跑一遍 `assertNotLockedOut`,不过就整体拒绝、一条都不写;路由映射 409(与
  setRoleAccess 那两条同款,`syncStaffRoute` 从前没有这个 catch)。
  `AccessLockoutError` / `assertNotLockedOut` 收可选 message —— 两个来路拒绝的理由不同,
  给人的下一步也不同(这条指向「先核对 provisioning 指的是不是同一个 realm」)。

**I3:JIT 与同步撞 `account` 唯一索引 → 500,且此人从此登不进。**
Keycloak 里删了人再建一个同名的(或用户名被让渡),新 sub 走 insert 就撞
`staff_account_key`,`server.ts` 的 onError 把 PG 原文原样回给调用方。
处置选的是**先查后让位**(不是捕异常):走到 insert 就说明按 sub 找不到他,本地那条同名的
必然是另一个 sub 的 stale 行 —— 把它的账号让位成 `账号#retired-<旧 sub 前 8 位>` 再插,
留一条 stderr;不删那一行(它身上可能还挂着在途工作)。`StaffRepository.findByAccount`
为此回到端口上(退役的是 OrgService 上的那个同名出口,不是仓储这一口)。
同步逐条落库改为**单条失败不停在半路**:失败的人点名进 `RosterSyncOutcome.failed`
(契约变更,api-report 已刷新),三个计数改成**真落下的条数** —— 半途失败的同步
不许报成一排绿数字。

顺手两条:
- `authenticate` 的 JIT 写挪到停用判定之后 —— 一个已停用的账号只要还攥着有效 token,
  从前每 60 秒就能驱动一次 updateIdentity 写库;「第一次见到的人要建档」原样保留。
- `keycloak-admin-roster` 自己又写了一遍 dedup+sort+滤噪声 → 改用 `normalizeRoles`。
  两处对「同一次归一」的理解一分叉,`identityChanged` 的逐字比对就会天天判「变了」。

测:roster-diff +8(两道下限保护 + activeRolesAfter 四种局面)、org-service +5
(整体拒绝且一条 setState 都没落 · 远端空着不停人 · 服务账号缺席不停 · 单条失败点名且
不停在半路 · 停用账号驱动不了 JIT 写)、同名让位 1(内存仓储照搬了唯一索引这条约束)。
文档:org/CONTRACT.md 不变量与影响半径、API 文档 §0(HEAD / 409)· §3(三道保护 + failed)· §7。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:42:19 +08:00
xcsandClaude Opus 5 5964a80b28 鉴权中间件:HEAD 按 GET 判 —— 不归一等于「只要登录就能跑任何 GET 处理器」
Hono 在 #dispatch 里把 HEAD 改写成 GET 再分发,而 `c.req.method` 读的是请求行上的
原始方法,仍是 'HEAD';路由注册表只装 HTTP_METHODS 那五个,于是 lookup('HEAD', …)
**必然 miss** → 落进 `?? 'any'` 兜底 → 一个 `ends: []` 的人(Keycloak 里还没配角色的
新员工)照样能执行三个端的全部 GET 处理器。不绕过认证(无凭据仍 401),够不到
POST/PUT/DELETE,响应体被框架丢掉 —— 但它绕过**端授权**:响应头照抄过去
(desk 那两处 content-disposition 带着资料文件名),而 GET 处理器不是都没副作用
(`GET /api/reports/:period` 一路走到 report-service 的 store.save)。

改:查表之前先归一方法;**票据那一路的 GET 判断用同一个归一后的 method**,
否则 HEAD 拿不到票据那条路,两条路行为不一致。

测(auth-middleware.test.ts +3):HEAD 到 ops 路由持只有 workbench 的 token → 403
(与 GET 同款)· HEAD 到自己端内的路由 → 200 · HEAD 上的票据与 GET 同款。
反向验证:撤掉归一那一行,前两条当场红(200 / 401),装回即绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:41:43 +08:00
xcs 7978c6faca 文档修订:评审四处 —— 「约100」改实数59(governance-ops 45 + governance 14)、ADR 引用改实际文件名、CONTRACT.md depcruise 措辞改准
评审指出「约 100 处守卫」是把行数(每处两行)误当处数,实测切换前 governance-ops.ts 45 处
+ governance.ts 14 处 = 59,其余路由文件 0 处;ADR-012 与 CLAUDE.md 各改一处。
ADR-012「后果」段引用的《前端接入手册》仓内不存在,改成实际路径
docs/前端对接说明-统一鉴权中心.md。org/CONTRACT.md「depcruise 已反查确认」改成
「depcruise 全量校验通过」——这一轮跑的是全量校验,不是针对性反查,原措辞强于事实。
2026-08-25 18:39:45 +08:00
xcs aa18701130 文档:ADR-012 统一鉴权中心;编码规范鉴权定调改为资源服务器 + 权限只到端;org CONTRACT.md 重写;debt 鉴权段;CLAUDE.md 铁规矩 5/6 2026-08-25 18:32:23 +08:00
xcs fc5afa9600 route-coverage 补下界断言防「数不到路由也绿」;跳过告警点名哪道保险没跑
评审 Important:missing 为空有两种可能——真的不漏,或者根本没数到路由
(app.routes 形状变了、createServer 早退)。空集是任何集合的子集,不设
下界这条 fail-closed 测试会从「会红」退化成「永远绿」。补
registered.length > 50 的下界,反向验证过(临时调到 1000 确认真的报红)。

无库跳过时的 stderr 告警原来只说连不上库,没说清「这道保险这次没跑、
漏的是哪一类」——按评审裁决不改 skipIf 行为(仓库级惯例),只改措辞
点名到位。
2026-08-25 18:17:03 +08:00
xcs 61622f94d5 启动自检 issuer 不一致拒绝启动;路由覆盖 fail-closed;Keycloak 配置解析测试(ADR-012)
上一个任务把统一鉴权中心接上了,但缺配置照常起、issuer 配错也照常起 ——
每个请求都 401 还查不出原因。boot() 开头补两道保险:缺
KEYCLOAK_URL/REALM/CLIENT_ID 直接拒绝启动;配了就拉一次
.well-known/openid-configuration 核对 issuer,不一致同样拒绝启动
(话里带期望/实际/KC_HOSTNAME,照后端接入手册第 5 步)。

另加路由覆盖 it 测试:Hono 里每条注册路由都必须能在 appRouteRegistry()
里查到归属,查不到就是一个没人管的权限口子。真库跑通,96 条路由全部
有归属、0 条查不到——把上一个任务里人工核对的结论固化成测试。
2026-08-25 18:04:21 +08:00
xcsandClaude Opus 5 8dc71db9d8 评审两条 Important:迁移连带清掉指向身份的两张配置表;治理台收件人为空时出声
节点负责人(排在轮值之前)与触发器确认人都是无 FK 的裸 staff id,而 reset-runs 按设计
不碰配置表 —— 切换后留着就是「把单派给不存在的人」与「命中的单没人点头全进死信」。
governanceStaff 在全新安装(还没人以 admin 登录过)时是空集,而调用方是死信通知:
收件人为空原本会静默丢掉「出事了」这件事,现在留一条说清怎么办的话。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 17:57:21 +08:00