fix(desktop): GitHub 增强搜不到时回退 gh CLI,空列表不再谎称从未提交 - #1224
Conversation
用户实测:本机 /issues 一条都看不到,而他 GitHub 名下在本仓有 34 条,页面却说
「还没有提交过 Issue」。逐层定位到三个叠加缺陷:
1) 通道在身份阶段一锤定音。resolveGithubEnhancement 里插件一旦报出身份就
`return { source: 'ghost' }`,gh CLI 从此永不被尝试 —— 把「身份可用」当成了
「这一路能查到数据」。实测该 PAT 是 fine-grained token:get_current_user 正常,
搜本仓被 GitHub 以 422 拒绝(未显式授权的仓库即使公开也搜不到),而本机 gh 的
OAuth token 能搜到全部 34 条,却因为插件「成功」了而根本没被调用。
修:service 层加 searchAuthoredIssuesFallback,主通道失败换本机 gh CLI 再试。
放 service 而不是 runtime —— runtime 是真实接线、没有单测,这条不变量必须能被
用例钉住。只对 ghost 主通道回退(gh-cli 自己就是兜底)。
2) 失败完全静默。搜索失败只 log.debug,默认不落盘;排查时日志里只有平台通道的 404,
这条路的失败线上不可诊断。提到 log.warn,并新增 githubEnhancementFailed 跨进程
回传,让 UI 有机会说明「你 GitHub 那部分也没进来」。回退成功时为 false ——
没有可见损失就不打扰用户。
3) 空态在撒谎。三路都没数据时 UI 断言「还没有提交过 Issue」。与 #1103 为
platform-unavailable 修过的是同一类(「空的本机兜底不能证明远端历史为空」),
平台那两路修了、增强这一路漏了,这次是用户先撞上的。
判据收口到 canTrustEmptyList():只有三路都查询成功才敢那么说,否则改用
emptyTitleUnverified「暂时查不到你的 Issue」。
超时预算:GHOST_SEARCH_TIMEOUT_MS 6s → 4s,给兜底留空间;增强总 deadline 仍是 8s,
两段合计不超预算(页面不比现在慢)。四语新增 emptyTitleUnverified 与
enhancementFailedHint(后者按产品决定给出插件令牌权限的可操作指引)。
变异验证:去掉回退 / 固定空态标题时,对应 4 条用例失败、其余不受影响。
门禁 GATE_EXIT=0(56 workspace PASS / 0 FAIL)、desktop typecheck 通过、
check:i18n-glossary 通过、改动文件无 lint 问题。
Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
|
| Filename | Overview |
|---|---|
| apps/desktop/src/main/github-issue/myIssuesService.ts | 引入通道健康状态、GitHub 搜索兜底、共享总预算和首屏快照写入判据。 |
| apps/desktop/src/main/github-issue/myIssuesRuntime.ts | 接入 gh CLI 搜索兜底、快照存储,并区分未配置与身份解析失败。 |
| apps/desktop/src/main/github-issue/myIssuesSnapshotStore.ts | 新增按账号路径隔离且对落盘数据进行严格清洗的首屏快照存储。 |
| apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts | 首屏先 hydrate 快照,并明确区分快照数据与本轮 fresh 查询结果。 |
| apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts | 收紧空列表可信判据,并根据增强通道状态选择提示。 |
| apps/desktop/src/main/maker-ipc/my-issues.ts | 新增受可信 renderer 来源保护的快照 IPC,并传递增强失败状态。 |
| apps/desktop/src/shared/myIssues.ts | 扩展查询结果与快照的跨进程共享契约。 |
Sequence Diagram
sequenceDiagram
participant UI as /issues Renderer
participant IPC as Electron IPC
participant Service as MyIssuesService
participant Ghost as GitHub 插件
participant GH as 本机 gh CLI
participant Snapshot as 账号级快照
UI->>IPC: 读取首屏快照
IPC->>Snapshot: read
Snapshot-->>UI: 上次列表或 null
UI->>IPC: 查询 fresh 列表
IPC->>Service: list()
Service->>Ghost: 解析身份并搜索
alt 插件搜索成功
Ghost-->>Service: Issue 列表
else 插件搜索失败且预算充足
Service->>GH: fallback 搜索
GH-->>Service: Issue 列表或失败
end
Service->>Snapshot: 健康结果写入快照
Service-->>UI: fresh 结果与通道状态
Reviews (5): Last reviewed commit: "refactor(desktop): 三路输入显式报健康,快照判据不再从残缺信息..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bea2dd85b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
该 PR 修复 Desktop /issues 页「GitHub 身份增强」在搜索失败时不会回退到本机 gh CLI、并导致空列表误导用户为“从未提交过 Issue”的问题;同时提升失败可诊断性(warn 日志 + 跨进程标记)并补齐相关 UI/i18n 与测试覆盖。
Changes:
- 在
MyIssuesService中为ghost增强通道引入可选兜底searchAuthoredIssuesFallback(本机ghCLI),主通道搜索失败时尝试回退并复用同一次总 deadline。 - 新增
githubEnhancementFailed跨进程字段,并在 UI notices 与空态标题中区分“确证为空” vs “本次不可确证为空”,避免空态撒谎。 - 增强通道搜索失败日志从
debug提升为warn,新增提示文案(四语)说明可能的 token 权限问题。
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/desktop/src/shared/myIssues.ts | 为结果模型新增 githubEnhancementFailed,区分“没配增强”与“配了但没用上”。 |
| apps/desktop/src/renderer/vite-env.d.ts | 同步 IPC 返回类型的 githubEnhancementFailed 字段默认值。 |
| apps/desktop/src/renderer/i18n/locales/zh-CN/common.json | 新增空态标题 emptyTitleUnverified 与增强失败提示 enhancementFailedHint。 |
| apps/desktop/src/renderer/i18n/locales/ko/common.json | 同步新增 emptyTitleUnverified / enhancementFailedHint 韩文文案。 |
| apps/desktop/src/renderer/i18n/locales/ja/common.json | 同步新增 emptyTitleUnverified / enhancementFailedHint 日文文案。 |
| apps/desktop/src/renderer/i18n/locales/en/common.json | 同步新增 emptyTitleUnverified / enhancementFailedHint 英文文案。 |
| apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts | 新增 canTrustEmptyList() 并在 notices 中加入增强失败提示 key。 |
| apps/desktop/src/renderer/features/issue-tracker/IssueTrackerFeatureLayout.tsx | 空态标题改为基于 canTrustEmptyList() 的两版文案,避免不可确证时断言“从未提交”。 |
| apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts | renderer 侧接入并保存 githubEnhancementFailed。 |
| apps/desktop/src/renderer/features/issue-tracker/tests/myIssuesNotices.test.ts | 覆盖增强失败提示与 canTrustEmptyList() 判据。 |
| apps/desktop/src/renderer/features/issue-tracker/tests/IssueTrackerFeatureLayout.test.tsx | 覆盖首屏/空列表在不同可确证条件下的标题选择。 |
| apps/desktop/src/main/maker-ipc/my-issues.ts | IPC 响应结构补齐 githubEnhancementFailed 默认值。 |
| apps/desktop/src/main/maker-ipc/tests/myIssuesIpc.test.ts | IPC 测试同步断言 githubEnhancementFailed 字段。 |
| apps/desktop/src/main/github-issue/myIssuesService.ts | service 层实现增强搜索失败后的 fallback 尝试、warn 日志与 githubEnhancementFailed 回传。 |
| apps/desktop/src/main/github-issue/myIssuesRuntime.ts | runtime 层接线 fallback(gh auth token)并抽取共用查询参数;调整 ghost 搜索超时预算。 |
| apps/desktop/src/main/github-issue/tests/myIssuesService.test.ts | 新增用例覆盖“主通道失败→fallback”“fallback 不可用/失败”“不重复预算”等不变量。 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@dashhuang 👋 这个 PR 还有 7 条 review conversation 没 resolve(apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts / apps/desktop/src/main/github-issue/myIssuesService.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
用户实测:点开 /issues 先看到一个「没有 issue」的状态,几秒后才跳出 35 条。 两个成因叠加: 1) 首屏没有任何本地数据可显示。service 那层的 60s TTL 缓存是**内存**的,进程一重启即 失效、冷启动必然 miss;而列表要等平台通道与 GitHub 增强都落地才出得来(增强走插件 失败 + gh CLI 回退时实测约 2s)。 2) 等待期间显示的是一句错误断言。#1103 为遵守 §7「取数期间界面不发生变化」让首屏保留 引导内容,但那个引导带着结论性标题 —— 有 35 条的用户先被告知「暂时查不到你的 Issue」 再跳成列表。比 loading 文案更糟:它是错的,而且照样是两次形态切换。 本次: - 新增 myIssuesSnapshotStore(electron-store + ownerScopedUserDataPath,形状照同目录的 submittedIssueLedger)。**不照搬** device-link/mirrorCacheStore 那 1500 行:它的 purge 队列、跨进程锁、作废屏障是为多设备消息文件与内联媒体设计的。但借用它的语义边界 —— 快照是可重建的首屏镜像、不是真相,fresh 一到即整体接管。 - 快照**刻意不含** degraded / githubEnhancementFailed / truncated:那三个描述的是 「这一次查得怎么样」,缓存它们会让用户进页面就看到一条过期的错误提示。 - 写入点放 settle()(结果落地的唯一收口,scope 校验已在那里),判据与内存缓存**同一个**: epoch 变了说明期间有提交成功过,落一份已知过时的镜像没有收益。不为快照另立判据。 - 读取走新 channel MY_ISSUES_SNAPSHOT(同一道 assertTrustedAppRendererEvent —— 快照含 issue 标题与 GitHub 用户名,是账号私有数据)。不给 listMyIssues 加 cachedOnly 参数: 那样得再引入字段区分「缓存是空列表」与「根本没有缓存」,把 list 的契约搞混。 - useMyIssues 里 fresh 与快照**分开存**,并新增 hasFreshData。快照的空列表**不构成** 「查证过的空」—— 若空态标题只看 canTrustEmptyList,首屏又会冒出「还没有提交过 Issue」, 就是这一族错误的第三次。hasFreshData 为 false 时整个标题不渲染(引导正文与 CTA 照旧 在场,它们无论有没有 issue 都成立)。 - 快照读取与真实查询**并行发起**,不 await 它再查:占位不该挡在最慢那条路前面。 变异验证:trustable 不看 hasFreshData / settle 不写快照 / 快照 url 改信落盘值 —— 4 条用例各自失败、其余不受影响。门禁 GATE_EXIT=0(56 workspace PASS / 0 FAIL)、 desktop typecheck 通过、改动文件无 lint 问题;相关用例 166 例全绿。 文案零新增(沿用 #1224 的 emptyTitle / emptyTitleUnverified),四语无改动。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
菜单项回答的是「点这里能干什么」,而它的邻居全是动作短语(帮助 / 检查更新 / 最新更新介绍 / 新建对话)—— zh-CN 与 ja 里 issues 是**唯一**一个孤立的英文名词, 既断了风格,也让不熟悉 GitHub 的用户不知道该不该点。 zh-CN 用「问题反馈」、ja 用「フィードバック」(ja 同级项同样全是日文,Issue 一样孤立)。 en 保持 Issues、ko 保持 이슈 —— 两者在本语言里本就是自然的入口词。 **这不是给 Issue 起中文译名**,术语裁决不变:指代该类对象的位置(页面标题、正文、 提示)仍写 Issue,因为点进去就跳 GitHub,名字必须对得上。判据与 glossary 里 ja 的 Jira 豁免同源 —— 跟用户实际看到的外部界面保持一致。已在 glossary 的 issue 条目 note 里写明这条区分,免得后来人当成漏统一。 顺带修掉 MenuButton 注释里的「议题」—— 那是被 forbidden 的旧译名。 check:i18n-glossary 通过(0 新增违规;改 glossary.json 后按提示跑了 i18n:glossary-doc 重新生成人读版)。desktop typecheck 通过、 run-unit-gate.sh GATE_EXIT=0(56 workspace PASS / 0 FAIL)。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
处理 #1224 的 7 条 review 反馈,归族后是 4 个问题: 1) 「没配增强」被当成「查询完整」(greptile P1 + codex P2 + copilot,4 条同族)。 canTrustEmptyList 原先只看 degraded === null && !githubEnhancementFailed,而没配 增强时后者是 false(没配不是失败)—— 于是「GitHub 账号那一路从未查过」被当成 「查过且为空」。平台侧不知道用户绕过 Cindy 直接在 GitHub 提的那些 issue,只有增强 查得到,所以那种用户会看到「还没有提交过 Issue」,**正是本 PR 要修的错误断言**。 判据补上 githubEnhancement !== null。代价是那句话只在配了增强的用户身上出现, 没配的看到「暂时查不到」—— 刻意保守,同本页反复确立的「错误信息比保守措辞更糟」。 注释也改准了(copilot 指出原注释写「三路都成功」与实现不符)。 2) 失败提示无条件指向插件页(codex P2 + auto-review P2)。githubEnhancementFailed 在 source === 'gh-cli' 时同样为 true(searchViaFallback 对非 ghost 主通道直接判失败), 而那种用户根本没在用插件 —— 给他「插件令牌权限不足、去插件页检查」等于指向不存在 的页面,排障方向也错(gh 用完整 OAuth token,失败多为网络或额度)。新增 enhancementFailedGenericHint(四语齐),按 githubEnhancement.source 选。 3) 兜底请求在 deadline 后仍在跑(greptile P2)。withDeadline 只停止等待,而 GithubClient 不支持 AbortSignal(加上会动到 git-context 等其它调用方,超出本 PR)。 改为**剩余预算不足就不启动**:低于 MIN_FALLBACK_BUDGET_MS(1.5s)直接判失败, 不发那次注定被丢弃、又照样耗 GitHub 额度的请求。判据收口在 canTryFallback(), searchViaFallback 里重复的那半条判断一并删掉 —— 两处判据迟早分歧。 (greptile 说的「连续刷新会累积」不成立:list() 有 in-flight 去重,force 也复用在途。) 4) 日志文案误导排障(copilot)。warn 固定写 "trying the fallback channel",但 gh-cli 主通道或没注入 fallback 时并不会真去试。现按 canTryFallback() 的结果分两种文案。 变异验证:三处实现改动(去掉 githubEnhancement 判据 / 提示不分来源 / 不做预算检查) 各自退回后,4 条用例失败、其余不受影响。门禁 GATE_EXIT=0、desktop typecheck 通过、 check:i18n-glossary 通过;相关用例 172 例全绿。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
范围更新:两个 PR 已合成这一个按 Dash 的要求,把原 #1229(首屏落盘快照 + 菜单入口文案)合并进本 PR —— 那是个 base 指向本分支的 stacked PR,它的 CodeQL 全套不触发(6 项 check vs 本 PR 的 12 项),等于安全扫描完全没覆盖。合并后消掉了这个代价,也不用再按顺序合。 本 PR 现在 4 个 commit,同一个目标(让 /issues 真的可用):
本轮 7 条反馈已全部处理并 resolve归族后是 4 个问题,逐条回在对应 thread 上。其中一条值得单独说:
这个 PR 前后修了三次同类错误(空账本不能证明远端为空 / 混合列表不能说只显示本机 / 快照的空不算查证过),第四次又在新加的判据里犯了。这一族的规律很清楚:只要新增一个「能不能确证」的判断,就要先问「这条数据源到底查没查过」,而不是「它有没有报错」。 另外两条修法与 reviewer 建议不同,已在 thread 里说明理由:
新增文案
验证变异验证:本轮三处实现改动各自退回后 4 条用例失败、其余不受影响。 实机(macOS,
未做:浅色模式目检(主题由 React 按设置注入 token,CDP 改不动;真切换要动用户的显示模式设置,而 dev 实例与正式版共享 userData)、macOS 系统菜单目检(要抢前台)、切号瞬间首屏(需两个真实账号,由 4 条 scope 用例覆盖)。 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8c9d1cc02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/main/github-issue/myIssuesService.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
同一条不变量的第 6、7 处缺口 —— 这一页的任何断言都只能声称**这一次真查证过**的 范围,「没查过 / 查失败 / 只查到一部分」都不得被当成「查过且结果如此」。 1. 降级结果覆盖了完整快照(codex P2) settle() 无条件写快照。一次离线刷新会把 35 条的完整快照覆盖成账本子集甚至空列表, 而快照刻意不带健康状况 —— 用户冷启动看到缩水列表加零提示;若仍离线,那份完整列表 就永久没了。 判据收在 isSnapshotWorthy():问「这一次有没有丢内容」,**不是**「有没有降级」。 两者不等价,混起来会直接废掉整个功能 —— platform-unavailable(读接口还没上线)是 当前所有用户的常态,把它算作不配写,快照永远写不出来。fetch-failed / not-signed-in 是平台本该有却没拿到,才真的丢了内容。 与 renderer 侧 canTrustEmptyList 刻意不合并:那边问「能不能断言从未提交」,空列表 + platform-unavailable 必须答否;这边问「这些内容能不能留给下次首屏」,同样组合答是。 一个管断言缺失,一个管展示已有,方向相反。注释已写明,免得后人来合。 2. 同一个洞的第二个入口:身份解析失败被咽成「没配」 runtime 把 gh 身份查询的异常 catch 成 null,于是与「没配」不可区分,service 判 failed: false。token 过期 / 被撤销 / GitHub 限流时,用户直接在 GitHub 提的那些 issue 静静消失,页面一个字都不说,缩水的结果还照样覆盖快照。 改成:返回 null 只表示没配(静默),失败一律抛出 = 配了却问不出身份(算 failed)。 插件那一步刻意不按此口径抛 —— 插件装了但从未授权是最常见情形,那属于「没配」, 对它抛错会让所有装了插件的用户都看到失败提示。 3. 由此新可达的组合暴露出第 4 次同类错误(在我自己上一轮的修复里) githubEnhancement === null && failed === true 以前不可达,现在会真的出现。而提示 判据写的是 `=== 'gh-cli' ? 通用 : 插件专属` —— default 分支又断言了自己不知道的事, 把「连来源都不知道」误判成插件故障,指向一个那种用户根本没有的插件页。 判据翻成「是不是 ghost」,未知一律落在保守的通用版。 验证:5 条变异各自被对应用例挡下(判据收紧成 degraded===null / 去掉增强那半条 / 去掉 worthiness 检查 / 解析抛错不算 failed / 提示判据反着写);仓库根 pnpm test:unit GATE_EXIT=0(56 PASS / 0 FAIL);desktop typecheck 与 eslint 通过。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
收敛检查点:一句话不变量 + 全部对称路径这个 PR 已经在同一条不变量上栽了 10 次,其中 3 次是在修前一次时新造的。逐条打补丁不收敛,所以已按收敛止损规则改结构(见文末「已状态机化」),并把模型写在这里当后续 reviewer(含 bot)的锚点 —— 对着模型审比对着 diff 猜便宜得多。 不变量
三路输入(平台通道 / 本机账本 / GitHub 增强)各自可能缺席,且缺席的理由不同:没配是正常状态,配了却用不上是内容真的少了,接口还没上线是平台侧压根没这份数据。这三者绝不能压成一个布尔。 对称路径清单(每处判据 + 未知时落在哪边)
两个刻意不合并的判据
一个管断言缺失,一个管展示已有。 把 #6 收紧成「只认 10 次里有 5 次是同一个形状
后续再加任何「能不能确证」的判断,要问的是「这条数据源到底查没查过」,而不是「它有没有报错」。 #8 另外给出一条教训那处原本写了「刻意的例外」,理由是「插件装了但从未授权的用户会被误报」。前提是错的 ——
已状态机化(第 9、10 次之后不再补特例)前 8 次都是「补一个特例」。第 9、10 次(账本读失败 / 身份解析还在飞就超时)暴露出根因是判据的输入本身残缺: 现在三路显式报状态: type ChannelState = 'ok' | 'absent' | 'failed' | 'unknown';
interface ChannelHealth { platform: ChannelState; ledger: ChannelState; enhancement: ChannelState }四态而非布尔,是因为两个消费者对同一状态的处置方向相反:
以前只有一个布尔,两个需求必然打架 —— 这正是「身份超时」修不干净的原因。加第四路输入时, 测试口径每处判据都有用例钉住两个方向(该说 / 不该说),并做变异验证:把判据改回错误写法,对应用例必须失败。累计验了 9 条,含「照 reviewer 建议直接实现 #6」那个改法,以及「把 已知覆盖缺口: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd5ed78b4d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/desktop/src/preload/preload.ts:4815
- preload 侧
electronAPI.maker.listMyIssues的错误返回类型少了githubEnhancementFailed字段,但 main 的 IPC 适配层与 renderer 侧类型声明都已把它作为稳定字段(错误分支固定为 false)。这会导致 preload 内的 API 声明与真实契约不一致,后续若在 preload/共享类型里复用该签名会产生类型漂移。
// /issues 首屏快照(上次结果的落盘镜像);没有 / 坏掉返回 null。非权威,fresh 一到即接管。
getMyIssuesSnapshot: (): Promise<import('../shared/myIssues').MyIssuesSnapshot | null> =>
ipcRenderer.invoke('maker:issues:snapshot-mine'),
// /issues 页面的「我的 Issue」列表;force=true 绕过 main 侧 60s TTL(手动刷新)。
listMyIssues: (
codex 指出的第二处入口,而我上一轮为那处写的「刻意例外」建立在一个**错误前提**上。 上一轮只让 gh CLI 那条通道的身份失败抛出,插件那条仍静默落到「没配」,理由写的是 「isCindyGithubGhostUsable 为真只说明插件装了且启用,问不出 login 最常见的原因是用户 从未授权」。但 isCindyGithubGhostUsable 含 isGithubCredentialSaved() —— 装了插件却 从未授权的用户**压根进不到那个分支**。前提判错,「保守静默」的结论也就跟着错:凭据过期 被撤销、通道超时、GitHub 限流,全被当成「没配」,于是既不提示,缩水的结果还照样覆盖了 包含 GitHub 直提 issue 的完整快照。 判据改成「**有没有通道配过**」而不是「哪一步报了错」:任一通道配过却一个身份都没拿到 就抛出 ⇒ service 判 failed ⇒ 不覆盖快照 + 出提示。插件问不出身份时仍先落到 gh CLI 再试 一次,恢复路径不变。 两处如实登记的局限: - 提示保持通用版。走到这里连来源都不知道,而未知一律落在保守那版(同本 PR 反复确立的 口径)。代价是插件凭据过期的用户看到「稍后可以点右上角重试」——不算误导,但帮助有限; 要给准确指引就得把「哪些通道配过」并进跨进程契约,那超出本 PR。 - runtime 不进单测,所以「什么时候抛」这个判断本身没有单测覆盖(本 PR 第 3 次被这一点 藏住 bug)。被测试钉住的是后果链:解析抛错 ⇒ failed ⇒ 不写快照 ⇒ 通用提示。 验证:仓库根 pnpm test:unit GATE_EXIT=0(56 PASS / 0 FAIL);desktop typecheck 与 eslint 通过;issue-tracker 相关 179 例全绿。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ded58439f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@dashhuang 👋 这个 PR 还有 2 条 review conversation 没 resolve(apps/desktop/src/main/github-issue/myIssuesService.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
同一条不变量连续第三轮被指出漏输入(先 degraded、再身份解析失败、现在账本失败与身份 超时)。前两轮都是补特例,这轮改结构 —— 按收敛规则,第 N 轮还在同一处打补丁就该停下来 状态机化,而不是再补第三个特例。 根因:isSnapshotWorthy 的输入是 MyIssuesResult,而那份结果里的健康信息**是残缺的** —— 平台的挤在 degraded、增强的挤在 githubEnhancementFailed、**账本的根本没有位置** (读失败被静默换成空数组)。信息不在输入里,判据就必然漏。 改成三路各自显式报 ChannelState: ok 查了拿到了(空也是真的空) absent 没配 / 那边压根没这份数据 —— 正常状态,不是损失 failed 本该有却没拿到 —— 内容真少了 unknown 连配没配都没问出来(整体超时打断在半路) 两个消费者从同一份 health 各自推导,**方向刻意相反**: - isSnapshotWorthy 要 ok|absent 全绿,failed 与 unknown 都拒写 —— 快照跨进程活到下次 冷启动又不带健康状况,覆盖错了就是永久数据丢失,不确定必须保守拒写; - githubEnhancementFailed 只认 failed,unknown 静默 —— 对没配增强的用户说「增强没用上」 是在断言我们不知道的事。 以前只有一个布尔,这两个需求必然打架;分开后「身份解析超时」终于能同时做到不覆盖快照 且不谎报失败。 本轮 codex 指出的两条由此闭环: 1. 账本读失败仍算「配写」→ 丢了全部本机记录的结果覆盖完整快照。现在 ledger 报 failed。 2. 身份解析还在飞时整体超时 → promise 未 reject,旧实现只标记「已 reject」,于是当成 「没配」。现在按解析停在哪一步分四态,pending ⇒ unknown。 absent 必须放行是这次最容易被"顺手收紧"掉的一条:platform-unavailable 是当前所有用户的 常态,当成丢内容的话首屏快照永远写不出来。已有用例钉住这个方向。 验证:4 条变异各自被对应用例挡下(账本失败仍报 ok / pending 当成 absent / isSnapshotWorthy 放行 unknown / 收紧成只认 ok);仓库根 pnpm test:unit GATE_EXIT=0 (56 PASS / 0 FAIL);desktop typecheck 与 eslint 通过。 Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com> Signed-off-by: Dash <dashhuang@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (3)
apps/desktop/src/renderer/features/issue-tracker/IssueTrackerFeatureLayout.tsx:272
- 这里的注释写“只有三路查询都成功、确实为空时…”,但 EmptyGuide 实际依赖的是 canTrustEmptyList(data) + hasFreshData 的判据;“三路都成功”容易被读成与底层通道健康状况强绑定(且该组件并未直接感知账本健康)。建议直接引用 canTrustEmptyList() 的语义,避免注释与实现口径漂移。
* 标题分两版:只有三路查询都成功、确实为空时才敢说「还没有提交过 Issue」。任一路
* 降级或失败时它就是一句错误断言 —— 用户在 GitHub 上有几十条 issue、只是这次没查到,
* 页面却告诉他从没提交过(实际发生过,见 canTrustEmptyList)。
apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts:46
- 这里的注释第一句写“只有三路都真查过且都成功才算”,但实现实际只基于 platform(degraded) 与 githubEnhancement 两个维度推断;“三路”容易被理解成把账本也纳入了可确证性判据,导致读者误判覆盖范围。建议把措辞改成“下面三个条件都满足”之类,避免把“条件数量”误写成“通道数量”。
/**
* 列表是否**可被确证是完整的** —— 只有三路**都真查过且都成功**才算。
*
* 空态标题靠它决定能不能说「还没有提交过 Issue」。三个条件各有必要:
apps/desktop/src/main/github-issue/myIssuesRuntime.ts:172
- PR 描述的“明确不包含”里写「配了插件但身份解析就失败仍走静默」,但当前实现里 resolveGithubEnhancement 在 ghostConfigured=true 且 gh token 缺失时会 throw;service 会把 enhancement.state 记为 failed 并通过 githubEnhancementFailed 触发 renderer 的 enhancementFailedGenericHint(即不再静默)。需要对齐:要么更新 PR 描述以反映新行为,要么调整实现让该分支仍保持静默(例如把身份解析失败归为 unknown/absent 并避免置 failed)。
* **返回 null = 一条都没配(正常状态,静默);抛出 = 配了却问不出身份**(要提示,且不许拿
* 缩水的结果覆盖首屏快照)。上一版把两条通道的身份失败都咽成 null,于是与「没配」不可
* 区分:凭据过期 / 被撤销 / 通道超时 / GitHub 限流时,用户直接在 GitHub 提的那些 issue
* 静静消失,页面一个字都不说,而缩水的结果还照样覆盖了完整快照。
*
* 判据是「**有没有通道配过**」,不是「哪一步报了错」——
* `isCindyGithubGhostUsable` 含 `isGithubCredentialSaved()`,所以它为真就意味着用户确实
* 存过 GitHub 凭据;「装了插件但从未授权」根本进不到这个分支(那时它为假)。同理 gh 那路
* 以「有没有 token」判配没配。任一通道配过却一个身份都没拿到,就是配了用不上。
*
* (曾经错在这里:以为插件那步含糊、怕对「装了没授权」的用户误报,于是让它静默落到
* 「没配」。但那种用户压根到不了这一步 —— 前提判错,结论也就跟着错。)
*/
async function resolveGithubEnhancement(): Promise<GithubEnhancementViewer | null> {
const ghostDeps = getSharedGithubUserSubmitterDeps();
// workdir 传 null:/issues 是全局页面,没有会话工作目录上下文。
const ghostConfigured = isCindyGithubGhostUsable(ghostDeps, null);
if (ghostConfigured) {
const login = await readGhostViewerLogin(ghostDeps);
// 插件问不出身份时不直接判死:gh CLI 可能有权限,下面照常再试一次。
if (login) return { source: 'ghost', login };
}
const token = await getSharedGhCliTokenSource().readToken();
if (!token) {
// gh 这一路没配。插件那一路要是配过,说明「配了却一个身份都没拿到」⇒ 失败。
if (ghostConfigured) {
throw new Error('github enhancement identity lookup failed on every configured channel');
}
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过,零 P0/P1。GitHub fallback 搜索与 snapshot 机制实现可靠。
|
搜索回退到 gh CLI 这个兜底思路很实用——配了 GitHub 增强却搜不到东西的时候不再给用户一个空白列表装没事了。snapshot 的三态健康守卫写得干净 👌 |
这次改了什么
摘要
用户报告:本机
/issues一条 issue 都看不到,而他 GitHub 名下在本仓有 34 条,页面却显示「还没有提交过 Issue」。逐层定位到三个叠加缺陷(都已实测确证,非推测):
1. 通道在身份阶段一锤定音,之后不回退。
resolveGithubEnhancement里插件一旦报出身份就return { source: 'ghost' },本机 gh CLI 从此永不被尝试 —— 这把「身份可用」当成了「这一路能查到数据」,而两者无关。实测该 PAT 是 fine-grained token:
get_current_user正常(所以 header 正常显示@dashhuang),搜本仓却被 GitHub 以 422 拒绝:仓库是 PUBLIC(
gh repo view确认)—— fine-grained token 的 search API 对未显式授权的仓库一律拒绝,即使仓库公开。而本机gh的 OAuth token 能搜到全部 34 条,却因为插件「成功」了而根本没被调用。2. 失败完全静默。 搜索失败只
log.debug,默认不落盘。排查时日志里只有平台通道的 404,这条路的失败线上完全不可诊断。3. 空态在撒谎。 三路都没数据时 UI 断言「还没有提交过 Issue」。这与 #1103 为
platform-unavailable修过的是同一类(「空的本机兜底不能证明远端历史为空」)—— 平台那两路修了,增强这一路漏了,这次是用户先撞上的。即使去给 PAT 补仓库授权能解决单个用户的问题,产品也必须兜住:PAT 的权限范围不可控,而失败静默时用户根本不会知道要去改它。
变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
/issues页面看不到自己的 issue);延续 feat(desktop): Issue 页展示自己提交过的 Issue #1103searchAuthoredIssuesFallback,主通道搜索失败时换本机 gh CLI 再试;只对ghost主通道回退(gh-cli自己就是兜底,没有下一条可换)debug→warn,并新增githubEnhancementFailed跨进程回传canTrustEmptyList(),新增emptyTitleUnverifiedenhancementFailedHint(含插件令牌权限的可操作指引)GHOST_SEARCH_TIMEOUT_MS6s → 4s 给兜底留预算GET /api/github/issues/mine仍未提供(platform-unavailable降级不变,接口上线后零改动生效)resolveGithubEnhancement的返回契约,且那种情况下空态标题已因platform-unavailable显示为准确的「暂时查不到」,优先级低;日志提到 warn 后至少线上可诊断UI 变化
深色模式实机截图(macOS,35 条数据下的列表态):见下方「手工验证」的取证。本次新增/改动的可见元素:降级提示条多一条(
enhancementFailedHint)、空态标题分两版。DESIGN.md§2 灰阶:新增提示沿用既有 notices 容器(bg-sidebar-item-hover+text-sidebar-muted),未引入任何语义色DESIGN.md§10 Token Selection Rules for New UI:无新增 token,复用既有 slot;未使用bg-[#xxx] dark:bg-[#xxx]形态DESIGN.md§11 Voice & Content:提示不断言页面上不存在的数据范围;空态标题只在能确证时才说「还没有提交过」;措辞不暗示「必须有 GitHub 账号」(只针对已配置插件的用户说令牌权限)i18n/GLOSSARY.md:Issue 保留英文(2026-07 裁决);pnpm check:i18n-glossary通过rounded-xl),无 4px / 6px 残留怎么验证的
自动验证
新增 7 条 service 用例覆盖回退不变量:主通道失败 → 调兜底并采用其结果且不算失败/兜底返回 null → 标记失败但主列表照常/兜底自己抛错 → 同上不打挂/
gh-cli主通道失败时不调兜底/主通道成功时不碰兜底/没配增强既不搜也不算失败/兜底也算在同一次总 deadline 内。变异验证(回退与空态判据都是「静默失败」型缺陷,没有能鉴别的用例等于没修):去掉回退调用、把空态标题固定回
emptyTitle后,对应 4 条用例失败、其余不受影响;恢复后全绿。手工验证
--preserve-runningpassive dev 实例(macOS,复用真实登录态与插件 PAT),经 CDP 对/issues发起force: true强制刷新:{"success":true,"count":35,"degraded":"platform-unavailable", "enhancement":{"login":"dashhuang","source":"ghost"}, "enhancementFailed":false,"truncated":false, "firstThree":[{"n":1222,"state":"open","sources":["github-account"]}, {"n":1082,"state":"closed","sources":["github-account"]}, {"n":1016,"state":"open","sources":["github-account"]}]}count: 35= GitHub 名下 34 条 + 账本里 1 条平台代发(改前是 0)。main 日志的完整取证链(两次调用一致,证明走的是回退而非「插件突然好了」):
这条 warn 也顺带验证了缺陷 2 的修复:改前是
debug、日志里什么都看不到。原始 422 文本只进 main 日志、不跨进程到 renderer(不含 token 或本地路径)。深色模式目检通过:列表行版式、状态点(实心 open / 空心 closed)、来源标记、
#编号 · 状态 · 类型 · 来源 · 日期 · 评论数元信息行、顶部常驻说明条,以及platformUnavailablePartialHint(混合列表下正确地没有谎称「只显示本机记录」)。回退成功时未出现enhancementFailedHint✓。未执行的验证
DESIGN.md§10 这不等于双模式已验证。原因:应用主题由 React 按设置状态注入 token 值,CDP 的Emulation.setEmulatedMedia只能改data-theme标记、颜色不跟着变;要真正切换必须改显示模式设置,而--preserve-running与用户主实例共享 userData,改它会影响用户正在使用的实例,因此没有动。未目检的具体元素:enhancementFailedHint提示条与emptyTitleUnverified空态标题在浅色下的对比度。风险
风险分类
影响与回滚
/issues页面的可选增强取数路径与空态文案。主通道(平台)与本机账本逻辑未动;提交 issue 链路未动。回退只在主通道已经失败时触发,不增加正常路径的请求量。/issues回到「插件搜不动就整路放弃」的行为;不涉及数据迁移、持久化格式或协议变更。🤖 Generated with Claude Code