feat: 新增四种抽选动画与「无动画」,补充单元测试与界面层测试 - #2
Open
happy66dev wants to merge 18 commits into
Open
happy66dev wants to merge 18 commits into
happy66dev wants to merge 18 commits into
Conversation
把「跟坐标打交道」和「跟摄像头打交道」分开: - 新增 FaceGeometry:letterbox 缩放、放大裁切、分块切分、位置判定, 不碰摄像头也不碰界面,可以离线单测 - 新增 FacePickState:回避最近抽过的人的记录改成实例, 原来挂在 static 上,测试之间会互相污染;随机数改为可注入,结果可断言 - CameraPicker 只留摄像头 I/O,改调上面两个类 顺带补上的防御: - 裁切区域落在画面外时不再抛 ArgumentException(Clamp 的下界会大于上界) - 分块数为 0 或负数时不再产出无穷多块 - 画面尺寸为 0 时不再算出 NaN,回避逻辑因此不会静默失效 - 配置被手改成 "DrawnThisRound": null 时兜底成空列表 - RosterService 的三个公开方法挡住 null 设置对象 - RosterService 补上 IDisposable:它挂着文件监视器,本来就该能 using 新增 xUnit 测试项目 tests/RandomPicker.Tests,89 个用例全通过, 覆盖名单解析与三种抽选模式、设置读写与坏 JSON、几何计算与边界、 回避状态、人脸框去重。 主项目 csproj 里排掉 tests/**:SDK 默认会把项目目录下所有 .cs 收进来, 不排掉的话测试文件会被当成插件源码一起编译。 Co-Authored-By: Claude Code <noreply@anthropic.com>
写清楚三件容易踩的事: - 插件必须在 ClassIsland 源码树里编译,源码树外可以用目录联接挂进去 - microsoft.windows.sdk.net.ref 是框架引用包,只能靠 PackageDownload 拉取 - 跑测试必须从源码树这一侧发起,直接对着插件仓库的真实路径会解析不到 ClassIsland.Core 顺带说明测试覆盖了哪些、以及相机 I/O、模型推理、界面这三块为什么只能人工验证。 Co-Authored-By: Claude Code <noreply@anthropic.com>
新增五个纯逻辑类(不引用 Avalonia,可离线单测): - AnimationGate:按宿主动画开关降级 + 时长夹取 - ScrollTicks:线性减速的换帧时刻表 - CsgoTrack:方块轨道排布与滚动距离 - SlotMachine:老虎机格数与逐格候选字收敛 - WheelLayout:转盘扇区、停在边界、滑入中选格的角度 外加 RevealAnimationPlanner(样式 → 排片表的映射)。 两个踩过的坑记在这里: - 中选者原本放在轨道的最后一轮,结果滚到终点时视口右半边是空的。 是被自己写的自检拦下来才发现的,现在改成中选者后面也留够方块。 - 动画等级判据必须是 >= 1:宿主默认等级就是 1,照抄某些地方的 >= 2 会让默认安装下动画永远不播,而开发机上根本看不出来。有专门的测试钉这条。 测试从 89 涨到 197。
悬浮钮的忙碌原因拆成拍照和播动画两种,文案各说各的, 用户才知道该等哪一步。抽完之后的收尾逻辑单独抽成一个方法, 以后播完动画和直接出结果都走同一条路,不会两边各写一遍。 Co-Authored-By: Claude Code <noreply@anthropic.com>
滚动名字、CSGO 开箱、老虎机、拼多多转盘四个控件都按 「属性存进度、Render 现算几何」写:动画只推一个进度属性, 画面上长什么样全靠纯函数算出来,几何本身已经有单测钉着。 RevealWindow 里加了动画层和结果层,播完靠透明度交叉过渡换过去, 尺寸只在开播前算一次,整个显示期间不变,窗口不会在屏幕中间抖。 结果在点击那一刻就定好了,动画只负责演,换任何样式都不影响概率。 宿主的动画等级、配置被改坏的时长、名单数据不满足样式, 这三类情况一律静默降级成直接出结果,绝不吞掉中选者的名字。 Co-Authored-By: Claude Code <noreply@anthropic.com>
五种样式各是什么效果、时长在哪儿调、老虎机对名单长度的限制、 转盘的边界滑动是怎么回事,都写清楚了。 Co-Authored-By: Claude Code <noreply@anthropic.com>
manifest 简介末尾、作者字段各带上「happy 二改」,另加一个自定义键留痕; README 顶部加提示条指回原仓库,文末补一节写清原作者、二改日期与改动范围。 自定义键是安全的:宿主读插件元数据用的是 IgnoreUnmatchedProperties, 认不出的键会被忽略;代价是插件列表界面只渲染已知字段,那一行不会显示出来。 Co-Authored-By: Claude Code <noreply@anthropic.com>
两个都是用户实测报回来的问题。 一、点第一下就崩,插件被宿主自动禁用。 结果层那个 DoubleTransition 漏写了 Property。DoubleTransition 不会从泛型 参数推出属性名,必须显式指定;它在窗口构造函数里建,跟播不播动画无关, 所以每一次出结果都会抛「Transition has no property specified.」。 二、系统重启后悬浮钮跑到屏幕左上角。 位置一直是存下来的,问题出在开窗那一刻显示器信息还没就绪:拿到的是空矩形, 或者缩放比例是个坏值。拿这种值照常算,Math.Clamp 的合法区间会塌成屏幕原点 那一个点,窗口就被硬拽到 (0,0)。摆位判断抽成纯类 WindowPlacement, 坏值一律返回「不要动」,另加一个短命重试计时器等信息就绪。 三、补上界面层测试。 上面第一个问题编译 0 错误、197 个纯逻辑测试全绿,只在「窗口真的被建出来」 那一刻现形。现在用 Avalonia 无头平台把窗口和四种动画控件真的建出来、 量出来、渲染成位图读像素,专门挡这一类。已验证:把 bug 塞回去, RevealWindowTests 立刻以同样的异常信息失败。 测试 197 -> 245,全部通过。版本 1.2.0.0 -> 1.2.0.1。 Co-Authored-By: Claude Code <noreply@anthropic.com>
两处改的是同一份设置,但各自只管刷新自己:从右键菜单改了动画样式或 品质权重,已经打开着的设置页不知道,下拉框和滑块还停在旧值上。 - PickerHostService 落盘后广播 SettingsSaved,让「别处改过了」这件事有声音 - 设置页在挂载/卸载里订阅、退订,收到广播就重读全部相关绑定 - 自己触发的落盘加标记忽略回声,否则拖滑块会被刚落盘的四舍五入值拽回去 - 新增 PickerHostServiceTests 三条:广播发得出、发的时候值已生效且已落盘、改几次发几次 - 测试项目补 Microsoft.Extensions.Hosting.Abstractions 引用(插件侧标了不复制, 测试输出目录里没有这个 dll,一构造宿主服务就 FileNotFoundException) dotnet test: 272 通过 / 0 失败。 Co-Authored-By: Claude Code <noreply@anthropic.com>
- manifest.yml 与 csproj 版本 1.2.0.1 → 1.3.0.0 - 动画那一节写明四种动画都是先快后慢的非线性曲线 - 新增「CSGO 名字品质」一节:五档品质、CS:GO 默认分布、权重是相对值、 只影响颜色不影响抽到谁、只出现在动画里不进结果 - 「动画期间再点」改写成钮上显示「跳」、点一下直接跳到结果 - 重新打了两个 .cipx 到 bin/(精选 8 文件 4.55 MB / 全量 18 文件 4.99 MB) Co-Authored-By: Claude Code <noreply@anthropic.com>
主人报「动画并没有触发」,测的是滚动名字。 根因:BuildAnimation 里建 Animation 时只给了 KeyTime、没给 Duration。 Avalonia 拿 Duration 当分母把 KeyTime 换算成 cue (Animation.cs: cue = KeyTime.TotalSeconds / Duration.TotalSeconds), 默认值是 TimeSpan.Zero,于是第一个关键帧算出 0/0 = NaN, Cue 的构造函数只接受 0~1、NaN 两个比较都不成立,当场抛 「This cue object's value should be within or equal to 0.0 and 1.0」。 那个异常被 RunAnimationAsync 里「绝不吞掉结果」的兜底接住, 症状就成了「结果照出、动画全无、还不报错」。 另外三个样式本来都写了 Duration,只有滚动名字漏了。 - ScrollNameAnimation 补 Duration = 排片表总时长,并把建动画抽成 internal BuildAnimation(),让「时长和时刻表对不对得上」能被单测验到 - 末帧关键帧钉死在总时长上:逐项 TimeSpan.FromSeconds 取整到 tick, 240 项能攒出微秒级误差,末帧 cue 会是 0.9999996 而不是 1 - RevealWindow 的兜底 catch 加一行 Debug 输出,别再让这类问题悄无声息 - 新增 AnimationPlaybackTests:结构上验 Duration 与每个 cue 落在 0~1, 行为上验「调度器空转完、时钟没走,结果不许提前交出来」; 另有一条故意复现该 bug 的证伪用例,把 Cue 会抛异常这件事钉下来 证伪过:把 Duration 那一行去掉重跑,两条新用例都红, 且只有 Scroll 那一支红(另外三个样式照过)。 dotnet test: 278 通过 / 0 失败。 Co-Authored-By: Claude Code <noreply@anthropic.com>
主人 2026-09-15 定:动画是这个功能本身,不是界面装饰。ClassIsland 的
「动画级别」管的是它自己的界面过渡,把它设成「禁用」不该连累抽选表演。
想彻底关掉动画,唯一的地方是插件右键菜单里的「无动画」。
- 删掉 AnimationGate.Resolve,连同它读 IThemeService 的那条路
- PickerHostService 直接用 _settings.AnimationStyle;
RevealAnimationPlanner.Build 对 None 本来就返回 null,不需要额外判一遍
- 删掉 6 条「按宿主动画等级降级」的用例,换成 2 条政策守卫:
· OnlyTheNoneChoiceTurnsTheAnimationOff —— 只有选无动画才不播
· NothingInThePluginMentionsTheHostsAnimationService —— 在程序集元数据里
扫 IThemeService 这个名字,防止有人把那条路接回来(用二进制扫描而不是反射,
因为反射看不见方法体里的引用)
证伪过:临时往 SaveSettingsInternal 里塞一句 typeof(IThemeService) 的引用,
扫描用例立刻变红。
dotnet test: 274 通过 / 0 失败。
Co-Authored-By: Claude Code <noreply@anthropic.com>
主人报「老虎机最后的抽取有问题,应该最后一个字展示一会会再继续」。 根因:排片表把总时长定成「每格 × 格数」,最后一格恰好在总时长那一刻才停住, 动画当场结束、立刻切去出结果,拼好的名字一帧都留不下。 - SlotMachine 新增 SettleSeconds = 0.8 秒,加进排片表的 Total - SlotPlan.Total 因此大于「每格 × 格数」,多出来的就是那截留白; 控件用它当 Animation.Duration,所有关键帧都落在「总时长 - 定格」之前, FillMode.Forward 于是把最后一帧的值延续到结尾,板面静止着让你看清名字 - SlotMachineAnimation 建动画抽成 internal BuildAnimation(),方便验留白 - 新增三条测试:排片表里多出定格、动画的最后一个关键帧落在总时长之前 且差值正好是 SettleSeconds、cue 仍全在 0~1 证伪过:把 Total 改回「每格 × 格数」重跑,三条全红。 另外 README 改掉「宿主调低动画级别插件也会安静」那句(上一条提交已经拆掉那条路), 并补上定格 0.8 秒的说明。 dotnet test: 276 通过 / 0 失败;两个 .cipx 已重打。 Co-Authored-By: Claude Code <noreply@anthropic.com>
主人这一轮提了三件事。 一、四个样式收尾都加 0.8 秒定格 之前只有老虎机加了,另外三个同样是「最后一帧刚出现就被结果顶掉」。 现在把定格提到 RevealAnimationPlan 上(SettleSeconds + MotionTotal), 由 RevealAnimationPlanner 统一给四种样式补上——集中在一处, 四个样式的收尾节奏必然一致,不会再漏掉某一个(这个 bug 已经漏过两次)。 各控件改成 Duration = 总时长、关键帧落在 MotionTotal 上: - 滚动名字:末帧钉在 MotionTotal - CSGO:终点关键帧落在 MotionTotal(顺带把背景也改成品质色,见三) - 老虎机:最后一格落在 MotionTotal - 转盘:第二段的 Duration 拉成「总时长 - 第一段」,终点仍是滑动结束那一刻 另外把「依次播完再放下一个」提到基类(BuildAnimations + PlayAsync), 转盘那两段写的是同一个属性,必须串行。 二、老虎机「候选只剩唯一」不再假转 候选字只剩唯一一种可能时(名单「张三、李四」,第一格定下「张」后第二格只剩「三」), 再转也是同一个字一闪一闪,像卡住。这些格子现在不占时间, 并且**等到真正在转的格子全部停稳的那一刻才一起亮**—— 一开场就显示会把答案剧透(第一格还在滚就知道是谁了)。 从某一格起往后全是这种格子时,动画提前收尾,比配置的时长短。 SlotPlan 新增 SpinSlotCount,动作时长 = 每格 × 要转的格数。 三、CSGO 整块背景跟着品质走 滚动时用指针下那一块的装饰品质,底色一路换色;停稳后换成中选者真正的品质并加深。 和方块用的是同一条规则,不提前剧透。 测试:282 通过 / 0 失败。 - 新增四种样式统一的时间不变量 theory(总时长=动作+定格、各段 Duration 之和=总时长、 关键帧最大时刻=MotionTotal、cue 全在 0~1、每段 Duration>0) - 新增「唯一候选那格不许开场就亮」——防剧透 - 新增老虎机 SpinSlotCount 的三条 - MotionTotal 改用 TimeSpan 直接相减:先化成秒再算会差出几微秒, 「总时长-动作时长==定格时长」会变成 0.8000001,看着没事其实已经不等了 - 老虎机的动作结束时刻改成从自己的排片数据算(每格 × 要转的格数), 不依赖「总时长里含不含定格」——直接构造的排片表没有那一段 两个 .cipx 已重打。 Co-Authored-By: Claude Code <noreply@anthropic.com>
原来的换字方式有两个毛病,看起来都是「卡住不动然后啪地跳成答案」: 一是候选只有两个字时(名单「张三、李四」的第一格),选字的游标每轮被 加了两次,下标奇偶性因此不变,每次都命中同一个字、每次都跳过它—— 整条序列退化成「同一个字重复 11 遍 + 结果」。 二是用 floor(easeOutCubic(进度) × (字数-1)) 选字,而缓动曲线只在进度 恰好等于 1 时才取到 1:结果那个字停留下来的时间是零,倒数第二个字却 从进度 0.55 一直霸占到结束(占整格 45%)。 改成等比递增的时间表(和滚动名字同一套 ScrollTicks,两个样式节奏一 致),并把「某一格该显示哪个字」抽成 ShownCharAt,渲染那条路走它, 这样错误改回来时测试真的会红。 证伪过:把两处旧写法分别塞回去重跑,SpinSequence_WithTwoCandidates_ ShowsBothCharacters、SpinSequence_CyclesAllCandidatesAndKeepsTheWinner OffTheSecondLastSlot、ShownCharAt_SpinsThroughBothCharactersAndLands TheWinnerEarlyEnough 三条全红,恢复后 298 项全过。 Co-Authored-By: Claude Code <noreply@anthropic.com>
「总时长 = 每格时长 × 格数」在加了「候选只剩唯一的格子不占时间、 从某一格起往后全唯一就提前收尾」之后就不准了,改成 × 需要转的格数。 Co-Authored-By: Claude Code <noreply@anthropic.com>
主人这一轮提了四条,加上中途补的一条,一共五处。 一、CSGO 的减速过程回来了(主人:前几次 commit 把动画过程破坏了) 读 Avalonia 源码确认了机理:AnimationInstance 先算 easedTime = Easing.Ease(t), 再拿这个<b>缓动后的时间</b>当坐标去关键帧的 cue 上插值。 之前「把定格并进 Duration」那批改动把 CSGO 的终点从总时长挪到了「总时长 - 定格」, 终点 cue 因此从 1.0 掉到 0.8;而 CubicEaseOut 在 0.8 处已经走完了 99%(1-0.2³), 滚动于是在动画的第 41% 就冲完了,后面一大截全是干等——可辨认的减速被整个挤没。 修法:定格从动画时长里挪出去,动画时长 = 动作时长,终点 cue 回到 1.0; 定格改由基类在播完之后原地等(FillMode.Forward 在放下绑定时会把终值写成局部值, 画面停得住)。四个样式一起改,定格时长统一从排片表抄——Load 拆成模板方法, 「抄」这件事只有一处写法,不会再有哪个样式被漏掉。 二、滚动名字改成按名单顺序滚(主人:抽中的人后面也有名字;参考 csgo 的滚动机制) 原来每一帧从「其他人」里随机挑,名字在乱跳;换帧间隔明明在等比变长, 画面却看不出节奏。现在帧序就是名单循环展开的一段(和 CSGO 拼轨道同一个思路), 中选者排在末帧,他前面依次是名单里排在他前面的那些人。 三、滚动名字的末帧停留被倒数第二帧占掉了(主人:他最后变慢的过程似乎消失了) 写关键帧时把「先累加再取值」的顺序弄反了,每一帧占用的都是下一帧的时长。 中选者那格本该停最久(等比数列末项,整段最长的一格),却被倒数第二个名字占去, 他自己一出现就进定格。改成先取值再累加,每帧拿回自己的时长。 (顺带删掉那个和循环首帧完全重复的起点关键帧——两个 cue 相同的帧会让插值除以零。) 四、CSGO 的指针不再每次都停在名字正中(主人:真实情况下停在中间概率很低) 停点加上随机偏移,取方块内任意位置(上限 45% 个方块宽,留 5% 余量给圆整); 指针底下仍然是中选者那一块,排片表自带的反查断言继续兜着。 五、转盘高亮改成始终跟随指针(主人补的:回转时会有剧透) 原来点亮的是「中选者那一格」加一个「停稳了没有」的判断, 而第二段的滑动方向是两种之一:其中一种会让指针先落在中选者旁边那一格上再滑进来, 那段时间里指针还没到、中选者却已经亮了,答案提前泄露。 现在高亮恒等于指针指着的那一格,任何角度下都看不出额外信息。 测试:307 通过 / 0 失败(新增 8 条)。 证伪过,每一项都把 bug 塞回去确认变红—— 「Duration 撑回含定格」→ Duration 断言红; 「终点关键帧提前 0.1 秒」→ cue 算出 0.975 而不是 1,红; 「停点固定居中」→ 停点不再变化,红; 「末帧不钉成中选者」→ 帧序断言红; 「关键帧时刻错位」→ 时间不变量红; 「高亮恒为中选者」→ 转盘那条红; 「定格不抄排片表」→ 四条 theory 红。 Co-Authored-By: Claude Code <noreply@anthropic.com>
主人这一轮两件事。
一、CSGO 的画面
铺满整个舞台、还跟着品质一路换色的那层背景是上一批改动加的,主人要求撤掉:
滚动时整屏一闪一闪,把真正要看的那排方块压住了。现在只留方块自己的底色。
另外方块底色往品质色上偏的比例从三成提到六成(停稳那块),滚动中的从一成四提到三成——
原来五个方块并排几乎都是一样的深灰,扫一眼认不出哪块是什么品质。
(背景撤没撤有测试盯着:舞台四角必须全透明,加回来就红。)
二、开机后悬浮钮跑到左上角
主人给的线索是决定性的:「开机后位置不对,但只重启 ClassIsland 位置就正常」。
这说明存下来的坐标是对的,坏的是开机那一次的恢复过程。链条有两环:
① 开机那几秒屏幕信息还没就绪,自动摆位一次都成功不了,窗口停在系统给的 (0,0);
② 退出时 CapturePosition 把 (0,0) 当成「主人的位置」存进了配置——
下次开机读到 (0,0)、继续存 (0,0),就此卡死在左上角,自己永远好不了。
单独重启 ClassIsland 时屏幕信息立刻可用,第一环不成立,所以看不出问题。
改了三处:
- CapturePosition 加了「位置被确认过才允许写」:自动摆位成功、或者主人亲手拖过,
这两种情况才写盘。其余时候(窗口还停在 (0,0))一个字都不写——
不记只是这一次开机位置不对,记错了就是下一次也一起坏。
- RestorePosition 改成「先摆位置、再判分辨率」:记下来的坐标是上次存好的正确值,
先摆上去,主人立刻看到的就是对的位置,不必等屏幕信息。之后再拿屏幕信息判
「记位置时那块屏幕还在不在」,判出分辨率真变过了才改摆默认角落。
反过来的话,屏幕信息迟到多久窗口就停在 (0,0) 多久。
- ClampToScreen 在摆位阶段结束前一律不干活:那段时间拿到的屏幕信息可能是
「非空但尺寸不对」的占位矩形,拿它夹取会把还没恢复好的位置钉到假屏幕上去。
顺带做了主人要的「分辨率变化时重置位置」:配置新增 WindowScreenWidth/Height,
记下位置时那块屏幕的尺寸;判据是「当前所有屏幕里还有没有一块尺寸相同的」——
不是「当前这块是不是一样」,后者在双屏下会误判(窗口摆在副屏上,开机那一瞬间
窗口还在原点,按当前屏幕算会拿主屏的尺寸去比副屏记下的值)。
另外加了一条自愈:坐标正好是 (0,0) 又从来没记过屏幕尺寸,那是老版本留下的脏数据,
直接恢复成默认位置——主人这次升级后开机两秒内就会自己摆好,不用手动拖一次。
测试:315 通过 / 0 失败(新增 8 条)。
证伪过,每一项都把 bug 塞回去确认变红——
「分辨率判定永远说屏幕还在」→ 重置那条红;
「存盘不设防」→ 「摆位没好就不许写」红;
「摆位成功不标记可信」→ 「摆完才允许写」红;
「把整块背景加回来」→ 舞台四角 alpha 从 0 变 255,红。
Co-Authored-By: Claude Code <noreply@anthropic.com>
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.
这个 PR 做了什么
在原作者 GordonYoung 的 RandomPicker 基础上做了一批二次修改,主要内容是新增抽选动画,
以及把可离线验证的逻辑抽出来、补上单元测试。共 18 个提交。
版本从 1.1.x 提到 1.3.0.0。
1. 抽选动画(新功能)
2. 结构与可测性
FinishReveal;忙碌态从散落的布尔量改成分类型的枚举3. 单元测试(新增)
新增 xUnit 测试工程
tests/RandomPicker.Tests,覆盖:转盘的指针归属与边界滑动、宿主动画开关的降级判据
界面层这一层专门挡编译期完全不设防的问题:过渡/动画漏写
Property、自绘控件忘覆写MeasureOverride导致量出 0×0、几何算成 NaN 让整帧空白——这些都只在「控件真的被建出来」的那一刻现形,曾经的崩溃 bug 就是这么漏过去的。
4. 修复
而退出时的
CapturePosition把 (0,0) 当成真实位置写进了配置,此后永久卡死。改为「位置被确认过(自动摆位成功或用户手动拖过)才允许写盘」
(0,0)脏数据(开机后自愈)Duration导致整段不播测试情况
结果:315 通过 / 0 失败
(注意:测试工程必须从 ClassIsland 源码树这一侧发起编译,直接对着插件仓库的真实路径运行会
因为
..\..\ClassIsland.Core解析不到而失败,原因和主项目只能在源码树里编译是同一个。详见 README「开发 → 测试」。)
相机 I/O 与模型真实推理不在自动化测试范围内,需要真实摄像头,只能在应用内
「设置 → 随机抽选 → 试拍一次」人工验证。
兼容性说明
原仓库的
id、name保持不变,因此本插件与上游插件不能同时安装,装之前需要先卸载原版。如果上游希望改掉这个约束,我可以再调整。
关于合并
改动跨度较大(18 个提交),如果维护者更希望拆成多个小 PR(例如「抽选动画」与「修复」分开、
或先合测试再加功能),我可以重新组织提交,拆成若干独立 PR。
🤖 Generated with Claude Code