全站留档库
所有留档条目的唯一索引,跨周汇总。 按大类(需求 / Bug / 经验总结 / 问题复盘)和问题类型筛选。 新条目一律先在这里登记,再写进当周周报。
累计留档27 条
需求14 条
Bug12 条
经验总结1 条
问题复盘0 条
最近更新2026-09-23
1. 分类口径
第一层永远是大类——周报里也是按这四个大类分段的,编号跟着大类走: W{周}-R{序} 需求 · -B{序} Bug · -E{序} 经验 · -P{序} 复盘。
| 大类 | 编号前缀 | 收什么 | 小类怎么分 |
|---|---|---|---|
| 1 需求 | R | 新功能 / 改造 / 配置支持 | 不分小类,直接 1. 2. 3. 排 |
| 2 Bug | B | 线上 / QA 报的问题,一个 bug 一条 | 不分小类,直接 1. 2. 3. 排 |
| 3 经验总结 | E | 可复用的手法,下次直接抄 | 不分小类,直接 1. 2. 3. 排 |
| 4 问题复盘 | P | 我哪一步做错了 + 流程怎么改 | 不分小类,直接 1. 2. 3. 排 |
Bug 和复盘的区别
Bug 写代码/配置错在哪,复盘写我的排查过程错在哪。
Bug 写清「配置摆错了」,复盘写「我为什么绕了三轮才查到配置」。
复盘只写有外部代价的那种,自己纠正回来的排查抖动不立条。
Bug 类再按问题类型打标签:
| 维度 | 取值 | 判定口径 |
|---|---|---|
| 问题类型 | 配置摆放 | Entity 位置、范围盒、表格数值填错 |
| 时序竞态 | 偶现、跟操作顺序有关、流送/加载相关 | |
| 状态残留 | 标志位漏复位、回调被覆盖、句柄复用 | |
| 语义分歧 | 两侧对同一字段理解不同、双份实现只改一份 | |
| 日志盲区 | 关键路径没日志,导致排查只能靠反推 | |
| 注释误导 | 注释与实现相反或过期 |
篇幅跟投入成正比,别把半天的活写成半个月的量
一条 bug 的深度块最多两个(复现时序 / 罪魁配置),卡片本身七八个单元封顶。
砍的顺序:① 截图没落盘就别留占位块;② 根因链能写进「原因」单元就别单开 flow;
③「关键证据」表和「总结」重复就只留总结;④ 需求卡里的实现步骤拆解,
能压成「总结」一段就压成一段。
判断:这条花了多久?净耗时不到一天的,整条(含深度块)控制在一屏半以内。
判断:这条花了多久?净耗时不到一天的,整条(含深度块)控制在一屏半以内。
速览表不出状态列
状态在每张卡的徽章上,速览表再抄一遍,整列全是「可复用 / 已复盘」,没有信息量。
速览表只留大类 / 序号 / 一句话三列。
小问题可以不总结、不复盘
经验和复盘不是每条 bug 都要配一份,凑数写出来的比不写更糟——把真正值钱的两条淹了。
经验只收别处没写过的:已经写进需求 / Bug 卡「总结」单元的那句话,不要在经验区再开一条, 那是同一句话抄两遍。判据:把这条经验删掉,读者会不会漏掉信息?不会 → 删。
复盘只收有外部代价的:别人白等、白试、返工、被误导、信任受损。 自己中途判错又自己纠正、没耽误任何人的,不立条——那是排查的正常成本,不是流程缺陷; 真有可复用的教训,归到经验里一句话带过。判据:代价栏能不能写出「谁受了什么影响」? 只写得出「白花了点时间」→ 不立条。
一周 1 个 bug + 1 个需求,配 2~4 条经验、1~3 条复盘就够了。7 条经验 5 条复盘那是硬凑。
经验只收别处没写过的:已经写进需求 / Bug 卡「总结」单元的那句话,不要在经验区再开一条, 那是同一句话抄两遍。判据:把这条经验删掉,读者会不会漏掉信息?不会 → 删。
复盘只收有外部代价的:别人白等、白试、返工、被误导、信任受损。 自己中途判错又自己纠正、没耽误任何人的,不立条——那是排查的正常成本,不是流程缺陷; 真有可复用的教训,归到经验里一句话带过。判据:代价栏能不能写出「谁受了什么影响」? 只写得出「白花了点时间」→ 不立条。
一周 1 个 bug + 1 个需求,配 2~4 条经验、1~3 条复盘就够了。7 条经验 5 条复盘那是硬凑。
纪律自查清单不写进周报
交付标准与留档规范是功能模块界面遵循的东西——
写周报前自己照着逐条过,但清单本身不许贴进周报。
十行勾选框插在复盘后面,读的人得整块跳过去才能接着往下看,纯挡阅读。
真有没做到的,写进对应那条复盘的「下次怎么改」——一句可执行的动作,比一整块打勾有用。
真有没做到的,写进对应那条复盘的「下次怎么改」——一句可执行的动作,比一整块打勾有用。
以读者视角写,不写对自己行为的总结
读者关心的是这件事的结论——哪个模块、什么原因、怎么改、下次怎么复用;
不关心我排查时怎么走神、先看了哪个文件、用了什么手法自我纠正。
经验只收模块知识 / 工具用法:「复活点是三级兜底」「
复盘只收有外部代价的,写成「流程补丁」而不是「行为检讨」—— 代价栏写别人受了什么影响,改法栏写一句可执行的动作。
判据:这条内容的主语是「这个模块 / 这个工具」,还是「我」?主语是「我」而且没有外部代价 → 不写。
经验只收模块知识 / 工具用法:「复活点是三级兜底」「
--noEmit 不产出文件」这种,
别人抄过去能直接用。「拿到截图先对事件」「先看日志再看代码」这类是我的行为总结,不写。复盘只收有外部代价的,写成「流程补丁」而不是「行为检讨」—— 代价栏写别人受了什么影响,改法栏写一句可执行的动作。
判据:这条内容的主语是「这个模块 / 这个工具」,还是「我」?主语是「我」而且没有外部代价 → 不写。
2. 全部留档
筛选按钮自动生成,前四个是大类,后面是问题类型与模块标签。点「全部」还原。
1.
【日常】
删掉客户端 GM 命令 TeleportPlayer(传送玩家到位置{X,Y,Z}),只删定义、不动调用方
模块:GM 工具 / GmCommand · TeleportPlayer
·编号:W39-R05
需求
客户端 GM 命令
TeleportPlayer(传送玩家到位置{X,Y,Z})从 GM 列表里删掉,只删定义、不动调用方。
关键方案
删掉
GmCommand 的 TeleportPlayerDescription / TeleportPlayerCategory / TeleportPlayer(args) 三个静态成员,import 保留。
结果
GmCommand.ts 无诊断,改动进 CL 837513;6 处字符串调用按约定保留。
1.
【关卡工具】
任务s1181145
技能集合表导出 JSON 交给服务器,复用宝箱那套 FNamiDTJsonExporter,行名为数字 Id、列含技能 Id 集合与集合自有 Tag
模块:技能导出 / NamiSkillDataExporter · NamiDTJsonExporter · 技能集合表 DT_SkillCollectionForLevel
·编号:W39-R04
需求
服务器要读技能集合表
DT_SkillCollectionForLevel,需要客户端导出 JSON 给它,口径对齐已有的 DTSkill.json;行 Id 必须是 int(行名即数字),列含技能 Id 集合与集合自有 Tag(Tag 按 FNV 哈希成 int32)。
关键方案
新增
FNamiLevelSkillCollectionJsonExporter 继承宝箱那套 FNamiDTJsonExporter:行名 Atoi 成 int32 当 Id、关掉 bWriteRowName;只在导出注册表加一个 ExportSkillCollectionStage、配置里加一组 SkillCollectionDTPath / SkillCollectionPropsToExport。
结果
产出
DTSkillCollectionForLevel.json,格式与 DTSkill.json 一致;改动留档 0.7.1 的 CL 829666、0.8.0 的 CL 834775。
1.
【主线】
b1275298
同图传送的完成判据从「满 15 秒放行」改成「落点地面确认可站」,卡住不设客户端上限、只每 100 秒打一条告警
模块:传送 / 同图传送安全等待 · TeleportSameSceneFlow · TeleportGroundProbe
·编号:W39-R03
需求
同图传送的「完成」不再由
TeleportSelfFallbackTimeoutMs = 15000 决定,改由落点九点探地决定:LoadDone(14716)上报、解锁移动、关无缝遮罩全部排在「地面确认可站」之后,卡住时不设客户端退出上限,只每 100 秒打一条告警。
关键方案
TeleportGroundProbe.WaitForGroundReady 每 50 毫秒复查九点探地,首次未通过先报一条「传送-找不到合适的地面」;100 秒告警受 TimerSystemConstants.MaxTime = 90000 限制,用每 10 秒轮询、累计到第 10 次才输出实现;异常分支只打 Log.Error,失败退登整段(RecoverFromLoadingFailure / 失败遮罩 / BackLoginView)已删除。
结果
判定权从计时器交还物理系统;卡住时可发现(每 100 秒一条带 7 个诊断字段的红告警),解除等待只剩新请求接管、交接、流程清理三个出口;已知代价是极端情况玩家只能重登。
1.
【衍生】
服务端反馈集合传送后不到半秒就收到 LoadDone;核对后确认客户端没抢跑,是 reason=15 下发了 (0,0,0),玩家被送到世界原点后坠落重生
模块:传送 / ServerEntityMoveSCNotify(15511) · ClientSameSceneLoadDoneCSReq(14716) · TeleportSameSceneFlow
·编号:W39-B04
·定责:服务器 待对齐
问题描述
服务端反馈集合传送后 542ms(客户端侧 509ms)就收到
ClientSameSceneLoadDoneCSReq(14716);核对后 0.5s 全部是场景加载链里的 SceneWaitWpStreamingCmd(0.506s),其余子命令 0.000s,客户端没有抢跑。
原因
真正的问题是 15511 下发的目标坐标是全 0:
distanceMeters 132.10072062540263 恰好等于玩家当前位置到世界原点的距离;WorldTeleportSelfSCNotify(15512)不带坐标字段,客户端只能用这个值,于是玩家被送到世界原点,贴地失败 → 悬空坠落 → 约 8 秒后坠落重生。
解决方案
请服务端确认集合传送(
reason=15)为何下发 position=(0,0,0) 并补上真实集合点坐标;客户端侧待服务端确认后再评估是否给贴地等待加超时兜底(当前无上限,最坏会让 LoadDone 永不上报、服务端 is_loading 一直挂着)以及是否给 GATHER_AREA 加专属分支。
1.
【主线】
传送 loading 之后地形流送「判据达成」的那一瞬画面闪一下:那一刻的关遮罩其实是空操作,相机硬切直接露了出来
模块:传送 / 同图流送 TeleportSameSceneFlow · Seq3 收尾 TeleportSequenceFlow · 流送判据 GameModeController
·编号:W39-B03
·定责:客户端 已修复
问题描述
飞书报障:传送 loading 后地形流送「判据达成」的一瞬间画面闪一下;对应实现是同图流送回调里先
ResetCamera() 硬切相机、再 CloseSeamlessTransitionMask('streaming-complete')。
原因
主动传送这一步的黑屏从头就没开(
OpenSeamlessTransitionMask 被 ShouldUseUiLoadingInsteadOfBlackScreen() 挡掉直接 return),所以那句关遮罩是空操作,真正撤遮挡在后面的 Seq3 就绪;相机硬切夹在两次之间。而报障人的包早于 2026/09/15 CL 815371,SceneLoadingTeleportCmd 里那对 OpenUi / CloseUi 命令还没加上,这一段连系统 Loading 都没有。
解决方案
CL 815371(09/15)把系统 Loading 提前到
TeleportTransitionFlow.OnStartLoad 就打开、一直盖到 Seq3 就绪,并把三处「超时强制关遮罩」改成只打日志;CL 827308(09/18)收口相机侧(ReleaseCamera 加目标校验、OnEnd 补 StopTeleportFade())。换 09/18 之后的包复验即可。
1.
【主线】
坠落重生完成后角色只能转镜头,输入掩码残留;诱因是低层黑屏无动画关闭时异常逃逸,打断了完成回调
模块:坠落重生 + 边界重生 · 低层黑屏 LowLayerBlackScreenController · 输入掩码 OnlyAllow
·编号:W39-B02
·定责:客户端 已修复
问题描述
偶现:爆烈洞窟之后的山洞坠落重生完成,角色只能转镜头。日志里
[坠落重生输入] 只有 3 条,第二轮之后再无恢复记录,OnlyAllow拦截 持续刷到世界清理。
原因
EndFallingReborn 把恢复输入的 SetEnableInputControl(true) 挂在 CurrentRebornEntityHandle?.Valid 上,第二轮句柄失效被静默跳过;诱因是第一轮 DoFallZoneRespawn 完成回调里 LowLayerBlackScreenController.CloseBlackScreen 裸 await 已被同步置空的静态 HidePromise,抛 TypeError 从同步 Emit 逃逸,打断回调后续复位。
解决方案
EndFallingReborn 去掉 Valid 守卫、无条件恢复输入;CloseLowLayerBlackScreenIfNeeded 给 CloseBlackScreen 补 .catch() + Log.Error。黑屏侧建议补 HidePromise 判空与 try / finally 保 OnClear()。
1.
【关卡工具】
任务s1183537
「发射关卡子弹」的子弹类型Id 加了编辑期存在性校验,读哪张子弹表由伤害来源与子弹类型共同决定
模块:关卡工具 / 关卡子弹 · 发射关卡子弹 LaunchBullet · 子弹表配置
·编号:W39-R02
需求
「发射关卡子弹」的「子弹类型Id」原来能手填任意数字,填了不存在 / 已删除的 Id 要等运行时才炸;需要在编辑期校验它确实存在于对应的子弹表里,而读哪张表由同级「伤害来源」与「子弹类型」决定。
关键方案
子弹类型Id 改成
createDataTableRowScheme(数据表行字段)下拉,表路径按伤害来源分三支解析:玩家走固定玩家表、怪物实体静态无表只做非空、Id > 10000 走通用 CDT_ 表、环境怪走角色配置的 BulletTableConfig;再在父对象 OnSchemeCheck 里补一次行存在性校验,覆盖「换伤害来源后旧 Id 失效」的情况。
结果
「子弹类型Id」变成可下拉选行;填不存在的 Id 编辑期即报错。
tsc --noEmit 退出码 0。
1.
【关卡工具】
任务s1179923
行为树新增「当传送完成」条件节点,带可选超时时间(默认 10 秒),传送判定与计时全在服务器
模块:行为树条件节点 · 传送 / 条件注册表 Condition(条件)· ChildQuest(子任务)
·编号:W39-R01
需求
行为树里要能等「玩家传送完成」:
Condition(条件)节点与 ChildQuest(子任务)节点下都要能选到这个条件,带可选「超时时间」,默认 10 秒,从第一个玩家客户端加载完成上报成功起算。
关键方案
两套条件注册表(
ECondition / EChildQuest)各三张 Scheme(配置结构)表全部登记;超时用 createFloatScheme 的 Optional + CreateDefault: 10000 实现,≤ 0 在校验里拦掉;判定逻辑全在服务器,客户端不新增运行时代码。
结果
两个下拉都能选到「当传送完成」,不填按 10000 毫秒走;
tsc --noEmit 退出码 0。
1.
【主线】
b1273319
传送功能未开启时客户端仍发包,被服务端拦截后没退出传送状态,只剩镜头可操作
模块:传送 / TeleportController · SceneItemController · 功能开启 FunctionalController
·编号:W39-B01
·定责:客户端 已修复
问题描述
传送功能未开启时客户端照常发 23707,服务端按
function:14 拦截并回 23708;客户端失败分支不退出传送状态,输入锁与主 UI 锁残留,只剩镜头可操作。
原因
发包前没做功能开启检查;23708 失败回包只打日志;而恢复链等的是服务端成功才广播的
ServerEntityMoveSCNotify(15511),被拒绝后这条链永远不触发。
解决方案
TeleportController.OnStartUp 主动传送前先用 IsOpenByFunctionType(EFunctionType.Teleport) 自查,未开启直接拦下并提示、不发包;新增 OnTeleportRequestFailed(),由 23708 失败分支调用,关遮罩 / 黑屏、RestorePlayerState 并按线调 OnEnd() 收尾。
1.
【undefined】
undefined
模块:undefined
·编号:W38-E01
手法
把「这次传送由谁遮丑」的判定链整条梳理完时,发现三处会让遮挡漏掉的口子(只读分析,未改代码):
① 插入 Loading 与跳过黑屏用的是两条不同源的判定
② 插入的那条 OpenUi 命令在传送链上并不创建 Loading 界面
③ 无缝窗口存在一段无遮挡档位 同步瞬移的遮挡有三个来源:入口瞬时遮罩、
另有两处更窄的口子:
① 插入 Loading 与跳过黑屏用的是两条不同源的判定
SceneLoadingTeleportCmd 用 TeleportModel.IsTeleporting 决定要不要插 SceneLoadingOpenUiCmd,而跳过黑屏用的是 ShouldUseUiLoadingInsteadOfBlackScreen。前者只由 InitData() 置位,传送门不经过它 → 该分支在传送门路径上恒为 false。两条判定分叉的结果是:黑屏确实被跳过了,却没有对应的 Loading 补上。② 插入的那条 OpenUi 命令在传送链上并不创建 Loading 界面
SceneLoadingOpenUiCmd.OnExecute 只有 IsLogin 分支才调 LoadingController.GameModeOpenLoading()(真正创建 LoadingView 的地方),非 Login 分支只做 OpenBlackView() + LoadingController.SetIsLoading(true),而 SetIsLoading 只发 Start/FinishLoadingState、跟界面无关。所以即便把 ① 的判定改成同源,这条分支也实现不了「传送点 / 传送门都要有 Loading 界面」。③ 无缝窗口存在一段无遮挡档位 同步瞬移的遮挡有三个来源:入口瞬时遮罩、
BaseMoveSyncComponent 自己的黑屏、以及升格出来的 uiload。前两者共用同一个闸门,默认配置下恒为「跳过」;uiload 则要求 !IsTeleportStreaming 且位移 > 100 米才升格。于是「表现被判为无缝(非 isHeavy)且位移 ≤ 100 米」这一段既没有黑屏也没有 Loading——命中它的场景是流送窗口,而不是原以为的「远距离」,前提要修正。另有两处更窄的口子:
IsTeleportStreaming 若因残留或并发被置真,ShouldPromoteSeamlessToUiLoading 会直接 return false,位移再大也不升格;若没收到 WorldTeleportSelfSCNotify(15512),待处理的表现类型没落库,OnLocalPlayerServerMoveApplied 会直接 return,整条路变成零遮挡。
1.
【主线】
传送过场收尾时重置动画模式,把角色链接的动画层清掉,落地后三段跳表现异常
模块:传送 / TeleportSeq3Binder · 关卡序列收尾
·编号:W38-B05
·定责:客户端 已修复
问题描述
传送过场播完后角色落地,三种跳的表现异常;日志里模式、实例、三个链接层都看着正常。
原因
序列收尾时业务侧又调了一次
SetAnimationMode(AnimationBlueprint)。该接口第二个参数默认 true,即使模式没变也会强制重新初始化动画实例,把由动画图表创建的链接动画层注销;而补挂逻辑只在「未挂载」标志为 false 时生效,没人再装回来。
解决方案
删掉收尾处手写的
SetAnimationMode,改由序列动画节的「完成时」=「恢复状态」归还姿态。播放设置里的完成状态覆盖替代不了它:只作用于序列结束时仍激活的 section。
1.
【主线】
传送快结束时用脱离卡死自杀,传送结束后角色身上的淡变材质残留、表现异常
模块:传送 / TeleportTransitionFlow · TeleportPresentation · TeleportController
·编号:W38-B04
·定责:客户端 已修复
问题描述
传送快结束时用脱离卡死自杀,传送结束后角色身上的淡变材质残留,表现异常(隐形 / 半透明)。
原因
淡入阶段先挂材质、等 800ms 才自己摘;这段窗口内死亡中断会让
OnEnd() 先改 FlowGeneration 作废代次,800ms 后淡入分支被 IsCurrentFlow 早 return,跳过自己的 StopTeleportFade,而 OnEnd() 的收尾分支没有兜底摘材质。
解决方案
OnEnd() 的 !KeepFadeInOutOnEnd 分支补一次 StopTeleportFade() 兜底;该方法幂等,KeepFadeInOutOnEnd 为 true(同图淡入淡出刚起步)时不清理,避免误杀淡出。
1.
【主线】
传送 V2 Seq2 → Seq3 衔接梳理(V2 表现 · 切场阶段与实测耗时)
模块:传送 V2 / TeleportV2Flow · Seq2 循环退出 · 切场分段 · Seq3 绑定
·编号:W38-R03
需求
Seq2 与 Seq3 之间的切场由多次跨帧等待拼成,但没有统一时间轴,出问题只能临时加日志逐条试。
关键方案
拆成 7 个阶段并区分「同帧完成」与「刻意跨帧」:换装等待与 binding 编译等待是仅有的两处跨帧等待,串联约 500 ms;落地收尾另外拉出单看。
结果
产出《传送 › V2 表现 › Seq2 → Seq3 衔接问题》专栏,含三类现象定位与两组实测耗时对照(换装 203 ms / binding 312 ms / 落地 ≈10 ms)。
1.
【主线】
传送加载规则:统一 Loading / 黑屏 / 无缝的遮挡分工,并把黑屏入口收口到单一判定
模块:传送 / TeleportPresentation · TeleportController · BlackScreenController · BaseMoveSyncComponent · SceneLoadingTeleportCmd
·编号:W38-R02
需求
需求要求「传送点、传送门发起的传送都要有 Loading 界面」,而黑屏挂 CG 层高于 Loading 层,两者并存时加载百分比被完全盖住,需要一条统一判定决定这次传送由谁遮丑。
关键方案
三处黑屏入口(入口瞬时遮罩 / 跨图入口 / 同步瞬移)统一收口到
ShouldUseUiLoadingInsteadOfBlackScreen,且该判定不依赖只由 InitData() 置位的 TeleportModel.IsTeleporting;位移超 100 米且流送未完成时把无缝升格成 uiload,另在 BlackScreenController 按 IsLoadingView 做全局兜底。
结果
黑屏与系统 Loading 互斥,无缝传送在流送窗口由升格出的 uiload 兜住;判定链与升格闸门登记进《传送 · 技术方案文档》§6.3。
1.
【主线】
副本 20100301 典狱长作战区域坠落重生有概率回到副本入口,而不是就近落点
模块:坠落重生 / FallingRebornController · 关卡 / 副本 20100301
·编号:W38-B03
·定责:关卡 已修复
问题描述
副本 20100301 典狱长作战区域坠落重生有概率回到副本入口,而不是就近落点。
原因
坠落重生点
FallingRebornLocation 只在 BP_UpdateFallingRebornArea_C 范围内才刷新,初始值 = 进本落点(副本入口);该区域没有范围盒,记录点整段战斗期间未更新,二次校验判不安全时又回退到 LevelBornPosition。
解决方案
关卡侧在典狱长作战区域补
BP_UpdateFallingRebornArea_C 范围盒,玩家站立时刷新就近重生点。
1.
【主线】
b1271064
副本结算时,特写 / 全家福里的结算角色模型全部不显示
模块:副本结算 / DungeonSettlementController · 技能序列换装 / SkillSequenceAvatarBindingSession · 换装队列 / AvatarJobController
·编号:W38-B02
·定责:客户端 已定责 待改
问题描述
副本结算时结算角色模型不显示:4 个角色
BindActor 正常,但结算窗口内 0 条「换装成功!」。
原因
战斗期技能序列换装回调读
SeqActor.LevelSequence.AssetPath 时抛异常(prepared sequence 的 Actor 不挂 LevelSequence),异常吞在资源回调栈里,却打断了 AvatarClothJob 的 OnJobCompletedCallBack,job 出不了队、MaxJobCount=4 的槽位被漏光,后续换装全部堵死。
解决方案
上游两处:序列路径取值加兜底 + 换装步骤回调包 try/catch 保证 job 必出队;结算侧随单清理 Seq1 / Seq3 废弃代码并补 4 处静默与崩溃隐患的日志/兜底。
1.
【主线】
重新进图后 #OpenDebugGameEnv 打开的 GM 调试信息面板被关闭且不再恢复
模块:Loading / LoadingController · 传送 / Seq3 换装遮罩 · Debug / GameEnvDebugView
·编号:W38-B01
·定责:客户端 已修复
问题描述
传送重新进图后,
#OpenDebugGameEnv 打开的 GM 调试信息面板被关闭且不再恢复(TimeInfoDebugView 同受波及)。
原因
落地阶段 Seq3 换装遮罩第二次打开系统 Loading,此时首段
FirstProgressPromise 已置空、重入保护失效,UiInterfaceHelper.Clear() 又做了一次全量清理,把已恢复的调试面板清掉。
解决方案
给
GameModeOpenLoading 加 keepExistingUi 开关,只有 Seq3 遮罩传 true 跳过清理。
1.
【主线】
传送点模块梳理(解锁状态与场景表现)
模块:传送点 / SceneItemController · EtTeleportComponent · TeleportMarkItem
·编号:W38-R01
需求
传送点解锁状态与场景表现分散在 SceneItemController / EtTeleportComponent / TeleportMarkItem 三处,需要理清职责与数据流向。
关键方案
解锁数据单一权威持有(
UnlockTeleportIdSet)+入口幂等;表现走 ChangeLevelTagByClient 纯本地切换,标记用 getter 实时转发,均不缓存副本。
结果
产出《传送点》模块文档,覆盖配置、解锁三条来源、表现、发起传送与最近传送点检索。
1.
【关卡工具】
多人传送 Action 编辑器配置支持
模块:编辑器 Scheme(配置结构)· 多人传送 Action
·编号:W37-R03
需求
现有传送 Action 只按单点处理,多人执行时会重叠。为
SetEntityPos、TeleportLevel、TeleportDungeon 增加“半径内随机 / 阵型”两种落点模式,并补齐统一朝向、黑屏类型和阵型指示编辑。
实现细节
SetEntityPos 保持根级可选 LandingConfig:随机模式保存 Radius,阵型点相对 PositionVariable;旧 ScatterRadius 可自动修复迁入随机模式并删除旧字段。TeleportLevel、TeleportDungeon 的“传送类型”新增 MultiLanding(多人落点):顶层共用 TargetPosition;随机模式内配置 Position + Radius,阵型模式不配置目标点,直接保存世界坐标 FormationPoints 与世界旋转 FormationRotations。阵型点新增时按 120cm 蛇形 X/Y 偏移,从候选点上方 400cm 向下检测至下方 1000cm,命中后只修正 Z;位置与旋转回写统一保留两位小数。随机半径默认值为 500cm。三个 Action 均支持可选 BlackScreenType;另为 SetPlayerPos 增加“建议使用 SetEntityPos”提示,并开放 TeleportDungeon 在玩法行为树中的配置入口。
结果
三类 Action 已覆盖半径随机与阵型多人落点;
TeleportLevel、TeleportDungeon 通过独立 MultiLanding 类型与其他四种传送类型隔离,旧数据可自动迁移。阵型人数不写死 8 人,新增点避免默认重叠与埋地。
验收
相关 TypeScript(脚本)文件通过 Prettier(格式检查);重复声明和重复字段已清理。完整
tsc-native 当前被两个无关 GM 接口缺失阻塞,使用项目 compile-skip-diagnostic 成功生成产物;LevelPlay.js、TeleportLanding.js、Action.js 已核对 MultiLanding、默认半径 500、两位小数、旧配置迁移、阵型偏移与贴地逻辑。
1.
【主线】
TeleportController 结构重构
模块:传送 / TeleportController
·编号:W37-R01
需求
TeleportController 原文件 3526 行且职责混合、回调嵌套;目标是保行为拆分,控制器只保留门面。
关键方案
拆出表现、地面探测、同图、序列、
Seq3 绑定和转场六个职责模块;公共 API(接口)与关键时序保护保持不变,回调统一为稳定引用。
结果
控制器降至 467 行;格式、静态检查、类型编译与 7 个 JavaScript(脚本)产物验收通过,PIE(编辑器内运行)回归待测。
1.
【主线】
显示倒计时时走到事件范围外退出事件,再次加入同一事件不显示倒计时
模块:行为树计时器(TimerAction)· 野外事件
·编号:W36-B03
问题描述详细版本
进 30100112 关卡 →
#pre_start_wild_event 514000077 → 进事件范围秒杀怪,倒计时正常出来 → 走出事件范围直到服务器判定退出事件 → 再走回去重新加入,事件本身还在跑(倒计时也还在走),但屏幕上不出倒计时 UI。与退出 / 完成事件倒计时仍显示是同一模块的两个方向,复现路径不同,分开两条。
原因
客户端手里的计时器只有增量 15016 一个来源,而增量只在计时器发生变化时下发——重新加入一个已经在跑的事件,服务器不会再补一条增量。
恢复用的数据其实一直都发了:34501
Model 里没数据 →
恢复用的数据其实一直都发了:34501
WorldJoinWildEventSCNotify 带 wildEventBt(BehaviorTreeMsg),里面就有 timerList。但 WildEventController.CreateWildEventBT 只把它整个交给 GeneralLogicTreeModel.CreateBehaviorTree 建树,timerList 全程没人读(客户端只在进关卡时消费过 levelBt.timerList)。Model 里没数据 →
GetDisplayTimerName() 返回 undefined → UI 判定「没有要显示的计时器」。
解决方案
①
②
③ 全量与增量两条落地路径写入前,都先把该树在分帧队列里积压的增删同步排空。
BombTimerController 加公开方法 ApplyTreeTimerList(treeInfo):从全量里取 type / btTypeId / timerList,走已有的 SetList 带归属落地,再 Emit BombTimerChanged 让 UI 自己刷出来。②
WildEventController.CreateWildEventBT 里加一行调用。它是野外事件建树的唯一入口,所以一处改动同时覆盖「重新 join」(34501)和「进关卡 / 重连时重建进行中的事件」两条路径。③ 全量与增量两条落地路径写入前,都先把该树在分帧队列里积压的增删同步排空。
CreateBehaviorTree / RemoveBehaviorTree 都是入队、下一帧 Tick 才执行,退出与重进间隔不到一帧时,队列里那条待执行的删树会 Emit GeneralLogicTreeRemove,把刚落地的计时器又清掉(不管数据来自全量还是 15016)。ApplyCreateBehaviorTree 开头自带的 FlushByTree 救不了——Process() 期间 IsDraining 为 true,flush 直接 no-op。
总结
一个 msgId 在 Net 层只能注册一个 handler——
另一条更通用:增量协议只在「变化时」下发,凡是「重进 / 重连要恢复显示」的数据,都必须另有一个全量来源。写消费代码时顺手确认全量里那个同名字段有没有人读——这次的
NetConnection.Register 发现 NotifyHandles 里已有就直接打 Error 并 return false,不是叠加也不是覆盖,后注册的那个静默失效。所以想在别的模块里消费一条已被占用的协议(这里 34501 归 WildEventController),只能从原注册点调过来,不能自己再 Register 一次。做法是把逻辑留在自己模块里暴露成公开方法,调用点只占一行,模块边界不糊。另一条更通用:增量协议只在「变化时」下发,凡是「重进 / 重连要恢复显示」的数据,都必须另有一个全量来源。写消费代码时顺手确认全量里那个同名字段有没有人读——这次的
timerList 在协议里躺了很久没人碰。
1.
【主线】
野外事件倒计时过程中退出事件或完成事件,倒计时 UI 仍留在屏幕上
模块:行为树计时器(TimerAction)· 野外事件
·编号:W36-B02
问题描述详细版本
进 30100112 关卡 →
服务器侧原来是「玩家离开副本时下发一条定时器协议通知关闭」修的,组长要求改成客户端自主控制(少一次往返,也不会因丢包导致 UI 与玩法状态不一致),服务器会删掉那段下发逻辑,并在增量协议 15016 里补了归属字段。
#pre_start_wild_event 514000077 开启事件 → 传送进事件范围 → 秒杀怪进入下一节点显示倒计时,此时走出事件范围或 #finish_wild_event 514000077,事件已经结束了但倒计时 UI 还在跑,一直挂到倒计时自然归零才收起。服务器侧原来是「玩家离开副本时下发一条定时器协议通知关闭」修的,组长要求改成客户端自主控制(少一次往返,也不会因丢包导致 UI 与玩法状态不一致),服务器会删掉那段下发逻辑,并在增量协议 15016 里补了归属字段。
原因
于是退出 / 完成野外事件、行为树被销毁时,客户端手里没有任何线索能判断该清哪条计时器,只能等
BombTimerModel.Timers 以 timerName 为 key,存的计时器不知道自己属于哪棵行为树。15016 WorldBehaviorTreeTimerParameterSCNotify 已经带了 type / btTypeId,但客户端只取 btTimerMsg 就丢掉了。于是退出 / 完成野外事件、行为树被销毁时,客户端手里没有任何线索能判断该清哪条计时器,只能等
GetDisplayTime 把值推到 0、状态自动置 Stopped 后才被 GetDisplayTimerName 过滤掉——这就是「倒计时仍显示」。
解决方案
① 计时器落地时记住归属:
② 清理挂在既有事件
③
④ 顺手修了 UI 侧的残留:见「顺带发现 ①」。
SetInfo / SetList 增加 treeIncId(type_btTypeId)与 btType 必填参数,存进 TBombTimerWithExtra。② 清理挂在既有事件
EEventName.GeneralLogicTreeRemove 上,按 TreeIncId 精确删(只删归属相符的,避免不同树的同名计时器互删),删到了就 Emit BombTimerChanged 让 UI 自己收起。③
OnLeaveLevel 兜底:只保留任务树(跨关卡存活)的计时器,其余含归属缺失的一并清掉。④ 顺手修了 UI 侧的残留:见「顺带发现 ①」。
总结
行为树派生出来的 UI,清理时机统一挂
前提是这类数据落地时必须带上
GeneralLogicTreeRemove,一个监听点就够——它由 ApplyRemoveBehaviorTree 统一 Emit,天然覆盖退出野外事件(34504)、完成(34505)、事件结束(34507)、服务器删树(15004)、以及离开副本时批量删树五个场景,不用每条协议各接一次,也不需要服务器专门下发一条「关闭 UI」。前提是这类数据落地时必须带上
treeIncId 归属;只按业务名(这里是 timerName)做 key 的缓存,树一销毁就成了没人认领的孤儿数据。
1.
【主线】
副本集合区域:进入区域后显示集合人数与自动传送倒计时,全员到齐或超时由服务器传送
模块:关卡 · 副本集合区域 / 集合提示 UI
·编号:W36-R04
需求
副本里摆一块集合区域,玩家进入后开始集合:屏幕上提示当前已集合人数 / 总人数,以及多少秒后自动传送;倒计时结束由服务器把仍在区域外的玩家强制传送到集合点。
人数统计、集合状态机、强制传送全部在服务器(
人数统计、集合状态机、强制传送全部在服务器(
gather_comp.cpp),客户端本期只做三件事:范围实体进出检测、提示 UI 显示刷新、边界特效反馈。
实现细节
UI 复用匹配模块的现成预设
提示由关卡级
倒计时完全以服务器
编辑器侧文案只出一行:
UiItem_MatchingMeetingTips,两行拆开:上行 TxtNum 出「(2/5)」,与预设自带静态文案同行排列;下行 TxtCD 出配置文案(含倒计时)。提示由关卡级
DungeonGatherController 驱动:监听 EntityGatherUpdateSCNotify(CmdId 14050)刷新,文案按广播里的 entityConfigId 直接查静态配置表(关卡实例ID高位为 0,与 PrefabDataId 同域),不依赖集合区实体是否在本地视野内;进关卡 / 重连靠 EndClientLoading 用全量 gatherInfo 补一次。倒计时完全以服务器
forceTeleportTimeMs 时间戳为准,本地只起一个 1s Timer 逐秒重算剩余秒数;算出 ≤ 0 就停 Timer 并隐藏 UI。编辑器侧文案只出一行:
TidTeleportText 挂 HideFlag: ETabFlag.AlwaysHide 隐掉正式文本行,Key 由 ObjectScheme.OnBaseModify 从 EdTid 自动同步,多语言不受影响。
总结
这次踩到的五条模块知识,除第①条外都跟集合区本身无关,做任何新 UI 都会撞上:
① 关卡级广播驱动的 UI,不能挂在 AOI 实体组件上。最初把提示做成实体组件
② 客户端运行时的 UI 资源配置读的是 SQLite,不是 json / csv。真正被读的文件是
③ 一个新 UI 界面要登记四处:
④ LGUI 控件是按预设
⑤ 集合区功能单人测不出来。服务器
① 关卡级广播驱动的 UI,不能挂在 AOI 实体组件上。最初把提示做成实体组件
EtDungeonGatherComponent(照 EtBoundaryRespawnComponent 的样子写),结果离集合区远的玩家完全看不到提示:服务器 EntityGatherComp::bcUpdateNotify() 走 LevelInst::bc() 遍历 player_map_ 全员下发(不是 bcAoi),而客户端场景物件实体是 AOI 增删的(ServerLevel.UpdatePartial 按 enterEntityList / leaveEntityList),远处玩家的组件压根不存在,广播只写进 Model 没人消费;跑远时 AOI 销毁实体触发 OnEnd() → Hide(),倒计时恰好在最需要它的那个人脸上被关掉;进关卡就在远处时组件不存在,全量兜底也不执行。改成关卡级 Controller 后组件整体删除。判据:这个 UI 该由关卡状态决定可见性,还是由某个实体在不在视野里决定?前者一律挂 Controller;实体组件只做「近处才该看见」的本地表现(特效 / 音效 / 交互提示)。同理适用于任何 LevelInst::bc() 全员下发的协议。② 客户端运行时的 UI 资源配置读的是 SQLite,不是 json / csv。真正被读的文件是
Client/Content/Aki/ConfigDB/db_ui_resource.db,表里存的是 FlatBuffers blob(SELECT BinData FROM UiResource WHERE Id = ?)。改完 u.UI资源.csv 必须导表,只改 csv 或 UiResource.json 运行时完全看不到。单表导出命令:dotnet ConfigTool.Cmd.dll config -j <proj> -r <configdir> -e -d -a -l -t 1 -f <filter>(-t 1 只导客户端,-f 指定单表过滤文件,几秒钟完事);导表工具是删掉旧 db 重新建,所以编辑器开着占用文件也不影响。另外查不到的 Id 会被 ConfigCommon.SaveConfig(key, NotFound) 缓存,改完 db 必须重启客户端,热重载 TS 没用。③ 一个新 UI 界面要登记四处:
EUiViewName 枚举 → UiViewStorage.RegisterUiTsInfo → UI资源表(即上面那个 db) → UiShow 表显示配置。少任何一处报的都是 [UiConfig.TryGetViewInfo] 找不到界面配置。④ LGUI 控件是按预设
LGUIComponentsRegistry.Components 的数组下标取的,不是按名字。代码里 ComponentRegisterInfos 填的那个数字就是 registry 里的位置,UI 那边调整登记顺序,代码必须跟着改;没有「按路径取嵌套控件」的 API。数量或类型对不上时报 [FindInitedComponentRegistryActor]。⑤ 集合区功能单人测不出来。服务器
total_num = getPlayerNum() 不可覆盖,startGather() 里紧接着就判 in_range_uid_set_.size() ≥ total_num,单人时 1≥1 直接进 FINISHED 并把 force_teleport_time_ms 清 0,客户端连倒计时都收不到。必须 2 人以上联机验证——也正因为如此,上面第①条那个「远处看不到」的问题单人自测根本暴露不出来。
1.
【关卡工具】
行为树 RemoveBuffAction 增加「移除全部」选项,一次清掉目标身上该 BuffID 的全部实例
模块:编辑器 Scheme · 行为树 RemoveBuffAction
·编号:W36-R03
需求
RemoveBuffAction 原来只能按
BuffCount 扣层数,策划要一个「全部移除」开关。IsRemoveAll 这个字段 proto 和服务器早就有了(RemoveBuffContent 第 4 个字段,两个 Action 实现都已经走 removeAllBuffByConfigId),只有编辑器侧没暴露出来,所以这次只改 TS 编辑器,字段名沿用 IsRemoveAll,不动协议和 C++。
实现细节
定成纯可选可叉掉:只给
IRemoveBuffAction 加 IsRemoveAll?: boolean,removeBuffActionScheme 加一个 createBooleanScheme,排在 Target 之后(UI 顺序 = Fields 声明顺序)。定成纯可选可叉掉:只给
Optional,不给 IsAlwaysShow——× 删除按钮的渲染条件是 Optional && !IsAlwaysShow,加了 IsAlwaysShow 就没有叉。CreateDefault 给 true:从「可选▼」里勾出这个字段本身就是「我要移除全部」的意思,勾出来还是 false 会让人以为没生效。
1.
【关卡工具】
Repeater(循环)节点增加 tick 间隔、无限循环、超时时长与超时结果四个配置项
模块:编辑器 Scheme · 行为树 Repeater 节点
·编号:W36-R02
需求
Repeater 节点原来只有
最大循环次数和循环退出条件两项,循环节奏和终止方式都不可配。策划要求增加四项,字段名与服务器 proto 对齐:TickIntervalMs 循环 tick 间隔(默认 1000ms,最小 500ms)、IsInfiniteLoop 无限循环(勾选后忽略最大循环次数)、InfiniteLoopTimeoutMs 无限循环超时时长(默认 -1 即永久)、IsTimeoutFail 超时后节点判定成功还是失败(默认成功)。
实现细节
编辑器侧
类型定义同步三处:
四项最终都定为纯可选(只留
repeaterBtNodeScheme 加四个字段,TickIntervalMs 挂 OnSchemeCheck 卡最小值 500(填 0 视为用默认值)。类型定义同步三处:
IRepeaterBtNode、导表脚本侧的同名镜像接口、以及 AI 建树用的 IBtPlanNode;两个 Executor 的 Repeater 分支补齐字段透传。四项最终都定为纯可选(只留
Optional,不给 IsDefaultCreate)——不配置就不写进 JSON,由服务器走 proto 默认值。
总结
编辑器字段的「可选」有三档,选错直接影响导出的 JSON 体积和默认值归属:
① 只写
② 加
③ 再加
渲染判断在
另外
① 只写
Optional → 真可选,不勾就不进 JSON,默认值归服务器;② 加
IsDefaultCreate → 新建节点时就写死进 JSON,老节点仍然没有;③ 再加
IsAlwaysShow + DefaultValue → 老节点也直接显示。渲染判断在
Obj.tsx:Optional && value === undefined && !IsAlwaysShow 就不渲染,所以②对老数据是看不见的,得去节点的「可选▼」里手动勾。IsAlwaysShow 必须配 DefaultValue,否则 OnBaseCheck 抛异常。另外
CreateDefault 是「从可选菜单勾选时填入的初始值」,不是「不配置时的值」——布尔项勾了却填 false 会让人以为没生效。
2.
【关卡工具】
为 Condition 节点增加副本集合功能(等待副本集合完成)
模块:编辑器 Scheme · Condition / ChildQuest
·编号:W36-R01
需求
「副本集合」原来只有 ChildQuest 节点有(
EChildQuest.WaitDungeonGather),
策划在 Condition 节点里配不出来,只能绕着改任务结构。需求是 Condition 节点也支持,
中文名「等待副本集合完成」。
实现细节
Condition 侧新建对等的
waitDungeonGatherConditionScheme,走完四层:
枚举 → 接口 → 穷举默认表 → scheme + 注册。实体选择器过滤
DungeonGatherComponent。判定纯服务器,客户端不写求值器。
范围
只加 Condition 节点的三种行为树(Quest / 关卡玩法 / 野外事件),存量配置零影响。
3.
【主线】
任务513000281
坠落重生把玩家送到门外,任务卡死
模块:软边界重生 / 坠落重生
·编号:W36-B01
问题描述详细版本
关卡 40065010 掉进坠落区域后软重生,落点在单向门外侧,玩家回不去。
原因
个人复活点 Entity 513002412 摆在
Z=-7155,埋在 StaticMesh 里;
FixBornLocation 贴地修正后弹到门外。
解决方案
将 513002412 的
Transform.Pos 移到门内可站立地面。
顺带发现
同一次排查挖出的三个待改项,不单独立条:
①
② FallingRebornController.ts:511 覆盖成功时没有日志,形成排查盲区(待改)。
③ EtBoundaryRespawnComponent.ts:585 注释写「不使用个人复活点」,实际
①
Params.Location 填了不生效——服务器 findRevivePointOpt() 只读 Entity 的
Transform.Pos,本次两者差 1400+ 单位(待对齐)。② FallingRebornController.ts:511 覆盖成功时没有日志,形成排查盲区(待改)。
③ EtBoundaryRespawnComponent.ts:585 注释写「不使用个人复活点」,实际
FallZoneRebornRuleId = 1 就是走服务器复活点(待改)。
起止
提出 W35(08/27)· 完成 W36(08/31)· 小耗时 → 只记完成周
3. 新增一条留档
推荐路径:一条命令搞定,别手写 HTML
写一个条目文件(照抄 weekly/data/_template.mjs),然后:
第二条命令校验全站内链、锚点、
手写的深度内容(时序表、截图、根因链)脚本不碰,只动卡片区和
node tools/archive.mjs weekly/data/我的条目.mjs
node tools/check.mjs
第一条命令自动做完五件事:分配编号 → 生成卡片插进当周周报对应小类 →
在本页 §2 顶部插摘要卡 → 追加搜索索引 → 重算所有序号 / 计数 / 速览表 / 飞书第 1 段。第二条命令校验全站内链、锚点、
div 配对、索引,退出码非 0 就是有问题。手写的深度内容(时序表、截图、根因链)脚本不碰,只动卡片区和
<!-- AUTO:xxx --> 标记之间的内容。改完手写部分跑
node tools/archive.mjs --rebuild 2026-W36 重算即可。
编号只在本页显示,周报页只显示序号
周报页面上条目就是 1. 2. 3.,编号藏在卡片的
正文引用别的条目要写标题,不写编号也不写序号:
data-id 属性里,不占版面。
本页保留编号——留档库要跨周检索,编号是身份,同一件事在 W37 的周报里还叫这个号。正文引用别的条目要写标题,不写编号也不写序号:
<a href="#rec-w36-b01">坠落重生落点在门外</a>,
不要写「见复盘 1」「Bug 2 的日志补充」。序号会随增删变动,而且「Bug 3」这三个字读者根本不知道指什么,
还得翻回去数一遍。data-ref 那套自动刷序号的机制不要再用了——
问题不是序号会过期,是正文里压根不该出现序号。
什么不写进周报:过程、还原掉的改动
周报交的是结果和沉淀,不是施工日志。以下三类明确不写:
判断标准:这段内容下周还有人会翻吗?会 → 写进「总结」或经验总结;
只是证明我干过活 → 不写。
不写
逐文件逐行号的改动清单——「ICondition.ts:212 加枚举、Condition.ts:1593 注册」这种,
CL 里全都有,写进周报只是把 diff 抄一遍,没人会看。留一句「过了哪四层」就够。
不写
已还原 / 已撤销的改动——自己多改后来又改回来的,等于没做,别列在需求卡里充数。
例外只有一个:这件事本身是教训,那就写进问题复盘大类,那里专门收「我哪一步做错了」。
同一件事不要在需求卡和复盘里各写一遍。
不写
列表页的一段话摘要——weekly/index.html 的周报列表只留
周次 + 日期 + 四类计数,不要「两条线:① … ② …」这种概括。
想知道那周干了什么,点进去看速览表,列表页重复一遍只是把首屏撑满。
一个 bug 只能一条
没有特殊说明的时候,一个 bug 就是一条。同一次排查顺带挖出来的附带问题
(配置字段语义不对、缺日志、注释写反了这类),全部塞进这条卡片的
「顺带发现」补充单元,用 ① ② ③ 编号列,不单独立条。
判断是不是同一条:现象是不是同一个、复现路径是不是同一条。是 → 一条; 真的是两个独立现象、要分别提单分别验收 → 才拆两条。
把一个问题拆成四条,速览表看起来热闹,实际是同一件事数了四遍——这属于凑数,不许干。
判断是不是同一条:现象是不是同一个、复现路径是不是同一条。是 → 一条; 真的是两个独立现象、要分别提单分别验收 → 才拆两条。
把一个问题拆成四条,速览表看起来热闹,实际是同一件事数了四遍——这属于凑数,不许干。
复盘卡不挂自嘲徽章
最丢人的一条 方向全错
白花时间 这类徽章一律不写。周报是交出去的东西,
自我贬损既不是信息也不是态度,只会让人怀疑这条能不能信。
复盘卡的
复盘卡的
.rec-mod 里只留模块;付出的代价写进下面的
代价单元(pc-cost),用「多花 1.5 天」这种客观量;
速览表状态列统一显示 已复盘。
下面是脚本背后的口径,手工补卡或改模板时对照着看。
1
定编号
2
在本页 §2 顶部插一张 .rec 卡
class="rec",
data-cat="大类 问题类型 模块名"(空格分隔,大类必须放第一个,
筛选按钮按出现顺序自动生成)。标题行用飞书格式:
.rec-head 里依次是
.rec-seq(序号)+ .line-tag(【主线】/【衍生】/【日常】)+
.task-id(任务{ID},没有任务号就整个 span 不出,别写「无任务号」)+
.rec-what(现象一句话);紧跟一行 .rec-mod 写 模块和编号,不显示责任归属或重复状态。正文字段单元,每个单元各占一行、不许改名不许调序:
Bug=模块 → 问题描述详细版本 → 原因 → 解决方案 → 总结; 需求=模块 → 需求 → 实现细节 → 总结; 经验=手法 / 详情;复盘=错在哪 / 流程怎么改 / 详情。
没有的单元可以不写。起止 / 环境 / 范围 / 详情属于补充单元,放在正式单元后面。
3
在当周周报里写完整版
时序 + 截图 + 关键证据 + 走过的弯路。weekly/{ISO周}.html,
用 留档模板 起手。
4
图片进 weekly/img/,登记搜索索引
命名 w{周次}-{序号}-{说明}.png;
在 assets/search-index.js 末尾加一条,否则全站搜索搜不到。