docs/00-overview/roadmap.md
这份文档回答一个问题:"这个仓库现在到底还差什么?"
它不是需求池,也不是 ADR,只是一份当前时点的现状快照,方便任何人(人或 agent)在挑下一个任务前先对齐现实。
维护约定:
最后一次盘点:2026-07-06(下午)。
这些不是当前 TODO,只是给读者一个"最近走到哪儿"的参考点。
| 模块 | 关键交付 | 入口文件 |
|---|---|---|
| PR timeline 事件识别 | reviewed / committed / copilot_work_* / ready_for_review 等特殊事件行 + 4 语言 + 25 widget 测试 | issue_timeline_item.dart |
| Reaction toggle | issue / comment 三态防抖 + 竞态锁 + mounted 校验 | issue_repository.dart |
| 通知中心 | subject.type 分派 / repo 筛选 / reason 快速筛选 chip / 图片错位 ValueKey 修复 / 侧滑三动作 | 通知模块 |
| Trend | 语言筛选歧义修复 + 时间/代码 icon | trend 模块 |
| Search | 历史 / Issue tab / Code tab / 抽屉动态化 / 空 q 假报错修复 | gsy_search_drawer.dart |
| PR 变更文件页 | 行级评审评论挂载 + reviewed body 色带 | pr 模块 |
| URL 编码根治 | +/:/# 高级修饰符二次编码 bug 从源头修 | address.dart |
| 事件识别扩容 | Discussion / DiscussionComment / PullRequestReviewThread / Sponsorship 四类事件 + 5 个 action 收编(本轮) | event_utils.dart |
这些是"上一批改动做完但没扫干净"的口子,改动量都小,一个 commit 就能收:
_translateAction 里还没收编的 action(走 default 分支透传英文 + 遥测):
auto_merge_enabled / auto_merge_disabled(PR 自动合并,越来越常见)marked_as_duplicate / unmarked_as_duplicate(issue 去重)dequeued / enqueued(merge queue,GitHub 2025 后主推)deployed / deployment_status修改点:_translateAction 加 case + 4 语言 arb 加 key。
Discussion 家族事件已经收编到词典(本轮 0b2cb46),单测 15 绿;
本轮尝试从真机首页动态 tab 走 CarSmallGuo 的 received feed 直接命中 DiscussionEvent 那一条,
拉了 40+ 条依然没打到(GitHub API 上明确有 1 条:JDDavenport / created / 666ghj/BettaFish / 2026-07-05 03:29:46,
位于 per_page=20 第 2 页第 4 位)。
分析后确认这条真机命中不该强求,改为代理证据闭环:
test/utils/event_utils_test.dart 15 case 已覆盖 Discussion / DiscussionComment / PullRequestReviewThread / Sponsorship 四类getActionAndDes switch 分支的事件(IssuesEvent / ForkEvent / WatchEvent / PushEvent)
在 40+ 条列表里全部正常渲染,无 UnknownEvent 空白卡片、无 EXCEPTION本轮附带发现新 bug(登记为独立跟进项,见 §2.5):GSY 首页动态 tab 有分页边界丢事件的嫌疑—— CarSmallGuo received feed 里明确存在的 DiscussionEvent(07-05 03:29)在 app 里滚遍时间戳区间也没出现。
现象:GitHub API /users/CarSmallGuo/received_events?per_page=20&page=2 第 4 条是
JDDavenport DiscussionEvent created 666ghj/BettaFish @ 2026-07-05 03:29:46;
但 GSY app 首页动态 tab 里下拉刷新 + 上拉加载后,从 hfye 关注 666ghj/BettaFish
直接跳到 CarGuo push 46dca6c,中间的 domesticmouse WatchEvent 与 JDDavenport DiscussionEvent 都不见。
怀疑路径:
requestRefresh 拉第 1 页 + doNext(res) 又 await res.next()(该闭包捕获的 page 仍为 1,实际再拉一次第 1 页),
然后 refreshData(resNext) 整体覆盖 dataList,第 1 次 refresh 后内存里其实只有第 1 页 20 条insert 里"清空后再插入,因为只保存第一页面"——db 缓存只有第 1 页loadMoreData 走 _page++ 拉第 2 页 append,但没有和第 1 页做时间戳排序去重,
存在"网络返回 event.id 与 db 缓存重复但 payload 差异"的边界跟进方式(下轮任务):
尝试的方向:把 refreshData 从"整体覆盖 dataList"改成"以 incoming 打头 + 保留旧 dataList 里不在 incoming id 集合的尾部元素",试图让下拉刷新不清空 loadMore 累加的 page=2/3。
为什么失败(reviewer F1 拍板):与 EventRepository.getEventReceived 的 db→net 双阶段模型冲突:
refreshData(res)(db 阶段),再 await doNext(res) → refreshData(resNext)(net 阶段)。bug25_fixed/ 里的截图看似"深段数据保留",实际证明的是 dataList 膨胀,不是 §2.5 描述的 JDDavenport DiscussionEvent 真的回到了 UI。修改已 git checkout 撤回,本地状态回到 §2.5 未修状态。
按可能性从高到低排:
doNext 里 res.next() 闭包捕获的 page 恒为 1:event_repository.dart#L18-L38 next() 是在 getEventReceived(page: 1) 时创建的闭包,闭包捕获的 page = 1。dynamic_bloc.dart#L48-L56 里 await res.next() 相当于再拉一次 page=1,而非"翻到下一页"。这条要么修 next() 让它带 page+1,要么直接删掉 doNext 分支(db 阶段就是 page=1,doNext 只是刷新一下"真实的 page=1"就够了,不应把结果当第 2 页用)。loadMoreData 依赖 dataList 做 id 去重,但没做 created_at 兜底稳定排序:GitHub received_events 是持续变化的流;两次请求间新事件涌入,会导致 page=N 尾部 event.id 在 page=N+1 头部再次出现(重复),也会导致 page=N+1 里含已在 page=N 边界外但时间比 page=N 尾还老的事件被裹进来。当前 loadMoreData id 去重能挡住第一种;第二种目前没保护但观察到的丢失现象(JDDavenport DiscussionEvent 从 UI 消失)不是这条能解释的——它本来就在 page=2 第 4 条位置。首要动作(下一轮先做这个):
requestRefresh 完成后 dataList 的所有 id / type / created_atrequestLoadMore 完成后 dataList 新增段的 id / type / created_atcurl /users/CarSmallGuo/received_events?per_page=20&page=2。禁止:
refreshData / loadMoreData 语义hfye 关注 666ghj/BettaFish 与 JDDavenport DiscussionEvent 时间戳相近,肉眼滚很难判断是否真的丢bug25_fixed/ 目录的证据(改动已撤回,证据无效)久违的技术债,不影响功能,但每次 CI 输出脏:
<...> 需要转义或反引号e70d5b0e70d5b0e70d5b0e70d5b0library 名多余e70d5b0publish_to: nonee70d5b0axisAlignment deprecated → 迁到 alignment: Alignment.topCenter/bottomCenter现状:flutter analyze = No issues found。
GitHub Actions 已在 build job 里加 flutter test 一步(Run unit / widget tests (whitelist))。
当前白名单(全部跑绿):
test/utils/event_utils_test.darttest/model/issue_timeline_event_test.darttest/widget/markdown_html_transformer_test.darttest/widget/issue_timeline_item_test.dart(本轮加回,之前挂 4 个 reviewed body 用例)test/page/issue/issue_timeline_merge_test.dart(issue-timeline-flash 回归契约)已解决:_reviewBodyCard
从"DecoratedBox 直接 non-uniform border + borderRadius"改为
"外层 uniform 半透 border + 圆角 + 内层 Stack 叠一条 3px 色带"。原本因 shrink-wrapping viewport
不能配合 IntrinsicHeight,也在实现里避开了。真机截图见
tool/dbg/smoke_08_issue_detail_scroll2.png
(Pull request overview 卡片左侧紫色 3px 色带 + 圆角边框,无断言)。
dart run build_runner build 时,riverpod_generator 阶段对以下三个 async provider 稳定报错:
Invalid argument(s): Cannot find import for AsyncValue in _CanonicalizedUri(package:riverpod/riverpod.dart), and could not automatically import it.Future<T> 或 AsyncNotifier 的 provider,g.dart 需要 emit AsyncValue<T>):
@riverpod 注解但同步 build(),g.dart 不 emit AsyncValue):
part 声明,不参与生成)真实版本组合(.dart_tool/package_config.json 与 pubspec 交叉验证):
flutter_riverpod: 3.0.3riverpod: 3.0.3(传递依赖)riverpod_annotation: 3.0.3riverpod_generator: 3.0.3analyzer_buffer: 0.1.12(AnalyzerBuffer._upsertImport 抛错所在包)四方严格对齐 3.0.3,不是版本 mismatch。
根因(本轮实测走完排查后重写,替换掉前一轮"6.3 vs 3.x"的错误猜测):
AnalyzerBuffer._upsertImport(_CanonicalizedUri('package:riverpod/riverpod.dart'), 'AsyncValue') 在 analyzer_buffer 0.1.12 lib/src/analyzer_buffer.dart#L538-L546 里,会先调 _namespace.findSymbol(uri, 'AsyncValue') 遍历当前源文件的 libraryImports2,取每个 import 的 definedNames2['AsyncValue']?.library2?.uri 与期望 URI 精确比对。
问题是 AsyncValue 实际定义在 riverpod-3.0.3 lib/src/core/async_value.dart#L414,通过 riverpod-3.0.3 lib/src/internals.dart#L1-L26 的 export 'src/...' 链层层 re-export 到 riverpod-3.0.3 lib/riverpod.dart#L1-L24。analyzer 侧的 Element.library2.uri 返回的是定义 library 的 URI(package:riverpod/src/core/async_value.dart),而不是 re-export 入口的 URI(package:riverpod/riverpod.dart)—— 两者永远匹配不上。
由于 _autoImport = false(_TargetNamespace 构造函数默认关闭),最终抛 Cannot find import for AsyncValue in ...。
关键结论:这是 riverpod_generator 3.0.3 对 re-exported 类型的 URI 假设错误,用户侧无法通过修改源文件的 import 绕开。本轮已实测:
import 'package:flutter_riverpod/flutter_riverpod.dart'; → 仍报同样错误import 'package:riverpod/riverpod.dart'; → 仍报同样错误两次修改都已回退到原状。
副作用已自行消失:build_runner 3.x 已移除 --delete-conflicting-outputs 参数(本轮实测输出 W These options have been removed and were ignored),即使 riverpod_generator 报错,也不再联动删除 .g.dart。本轮 2 次跑 build_runner 前后 3 个 .g.dart 字节数与时间戳完全一致(4197 / 4674 / 2889,11:24:26 AM)。前一轮 roadmap 记录的"删掉却不重生成"痛点由此关闭。
当前状态:既有 3 个 .g.dart 是有效生成产物(app_state_provider.g.dart 早前生成时环境不同),运行时正常。只要不主动 git rm 或手动清理,就不受影响。
根治候选(不在本轮工作范围):
riverpod_generator 4.0.4(2026-06 中旬发布,跨越 4.0.0 ~ 4.0.4 五个版本),配套升级 flutter_riverpod / riverpod_annotation 到 4.x。属于跨大版本升级,可能牵涉 ProviderContainer / Ref / AsyncNotifier 等运行时 API 破坏性变更,波及 app_state_provider.dart 里的 globalContainer = ProviderContainer() 与 5 个 @riverpod 消费点。需独立任务拉一栏,含 pub deps → 迁移指南 → 全量回归。riverpod_generator 到 2.6.5(AnalyzerBuffer 尚未引入 _upsertImport 那套流程),但会失去 3.0.0 之后的 stateful hot-reload / new element model 收益。不推荐。日常操作约束(本轮沉淀,供后续人 / agent 参考):
E riverpod_generator on ... 那三条错误(它们不会破坏已生成产物,也不会阻断 json_serializable 阶段的输出)。看的是 wrote N outputs 里 N 是不是符合预期,不看 exit code。dart run build_runner build --build-filter="lib/model/*.dart"(本仓库当前不强制这么用,但作为环境优化路径记录)。@riverpod 源文件语义(改返回类型 / 加参数)时才需要单独任务处理这个环境问题,否则默认容忍。背景:CI 一直在 build 阶段挂,日志显示
lib/widget/pull/nested/nested_refresh.dart:526:15: Error: No named parameter with the name 'alignment'。
原因是本轮 §2.3 早期已把 nested_refresh.dart#L526
的 axisAlignment: 1.0/-1.0(deprecated after Flutter v3.41.0-1.0.pre)迁到了新 API
alignment: Alignment.bottomCenter/topCenter;这个新参数在 Flutter 3.41.6 stable 里还没引入,
而 CI workflow 里 subosito/flutter-action@v1 恰恰锁在 flutter-version: '3.41.6'。
本地长期跑 3.44.x,CI 一直落后 → 出现"本地绿 / CI 红"的经典 SDK 漂移。
修复方式(用户拍板:升 CI 而非降代码 API):
3.38.4 → 3.44.1(用 FVM 作为唯一版本契约源)build 与 apk):
subosito/flutter-action@v1 → @v2(v2 起支持 flutter-version-file)flutter-version: '3.41.6' → flutter-version-file: .fvmrc(读同一份 FVM 契约,杜绝双写漂移)channel: stable + cache: true.fvmrc + FVM遗留:Flutter 3.44 有 Material/Cupertino 拆包 deprecation warning(package:flutter/material.dart 仍可用,只出 warning);
当前 CI 不带 --fatal-warnings,不阻塞,等后续再迁移。riverpod_generator 3.0.3 URI mismatch(§2.6)
是完全独立的问题,不随本次 CI 升级解决,仍需按 §2.6 建议独立跨版本升级任务处理。
边界前提:GSY 定位是 GitHub 的只读 + 评论客户端,不承担写 PR / 提交 review / 建仓库这类作者行为。 下面清单已按这条边界筛过。
Discussions 阅读页
Discussion 事件已经识别,动态流里能看到"在 xxx 创建 讨论",但点进去没页面。
可行路径:复用 issue detail 那套 timeline 骨架,接 /repos/{o}/{r}/discussions/{n} GraphQL。
进度跟踪(分阶段推进,避免一口气吞完):
✅ 骨架阶段(上轮):GraphQL 单接口 + 空壳页 + 4 语言 fallback 文案
readDiscussion 查询,含 category / author / bodyHTML / answer / upvoteCount / comments(first:30) + replies(first:10)getDiscussion(owner, name, number) Future 封装Text 直出(下一子任务替换为 Markdown/HTML widget)goDiscussionDetail(context, owner, name, number) 入口discussion_load_failed / discussion_not_found / discussion_retry / discussion_answered_badge / discussion_empty_body / discussion_skeleton_notice / discussion_comments_count✅ event 路由接入(本轮):动态流卡片直连详情
EventDiscussionRef(只留 number 字段),EventPayload.discussion 走 json_serializable 自动解析ActionUtils switch 追加 DiscussionEvent / DiscussionCommentEvent case:payload.discussion?.number 拿到就走 goDiscussionDetail,缺失时回退 goReposDetail(不假装成功)discussion.number 时反解为 int,缺失时保持 null;总测试数 119 → 121 全绿✅ 内容渲染阶段 + 仓库详情 tab(2026-07-20 拓宽 → 2026-07-27 主体完成):
bb96480):
_buildCommentsSection / _buildCommentCard / _buildReplyRow / _buildLoadMoreFooter,一级 comment 走 GSYMarkdownWidget 渲染 bodyHTML,replies 平铺前 3 条 + "还有 N 条回复"尾巴DiscussionCommentsPage 容器 + pickCommentsPage(防御式提取:空 endCursor 归 null / Map<dynamic,dynamic> 规范化)+ mergeCommentsPage(尾部追加、pageInfo/totalCount 用新页)readDiscussionCommentsPage(first, after) 变体,字段与 readDiscussion.comments 完全对齐getDiscussionCommentsPage(fetchPolicy=noCache 走真实分页)hasNextPage=true 展示"加载更多"按钮,点击 _loadMore 追加;hasNextPage=false 展示"没有更多评论了";失败展示重试discussion_comments_empty / discussion_comments_load_more / discussion_comments_load_more_failed / discussion_comments_no_more / discussion_comments_reply_more_hint 5 key ×4 语言pickCommentsPage 7 case(空/缺字段/类型错/正常/pageInfo 缺/空 endCursor/规范化)+ mergeCommentsPage 2 caseBettaFish #522(1 评论)+ #511(3 评论含 @mention + 代码块)在 Android 33 zh 8.1.0 上单卡/多卡/footer 空态命中,无 Dart 侧 Exception,证据落 tool/dbg/discussion_comments_smoke/16_discussion_522.png / 17_discussion_522_scroll.png / 19_discussion_511.png / 22_discussion_511_s5.pngb0e4042):入口条件 repository.has_discussions=true,无则不显示 tab(不显示空态,避免误导用户去点)6c697cc → b762edf → a60f884)—— 详情页 markdown 图片链路已改 errorBuilder + 保留 JWT 签名;2026-07-27 真机验收发现 8.1.0 apk #511 body 段图片以蓝链文本呈现(imageBuilder 未被触发),排查确认两条独立 bug 路径:<a></a> HTML 结构被 label 转义(b762edf 已修)+ []\n(href) 换行断开导致 CommonMark 判为未闭合 link(a60f884 已修)。代码层两条路径均已修复且单测覆盖,真机验收挪到下一轮(需先解决 fixture 设备旋转 override 问题,详见 §3.1 剩余分支 pt.4)hasNextPage=true 真机上没自然触发_buildReplyRow 未在真机截图上显式命中b762edf + a60f884),真机复核挪到下一轮 milestone,前置依赖是 fixture 设备旋转排查,见 §3.1 剩余分支 pt.4⏳ §3.1 剩余分支(下一子任务):
👍/🎉/❤️/🚀/👀/😄/😕/👎 8 类):Discussion 本体 + 每条 comment 都要挂
reactionGroups { content viewerHasReacted reactors { totalCount } };新增 mutationAddReaction / mutationRemoveReaction 与 addReactionToSubject / removeReactionFromSubject 包装函数kReactionContents / kReactionEmoji / ReactionSummary / pickReactionGroups(GraphQL 返回规范化,宽容处理未知枚举 / null / 类型错)/ applyLocalReactionToggle(乐观 toggle,幂等,遵循 kReactionContents 顺序)flutter test test/page/discussion/reaction_groups_test.dart 全绿count>0 分组以 chip 形式展示,viewerHasReacted 用 primaryColor 描边 + 淡背景高亮;尾部 + 与长按 bar 都触发底部 sheet 8 类 chip 选择器subjectId 并发一律丢弃)→ 乐观 applyLocalReactionToggle → 走 mutation → 成功用返回的 reactionGroups 覆盖 / 失败回滚到调用前快照并弹 SnackBar(走 discussion_reaction_failed 文案)discussion_reaction_add / discussion_reaction_login_required / discussion_reaction_failed / discussion_reaction_a11y + 8 类 emoji a11y label(reaction_thumbs_up … reaction_eyes),flutter gen-l10n 已重跑flutter analyze lib/page/discussion/discussion_detail_page.dart No issues found!addReaction / removeReaction,没有 create/delete discussion 之类越界动作logcat -d -s flutter 空异常[deleted] 边界 + release-linked footer
#417(answered + bot + self-answer + 嵌套 reply)/ #697(deleted 404)/ #511(release footer)discussion.author / answer.author / comment.author / reply.author 全部补 __typename,作为 Bot 判定的唯一权威依据(GitHub GraphQL 保证 Bot/User/Organization/Mannequin 四值);answer { id author { login __typename } } 支撑 self-answer 判定selfAnswered 计算(answered=true && authorLogin!=null && authorLogin==answerAuthorLogin,任一 login null 走 ghost 分支不误命中),header answered chip label 切成 discussion_answered_by_author_badge__typename == 'Bot' 时在 _buildCommentCard 与 _buildReplyRow author 名字后挂橙色 chip,与绿色 answer chip 视觉隔离[deleted] 兜底:author == null 且 bodyHTML 为空时改用 discussion_comment_deleted_body("该评论已被删除"),仍有内容时保持渲染 body(GitHub 允许 body 保留但 author 消失)discussion_answered_by_author_badge / discussion_comment_bot_badge / discussion_comment_deleted_body;flutter gen-l10n 已重跑flutter analyze 全仓 No issues found!;flutter test test/page/discussion 35/35 全绿(复用既有单测,UI 侧改动无新 model 单测)#417 走 answered + self-answer + bot chip + 嵌套 reply 截图,#697 走 deleted body 截图;release-linked footer 卡真机截图与 pt.1/2/4 一并做Invoke-WebRequest 拉 BettaFish #511 discussion HTML 页面,抓 build/smoke/disc_511_page.html 全量 324KB,line 1619 命中真实 footer 结构):<hr><em>This discussion was created from the release <a href="https://github.com/{owner}/{repo}/releases/tag/{tag}">{title}</a>.</em>,真实 fixture tag=v3.0.0,title 是中文"微舆v3.0.0"(strippedBody, info?);正则保守约束(<hr> 自闭合 / caseInsensitive / 允许前后空白与换行 / href 必须命中 /releases/tag/{tag} 形态 / title 不含 < 避免吞下一个标签 / footer 必须在 body 末尾,中途出现的 releases-tag 链接不吞);HTML entity 只做 5 项最小解码避免引依赖extractReleaseFooter 抽离,剩余 HTML 交 markdown;footer 独立成 GSYCardItem 卡(local_offer_outlined 图标 + discussion_release_footer_title 提示 + info.title 加粗标题 + info.tag primaryColor chip + chevron_right),整卡 InkWell 走 NavigatorUtils.goGSYWebView 打开 release 网页,复用 header "查看 GitHub" 一致的出站入口discussion_release_footer_title(zh"关联的 Release" / en"Linked release" / ja"紐付いた Release" / ko"연결된 Release");flutter gen-l10n 已重跑flutter analyze 全仓 No issues found!;flutter test test/page/discussion 50/50 全绿(35 原有 + 15 新增)#511 就是这条 UI 分支的天然 fixturehasNextPage=true 需换 fixture 到 vercel/next.js 或 expo/expo 里挑一条 >30 comments 的 discussion(对照组 fallback,需要在 PR 描述里显式提出后再落到 fixture 表)
type:discussion,改用 Invoke-WebRequest 拉 vercel/next.js/discussions?discussions_q=sort:top HTML → build/smoke/nextjs_discussions_top.html 585KB,正则 aria-label="(\d+) comments?:[^"]*"[^>]*href="/vercel/next\.js/discussions/(\d+)" 一次抽出 25 条候选,评论数从 2616 到 25 均匀分布)vercel/next.js#37136「RFC: Layouts」(RFC/Announcement category, Closed, 480 comments),理由:480/30≈16 页足够压测 loadMore 循环但不过分,比 #41745(2616)体感更贴近生产 discussion;详情页 HTML (build/smoke/nextjs_37136.html 2.1MB) 里 <button>Load more…</button> 与 data-timeline-item-src="/vercel/next.js/discussions/37136/timeline_anchor?after=...&before=..." 并存,服务端明确 hasNextPage=true(HTML 第一屏 render "135 hidden items" 提示 + Load more 按钮),完美匹配 GSY 移动端 first:30 + hasNextPage=true loadMore 触发路径vercel/next.js#41745(2616 comments,>87 页),仅在验证 endCursor 边界或"极长 dataList 滚动性能"时启用vercel/next.js → discussions tab → 打开 #37136 → 滚到 comments 段尾部 → 点"加载更多评论" 按钮 → 抓改动前 dataList.length / 改动后 dataList.length / 底部"没有更多评论了"或再次 Load more 的截图,logcat 无 GraphQL 401 或 rate_limit 报错;截图路径按 AGENTS.md 三段式写入完成汇报vercel/next.js#49607「Google fonts break with tailwindcss」125 comments,比 #37136 的 480 快 4× 且同样触发 hasNextPage=true 分支):
#3.1 pt.3 GraphQL 分页 + _buildLoadMoreFooter 4 态显隐 + _loadMore 的 setState/merge 端到端在真机 release build 上是否闭环flutter analyze / build_runner;沿用 versionName 8.1.0 release apk(含 6c697cc + a60f884)jfxgpjeul7lrpjkz(Redmi K60 · Android 13 · MIUI,前置 tool\ai\smoke\probe_device_rotation.ps1 -Apply 关自动旋转 + wm user-rotation lock 0,wm size 1080x2400 与 exec-out screencap 尺寸一致,坐标 tap 无偏移)
hasNextPage=true 分支命中sahariar-safin @ 2023-06-10)append 到 dataList 尾部,_loadingMore transient 态未卡死mergeCommentsPage 追加语义正确logcat:GSY release build 下 logcat -d -s flutter:V 输出 0 行(Android 8+ non-debuggable 权限限制),改用 20_logcat_gsy_lines.txt 全 tag pid grep 也为空 —— 属工具链限制而非 exception silent,本条已列入下方"已知缺口"hasNextPage=false 已通过 test/page/discussion/discussion_comments_page_test.dart 单测覆盖;hasNextPage=true → 点按钮 → 拉下一页 → merge → 新评论渲染」已获得真机截图证据链,pt.3 loadMore 特性在 §3.1 里降为 ✅ 已完成#49607 作为真机路径更短的推荐 fixture(125 comments,滚 3~4 屏即触底)6c697cc)上跑通 #511 body 段截图(37_disc_511_top.png / 38_disc_511_scroll1.png / 39_disc_511_scroll2.png),logcat 无 Image.network failed 记录(40_logcat_pid.txt 只 69 字节的 gralloc 噪声)[] 与外层链接 (hrefUrl) 之间夹了一个换行符,flutter_markdown_plus 走 CommonMark 严格解析时会因此把整段 []\n(hrefUrl) 判为"未闭合 link"→ 全段降级为纯文本,imageBuilder 从未被触发,UA/errorBuilder 分支自然不生效。a60f884):在 gsy_markdown_widget.dart 顶层新增 mergeLinebrokenImageLinks,在 _processMarkdownImages 开头调用;正则 \[!\[([^\]\n]*)\]\(([^)\s]+)\)\][ \t]*\n?[ \t]*\(([^)\s]+)\) 保守匹配——alt 不含 ] 与换行、URL 不含空格 / 右括号,避免误吞下一行独立段落或独立普通链接。已是单行的表达式命中后原样输出(幂等),与 <a></a> 直通分支输出的单行形态无冲突。flutter test test/widget/markdown_linebroken_image_link_test.dart 8/8 全绿;flutter analyze 目标文件零告警。<a></a> HTML 结构直通修复(另一条独立 bug 路径,非本 pt.4 根因)
bodyHTML 里 GitHub 生成的 <a href="X"></a> 走 _dispatch <a> 分支时,label = "" 被 _escapeMdLinkText 转义成 \!\[Z\](Y),最终产物 [\!\[Z\](Y)](X) 是 CommonMark 无法识别的字面文本,imageBuilder 永远进不去。<a> 分支新增 _extractSoleImgChild 辅助——当 <a> 的唯一有效子节点是 `` 时(允许前后空白 text 节点),直接吐出 [](href),alt 单独走 _escapeMdLinkText,src / href 走 _escapeMdUrl(防空格 / 右括号伪造)。混合子节点(多张图、img+文字)仍回落通用 label 转义分支,安全优先。] 转义),52/52 全绿;flutter analyze lib/widget/markdown test/widget/markdown_html_transformer_test.dart No issues found!。adb install -r build/app/outputs/flutter-apk/app-debug.apk)通过,但当前 fixture 设备 jfxgpjeul7lrpjkz 的 GSY 主 Activity 以 landscape 内容渲染(adb exec-out screencap -p 输出 2400x1080,与竖屏 wm size 1080x2400 / mRotation=ROTATION_0 不一致),adb input tap 用竖屏物理坐标全部落偏,导航到 BettaFish #511 详情页失败。按 AGENTS.md 分级要求 "稀有分支覆盖率无法靠真机保证时,优先加模型层单测 + 真实 fixture" 补齐单测(含 private-user-images JWT URL 真实 fixture 形态 + 换行断开真实形态)后停在此层。adb shell settings get system user_rotation / adb shell settings put system accelerometer_rotation 0 关闭自动旋转,再 settings put system user_rotation 0 强制竖屏 override;adb shell wm user-rotation lock 0(部分厂商 ROM 才支持);adb reboot)后立即抓 wm size / dumpsys display | grep -Ei 'rotation|orientation' 观察 boot 之后 rotation 是否恢复;-Apply 才写 override。真机 (device jfxgpjeul7lrpjkz, 1080x2400) baseline 已抓(evidence 落 tool/ai/smoke/evidence/20260728_1518/rotation_probe_report.txt,.gitignore 忽略不入库):
wm size 1080x2400 与冒烟脚本坐标假设一致 ✓mCurrentOrientation=0 / rotation 0 / installOrientation ROTATION_0 设备当前就是竖屏settings get system user_rotation: 0(override 已存在)settings get system accelerometer_rotation: 1 ← 就是这条:自动旋转开着,冒烟中途手机翻身即会打乱绝对坐标 tap,正是 pt.1/2/4/5 真机验收 tap 落错的根因wm user-rotation free(未 lock);ROM 支持该 subcommand,-Apply 时会走 wm user-rotation lock 0 加固pwsh -NoProfile -File tool\ai\smoke\probe_device_rotation.ps1 -Apply -Device jfxgpjeul7lrpjkz,把 accelerometer_rotation 写成 0,即解锁 pt.1/2/4/5 的真机验收前置logcat -d -s flutter 无 Image.network failed 或 label 转义相关 warning;截图路径按 AGENTS.md 运行时冒烟规范 写入完成汇报三段式。#417(1 comment + 1 reply)或 #309(3 comments + 长链)复核 _buildReplyRowflutter_markdown_plus 的 pre 标签内置渲染硬编码使用 Scrollbar 包裹横滚 SingleChildScrollView(见依赖仓库 flutter_markdown_plus/lib/src/builder.dart L344-L357),Scrollbar 会独占 Axis.vertical 的 HorizontalDragGestureRecognizer/VerticalDragGestureRecognizer,导致外层 discussion_detail_page.dart 的 SingleChildScrollView 在 code block 落点收不到垂直 drag。真机现象即 pt.3 已知缺口 a):滚到 y≈1800 的 Inkvii 评论卡内嵌 code block 后,垂直滑动被拦截,第 2 次 loadMore 与"没有更多评论了"末态无法自动触发。MarkdownElementBuilder,visitText 返回无 Scrollbar 的 SingleChildScrollView(scrollDirection: Axis.horizontal, physics: ClampingScrollPhysics) 包裹 Text.rich(GSYHighlighter 高亮 span 挂在 preferredStyle 之下,配色不丢);gsy_markdown_widget.dart 里 Markdown 增加 builders: {'pre': SafePreBuilder(...)} 注册。pubspec.yaml 显式补 markdown: ^7.3.0(覆盖 visitText(md.Text, TextStyle?) 签名需直接 import package:markdown/markdown.dart,否则 depend_on_referenced_packages 会告警)。Markdown 渲染带 code block 的 md 后子树里可查到 SingleChildScrollView(scrollDirection: horizontal);② 同一子树里 Scrollbar 计数为 0;③ 高亮内容(模拟高亮器返回的自定义 TextSpan)落到 Text.rich 且保留自定义 TextStyle;④ code block 继承 styleSheet.codeblockPadding。flutter test test/widget/(含既有 issue_timeline_item / linebroken_image_link / html_transformer 等)89/89 全绿;flutter analyze lib/widget/markdown/safe_pre_builder.dart lib/widget/markdown/gsy_markdown_widget.dart test/widget/safe_pre_builder_test.dart No issues found!。vercel/next.js#96370「Turbopack Error Report」,天然含超长垂直 code block,比原计划 #49607 走 loadMore 的路径更短更直接,也更贴切验证 vertical drag 穿透):
flutter build apk --release --target-platform=android-arm64 --no-shrink 成功产出 build\app\outputs\flutter-apk\app-release.apk (17.7MB);adb install -r(保留 SharedPreferences TOKEN_KEY,遵守 AGENTS.md「禁止 flutter install」条款)。jfxgpjeul7lrpjkz(Redmi K60 · Android 13 · MIUI,本轮再次跑 probe_device_rotation.ps1 -Apply 关自动旋转 + wm user-rotation lock 0);截图落 tool/dbg/discussion_codeblock_gesture_smoke/:
#96370 详情页顶部,标题 Turbopack Error: Failed to write app endpoint /page 可见,body code block 显示到 Execution of parse_css failed → 手势验证的前置基线(540, 1800) → (540, 800) 垂直上滑 → 页面整体上滚约 1000px,标题已滚出屏幕,code block 显示到底部 The process cannot access the file becaus,「添加反应」栏与「暂无评论」空态露出 → 决定性证据 A(vertical drag on code block 被外层 scrollview 接管)(540, 800) → (540, 1800) 下滑(起点仍落在 code block 内),页面回到顶部标题态 → 证据 B(反向 drag 同样穿透)(900, 900) → (100, 900) swipe → code block 内部横滚到显示 ContentIndex\14c8f5d6-5e03-4214-a617-0b...、used by another process. (os error 32)、visited_intern failed 等原来被截断在右侧的内容 → 决定性证据 C(horizontal drag 仍归 code block 内层横滚 SingleChildScrollView,横滚能力未被牺牲)Exception|FATAL|flutter.*E/ 只出 mi_exception_log errno=2(MIUI 系统开机通用告警,与 GSY 无关),GSY 侧 0 异常#96370(0 comments 空评论态)后已能直接从 body code block 落点验证 vertical drag 穿透,比 loadMore 场景更纯粹(不掺 GraphQL 分页噪声);#49607 的 60→90 loadMore 分支仍未验(但 pt.3 已单独 ✅ 完成首次 loadMore,方案 C 修复的是手势层,与 GraphQL 层正交,本证据链足以外推);② release build logcat 权限限制导致无渲染树 debug 输出(同 pt.3 已知缺口 c),走"UI 可见性 = 手势穿透"的间接证据链)。flutter_html / webview 之类重方案(收益 vs. 依赖膨胀不划算);不改 flutter_markdown_plus 的其它 builder(本轮只替换 pre,code/inline code 仍走默认渲染);不做 code block 内容拷贝/长按菜单(属独立特性,未来单独立项)。Fixture 契约(2026-07-20 API 探针实测):
CarGuo 生态全军覆没:主仓 + 5 个 CarGuo 名下仓库 has_discussions=false(未认证 REST 实测)。
Discussion fixture 必须走外部妥协项。
首选外部妥协项:666ghj/BettaFish(41k stars, User owner_type, 与 §2.5 DiscussionEvent 来源一致,见 event_utils.dart#L396-L412)
| Discussion # | Category | Answered | Comments | 覆盖场景 |
|---|---|---|---|---|
| #511 | 📣 Announcements | - | 2 | Maintainer 徽标 + reactions (👍30🎉11) + upvote=8 + release-linked footer + image embed |
| #417 | 🙏 Q&A | ✅ | 1 (+1 reply) | Answered 徽标 + bot 评论 + author self-answer + code block + 嵌套 reply + 完整 markdown |
| #309 | 🙏 Q&A | ✅ | 3 | Answered + 长评论链 |
| #418 | 🙏 Q&A | ❌ | 9 | Unanswered + 长评论链(最多) |
| #680 | 💬 General | - | 1 | 短标题 + 短 body(GraphQL 边界) |
| #121 | 💬 General | - | 1 | 孟加拉语标题 হাই(多语言渲染) |
| #134 | 💬 General | - | 0 | 0 comment 空状态 |
| #697 | (deleted) | - | - | 已删除 404 边界(真机进入应显示 discussion_not_found) |
对照组(同时探过、has_discussions=true 但 GSY 生态无关联,仅作 fallback):
vercel/next.js (141k★) / vuejs/core (54k★) / expo/expo (51k★) / supabase/supabase (107k★) / shadcn-ui/ui (119k★)
外部妥协项 · Discussion loadMore fixture(2026-07-28 探针实测,BettaFish 讨论区 comments 全部 ≤30,hasNextPage=true 靠 BettaFish 天然覆盖不到,必须走对照组):
| Discussion # | Category | State | Comments | 覆盖场景 | 探针依据 |
|---|---|---|---|---|---|
| vercel/next.js#49607 | Help | Open | 125 | 推荐 loadMore 首次触发(真机路径最短,30 → 60 只需滚 3~4 屏即触底出 loadMore 按钮) | 2026-07-29 真机验收命中 fixture(见 §3.1 pt.3 「2026-07-29 真机验收完成」),实测首次 loadMore 后 dataList 由 30 增至 60 且新评论 sahariar-safin @ 2023-06-10 可见 |
| vercel/next.js#37136 | RFC / Announcement | Closed | 480 | 多轮 loadMore 压测(480/30≈16 页) | HTML 明确 render "135 hidden items" + <button>Load more…</button> + timeline_anchor?after=...&before=... |
| vercel/next.js#41745 | (待现场核实) | (待现场核实) | 2616 | 备用压力测(endCursor 边界 / 极长 dataList 滚动性能) | 同批列表页正则命中最大值,评论量约 87 页 |
fixture 使用约束:
build/smoke/ 且 .gitignore 忽略,reviewer 需要复核时按上表 URL 用 Invoke-WebRequest 重新抓取即可#37136 后续 hasNextPage 落到 false(服务端一次性 render 完 480 条不太可能,但存在 GitHub 侧策略调整可能),换到 #41745;vercel/next.js 若关闭 discussions,从对照组 expo/expo / supabase/supabase 里挑一条 >30 comments 的顶置 discussion 顶替明确不选:flutter/flutter / microsoft/TypeScript 都 has_discussions=false,别再往这两个仓库塞探针了。
写操作边界(对齐 AGENTS.md 允许/禁止清单):
addUpvote mutation,边界暧昧(不改讨论内容但影响排序),本轮不实现,留在 PR 描述里显式提出后再拍板Notifications 分组视图 目前是扁平列表 + reason 筛选。官方 app 是按 repo / subject 折叠。 修改点:通知模块加分组 header,不改数据源。
PR review thread resolved 徽标 + 操作
本轮 arb 已经有 event_action_resolved / unresolved,但 PR 详情页里 review thread 的状态没渲染。
修改点:pr detail 页 review comment 分组渲染时加色条 / 图标;2026-07-06 边界拍板允许把操作层也做进来(长按 / 侧滑触发 resolveReviewThread / unresolveReviewThread mutation),仅操作层,不做"未 resolved thread 计数"这类仪表盘。
这一小节的存在意义:前面 §3.1-3.3 罗列了功能待办,但没有把"用哪个仓库验证"钉死。
Discussions 阶段就吃过这个亏——事件识别做完才发现 GSY 关注账号里根本没 discussion 事件,
真机路径覆盖不了,只能补单测。这一节把每个待办功能预先绑定测试 fixture,
挑活前一眼看清是否能上真机;⚠️标记的项目要在正式开工前先跑 API 探针确认。
Fixture 优先级规则(与 AGENTS.md 允许 / 禁止的写操作清单 一致:禁止造数据,全部用真实数据):
GSYVideoPlayer / GSYGithubAppKotlin / GSYGithubAppCompose / GSYGithubAPP / gsy_flutter_bookCarSmallGuo 的通知 / 关注数据(首选,因为不用切账号)flutter/flutter / dart-lang / defunkt 等)—— 仅在前三档无数据时启用,必须显式标注为"外部妥协项"探针结果快照(2026-07-06 用 CarSmallGuo 的 gho_ token 实测,GET /rate_limit 显示 core 4997/5000,探针零压力):
| 探针 | 结论 |
|---|---|
GET /repos/CarGuo/gsy_github_app_flutter/milestones?state=all | count=0 → 主仓无 milestone,需外部 fixture |
GET /repos/CarGuo/gsy_github_app_flutter/issues?state=all&per_page=10 | #938 有 assignees=[CarGuo,Copilot];其他 9 条都是 (none) → assignee 挂件用 #938 即可 |
GET /repos/CarGuo/gsy_github_app_flutter/pulls/938/comments | 2 条 line-level review comment(Copilot 对 android/app/build.gradle L41 / L84),完美 review thread fixture |
GET /users/CarGuo/gists | count=0 → CarGuo 无公开 gists,fallback 到 defunkt(已实测 3 条非空) |
GET /notifications?all=true&per_page=30 | 30 条通知 reason 分布 subscribed x 28 / manual x 2 → reason chip filter 需要更丰富 reason,实际验证时可去用 mention / review_requested 相关的仓库 |
POST /graphql user.pinnedItems(first:6) | CarGuo pinned 6 个仓库:GSYVideoPlayer ★21458 / gsy_github_app_flutter ★15461 / GSYGithubAppCompose ★125 / GSYGithubAPP ★2485 / GSYGithubAppKotlin ★1586 / gsy_flutter_book ★4618 → 完美 pinned fixture |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | 公共事件流 /events | 无需 fixture | 全站流,任何账号登录后即有数据 |
| 2 | 组织事件 /orgs/:org/events | flutter / dart-lang 组织(外部妥协项) | CarGuo 是个人账号,无组织 fixture |
| 3 | Repo 网络事件流 /networks/:o/:r/events | ✅ 主仓(fork 数够) | Address.getReposEvent URL 已在但无 UI 消费 |
| 4 | 事件类型 filter | ✅ 主仓首页混合事件流(本轮真机截图 smoke_ci344_01_home.png 已证 6 张卡多种事件混排) | 纯本地过滤 |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | Release 详情页 + reactions | ✅ 主仓 releases(8.0.0 已在真机日志出现) | release_page.dart 是列表未做详情 |
| 2 | Topics chip | ⚠️ 需先跑 GET /repos/CarGuo/gsy_github_app_flutter/topics 确认非空 | 若空,退到 flutter/flutter(topics 稠密) |
| 3 | Labels chip | ✅ 主仓 Address.getReposLabels URL 已在 | 主仓 labels 页非空 |
| 4 | Milestone | 主仓无 milestone(探针实测 count=0) → 用 flutter/flutter(外部妥协项) | 已实测确认 |
| 5 | Branches 切换 | ✅ 主仓多分支 | Address.getbranches URL 已在 |
| 6 | Contributors / Stargazers / Watchers 列表页 | ✅ 主仓 fixture ★15461(探针实测) | UI 只有数字未做列表 |
| 7 | Compare 视图 | ✅ 主仓 423c762...bf557aa(本轮实际存在的两个 commit) | Address.getReposCompare URL 已在 |
| 8 | Language 分布条 | ✅ 主仓多语言(Dart + Java + ObjC + Kotlin) | GET /repos/:o/:r/languages |
| 9 | Community Health | ✅ 主仓 GET /repos/CarGuo/gsy_github_app_flutter/community/profile | 主仓有 LICENSE / README 完整 |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | Assignee / Milestone / Label 挂件 | ✅ Assignee 用主仓 #938(实测 assignees=CarGuo,Copilot);Milestone 用 flutter/flutter;Label 用主仓 | 单一 issue detail 页混合 fixture |
| 2 | PR 状态 badge (draft / mergeable / conflicts) | ✅ 主仓 #938 | AGENTS.md 已固化 |
| 3 | PR reviews 完整列表 | ✅ 主仓 #938 | AGENTS.md 已固化 |
| 4 | PR commits | ✅ 主仓 #938 | AGENTS.md 已固化 |
| 5 | PR review thread 阶段 A/2 | ✅ 主仓 #938(实测 2 条 line-level comment:Copilot 对 android/app/build.gradle L41 / L84) | 完美 review thread fixture |
| 6 | Issue comment reactions | ✅ 主仓 issue #643(README 里"登录失败"高流量 issue) | Address.getIssueCommentReactions URL 已在 |
| 7 | Timeline 分页 | ✅ 主仓 issue #13(README 明示"所有运行问题请点这里",历史巨长 issue) | 完美长 timeline fixture |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | Commits 搜索 | ✅ 搜 repo:CarGuo/gsy_github_app_flutter Copilot 命中 | 主仓自搜 |
| 2 | Topics 搜索 | ✅ 搜 flutter / github-client | 全站,主仓也在结果集 |
| 3 | Labels 搜索 | ✅ GET /search/labels?repository_id=142308181&q=bug(142308181 是主仓 id,本轮真机日志 repo:{id:142308181} 已确认) | 主仓 id 已固定 |
| 4 | 搜索历史 UI 增强 | 无需 fixture | 本地 search_history_repository 数据 |
| 5 | 高级筛选 chip | 无需 fixture | 纯 UI |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | 用户公共 Gists | CarGuo 无 gists(实测 count=0)→ 用 defunkt(外部妥协项,探针实测 3 条非空) | 已实测确认 |
| 2 | 贡献日历 GraphQL 版 | ✅ CarGuo(活跃开发者,calendar 稠密) | 主用户已够 |
| 3 | Pinned Repositories | ✅ CarGuo 6 个 pinned:GSYVideoPlayer / gsy_github_app_flutter / GSYGithubAppCompose / GSYGithubAPP / GSYGithubAppKotlin / gsy_flutter_book(探针实测) | 完美 fixture |
| 4 | Following/Followers 交集 | ✅ CarGuo × CarSmallGuo(后者是 adb 当前登录账号,天然双账号) | 双账号 fixture |
| 5 | 组织详情页 | flutter / dart-lang(外部妥协项) | CarGuo 不是组织 |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | 仓库级通知筛选 | 依赖 CarSmallGuo 订阅——已实测 30 条通知都是主仓 subscribed → 主仓即可 | 单仓测试足够 |
| 2 | reason chip filter | CarSmallGuo 当前 reason 单一(subscribed x 28 / manual x 2)→ 实际验证时通过手动 mention 其他账号或订阅 flutter/flutter 扩容 reason 分布 | 已实测确认 |
| 3 | since / before 窗口 | 无需 fixture | 参数扩展 |
| 4 | 未读 badge | 无需 fixture | 现有数据算 |
| # | 待办功能 | fixture 锚点 | 备注 |
|---|---|---|---|
| 1 | Reactions 铺满(release / commit comment 等) | ✅ 主仓 issue #643 + release 8.0.0 | 主仓够用 |
| 2 | Markdown / Emoji 渲染 | 无需 fixture | POST 任意 md |
| 3 | License 列表 | 无需 fixture | 全站 API |
| 4 | Rate Limit 诊断入口 | 无需 fixture(当前 token 自查,本轮探针实测 core 4997/5000) | 天然自证 |
| 5 | Stars 增长曲线 | ✅ 主仓(README 已用 star-history 外链 badge,数据密度够画) | 主仓够用 |
⚠️ 项必须先跑一次 API 探针把它转成 ✅ 或"外部妥协项",再动代码这些不写清楚,"该不该做"就没答案,不要贸然开工。
状态更新(2026-07-06):已在 AGENTS.md 允许 / 禁止的写操作清单 拍板转正。roadmap 这段保留分类原文供理解演进用,以 AGENTS.md 为准。
拍板产生的下游影响:
清单以 AGENTS.md 为准,任何新增写操作需要在 PR 描述里显式提出并同步更新该清单。
26 个事件 case + 30+ action 词典塞在一个 switch。要不要按事件族拆成
event_utils_pull_request.dart / event_utils_issue.dart / event_utils_discussion.dart?
先不拆,除非下一次要加事件族时再顺手拆。
en 有 @key.placeholders 元数据,zh / ja / ko 没有。gen-l10n 能用,
但新增 key 时必须记得只在 en 里加 metadata,是个隐藏坑,本轮就差点踩。
选一:
Object → 得手工修一遍 dart 调用点)。Redux / Riverpod / Provider / Signals 现实并存,按 ADR-0001 是故意保留的教学负担。 建议做一件事:画一张"哪个模块用哪套"的当前快照贴到 state-management-matrix.md。 这不是消除并存,只是让 agent 一眼看清哪里该用哪套。
AGENTS.md 要求"中等以上改动默认拉 reviewer subagent", 但没定义什么叫"中等"。本轮 Discussion 事件收编(11 files / 418 insertions)严格说该走 reviewer 但没走。
建议补一条量化门槛:
tool/ai/smoke/ 现在有:
relaunch_app.sh —— 冷启动 GSY 到首页(早期就有)open_pr_timeline.sh —— PR timeline 冒烟入口(上轮沉淀)open_home_dynamic.sh —— 首页动态 tab 走完刷新/加载/滚动 5 张证据截图(本轮沉淀)还差:
open_search_code.sh:Search → Code tab → 输关键字open_pr_files.sh:进 PR → 变更文件页按不同偏好给三条路,任选:
marked_as_duplicate/unmarked_as_duplicate 或 dequeued/enqueued 两组 action。10 分钟一个 commit(auto_merge_* 已在 2026-07-06 收)。