feat(automation): add email-triggered automations - #70
Merged
Merged
Conversation
设计邮件触发功能的完善与平台集成方案:向导内联邮箱配置、 系统邮箱 IMAP 设置、服务端校验修复与健壮性改进。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…mailboxes 系统邮箱退场:所有邮件触发自动化必须绑定用户自配邮箱。 消除跨用户重复触发风险,并移除 ragent-service 外部依赖。 Co-Authored-By: Claude Code <noreply@anthropic.com>
存量处置确认为直接删除,不做暂停过渡、不保留兼容路径。 补充关联数据清理、运行历史保留说明与发布要求。 Co-Authored-By: Claude Code <noreply@anthropic.com>
功能从未被实际使用,无存量数据、无需发布说明与备份, 迁移语句降级为幂等防御性清理(预期 0 行)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
趁三张邮件表为空,将 mailbox:<id> 字符串编码改为整数 mailboxId, 拆除为 system 服务的编码脚手架。修正迁移语句顺序: 遗留数据清理必须先于列类型变更,否则非数字值会导致 ALTER 失败。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…sidual risk 修正"彻底消除重复触发"的错误结论:mailboxId 唯一不等于物理邮箱唯一, 跨用户与同一用户均可重复注册同一物理邮箱,各成独立管道导致重复处理, 且优先级机制在此场景下失效。平台不阻止该行为(已确认), 作为有意接受的残余风险记录在第九节。 Co-Authored-By: Claude Code <noreply@anthropic.com>
三张邮件表为空,改为删除空表后按新结构重建,不做结构迁移。 消除 ALTER COLUMN TYPE 的硬失败路径、迁移顺序约束, 以及版本化迁移表与独立脚本的需要。非空时抛错中止。 Co-Authored-By: Claude Code <noreply@anthropic.com>
两人共用同一邮箱属协作失误而非正常用法,且向导的邮箱下拉 天然引导用户复用既有记录。将"已知限制"降级为行为说明, 并去掉与现有向导文案重复的提示。 Co-Authored-By: Claude Code <noreply@anthropic.com>
补充新窗口接手所需但不属于任何模块的上下文: ensureAutomationTables 的失败重试行为、向导内联于 page.tsx、 ENABLE_CRON 前置条件、文件规模、i18n 与测试约定、行号时效性。 Co-Authored-By: Claude Code <noreply@anthropic.com>
键格式统一(模块 D.1):mailbox:<id> 字符串编码的唯一存在理由是让 "system"
成为合法值,改为整数 mailboxId 后该编码成为累赘。三张邮件表为空,趁此唯一
窗口直接改结构,不做迁移。
- trigger_config.mailboxKey(字符串)→ mailboxId(正整数)
- 三张邮件表 mailbox_key VARCHAR(128) → mailbox_id INTEGER,写在新的
CREATE TABLE 定义里;ensureAutomationTables() 内按表加一段 DO 块:
旧列存在且表为空则 DROP 后重建,表非空则 RAISE 报错中止,
已为新结构时静默 no-op(自失效、幂等)
- 删除调度器 mailboxIdFromKey(),分组键改为 ${userId}:${mailboxId}
- 邮箱记录 API 取消 key: "mailbox:<id>",直接暴露整数 id;
deleteAutomationMailbox 的依赖检查同步改为按整数 mailboxId 匹配
- 新增 lib/automation/mailbox-id.ts 纯函数(normalizeMailboxId /
requireMailboxId / mailboxGroupKey)与单测;读取邮箱标识的地方不再
回退 "system",缺失时抛 MAILBOX_ID_REQUIRED
Co-Authored-By: Claude Code <noreply@anthropic.com>
…l-rules.ts 规则匹配逻辑去重(模块 D.6):三份独立维护的副本合并为单一实现, 让向导里预览的命中结果与调度器实际执行的结果不可能分叉。 - 新增纯函数模块 lib/automation/mail-rules.ts(零 import,同构,同时被 服务端调度器与 "use client" 的自动化页引用,不引入任何服务端依赖) - 调度器删除 mailRuleSource / doesMailRuleMatch / mailAutomationMatches / mailRuleText / mailRulesSummary 与两个取值辅助函数,改为经 mailRuleSetFromTask() 适配后调用共享实现 - 前端删除同一套逻辑(含测试器与冲突检测用到的 normalizedMailRule / mailRuleSetsEqual / mailConflictLevel),改为 automationMailRuleSet() 适配 - store.ts 删除第三份副本 emailRuleSummary(spec 只提到两份),精简摘要 格式原样迁移为 mailRulesBriefSummary:它与共享摘要的输出格式不同 (只显示首条 + 条数,不区分存在性判断),统一会改变接口返回文案, 属行为变更,留待控制器裁决 - 规范化类型与字段、操作符清单集中到模块,用 as const + 字面量联合, 不使用 enum / namespace(--experimental-strip-types 无法转换) - 新增 test/mail-rules.test.ts:字段 × 操作符矩阵、AND/OR 模式、 空值与大小写边界、附件扩展名提取
…server-side 邮件触发的服务端校验(模块 D.2 / D.3)与死代码清理(D.8)。 - createAutomation / updateAutomation 的邮件分支:mailboxId 先归一化为正整数, 再用 getAutomationMailboxForUser(userId, mailboxId) 校验归属;邮箱不存在或 不属于该用户一律抛 MAILBOX_NOT_OWNED,不再有 "system" 特例 - 两个自动化 API 路由把 MAILBOX_NOT_OWNED 映射为 400「监听邮箱不存在或无权使用」, 与既有的 MAILBOX_ID_REQUIRED「请为邮件触发选择监听邮箱」并列,不回落 500 - mailboxLabel 改为由邮箱记录派生(mailboxLabelFromRow),客户端传入的 mailboxLabel(含 triggerConfig.mailboxLabel)一律忽略;派生规则与 mailboxes.ts 的 mailboxRowToApi().label 同源,向导选中的邮箱与列表显示 的邮箱不可能分叉 - 新增 test/mailboxLabel.test.ts 覆盖派生规则的名称/邮箱/缺失/伪造字段边界 - 删除死代码:pages/api/automation/check-email.ts、send-email.ts、 pages/api/v1/automation-email/claim.ts(全仓库含文档均无调用方,调度器直接 调用 store 函数)与 app/automation/page.tsx 的 LEGACY_DEMO_AUTOMATION_NAMES (定义处唯一引用,早已无人使用) Co-Authored-By: Claude Code <noreply@anthropic.com>
模块 B 主路径:邮件触发不再写死系统邮箱,改为在向导第 2 步选择或内联配置 监听邮箱;未保存任何邮箱时下拉默认落在「+ 配置新邮箱…」并展开表单,不留 空状态死路。 - 选择器 + 内联表单(地址/名称/服务器/端口/账号/密码/文件夹),「测试连接」 调 POST /api/v1/automation-mailboxes(服务端保存前真实连接 IMAP),成功后 用响应 id 作为本次自动化的 mailboxId;失败原因内联展示,不提供跳过路径。 - buildAutomationPayload / triggerDetail / localizedTriggerDetail 的监听邮箱 一律由选择派生;遗留行(无整数 mailboxId)显示「监听邮箱未配置」而非回退 系统邮箱,编辑时会要求重新选择而不是静默下发空值。 - resetWizard 默认选中第一条已保存邮箱(模板同样走这条默认)。 - mailFolder 跟随所选邮箱记录的 folder(调度器实际使用的就是它)。 - D.7:新增纯函数 selectMailboxScopedAutomations,按 mailboxId 相等过滤并在 邮箱选择变化时重算,修正跨邮箱误报冲突与错误的 winner 预测。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…late 评审 Findings #1(Minor 升级为必修):邮箱名单加载回调在 current 非空但不在 items 里时会写成 items[0].id。可达路径是"先打开编辑弹窗、名单后返回",结果是界面显示 一个看起来正常的选择,保存即把自动化改绑到另一个收件箱——静默、且监听的邮箱与 用户认知不一致。 改为派生状态,而不是在加载回调里猜用户意图: - mailbox-id.ts 新增 defaultMailboxSelection(只有还没有选择时才用名单第一条) 与 isMailboxSelectionUnresolved(有选择、名单已加载完但查不到它),两者均为 纯函数并有单测(含针对本缺陷的回归用例)。 - page.tsx 用 mailboxFormVisible = mailboxFormOpen || mailboxSelectionUnresolved 统一此前读 mailboxFormOpen 的六处行为点(选择器显示值、选中邮箱、D.7 候选范围、 payload 派生、保存前校验、表单/警示渲染),不可解析时展开表单、给出明确说明并在 保存前拦下,改绑只能由用户显式选择触发。 - 顺带(同一窗口的第二个症状):resetWizard 的展开条件改为 mailboxesLoaded && mailboxes.length === 0,名单未到时不再预先展开表单。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…d cursor upkeep
模块 C(轻量邮箱管理入口)+ D.4 游标重置 + D.5 游标清理。
前端(新文件 app/automation/components/MailboxManager.tsx,next-intl):
- 自动化页头部新增次级入口「邮箱管理」,抽屉列出 名称 / 邮箱 / IMAP 服务器 /
状态徽标 / 最后错误,逐条支持编辑与删除;空状态指向向导的新建入口,不重复
向导的建号流程
- 每行内联编辑表单(名称/地址/账号/密码/服务器/端口/文件夹),密码框恒为空并
提示「留空表示不修改现有密码」,表单内还写明改连接信息会重新建立收信基线
- 删除沿用现有 DELETE:409 依赖清单用本地化文案说明还差几步
- 编辑/删除成功后回调宿主页重新拉取邮箱名单:向导的名单只在挂载时拉一次,
不刷新会让用户以为没保存成功(回拉时会保留用户已有的选择,查不到的选择由
isMailboxSelectionUnresolved 要求重选,不会静默改绑)
- i18n:新增 messages/{zh-CN,en}/automation.json(成对维护)与 types/i18n.d.ts
的 automation 命名空间登记;自动化页存量 tt() 文案不动
后端:
- lib/automation/mailboxes.ts 新增 updateAutomationMailbox:先真实连接 IMAP,
通过才落库;密码留空(未提供或空串)保留原凭据;连接身份变了重置游标;
非本人 mailboxId 返回 null(路由 404)
- pages/api/v1/automation-mailboxes/[id].ts 新增 PUT 分支,只收下请求里真正
出现过的字段(与 POST 的补齐默认值相反),并沿用创建路径的连接失败语义
- D.4:IMAP 主机 / 账号 / 文件夹变更后把游标置为 initialized=false 且
last_uid=0(saveAutomationEmailMailboxCursor 用 GREATEST 写回,只置 flag
会让换到 UID 空间更小的新邮箱时漏掉全部新邮件)
- D.5:deleteAutomationMailbox 在删除成功后一并删除本人在该 mailbox_id 上的
游标行,按 created_by_user_id + mailbox_id 限定;依赖检查返回 409 时不动游标
- 游标表就绪判定同时覆盖「表不存在」与「仍是旧列 mailbox_key」两种窗口,
避免升级后 store 尚未被调用时的一次邮箱更新报 500
重构(避免同一套规则写第三份):
- 抽 lib/automation/mailbox-input.ts:邮箱配置的归一化校验(创建/编辑共用)与
编辑合并语义;mailboxes.ts 的私有 normalizeInput 迁入此处
- 抽 lib/automation/mailbox-credentials.ts:加解密往返可被 node:test 直接断言
(mailboxes.ts 依赖 lib/db,测试进程不连库)
- 抽 lib/automation/mailbox-errors.ts:POST/GET/PUT 共用一份错误码映射,
顺带修掉「MAILBOX_IMAP_PORT_INVALID 被连接失败启发式抢走、把错误码原文
当文案返回」的既有问题
测试(新增 28 例,全套 594 例):
- test/mailboxUpdate.test.ts:密码留空(空串与未提供)保留原值、新密码不 trim、
D.4 只对主机/账号/文件夹重置(改密码/名称/端口/加密方式不重置)、大小写与
空文件夹等写法差异不重置
- test/mailboxCredentials.test.ts:加解密往返(含中文/空格/超长)、同明文两次
密文不同、篡改与换密钥必须失败、非法格式抛 MAILBOX_CREDENTIAL_INVALID
- test/mailboxForm.test.ts:编辑表单密码可留空、编辑请求体总是下发 password
…ssion guard 评审 Findings:D.4/D.5 的游标 SQL 没有任何提交进仓库的测试——写坏(用回 Task 1 之前的 mailbox_key、漏掉 last_uid=0、漏掉 created_by_user_id 范围限定、删掉 D.5 的清理) 不会有任何测试变红。集成验证跑的是 mailboxes.ts 的逐字副本、未提交,因此没有回归价值。 本次按仓库既有做法(test/skillsProxyQuery.test.ts 的"读源码文本并断言")补齐。 - 新增 test/mailboxCursorSql.test.ts(6 例):两条游标语句都用整数 mailbox_id; 去掉注释后的可执行 SQL 里不得出现旧列名 mailbox_key(注释里提及允许); D.4 重置同时归零 last_uid 并把 initialized 置为 FALSE;D.4/D.5 均按 created_by_user_id + mailbox_id 限定范围;D.4 的身份三元组只含主机/账号/文件夹 (按花括号配对截取 mailboxIdentityChanged 的函数体,断言其中不出现 password/name); 重置以 cursorResetRequired 为条件、清理以 deleted 为条件(409 时游标必须保留)。 条数用"恰好一条"断言:语句被删(0 条)或被复制(2 条)都会红。 - 顺带(评审可选项):PUT 请求体的解析从路由搬到 lib/automation/mailbox-input.ts 的 mailboxUpdateInputFromBody(零依赖模块)。直接 export 路由里的 parseUpdateInput 做不到 ——测试 import 该路由会经 @/lib 别名拉到 lib/db,而本套件不连数据库、不 import @/lib。 搬进纯模块后,"字段未提供 vs 空串"在这一层可被直接断言,路由改为 import 它(行为等价)。 - test/mailboxUpdate.test.ts 新增 4 例覆盖该解析:缺口不补默认值;未提供与空串可区分; 端口数字/数字串都收、空串与 null 视为未提供、非数字留给服务端 400;imapSecure 只收布尔。 可失败性证据(每次变异后 git checkout 还原并用 git diff --quiet 确认):D.4 改用 mailbox_key → 3 红;去掉 last_uid=0 → 1 红;D.5 去掉 created_by_user_id → 1 红; 删掉 D.5 调用点 → 1 红;把 password 写进身份比较 → 1 红。
…nd dedup retention 模块 E(健壮性改进): - E.1 邮箱状态与错误:automation_mailboxes 新增 last_error / last_error_at (ensureAutomationTables 里用「表存在判断 + ADD COLUMN IF NOT EXISTS」补列, 可重复执行;表由 mailboxes.ts 按需创建,裸跑 ALTER 会在首次部署时 42P01)。 调度器在真实连接路径上写入:连不上记 error + 原因,连上恢复 connected 并清空。 列表接口下发 lastError / lastErrorAt,抽屉的状态徽标与「最后错误」因此不再是装饰。 - E.2 连接失败提醒:listAutomationNotifications 新增第三段派生,来源为 automation_mailboxes 中 status='error' 的行,kind 复用 email_failed。 eventKey 固定为 mailbox-error:<mailboxId>(不含时间戳):邮箱恢复前只有一条稳定提醒, 恢复后自动消失;前端按 eventKey 去重的 toast 已能防重复,不另加服务端去抖。 - E.3 加密密钥前置条件:AUTOMATION_MAILBOX_SECRET 写入 env.example 并标注为部署 前置条件(未配置时回退 JWT_SECRET,密钥一变已存密文永久不可解密)。 - E.4 去重表保留期:automation_email_processed_messages 保留 30 天,随调度器每天清理 一次(同一个 node-cron,不另起计时器);截止时刻在 JS 里算好作为参数传入。 测试:新增 19 例(eventKey 的字符串契约、提醒派生、保留期截止时刻计算,以及 ALTER 守卫 / 状态写入路径 / 第三段查询 / 清理 SQL 的源码文本守卫),全部做过定点变异 验证可失败。抛弃库上验证了整段 SQL 的可重复执行、状态→提醒→恢复的链路与清理范围。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…ey-rotation recovery 模块 E.3(评审 Ruling 22:转交事项 1 撤回)。原先只有"密文格式非法"才抛 MAILBOX_CREDENTIAL_INVALID;密钥不符时 Node 抛的是 OpenSSL 原文 「Unsupported state or unable to authenticate data」,既不在接口错误映射表里也不在连接失败 启发式里,于是 E.3 点名要处理的场景(轮换密钥 / 事后补配 AUTOMATION_MAILBOX_SECRET) 恰好是唯一还落成 500 的那条路径。 - mailbox-credentials.ts:GCM 那一段(createDecipheriv / setAuthTag / update / final) 包进 try/catch,认证失败统一重抛 MAILBOX_CREDENTIAL_INVALID(原始原因挂在 cause 上, 运维排查要看它,用户不需要)。 - mailbox-errors.ts:新增 mailboxErrorDisplayText(与 mailboxApiError 共用同一张表); mailboxes.ts 的 markAutomationMailboxConnectionError 在落库前先过它,否则抽屉的 「最后错误」与通知中心会显示 MAILBOX_CREDENTIAL_INVALID 这个裸错误码。 - mailboxes.ts / mailbox-input.ts:编辑时若请求带了新密码就不去解密旧密文 (mailboxUpdateSuppliesPassword 判定,空串与未提供都算"没带")。此前 mailboxConnectionFromRow(existingRow) 在合并前无条件执行,密钥轮换后用户照着 「请重新填写授权码」重填会一直失败——从没带过旧密码的解密这一步开始就抛了, 那条自救路径是死的。 测试:新增 4 例(映射函数、写库前映射的接线、是否带新密码的判定、条件解密的接线), 并把 test/mailboxCredentials.test.ts 的两条 assert.throws 从"无匹配器"收紧为 消息恰好等于 MAILBOX_CREDENTIAL_INVALID(换密钥与密文被篡改两条路径)。5 条定点变异 全部验证可失败。抛弃库上以逐字复制的 mailboxes.ts 跑通 10 项:轮换后不带新密码报 可识别错误码、带新密码保存成功且新密文可用当前密钥解回、落库文案为中文而非错误码。 Co-Authored-By: Claude Code <noreply@anthropic.com>
模块 A(Task 7,实施顺序最后一步)。平台不再提供共享监听邮箱:每封邮件被
N 个用户各自的自动化重复触发(三人各建"客户询价处理"→ 一封询价被处理 3 次)
是原设计的结构性缺陷,功能从未被实际使用,因此无存量迁移负担。
- store.ts:ensureAutomationTables() 末尾追加唯一的破坏性语句
DELETE FROM automation_tasks WHERE trigger_type='邮件触发'
AND trigger_config->>'mailboxKey'='system'。
WHERE 严格双限定:等于判断不匹配 NULL,其他触发类型与自定义邮箱行都不受影响;
预期影响 0 行,纯防御(万一有残留行,它在新代码下会每 10 秒报一次调度错误)。
同一函数失败后 initPromise 会被置空、整块 SQL 每次 store 调用重试,所以这条
语句必须不可能失败——注释里把这点写清楚(原先误写为"每次调用都会重跑")。
- store.ts / automation-scheduler.ts:展示层兜底改 storedMailboxLabel(...) ??
"监听邮箱未配置";调度器改用新增的 requireMailboxLabel,缺失即抛
MAILBOX_LABEL_REQUIRED,不再兜底成一个已下线的邮箱名。
- mailbox-id.ts:新增 MAILBOX_SYSTEM_RETIRED / MAILBOX_LABEL_REQUIRED、
requireMailboxLabel、storedMailboxLabel;requireMailboxId 单独识别退役哨兵值
"system",与普通非法值分开报错。
- API:POST/PUT 两条路由把 MAILBOX_SYSTEM_RETIRED 映射为
「系统邮箱已下线,请配置监听邮箱」,而不是通用的"缺少监听邮箱"。
- page.tsx:运行详情监听邮箱兜底「系统邮箱」→「未知」。
- 保留 A.6:调度器的主题前缀防循环判断不动——结果邮件可能从用户自己的邮箱发出。
测试:新增 test/automationSystemMailboxRetired.test.ts(5 例源码守卫,
钉住清理语句的位置与 WHERE 范围、A.6 前缀判断、§十一 无兜底、A.4 专门文案),
与 test/automationMailboxId.test.ts 的 4 个新用例。6 处守卫逐一做故障注入,
全部确认可翻红后还原(删语句 / 放宽 WHERE 为 OR+IN / 删前缀判断 /
恢复 || "系统邮箱" 兜底 / 删哨兵分支 / 改文案)。清理语句的语义在抛弃库
ragent_task7_scratch 上端到端跑过:8 行夹具里只删掉 system 那一行,其余
(自定义邮箱、NULL 配置、mailbox:5 旧键、定时触发、Webhook、完成触发、
以及"定时触发却带 mailboxKey='system'")全部保留,重跑幂等。应用库未触碰。
pnpm test 636/635(1 条失败是 test/appsTenantFilter.test.ts 的已知基线);
tsc --noEmit 与基线逐文件错误数一致(177/177,零新增);
LF 归一下 biome 零新增诊断;pnpm build 成功。
Co-Authored-By: Claude Code <noreply@anthropic.com>
…4/D.2 guards 最终整分支评审的修复波次(Finding 1-5)。主项是合并阻塞级的静默漏邮件。 - Finding 1(合并阻塞):POST /api/v1/automation-mailboxes 是 ON CONFLICT (created_by_user_id, email) DO UPDATE,用户重填一个已登记的地址、 却换了主机/账号/文件夹时,既有记录被就地改写而游标保持旧高水位——调度器随后 拿旧基线去比新服务器的 UID,所有 uid <= 旧值的邮件被静默丢弃,状态仍显示 「已连接」。D.4 在 Task 5 只接到了 PUT 路径上,而该要求的主体是**邮箱**、不是端点。 现在创建路径同样在写入前取出既有行、写入成功后按同一判定重置游标;身份比较与 重置 helper 都复用 PUT 那一份(mailbox-input.ts 新导出 mailboxIdentityChanged 与 createMailboxCursorResetRequired),不另写一份比较。 - Finding 2:store.ts 的规则白名单改从 mail-rules.ts 的 MAIL_RULE_FIELDS / MAIL_RULE_OPERATORS 派生(向导下拉渲染的就是这两份清单)。此前是两份字面量, 新增字段会出现"能选、能提交、写库时被静默丢弃"的前后端分叉。 - Finding 3:§十一 兜底守卫改为匹配 || 与 ??,并按目录扫描 lib/、app/、pages/ 的全部源码,不再是一份手写文件清单。 - Finding 4:deleteAutomationMailbox 的依赖检查增加 `OR trigger_config ? 'mailboxKey'`(键存在性判断,不取值、不解码)。此前只认整数 mailboxId,被遗留 mailboxKey 行引用的邮箱可被删除;A.1 的清理只删 mailboxKey='system',其余遗留键会原样留下。 - Finding 5:requireOwnedMailbox 的 committed 守卫(创建/更新两条邮件分支各恰好 一处调用、失败抛 MAILBOX_NOT_OWNED、归属谓词仍按 id + created_by_user_id)。 测试:新增 8 例(创建路径 D.4 的纯函数判定 3 例 + 源码接线守卫 1 例、 automationMailboxOwnership.test.ts 3 例、mail-rules 的同源守卫 1 例)。 6 次定点翻转全部确认可翻红后逐字节还原:删创建路径的重置 → 源码守卫与抛弃库 集成探针同时红、游标停在 5000;把既有行查询挪到 upsert 之后 → 顺序断言红; createMailboxCursorResetRequired 取反 → 纯函数用例红;在 lib/ 下放一个 `?? "系统邮箱"` 的新文件 → 新守卫红,而**旧版守卫在同一棵树上仍全绿**; 删掉更新分支的 requireOwnedMailbox 调用、去掉归属谓词的用户条件 → 各自红; 把白名单退回内联字面量 → 同源守卫红。 抛弃库 ragent_finalfix_check(应用库未连接)上 14 项集成断言全通过:换主机/换 文件夹后游标被打回 (last_uid=0, initialized=false)、只改名称/密码/端口不重置 (基线保持 5000)、新地址是插入且不产生游标行;遗留 mailboxKey 行使删除返回 deleted=false 并列出该行,非邮件触发的 mailboxKey 行与指向别的 mailboxId 的行都 不构成依赖(删除照常),整数 mailboxId 命中的原有语义不变。 pnpm test 644/643(1 条失败仍为 test/appsTenantFilter.test.ts 的已知基线,同一原因); tsc --noEmit 177 与基线一致(零新增);biome 逐文件诊断数与 HEAD 逐项一致,零新增。 pnpm build 未跑(类型证据由 tsc 单独给出)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
`2fbf927` (2026-09-08, "fix: hide email trigger and fix automation run persistence") removed 「邮件触发」 from the trigger picker along with 247 lines of wizard UI. That commit recorded no reason, so the hide's motivation is unknown — see the spec's §一 勘误. Consequence for this branch: the step-2 email block, and therefore the entire inline mailbox picker built by module B, was reachable only via the 「客户询价处理」/「售后投诉处理」 templates or by editing an existing email automation. A user creating an automation from scratch could not reach it at all, which spec §十一's first acceptance criterion silently depended on. The line is restored verbatim, in its original position (second, between 定时触发 and Webhook). Nothing else from `2fbf927` is needed: the inline mailbox form it also removed was independently rebuilt by module B. Also annotates the spec (§一, §十一) with four findings from the implementation, as annotations rather than rewrites: - §一: the trigger was not merely unconfigured but absent from the picker - §十一#1: depends on this restore shipping - §十一#6: self-contradictory — it forbids any system branch while §四 A.4 requires one; the implementation recognises the sentinel and throws - §十一 last: `pnpm check:ci` fails on pristine `main`, so no branch can satisfy it; the operational bar was "no new diagnostics" Tests: 644/643 (the one failure pre-existing on main before this branch). Build: exit 0 on a cleared .next. Co-Authored-By: Claude Code <noreply@anthropic.com>
邮件触发的收信链路此前调用 ragent-service 的 `POST /api/v1/email/unread-config`
——**该端点不存在**。它只以临时补丁脚本的形式存在过(`adce8b1:patch_backend_multi_mailbox.py`
往容器里的 `/app/app/api/v1/endpoints/email.py` 追加代码,跑完即弃),从未进入部署镜像
(镜像构建于 2026-08-24)。于是每次保存邮箱、每 10 秒一次的轮询都 404,`"Not Found"`
不含任何关键词、落不到 400 分支,被兜底成 500。
按规格的既定方向把收信搬进 ragent 进程:
`instrumentation.ts` 本来就在 Next 进程内跑调度器(`ENABLE_CRON=true`),不是新模式。
- 新增 `lib/automation/imap-client.ts`:Python 参考实现的移植(imapflow + mailparser)。
纯逻辑抽成可喂参数的函数(`planMailboxFetch` / `extractBody` / `extractAttachmentNames`
/ `toInboxMessage`),不连网即可单测。三处反直觉处按参考实现保真:
1. `mailboxOpen(folder, { readOnly: true })` —— 只读打开,绝不动用户的已读状态
(处理与否只由游标与去重表决定,用户在客户端读过的信仍会被处理);
2. `latest_uid` 取 `uid search ALL` 的**最大值**,在游标判断之前算好。它是调用方建
基线的依据:不传 `afterUid` 时只回报它、一封不取(返回 0 会让下次轮询重放历史);
3. 一批取游标之后**最早**的 20 封(`[:20]`)。`patch_backend_mail_batch.py` 专门修过
这个 bug——游标逐封推进,取最新一批会让它跳过中间所有邮件,永久静默丢信。
正文纯文本优先、HTML 兜底(解析时传 `skipHtmlToText` + `keepCidLinks`,否则
mailparser 会把 HTML-only 邮件的 HTML「翻译」成纯文本塞进 `text`,再也分不清
「本来有纯文本」与「只有 HTML」);附件只取带 filename 的 part 的文件名;
8 个字段恒存在;失败文案统一带 `IMAP 收件失败:` 前缀;连接超时 15 秒。
- 新增 `pages/api/v1/email/unread-config.ts`:薄路由,入参 `mailboxId` + `afterUid`,
服务端按 `getAutomationMailboxForUser` 查库取凭据(不收原始连接信息,不留 SSRF 面)。
调度器与「保存前试连」直接调用实现函数,不走 HTTP。
- 三个调用点去掉 `authorization`,删除只用于打那个不存在端点的 `mailbox-client.ts`;
`updateAutomationMailbox` 的 `options.authorization` 一并移除,调度器里随之失去
唯一用途的 `serverAuthorization` / `requiredEnv` 与 jsonwebtoken import 也删掉。
- `mailbox-errors.ts`:连接类错误匹配改为大小写不敏感并补充 `ECONNREFUSED`/
`ENOTFOUND`/`ETIMEDOUT`/`getaddrinfo`/`socket timeout`/`certificate` 等形态。
- 新增 `types/mailparser.d.ts`(该库无自带类型,也没有可用的 @types),只声明用到的部分。
测试:新增 `test/imapClient.test.ts`(21 例)+ `test/mailboxHealth.test.ts` 2 例。
6 处守卫逐一翻转确认可翻红后逐字节还原:批处理 `slice(0,20)`→`slice(-20)`(1 例红)、
latest_uid 改为取本批最大值(4 例红)、`readOnly: true`→false(1 例红,守卫此前会被自己
的文档注释满足,已先剥注释)、删 `skipHtmlToText`(1 例红)、附件不做 filename 过滤
(2 例红)、错误匹配去掉大小写归一(2 例红)。
另在真实 imapflow 上跑过连接失败路径(临时探针,未入库):
127.0.0.1:1 明文 / TLS 均得到 `IMAP 收件失败: connect ECONNREFUSED 127.0.0.1:1`、
坏主机名得到 `getaddrinfo ENOTFOUND …`,三者经 `mailboxApiError` 都是 400;参数缺失
得到 `IMAP 连接参数不完整:缺少IMAP 服务器`(400)。
pnpm test 667/666(1 条失败仍是 test/appsTenantFilter.test.ts 的已知基线,同一原因);
tsc --noEmit 178 = 基线 177 + 新测试文件的 1 条 TS5097(本仓库每个测试文件的相对
import 都带 .ts 后缀,48 个文件各产生一条;去掉后缀在 node 下 ERR_MODULE_NOT_FOUND,
无法规避);LF 归一后 biome 诊断与 HEAD 逐项一致,零新增;pnpm build 成功。
Co-Authored-By: Claude Code <noreply@anthropic.com>
… a bad search result 评审 Important I1 / I2。丢失邮件的那条关键链路(`[:20]` 批处理顺序、`latest_uid` 取值 与位置、游标语义)原样未动。 I1:`extractAttachmentNames` 读的是 `mailparser` 的附件分类,**不是**参考实现的 `msg.walk()` + `part.get_filename()`。实测确认两处偏差(两个方向都写进了注释): - 少报:带文件名但被判成正文的 part 不计入——`Content-Disposition: inline; filename="page.html"` 的 text/html、或只有 `Content-Type: …; name="notes.txt"` 而没有 disposition 的 part(两者内容都进正文); - 少报:`message/rfc822` 的**内层** part 不计入(mailparser 在 message/rfc822 处 break, 不下钻),容器自带 filename 时才计入。 影响 `是否包含附件`/`附件名称`/`附件类型` 三个规则字段,而调度器未命中也会推进游标, 所以漏报会让那封信的触发永久消失(丢的是触发,不是信)。完全对齐要放弃 `simpleParser` 自己走 MIME 树,会丢掉当前正确的形态(编码词文件名、RFC 2231、嵌套 multipart、逐 part charset),故按现状记录 + 用测试钉住,而不是手写 MIME 遍历。 新增两例(夹具 `INLINE_NAMED` / `FORWARDED`)断言**当前、已被记录的**行为:被判成正文的 part 的 `attachments` 为 `[]`(并断言内容确实进了正文),转发邮件只报容器那一层。 I2:`search` 非数组此前被静默当成空文件夹。它在**基线调用**上会写入 `latest_uid: 0`, 下一次轮询就从 0 开始按 20 封一批重放整个邮箱历史——与细节 2 要防的是同一类失败、 方向相反。改为 `uidsFromSearchResult()`:非数组即抛 `IMAP 未返回 UID 列表`(含 `IMAP`, 映射层照旧 400)。抽成纯函数而非内联一行,是为了这个守卫本身可被单测覆盖。 翻转验证(两处都确认可翻红后逐字节还原): - 在 `return names;` 前注入"把正文 part 的名字也算作附件" → 「被判成正文的 part 不计入附件」 红,22/23;(首次注入写在 for 循环体内没有翻红——该夹具的 attachments 本就是空数组, 循环体一次都没进,注到循环外才红。) - `uidsFromSearchResult` 退回静默兜底 → 「search 没返回数组时直接失败」红,22/23。 测试 670/669(新增 3 例;唯一失败仍是 test/appsTenantFilter.test.ts 的已知基线); tsc --noEmit 178 与上一提交持平(新增的仍是那条测试文件 .ts 后缀 import 的 TS5097); 两个改动文件 biome 零诊断;pnpm build 成功。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…note 评审指出上一轮写进注释的那句话本身是错的,且是同一类问题(注释误述已验证的库行为)。 本轮**无行为改动**,只更正记录;批处理、latest_uid、游标语义与五个 Minor 未动。 自己用与实现相同的选项(`skipHtmlToText: true, keepCidLinks: true`)实测了七种容器形态, 结论随 `Content-Disposition` 而变: - 容器**不透明**(内层完全看不到):无 disposition、`attachment`、带 base64 传输编码; 此时只见容器自己——带 filename(含只用 `name=` 参数)就报容器名,什么都没带则整条被过滤。 - 容器**透明**(`Content-Disposition: inline`):内层 part 会报出来(内层的 `attachment; filename="inner.pdf"` 可见),而**容器自己的 filename 反而丢掉**,内层正文 还会并进正文——与不透明那三种正好相反。 机制也不是"mailparser 不肯下钻":`@zone-eu/mailsplit` 的 `message-splitter.js` 只对 `Content-Disposition: inline` 的 `message/rfc822` 设 `messageNode = true`(分叉并继续下钻), mailparser 对该节点 `break` 才使容器自己不进附件列表。原注释把因果说反了。 同时收紧 `uidsFromSearchResult` 的注释:`imapflow@2.0.2` 的 `search` 不止在未选中邮箱时失败 ——`commands/search.js` 在命令抛错时 catch 住并返回 false(服务端 NO/BAD、命令中途断连都在 这里),`imap-flow.js` 在没选中邮箱时返回 undefined。守卫因此比原注释说的更值得存在; 补一句空文件夹是 `[]`(非空真值,不被 `|| false` 吃掉)所以不会误伤。 测试侧:转发用例改名为「不带 disposition 的转发邮件内层不可见…」(原名重复了被证伪的说法), 夹具内两处同样的错误说明一并更正,并注明本夹具只覆盖不透明那两种;顺带删掉被下一行蕴含、 永远不可能独立失败的 `assert.match(message.body, /正文/)`。 报告三处同类更正:§5 表格第 5 行标注"不是字面达成"、上一轮 I1 小节里那句被证伪的话就地标注、 §八 交接记录第 2 条重写为按 disposition 分类的准确版本(避免带进规格更正)。 测试计数不变:670/669(唯一失败仍是 test/appsTenantFilter.test.ts);imapClient 23/23; tsc --noEmit 178 持平;两个改动文件 biome 零诊断。 Co-Authored-By: Claude Code <noreply@anthropic.com>
…ocal-IMAP dependency change
Carries the implementation's hand-off records into the spec, where they
otherwise existed only in gitignored scratch and would have been lost.
§十二 gains two items:
8. Receiving is now in-process (`lib/automation/imap-client.ts`, with
`pages/api/v1/email/unread-config.ts` as a thin wrapper); sending still goes
through ragent-service's `POST /api/v1/email/send`. So `EXTERNAL_API_BASE_URL`
no longer matters for receiving but is still required for sending.
9. A recorded deviation in attachment detection, which feeds the
`是否包含附件` / `附件名称` / `附件类型` rule fields. Because the scheduler
advances the cursor even on a non-match, a missed attachment permanently
suppresses the trigger for that message (it cannot lose the message).
Two differences from the original Python reference, both measured against
mailparser 3.9.26 / mailsplit 5.4.16:
- parts carrying a filename that mailparser classifies as body are not counted;
- `message/rfc822` behaviour depends on the container's own disposition —
opaque (no disposition, an unrecognised disposition token, `attachment`, or
any transfer encoding outside 7bit/8bit/binary, including base64 and
quoted-printable) hides the inner parts and reports the container's own
filename if it has one; `inline` exposes the inner parts and drops the
container's filename.
Full parity was rejected deliberately: matching `msg.walk()` semantics would
mean abandoning mailparser for a hand-rolled MIME walker, putting the
currently-correct handling of encoded-word filenames, RFC 2231, nested
multiparts and per-part charsets at risk to gain parity on two narrow shapes.
Its test fixtures cover only the opaque shapes; the transparent-container
behaviour is recorded but unpinned — noted in the item so the next editor
adds a fixture before changing that function.
Co-Authored-By: Claude Code <noreply@anthropic.com>
同一封邮件命中的每条自动化改为各领各的执行,不再由优先级选出一条胜出。 关键发现:真正保证「只有一条胜出」的是 automation_email_processed_messages 上的唯一键 (created_by_user_id, mailbox_id, message_key) —— 它不含 automation_id, 所以第二条规定连 claim 这一步都过不去。唯一键加列才是本次改动的开关, 优先级排序只是决定谁去抢。 Co-Authored-By: Claude Code <noreply@anthropic.com>
5 个 task:唯一键加 automation_id(与 claim 的 ON CONFLICT 原子)→ 调度器 全量并发执行 → 清除 priority/winner 读写与两列 → 前端 → 端到端验证。 每个 task 之间测试全绿且可独立 review;数据库改动与代码改动的同批发布约束 写在 Global Constraints 里。 Co-Authored-By: Claude Code <noreply@anthropic.com>
应用进程不再创建/修改任何表。表结构由部署时执行的 db/automation.sql 建立, 应用侧只做一次存在性校验(缺表即抛 AUTOMATION_SCHEMA_MISSING 并点名)。 - 新增 db/automation.sql:10 张表最终结构 + 15 个索引 + 两段自检 (旧结构 mailbox_key 直接 RAISE EXCEPTION;已存在的表 RAISE WARNING) - 新增 lib/automation/schema.ts 承载校验清单与 assertAutomationTablesReady - store.ts / mailboxes.ts 删除 47 条 DDL,建表函数改为一次 to_regclass 查询 - 删除 mailboxes.ts 的自动化游标表存在性兜底(其前提已不存在) - 更新两个源码守卫测试,新增防漂移测试(SQL 文件 / 校验清单 / 代码引用三方一致) Co-Authored-By: Claude Code <noreply@anthropic.com>
真正决定「一封邮件只能被一条自动化领走」的是 automation_email_processed_messages 上的唯一键,不是优先级排序。唯一键从三列改为四列,命中的每条自动化各领各的。 两个改动是刚性配对,必须同批发布: - db/automation.sql:CREATE 里的匿名三列 UNIQUE 改为显式命名的四列约束 automation_email_processed_once_per_automation(全新库用);文件末尾追加 ALTER(已有库用,DROP 旧截断名 + 条件 ADD 新约束,已存在的库正常迁移) - lib/automation/store.ts:claimAutomationEmailMessage 的 ON CONFLICT 目标 改为同样四列,并补文档注释说明新语义(返回 true = 本条自动化首次领取) 少改任何一半,PostgreSQL 会以「no unique or exclusion constraint matching the ON CONFLICT specification」拒绝每一次 claim。 守卫测试钉住这两处的四列一致性(源码文本断言,测试套件不连数据库)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
同一封邮件命中的每条自动化各自 claim、各自执行(Task 1 已把去重表唯一键放宽为 含 automation_id)。执行用 Promise.allSettled 并发,失败互不影响,游标在全部 settle 之后推进 —— 保持与改动前一致的 at-most-once 语义。 settle 结果被检视:requireMailboxLabel / prepareEmailAttachments / createRun 在 executeEmailAutomation 内部 try 之外,其抛出原先经分组处理器落日志,若只用 裸 allSettled 会变成完全静默。现在逐条记 console.error 并带 automation id。 新增 test/automationEmailParallelRun.test.ts:4 条源码守卫(逐条 claim、 allSettled、settle 结果被检视、游标顺序),全部按 RED→GREEN 验证。 Co-Authored-By: Claude Code <noreply@anthropic.com>
Task 2 让一封邮件命中的每条自动化各领各的、全部并发执行之后,priority 与 winnerAutomationId 已经没有任何读者——保留它们只会让下一个人以为还存在 「谁优先」的仲裁。 - db/automation.sql:automation_email_rule_events 的 CREATE 去掉 winner_automation_id / priority 两列(全新库用);文件末尾追加两条 DROP COLUMN IF EXISTS(已有库用,全新库上是 no-op) - lib/automation/store.ts:删掉 normalizeEmailPriority 与其全部调用点 (创建/更新的邮件分支、triggerDetail 文案、automationRowToApi 的 mailPriority);AutomationEmailRuleOutcome 去掉 "suppressed_by_priority"; 评估入参去掉 winnerAutomationId / priority;INSERT 列清单与占位符 同步从 13 缩到 11;getAutomationEmailRoutingStats 去掉 suppressed 桶 与 recent 里的两个字段 - lib/cron/automation-scheduler.ts:去掉 executeEmailAutomation 的 triggerContext.priority 与 processEmailMailboxGroup 里 Task 2 留下的 三处残留(outcome 联合类型、winnerAutomationId、priority) test/automationSchemaFile.test.ts 追加两条源码守卫:建表语句不得再声明这两列, lib/automation 与 lib/cron 下不得再出现四个相关标识符。 DB 侧验证(一次性库,未触碰 ragent):全新建库导入以「自动化表 10 张」结束、 重复导入退出码 0;旧结构的库导入后两列被清掉且既有行保留。另用一次性库跑了 真实的 recordAutomationEmailRuleEvaluations 与 getAutomationEmailRoutingStats, 确认列清单/占位符/参数个数对齐、两条 SELECT 引用的列都还在。 Co-Authored-By: Claude Code <noreply@anthropic.com>
删掉 mailPriority 的 state、类型字段、向导表单、提交 payload 与各展示点; 规则测试器不再挑胜出者(matchedCandidates 就是最终结果,不再排序),冲突提示 改为告知"命中的自动化都会各自运行一次",统计卡片去掉"被高优先级截获"。 刻意保留两处:EmailRoutingOutcome 的 suppressed_by_priority 成员与它的只读 文案分支(库里存量行仍会经统计 recent 返回,删掉映射会让这些老行渲染成"未命中"); 统计卡片保持互相独立的计数器,不引入合计或"相加等于 scanned"的假设。 Co-Authored-By: Claude Code <noreply@anthropic.com>
终审的五处修复,都在「静默失效」这条线上: - lib/automation/schema.ts:assertAutomationTablesReady 除十张表之外,再核 automation_email_processed_once_per_automation 是否存在、且列是四列。旧的三列唯一键下 claim 的 ON CONFLICT 找不到匹配约束(42P10),而十张表一张不少——只查表名拦不住, 故障会推迟到第一封来信。约束名是迁移里显式指定的,列用 pg_get_constraintdef 核, 沿用同一个 AUTOMATION_SCHEMA_MISSING 报错路径与缓存重置写法。 - README.md 部署段:db/automation.sql 必须手跑(给出 docker exec 命令),跳过则应用照常 启动、调度器在启动时失败一次且不重试,三个自动化 cron 全不注册(含定时触发), 改完库要重启进程。 - db/automation.sql 迁移段:写明两段迁移与代码同批上线——唯一键是开关,DROP COLUMN 则 相反(先删列会让旧代码的事件 INSERT 报列不存在),要拆就「先代码、后 SQL」。 - 注释清理:执行日志处枚举的抛出点改成本次提交真实存在的 requireMailboxLabel / requireMailboxId / createRun(prepareEmailAttachments 在库里不存在);三处 「优先级竞争」措辞改为「claim 与去重的作用范围」。 验证:一次性库导入当前 SQL → 校验不报错;导入旧结构(三列唯一键)→ 报缺少约束; 导入同名但三列的变体 → 报定义不符;三库均已删除,未触碰 ragent。另用一次性库确认 新 claim 成功、旧唯一键下 42P10、旧代码的事件 INSERT 报列不存在(迁移顺序注释的三条前提)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
Rebuilds the 10 automation tables from db/automation.sql by dropping and recreating them, because that file's CREATE TABLE IF NOT EXISTS silently skips existing tables -- it neither verifies nor fixes their structure. Refuses to run when any table has rows: dropping would take encrypted IMAP credentials and user-built automations with it. --force overrides after printing the row counts and asking for confirmation.
The guard as written looked up saveAutomationEmailMailboxCursor from the top of processEmailMailboxGroup, which also calls it once to establish the baseline -- so the assertion failed against correct code.
Adds lib/automation/agent-payload.ts, a zero-dependency pure module that builds the ragent-service request body and the email prompt -- split out so the payload shape can be pinned by unit tests, which cannot resolve the @/ alias. Fetches each message's attachments over IMAP alongside the body and lists delivered/skipped names in the prompt.
脚本里用 `mapfile -t TABLES < <(...)` 收表名,而 mapfile 是 bash 4 才有的内建。
macOS 自带 /bin/bash 是 3.2,`#!/usr/bin/env bash` 在那边解析到它就是它 ——
于是这一步打一句 `mapfile: command not found`,TABLES 保持为空,紧接着
`[ "${#TABLES[@]}" -gt 0 ]` 在 `set -u` 下让脚本退出,屏幕上只剩一句
「从 $SCHEMA_SQL 里没解析到任何 CREATE TABLE —— 文件格式变了?」。
指向的解释完全错了:文件没问题,是解释器太老,照着那句提示查会白费很久。
而这个脚本的用法里明确包含本机 docker 开发这一路(文件头:默认走 docker exec 进
postgres 容器,与 docs/assets/quickStart/init-dev-env.sh 同一套路),不能假设
只在 Linux 服务器上跑。
改成 while-read 循环,bash 3.2 起就能跑;在 bash 5 上行为完全一致。
已在 macOS bash 3.2.57 上跑通 --check 与整组重建两条路径。
本仓是**公开仓**,平台的整体表结构由后端仓 ragent-service 统一维护(docker/db/)。
把 automation 的 10 张表定义放在这里,等于让公开仓持有平台表结构的一份副本,而
副本不会自动同步、读的人却会以为它是真的 —— docs/assets/quickStart/SOURCE.md 就是
为这件事写的(schema.sql / seed.sql / compose 文件全都是从后端仓拷来的**快照**,
那里还记着上一次漂移:后端 Dockerfile 的 Node 版本与 PDF 工具链都变了,副本毫无察觉,
照着旧副本搭出来的环境跟实际的不一样且看不出来)。
改法照后端仓里已有的先例:`docker/db/automation.sql` 独立成文件、文件头写清归属
(「归前端所有」),不并进 schema.sql —— 理由与那份 process-management.sql 写的相同。
本仓**不保留副本**。
连带改动:
- README / deploy/README:安装说明改为指向后端仓那份脚本,并说明它是部署前置、
漏跑的后果(调度器启动即失败且不重试,界面正常但一封邮件都不处理)。
- deploy/init-automation-db.sh:不再写死 `db/automation.sql`。按
$AUTOMATION_SQL → $RAGENT_SERVICE_DIR/... → ../ragent-service/... 依次定位,
三个都没有就**直接失败**(退回"某个看起来像的路径"只会让失败推迟到更难懂的地方)。
顺带把 `--help` 的截取范围从写死的行号改成"直到第一行可执行语句",往表头加东西
不会再默默截断说明。
- lib/automation/schema.ts、store.ts:文案与注释指向新位置;两条
AUTOMATION_SCHEMA_MISSING 的报错信息是现场唯一线索,必须能直接告诉人脚本在哪。
测试守卫的改法(这是本次最容易做错的一处):
`test/automationSchemaFile.test.ts` 原来靠 readFileSync("db/automation.sql") 做
「建表脚本 == 校验清单 == 代码实际查的表名」三方一致校验。脚本一移出本仓,那几条会
以 ENOENT 直接红 —— 但**不能就这么删掉**,它是这次改造唯一防「代码改了、SQL 忘了改」
的东西,两边漂移都不会有任何类型错误。
于是拆成两半:
- 「清单 == 代码实际查的表名」不依赖后端仓,**永远跑**,覆盖最要紧的两种漂移
(清单多表 → 每次启动抛错、整个自动化瘫痪;代码查了清单外的表 → 校验永远发现不了);
- 涉及建表脚本的几条改为在能定位到脚本时才跑,**定位不到就跳过并把原因打在报告里**,
不是假装通过 —— 「测试全绿」比「少校验一方」危险得多。
定位与跳过语义收进 test/automationSqlPath.ts 一份,没有在三个测试里各抄一遍:
三个候选路径 + 两个环境变量 + 一句统一的跳过理由,抄三份迟早走形,而走形的表现正是
某一条断言悄悄不再校验。同目录另一个测试(原来也直读那份 SQL)一并接上。
本文件名不以 .test.ts 结尾,不会被 `node --test test/*.test.ts` 当成测试收走。
验证(macOS,Node 22):
- 有后端仓检出:701/701 通过,无跳过;三种定位方式(AUTOMATION_SQL /
RAGENT_SERVICE_DIR / 同级目录)都验过。
- 无检出:694 通过 + 7 跳过、0 失败,跳过原因可见。
- 后端仓那份脚本已在全新库(postgres:16)跑到「自动化表 10 张」;
部署脚本在 bash 3.2 上走通 --check 与「整组重建 → 复查三项全绿」。
…nse behavior Revised the instruction in the email automation question to specify that responses should not be sent unless explicitly indicated by the user, enhancing clarity in the automation's expected behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation