Skip to content

洞察生成失败时可以一键重新生成(服务器与本地模式同形) - #2

Merged
AliceLJY merged 1 commit into
mainfrom
claude/insight-retry
Sep 25, 2026
Merged

AliceLJY merged 1 commit into
mainfrom
claude/insight-retry

Conversation

@AliceLJY

Copy link
Copy Markdown
Owner

分步计划

  1. 基线:从最新的 origin/main(b6dbdaa,已包含任务 1 的合并)开出 claude/insight-retry。
  2. 服务器:新增 POST /api/insights/retry,含输入校验和联系人存在性检查。成功与失败的语义与确认卡片时的洞察部分完全一致。
  3. app 接口:app/src/types.ts 加类型,app/src/api.ts 加服务器请求,app/src/connection/dispatch.ts 按目标分发,app/src/local/api.ts 加本地实现。
  4. 界面:flow-context 新增动作 applyInsightRetryResult,洞察页的失败提示卡里加「重新生成洞察」按钮。
  5. 测试:服务器 4 个路由测试,本地模式 2 个,dispatch 1 个,flow-context 4 个;另外临时改坏实现,确认新测试会变红。
  6. README:中英文分别更新。
  7. 交付:跑通验证命令,推送,开 draft PR,盯 CI。

新接口

服务器:POST /api/insights/retry

请求:{ "contact_ids": [3, 5] }

情况 返回
成功 200 { ok: true, data: { insight_status: "ok", insights: [...] } }
模型失败 200 { ok: true, data: { insight_status: "failed", insight_error: "…", insights: [] } }
contact_ids 缺失、不是数组或为空 400 INVALID_REQUEST_BODY「contact_ids must be a non-empty array」
某个元素不是正整数("3"、true、null、1.5、0、负数都不行) 400 INVALID_ID「Invalid contact id」,与 parsePositiveId 同一个错误
去重后超过 100 位 400 TOO_MANY_CONTACTS
有联系人不存在 404 NOT_FOUND「Contact 999 not found」,与 GET /api/contacts/:id 完全一致,此时不调模型
  • 与确认卡片时一致:insight_error 走同一个 getPublicErrorMessage:已知错误显示原因,其它显示「Unexpected insight generation error」。成功时新洞察替换这些联系人原有的洞察。
  • 不留半截数据:generateInsights 先对所有联系人调完模型,最后才用一个事务写库,所以中途失败什么都不写。测试里第一位联系人成功、第二位失败,库里的洞察前后完全一样。
  • 重复的 id:只算一次,每位联系人只调一次模型。
  • 上限 100:每位联系人是一次模型调用。上限要给单个请求封顶,同时要高于一批(最多 20 张截图)实际能涉及的联系人数。
  • 请求体根本不是合法 JSON:返回 500。这是现有的全局错误处理,在路由代码之前就处理掉了,/api/cards/:id/confirm 也一样。改它会影响所有 JSON 路由,这次没动,可以另开任务改成 400。

app:retryInsights({ contactIds })

  • 返回类型:InsightRetryResponse,定义为 Pick<ConfirmCardResponse, "insight_status" | "insight_error" | "insights">,保证与确认卡片时的洞察部分同形。服务器侧在 server/src/app.ts 用同样方式定义;没有新增共享类型,所以 shared/types.ts 没改。
  • 服务器模式:调上面的接口。
  • 本地模式:照 confirmCard 的写法,providerFactory.createTextProvider(options.keys) 加 generateInsights({ db: options.store, contactIds, provider, now: now() })。成功返回 ok;任何异常都返回 failed,错误信息「洞察生成失败,请检查模型配置后重试。」,与 confirmCard 共用同一个常量。
  • 分发:dispatch 和 confirmCard 一样按当前目标分发,已测本地、服务器、网页三种目标。
  • 与服务器的一处差别:本地模式没有输入校验,不存在的联系人由 generateInsights 直接跳过,不报 404。原因有二:说明要求本地照搬 confirmCard 的写法;而且本地模式可以删联系人,如果照服务器那样报错,删过某位联系人之后,这一批的重试就永远失败。

界面行为(app/app/insights.tsx)

  • 洞察页原有的失败提示卡「这次洞察暂时没生成,但档案已经保存。」下面多了一个「重新生成洞察」按钮,用本次流程的 affectedContactIds 调 retryInsights。
  • 运行中按钮禁用,文案变成「正在重新生成…」,另有一个 ref 防连点。
  • 处理目标已变更(targetMismatch)或没有受影响联系人时,不显示按钮。
  • 结果回来时如果 isFlowGenerationCurrent 已不成立(例如已经开始新的一批),直接丢弃:不写进新流程,也不弹提示。applyInsightRetryResult 内部会再按 generation 检查一次。
  • 成功:新洞察按 id 并入列表,新的在前,同 id 以新的为准;用的是和 applyConfirmResult 同一个合并函数 mergeInsightRecords。hasInsightFailure 被清掉,提示卡消失。洞察列表一变,页面原有的加载逻辑会补齐缺失的联系人详情,依据就是这些联系人的 observations。
  • 失败:提示卡保留,用页面原有的 showError 显示 insight_error。请求本身失败(网络、404)时也用 showError 提示。
  • 诊断日志:复用已有的 insights_start / insights_ok / insights_error,detail 带 retry 前缀,没有新增事件类型。

flow-context 的实现方式

  • insights 与 hasInsightFailure 原本是两个独立的 state,现在合成一个 FlowInsightState。这样一次更新就能同时改列表和失败标记,而且基于最新状态。
  • 确认卡片和重试都走纯函数:applyConfirmInsightsToFlow 与 applyInsightRetryResultToFlow。合并去重、清标记、失败保留、过期丢弃都能直接测。
  • 确认卡片的行为没有变:失败标记在后续确认中一直保留,直到重试成功;有测试锁定这一点。

动了哪些文件

  • 服务器:server/src/app.ts(新路由、校验、类型)、server/src/tests/app.test.ts(4 个新测试)。
  • app:
    • app/src/types.ts:InsightRetryRequest / InsightRetryResponse
    • app/src/api.ts:服务器请求与导出
    • app/src/connection/dispatch.ts:接口与分发
    • app/src/local/api.ts:本地实现,失败文案提成常量
    • app/src/flow-context.tsx:合并状态、纯函数、新动作
    • app/app/insights.tsx:按钮与重试流程
  • 测试:
    • app/src/tests/local-api.test.ts:成功与失败两条路径
    • app/src/tests/dispatch.test.ts:分发
    • app/src/tests/flow-context.test.ts:合并、失败保留、过期丢弃、确认行为不变
    • app/src/tests/upload-batch.test.ts:它的 RoutedApi 测试替身必须补上新方法,只加了一个未使用的桩
  • 文档:README.md、README_EN.md 的「后续工作 / Future Work」删去「洞察重试端点」;「Agent 环 / Agent Loop」第 7 步补了一句(中文:「洞察生成失败时,档案照常保存,可以在洞察页一键重新生成。」)。中英文各写各的。
  • 没动:shared/core/schema.ts、shared/core/migrations.ts、shared/types.ts、deploy/、.github/、PLAN.md、docs/PLAN-V2.md、版本号、tag、打包脚本;没加依赖。
  • 测试全部用合成数据(示例联系人甲、示例联系人乙 之类)和假 provider。

本机验收(owner)

网页版需要重新构建(bash scripts/build-web.sh),手机端需要用这个分支的包。

服务器模式

  1. 用有效的 key 上传一张截图,停在确认页,先不要确认。
  2. 把服务器 .env 里的 DEEPSEEK_API_KEY 改成无效值,重启服务器。没配 DeepSeek 的话,洞察走 DashScope 的 Qwen 文本模型,就改 DASHSCOPE_API_KEY。
  3. 确认卡片,然后进入洞察页。应看到「这次洞察暂时没生成,但档案已经保存。」和「重新生成洞察」按钮。
  4. key 还是无效时点按钮:请求期间按钮变灰,显示「正在重新生成…」;结束后弹出服务器给的错误原因,提示卡还在。
  5. 把 key 改回来,重启服务器,再点按钮:提示卡消失,洞察出现在列表里,每条的依据都能展开;联系人页里也能看到这些洞察。
  6. 查库确认写入:sqlite3 server/data/mailuo.sqlite 'SELECT contact_id, kind, content FROM insights ORDER BY id DESC LIMIT 5;'。

本地模式(安卓)

  1. 联网状态下上传一张截图,停在确认页。
  2. 打开飞行模式,再确认卡片。执行在手机本地完成,洞察那一步因为没网失败,洞察页出现提示卡和按钮。
  3. 仍在飞行模式时点按钮,应提示「洞察生成失败,请检查模型配置后重试。」,提示卡还在。
  4. 关掉飞行模式后再点:提示卡消失,洞察出现;联系人页也能看到。

两种模式都看一下

  • 在洞察页等待重试结果时切到别的处理目标(比如换服务器地址),按钮应消失;切回来按钮会恢复。
  • 连点按钮只会发一次请求。

验证命令输出

两条命令都在本次会话里原样跑过。完整 TAP 输出很长,下面只保留版本、安装摘要、测试汇总和 typecheck 结果。

server:cd server && npm ci && npm test && npm run typecheck,整条命令退出码 0。

$ node --version
v22.22.2
added 83 packages, and audited 84 packages in 3s
found 0 vulnerabilities
> mailuo-server@0.1.0 test
> node --import tsx --test src/tests/**/*.test.ts
# tests 267
# suites 0
# pass 267
# fail 0
# cancelled 0
# skipped 0
# todo 0
> mailuo-server@0.1.0 typecheck
> tsc --noEmit
(退出码 0)

app:cd app && npm ci && npm test && npm run typecheck。npm test 有 5 个失败,退出码 1,&& 链因此没走到 typecheck;typecheck 单独跑,退出码 0。

$ node --version
v22.22.2
added 583 packages, and audited 584 packages in 25s
> mailuo-app@1.0.0 test
> node --import tsx --test src/tests/**/*.test.ts
not ok 169 - out-of-order interaction stays blocked while its contact anchor is pending
not ok 170 - review fields show and block a rejected same-screenshot contact dependency
not ok 171 - review fields preserve cross-screenshot batch wording
not ok 172 - review fields follow core precedence for multiple same-screenshot contacts
not ok 173 - review fields do not replace an existing or deferred interaction link
# tests 205
# suites 0
# pass 200
# fail 5
# cancelled 0
# skipped 0
# todo 0

$ npm run typecheck
> mailuo-app@1.0.0 typecheck
> tsc --noEmit
(退出码 0)

这 5 个失败与本改动无关,只出现在这个云端容器里,情况和任务 1 的 PR(#1)里说明的一样:

  • 5 个都在 app/src/tests/review-fields.test.ts,报错是 ERR_INVALID_RETURN_PROPERTY_VALUE。原因是容器里只有 Node 22.22.2,它的同步模块 hook 不接受 CommonJS 返回空 source;CI 用的 Node 22.23.2 没有这个问题。
  • 未改动的 main 在这个容器里同样是这 5 个失败,而任务 1 的 PR 在 CI 上 app 全过。本 PR 的 CI 结果以 checks 为准。

🤖 Generated with Claude Code

https://claude.ai/code/session_01KTuFAfAdgU7H5k9kFMWkVP


Generated by Claude Code

- server:新增 POST /api/insights/retry,body 为 { contact_ids }。非空数组、正整数、
  去重、上限 100 位,非法时返回 400;联系人不存在时与 GET /api/contacts/:id 一致返回 404。
  成功 200 insight_status ok;模型失败也是 200,insight_status failed,insight_error 走
  与确认卡片相同的 getPublicErrorMessage,数据库不写半截
- app:新增 retryInsights({ contactIds }),服务器模式走新接口,本地模式照 confirmCard
  的洞察段落实现,经 dispatch 按当前目标分发
- app:洞察页失败提示卡里加「重新生成洞察」。运行中禁用;目标已变更或没有受影响联系人时
  不显示;流程已过期时丢弃结果。成功时按 id 合并新洞察并清掉失败标记,失败时保留提示卡
  并显示 insight_error
- flow-context:洞察列表与失败标记合成一个状态,确认与重试的合并都走可测的纯函数
  (applyConfirmInsightsToFlow / applyInsightRetryResultToFlow),
  新动作 applyInsightRetryResult
- README 中英文:后续工作删去洞察重试一条,Agent 环第 7 步补一句可在洞察页重新生成
- 测试:服务器成功写库、失败不写库、非法 body 400、联系人不存在 404;本地成功与失败;
  dispatch 分发;flow-context 合并、失败保留、过期丢弃

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTuFAfAdgU7H5k9kFMWkVP
@AliceLJY
AliceLJY marked this pull request as ready for review September 25, 2026 18:41
@AliceLJY
AliceLJY merged commit dffbba4 into main Sep 25, 2026
2 checks passed
@AliceLJY
AliceLJY deleted the claude/insight-retry branch September 25, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants