Files
Jia Suiandfanlusky 3b1206ea82 fix(windows,engine): 提交键回复等待不再因超时重置会话或丢字 (#1019)
* fix(windows,engine): 提交键回复等待不再因超时重置会话或丢字

服务端按键处理偶发数百毫秒尖峰时,提交键(空格顶首选 / 确定选词 / 标点顶高亮候选)
的回复等待会走到会话内那条 300ms 同步读;一超时就被判成 TransportUnavailable
→ DeliveryAmbiguous → Transport 完整重置(丢键 + 吞队列 + 清组合 + 轮换 epoch),
用户看到的是整段已输入内容消失。

- 删除 FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS 那条破坏性兜底读。会话侧改为非阻塞探测,
  拿不到回复返回「未就绪」,由 session 自身在 DoEditSession 内交回,沿用同一个
  FANY_IME_COMMIT_REPLY_TIMEOUT_MS 分片轮询窗口,不重开窗口、不新造等待原语。
- 交回必须发生在 session 内部:RequestEditSession 用 TF_ES_ASYNCDONTCARE,TSF 异步授权
  时调用方读不到 session 的 HRESULT;若靠调用方读返回值,该键会既不丢也不提交,
  deferred 闸门再不打开(后续按键全部只入队)。也不能改用 TF_ES_SYNC(WinWord 上不成立)。
- 真失败(写失败 / 读失败 / 坏帧 / 字节数不符 / 无 request id / 无管道)仍判 Transport 重置。
- 预取覆盖面扩到 candidate 类 finalize(空格顶首选等),堵住原先只能靠那条 300ms
  兜底读的路径。
- 新增按键级延迟诊断 bench_key_latency_diag(build 里有同名 exe 但源码已不在树),
  以及两份对照测试:commit_reply_not_ready(回复三态与交回状态机)、
  candidate_consistency(同配置下候选集合/顺序/分页逐字一致,引擎侧护栏)。

实测(bench_key_latency_diag):稳态逐键 median 0.6~0.8ms / max 12.5ms,尖峰指向内存
压力下的页回收而非稳态算力。在「结果完全不变」的约束下,预热 / 缓存 / 投机预解码均无效
或反向,故本次不改引擎打分路径。

* fix(windows): 直接标点提交会话补齐未就绪交回,并写准窗口沿用边界

审查 `7aca5e5d` 发现的问题:

- `CPunctuationCommitEditSession` 在 `FANY_S_COMMIT_REPLY_NOT_READY` 时只抑制了 token
  结算(`applied` / `_FailDeferredKey` 都不走),**没有**像 `CKeyHandlerEditSession` 那样把
  请求交回 `_PollCommitReply`。一旦走到这条分支——`_HandleCompositionPunctuation` 的高亮候选
  分支要求 `_candidateMode != CANDIDATE_NONE`,而派发该会话的条件是
  `_candidateMode == CANDIDATE_NONE`,两者在 session 执行时才求值——deferred token 会永久留在
  飞行中,后续所有按键只入队不处理,正是本 PR 在别处修掉的失效模式。现在两处都交回,
  并把原因写进注释,避免下一个人再照半个写法抄一遍。
- `_HandOffUnreadyCommitReply` 的两处注释与实现不符或不够具体:交回沿用第一片已经开始的窗口;
  只有从未开窗的路径(标点处理函数在派发时高亮候选条件为假、session 执行时条件成立)才在此
  开窗,那种情况键一点没等过、`waitedMs` 为 0。另一支(无法保活 → `_FailUnansweredCommitKey`)
  只在请求不属于协作轮询时到达,实务上即 `commitReplyMessage == 0`:派发没能用消息窗,
  没有可交回的对象。

签名相应扩展:`_RequestDirectPunctuationEditSession` 新增 `commitReplyMessage` /
`commitReplyDeadlineTick`,`CPunctuationCommitEditSession` 携带它们。没有行为回退,两个新参数
默认 0 时与改动前一致(而那正是「无消息窗」的既有降级路径)。

验证:windows x64 Release 构建通过,`ctest` 17/17 通过,`clang-format 18.1.8 --dry-run
--Werror --style=file` 对本次涉及文件全过。

* fix(windows): 把 _HandOffUnreadyCommitReply 移到公共区,修 CI 编译失败

CI 的 Windows x64 与 Windows Win32 两条腿都挂在同一处编译错误:

  MetasequoiaIME_AsyncRequests.cpp: error C2248:
  “CMetasequoiaIME::_HandOffUnreadyCommitReply”: 无法访问 private 成员

上一提交给 CPunctuationCommitEditSession 补「未就绪交回」时调用了这个成员,但它在 private
区。CKeyHandlerEditSession 能调是因为它在 MetasequoiaIME.h 里被显式声明为 friend;而
CPunctuationCommitEditSession 定义在 MetasequoiaIME_AsyncRequests.cpp 的匿名命名空间里
(该文件 31-254 行),头文件里的 friend 声明指不到它,加 friend 也无效。

处置:把该声明移到公共区,与同样由 session 调用的 _FailDeferredKey、_CompleteDeferredKeyReplay、
_HandleCompositionPunctuation 并列,并在注释里写明「它必须公共」的原因,免得下次有人顺手挪回私区。
仅可见性与注释,行为零变化。

验证(triplet 与 CI 一致,且这次看退出码、不截断构建输出):
- x64-windows-static:cmake --build build --config Release --parallel 退出码 0;
  ctest --no-tests=error 17/17。
- x86-windows-static:cmake --build build32 --config Release --parallel 退出码 0;
  ctest --no-tests=error 17/17。
- clang-format 18.1.8 --dry-run --Werror --style=file 对本次改动文件通过。

另:上一轮「构建通过」是我误报——当时用 Select-String 过滤构建输出又加了 -Last 8,错误行被截掉,
且没有检查 $LASTEXITCODE;ctest 过的是测试目标,与 DLL 编译失败并不冲突。

* fix(windows): 提交回复交回后轮询不再停摆,同步兜底保留有界等待

- 轮询定时器按请求 id 判断是否仍在等待:session 交回时换了新 token,
  按旧 token 查会误判为已结算并 KillTimer,交回的请求从此无人再分发。
- 没有轮询可交回的 session(同步 _InvokeKeyHandler 路径)恢复
  FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS 的单次有界等待,不再一读不到就丢键重置。
- 确定并输入在提交回复为传输失败时不再继续键入字符并返回 S_OK。
- session 交回占用新槽位时同样受 kMaxPendingAsyncKeys 约束。
- 修正注释:未就绪由 session 自己的完成钩子交回,不是消息处理函数。
- 测试:覆盖轮询分发、等待上限与满表交回;SAL 剥离按最长前缀匹配;
  引擎一致性用例③不再清缓存,否则测不到跨输入的缓存泄漏。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-17 16:19:26 +08:00

18 KiB
Raw Permalink Blame History

AGENTS.md — windows

组织级约定见 组织 AGENTS.md,仓库地图、组件边界和发布流程见仓库根 AGENTS.md。本文件只规定 TSF 前端本身:COM / 焦点 / 线程、注册和本目录的构建。路径都相对于 windows/。

本目录地图

位置 职责
src/IME/ TSF 文本服务主体、激活/停用、消息窗口、后台 IPC 与主题监听生命周期
src/Key/ ITfKeyEventSink 按键预判、异步按键队列与 edit session 分发
src/Composition/ 预编辑缓冲、光标、按键表、preserved key、输入模式与标点/全半角状态
src/Edit/ TSF edit session、文本范围和 composition 写入
src/UI/、src/Candidate/ TSF 候选 UI element、候选状态、候选提交与布局通知;可见候选窗主要由 Server 绘制
src/IPC/ Main / ToTsf / Worker / Aux Named Pipe 协议、握手、请求配对和重连状态
src/Thread/、src/Tf/ 焦点、context stack、文本布局等 TSF sink
src/Compartment/、src/LanguageBar/ 中英文、标点、全半角 compartment 与语言栏按钮
src/Register/、src/DisplayAttribute/ COM/TSF 注册及预编辑显示属性
src/Global/、src/Header/、src/Utils/ 全局状态、资源/常量、COM 与 Win32 工具
src/DllMain.cpp、src/Server.cpp DLL 入口、class factory、引用计数和注册导出
image/、src/IME/MetasequoiaIME.rc DLL 内嵌图标与资源
scripts/ 环境生成、Debug/Release 双架构构建、注册和签名脚本

核心输入链路

宿主按键
  → OnTestKeyDown / _IsKeyEaten(同步决定是否吃键)
  → OnKeyDown / _DispatchKeyDown
  → Main Pipe 发给 Server
  → request_id 对应的回复或异步 Worker Pipe 消息
  → 消息窗口回到 TSF 所属线程
  → ITfEditSession 修改 composition / 提交文本
  → TSF UI element 与 Server 候选窗同步

查问题时先沿这条链路定位。按键判定主要看 src/Key/KeyEventSink.cpp(延迟按键队列与失败处理在 KeyEventSink_DeferredKeys.cpp,失败分类在 DeferredKeyFailurePolicy.h,输入模式热键在 KeyEventSink_Hotkeys.cpp),编辑行为看 src/Key/KeyHandler.cpp 与 src/Key/KeyStateCategory.cpp,协议与连接状态看 src/IPC/Ipc.*, 激活、焦点和工作线程生命周期看 src/IME/MetasequoiaIME*(Worker Pipe 读线程在 MetasequoiaIME_IpcWorker.cpp,消息窗口与主题监听在 MetasequoiaIME_WindowProc.cpp, 按键路径发起的 edit session 与异步按键请求表在 MetasequoiaIME_AsyncRequests.cpp)与 src/Thread/。

TSF / COM 硬约定

  • Test 与实际处理必须一致:OnTestKeyDown 决定宿主是否还能收到按键。一旦返回“吃掉”,后续 IPC 超时或处理分支不能再把该键交还宿主。新增按键或模式时,同时检查 fresh composition、普通 composition、候选态、离线 fallback 和 _DispatchKeyDown 的实际分支,避免丢键或重复上屏。
  • 文档修改只在 edit session 内完成:composition 的开始、更新、提交和删除必须通过 ITfContext::RequestEditSession 获得有效 cookie;后台线程和窗口回调不得直接写 TSF range。
  • TSF 对象遵守 apartment 与线程归属:不要把 ITfContext、ITfComposition、presenter 等 COM 对象交给后台线程直接调用。跨线程结果通过现有隐藏消息窗口、PostMessage 和队列回到 owner 线程;同步 fallback 只能沿用已有且已验证的路径。
  • 异步回调必须验证所属会话:延迟按键和 Server 回复可能晚于失焦、composition 结束或 DLL 停用。应用前核对现有的 focus token / generation、composition epoch、local reset token、 延迟按键的派发 token(代码里仍叫 deferredReplayToken);过期结果应丢弃,不能作用到新会话。
  • 引用与 teardown 成对:新增 COM 接口、sink、HANDLE、线程、窗口或 presenter 时,在所有失败 分支与停用路径补齐 Release、Unadvise、关闭和 join。不要在析构/停用后留下会访问 this 的 posted message 或 watcher。
  • DllMain 保持最小化:它处于 loader lock 下,禁止在其中连接管道、启动 Server、创建线程、 等待锁或做复杂初始化。重活放到文本服务激活后的既有生命周期中。
  • DLL 会注入第三方宿主进程:保持静态 CRT(MultiThreaded[Debug])与 /Zc:threadSafeInit- 约定,避免和 QQ、浏览器等宿主的 CRT/TLS 冲突;不要随意增加全局构造器、 进程级 hook 或修改宿主状态。

IPC 协议——单一契约

协议定义的唯一来源是 ../engine/contracts/,Server 读的是同一份——引擎已经是本仓的目录,两侧由同一个提交钉住,不可能不一致。TSF 仅引用头文件,不链接引擎运行库。这里保留已发布的固定宽度 Win32 ABI:

  • 修改管道名、opcode、字段、容量或对齐时,在 ../engine/contracts/ 中修改并保留 ABI 检查,两侧的改动落在同一个 PR 里。禁止恢复两份手写定义。
  • 主连接的版本化 ClientHello 必须获得关联 request_id 的 ProtocolReady 后才可发键;协议确认包不能作为候选或上屏文本。新 Server 兼容旧 DLL 的未版本化 hello;新 DLL 对旧或不兼容 Server 使用现有原始输入回退。版本与能力规则见共享 contracts/README.md。
  • WCHAR 按 16 位 UTF-16 码元传输;不要用 wchar_t 在非 Windows 平台上的大小推断协议布局。
  • 已发布 opcode 只追加、不复用、不重排。未知的较新 Worker opcode 应忽略,不能因此拆除连接。
  • request_id == 0 表示 unsolicited,UINT64_MAX 表示本地无请求;不得当成普通请求回复配对。
  • PipeReady 是反向管道握手帧,只在注册阶段消费;Worker handle 在握手完成前不能发布,Main pipe 也不能抢先进入可用状态。
  • Server 是候选选择、翻页与配置化导航的权威。TSF 本地影子状态只用于预判,不能在回复 到达后反向覆盖 Server 已更新的候选状态。
  • DefinitelyNotSent 与 DeliveryAmbiguous 都不重发:后者可能已被 Server 接收,重发会双写; 前者也没有回放可走,按传输失败丢键。TransportUnavailable 是本地控制结果,绝不能当候选文本提交。
  • 改协议后至少构建 TSF 的 x64/x86,并运行 Server 侧的 IPC 协议测试 tests/src/test_ipc_protocol_constants.cpp 所属测试目标。

按键失败:不回放

被吃掉的键要么被处理,要么被有意吞掉;绝不交还宿主,也绝不处理两次。会话在组合中途出错时 清掉组合、从空开始,失败那一刻已排队的键一并吞掉(它们是按即将丢弃的组合分类的)。不要 重新引入按键回放或恢复前缀:回放带来过闪烁、双上屏和重试循环(#259、#404、#427)。

失败经 _FailDeferredKey(有在飞的键)或 _ResetSessionAfterFailure(没有)处理,由 DeferredKeyFailurePolicy.h 决定代价。新增失败点时选对原因走这两个入口,不要单独调用 MarkNamedpipeSessionDirty*——那会漏掉清队列,也会把管道没坏的问题升级成完整重置:

类别 原因 动作
Stale focus token、组合纪元、reset token 或焦点代已被取代 只丢这颗键,不再发起重置
Resync 宿主拒绝 edit session(S_FALSE、E_NOTIMPL 等)、RequestEditSession 失败、异步投递失败,以及 OnEndEdit 组合属性检查、文档不匹配等非传输问题 丢键、吞队列、本地取消组合;不轮换 token、不关管道,闸门重开时在同一 token 上发 HideCandidateWnd 清 Server 组合
Transport 写失败、回复读失败/坏帧、TransportUnavailable、DefinitelyNotSent、DeliveryAmbiguous、提交回复 1500 ms 窗口耗尽、已在等回复的提交键在同一焦点下被组合纪元取代、worker 断开 丢键、吞队列、完整重置(新 focus token、重开回复管道)
Offline 离线原始通道里的宿主失败 丢键、吞队列,保留本地组合(此时它是唯一权威)

翻页与高亮移动键(PageUp/PageDown/Up/Down/Tab)的回复只是确认,缺失是软错过,不拆管道、不清 队列;其余候选键可能上屏,保留「歧义即丢键 + 传输重置」。提交回复的总窗口是 FANY_IME_COMMIT_REPLY_TIMEOUT_MS(1500 ms),但等待跑在宿主 UI 线程上,不能一次阻塞读完: 消息处理函数用 _PollCommitReply 做不阻塞的轮询,没到就把请求放回表里、由 TIMER_COMMIT_REPLY_POLL 唤醒再试,并按请求 id 顺序结算。会进 edit session 的提交路径 (确定、数字选词、标点顶高亮候选、空格顶首选)一律先经消息处理函数取回复、按请求 id 缓存, session 直接消费;session 自己那次读(TryReadCommitReplyFromServerPipe)只剩「缓存被清掉」 的竞态,它不阻塞、也不再把超时当传输失败——读不到就返回 FANY_S_COMMIT_REPLY_NOT_READY, 并由 session 自己在完成钩子里把请求交回 _PollCommitReply(窗口仍是同一个 1500 ms, _HandOffUnreadyCommitReply)。没有轮询可交回的 session(消息窗口不可用、按键直接走同步 _InvokeKeyHandler 的路径)不能让出,仍保留 FANY_IME_COMMIT_REPLY_SESSION_WAIT_MS(300 ms)的 单次有界等待,超时按传输失败处理;由 _SessionCommitReplyWaitMs 按 session 记录的轮询消息判定。 交回必须发生在 session 侧:RequestEditSession 用的是 TF_ES_ASYNCDONTCARE,TSF 可能把 session 排到之后执行,那时消息处理函数早已返回、根本看不到 这个结果,只有 session 自己的钩子在两种情形下都会跑。新增会上屏的异步路径必须接入同一套轮询, 不要在 session 里阻塞等回复,不要把「结果要靠调用方从 _InvokeKeyHandler 的返回值里读」 当作契约,也不要让 session 的超时触发会话重置。

焦点、候选与离线回退

  • TSF 激活/停用与窗口瞬时失焦不是同一事件。切换到别的输入法使用 ClientDeactivated;宿主内部 的临时焦点变化使用 suspended/reset 流程。Excel 单元格编辑、浏览器和 Windows Text Input Host 对焦点的处理不同,修改时都要回归。
  • 候选 presenter teardown 时要先隐藏并清除当前 Server 会话,同时防止旧 cleanup 消息清掉 新 composition 的候选;保留现有 session-active 与 async-cleanup 防护。
  • Server 不可用时只能提交 TSF 已确定拥有的本地原始输入。没有候选权威时不要猜测候选文本;服务 恢复后先完成 focus session 激活/reset,再排空 deferred keys。
  • CN→EN、标点和全半角切换同时涉及 preserved key、TSF compartment、语言栏和 Server 状态。 composition 中切换模式时须保持“先正确提交,再应用 compartment”的既有顺序,避免宿主双提交。
  • raw / pinyin / empty 预编辑样式定义在 src/Global/FanyDefines.h;新增样式时同步检查配置读取、 caret 映射和 Server 返回的 Preedit 消息,不能只改显示字符串。

资源、注册与日志

  • CLSID、profile GUID、compartment GUID、语言栏 GUID 与注册表路径属于持久身份。除非明确进行迁移, 不要重新生成或改写;改动时同时核对 Register/、Globals.*、.rc 与卸载路径。
  • 新增/替换图标时同步资源 ID、.rc、CMakeLists.txt 和 LanguageBar 的索引;浅色/深色图标应成对。
  • 语言配置(profile)图标:安装器把 image/msime.ico 装成 DLL 上一级目录的 MetasequoiaImeTip.ico, RegisterProfiles 发现它就注册这个固定路径,否则退回 DLL 内嵌的 IDIS_METASEQUOIAIME。不要改回总是 注册 DLL 路径:版本目录每次升级都变,任务栏会在换目录时取图标失败,改显示「简体」直到资源管理器重启。
  • 安装器把 DLL 装到 System32\IME\MetasequoiaIME\msime_v*\(x86 在 SysWOW64 下同名目录),不要挪回 Program Files:CS2 等开启 Trusted Mode 的游戏只放行「位于系统目录且有代码签名」的第三方 in-proc DLL, 否则输入法在游戏里根本切不出来。用 linstall.ps1 从构建目录注册的开发版不满足这一条,游戏内不可用。
  • 语言配置的登记名称不总是 TEXTSERVICE_DESC:设置页「游戏兼容」会把它换成 CS2 / Dota 2 白名单里的名字 (游戏按登记名称决定要不要自己处理输入法消息,不在名单里就弹系统默认组合窗,独占全屏下会把游戏踢出全屏)。 选择记在 HKLM\Software\Metasequoia\MetasequoiaIME\GameCompatProfileName,RegisterProfiles 注册时以它为准—— 安装/升级重新注册会覆盖名称,不读它用户的选择就静默丢了。开启时 ActivateEx 也不再对这些游戏忽略 UI-less 声明。
  • 生产日志不得记录用户原始按键、预编辑、候选词或提交文本。性能与 IPC 诊断记录耗时、请求 ID、 token、状态和错误码即可;调试日志也应尽量避免持久化用户输入。

生成文件与配置真源

python .\scripts\prepare_env.py 生成本机 .clangd,并只更新 CMakePresets.json 中的 vcpkg 路径。CMakeLists.txt 和 preset 结构直接维护;脚本不会用旧模板覆盖它们。CMakeUserPresets.json 中的架构配置继续继承这份 preset。

  • 不提交生成的新个人绝对路径或 build* 等产物。
  • 修改 .clangd include 列表时,同时检查模板和生成脚本。
  • CI 在临时合仓目录实际运行环境脚本,验证 CMake 源文件、Release preset 和用户字段不会丢失。

代码风格

  • 使用 C++17、Windows Unicode API、SAL 标注和项目现有的 4 空格 Microsoft 风格。
  • 格式化遵循本目录自己的 .clang-format——仓库没有根目录 .clang-format,每个组件子树各有一份、规则互不相同;本目录这份是 Microsoft 风格、明确 SortIncludes: false,不要自动重排 include。
  • 修改 .cpp / .h 后对本次涉及的文件运行:
clang-format -i .\src\Path\Changed.cpp .\src\Path\Changed.h
  • COM 出参失败时置空;检查 HRESULT / Win32 返回值;所有权不明确时优先沿用项目现有 SafeRelease、RAII 或 teardown 模式,不要混用不兼容的智能指针约定。
  • 不做与任务无关的大范围格式化或命名重构。TSF 状态机对时序敏感,小而可验证的改动优先。

注意:新增的代码尽量保持这个风格就可以了,不要去尝试格式化原有的代码。不严格要求新增代码保持这个风格。

构建与验证

环境:Windows、Visual Studio 2026、CMake 3.25+、vcpkg、Python 3.10+;首次准备:

python .\scripts\prepare_env.py

Debug:

.\scripts\lcompile.ps1 64
.\scripts\lcompile.ps1 32

Release:

.\scripts\lcompile-release.ps1 64
.\scripts\lcompile-release.ps1 32

本目录的 windows_ipc_contract 在 x64/x86 上验证协议布局、版本握手与语音分帧;CI 还构建 Server 和 GUI 框架,并在锁定词库上运行 Server 测试。这些检查不能替代真实 TSF 宿主行为回归。按改动范围做手工回归:

  • 至少验证 x64 宿主;协议、注册、资源或发布改动同时验证 x86 DLL;
  • 输入首键、连续输入、空格/数字键盘选词、翻页、退格、光标移动和标点提交;
  • composition 中与空 composition 下切换中英文、标点、全半角;
  • 失焦/回焦、切换输入法、Server 未启动/重启/断管恢复;
  • 涉及兼容性时覆盖 Win32 EDIT、Excel、Chromium 系浏览器及 UWP/现代 TSF 宿主。

本地注册脚本会执行提权后的 regsvr32,属于系统状态修改,不应作为普通验证自动运行。只有用户明确 要求安装/卸载时才执行:

.\scripts\linstall.ps1
.\scripts\luninstall.ps1

签名脚本包含本机 Windows SDK 路径和证书占位符;没有对应证书时不要运行或提交真实 thumbprint。

需要生成可安装包做本地测试时,从仓库根目录开始,严格按以下顺序执行。任一步失败都先 停止并修复,不要继续使用旧产物打包:

Set-Location .\windows
.\scripts\lcompile-release-both.ps1

Set-Location ..\server
.\scripts\lcompile-release.ps1

Set-Location ..\installer
.\Prepare-PackageFiles.ps1 -RepoRoot .. -TsfDirectory windows -ServerDirectory server -NoticesDirectory .
.\Sign-PackageBinaries-Local.ps1
.\Compile-Installer.ps1

Prepare-PackageFiles.ps1 会收集 Server Release、x86/x64 TSF DLL、词库和前端资源;因此必须在两个 C++ 组件编译完成且相关前端/词库产物已准备好之后运行。Sign-PackageBinaries-Local.ps1 使用本机 测试证书,只用于内部安装验证;它可能创建证书并尝试写入本机受信任存储,不得把本机证书指纹或 私钥提交到仓库。Compile-Installer.ps1 会调用 Inno Setup 命令行编译器 ISCC.exe 编译同目录的 msime_setup.iss,安装包输出到该版本目录的 Output\。机器上须预先安装 Inno Setup 6.6+;若脚本 无法自动找到编译器,可显式传入:

.\Compile-Installer.ps1 -IsccPath 'C:\Program Files\Inno Setup 7\ISCC.exe'

提交纪律

一次改动经常同时落在 windows/、server/ 和 engine/,现在它们在同一次提交里。提交前执行 git status --short,只暂存本任务涉及的显式路径,禁止 git add -A / git add . 把构建产物或其他 会话改动卷入提交。

提交信息沿用项目现有工程风格,推荐 type(scope): 摘要。不要添加 Co-Authored-By、 Generated with 或其他 AI 生成标记。