Appearance
审计报告
背景:八城审计 + 物理引擎审计 + 工具链审计后,沙滩上露出 48 颗石子。 过程:分级——红 3(XSS/loadPMXFile/材质启用)+ 黄 15(性能/状态/崩溃)+ 绿 30(代码质量/架构/UI/扩展)。优先级由用户痛感决定。
第四镜厅的灯熄灭的时候,外交官的笔记本已经写满了七页。
八座城邦的第一轮修复、物理引擎的子步重力、议会之墙的定时器泄漏、工具链的 ESLint 引入——所有这些像潮水一样退去,露出了沙滩上的石头:48 项遗留问题。
不是 bug。是"修了更好,但不修也能跑"的东西。是每一个开发者都会在注释里写下 // TODO: fix later,然后 later 永远不来的东西。
外交官坐在议会的圆厅里,面前是一张长桌。桌上铺着一张巨大的纸,他把 48 颗石子摆了上去。
三颗红石子
第一排是三颗红石子。
红石子意味着高风险——现在不炸,迟早要炸。炸的时候可能只是一个用户输入的弹窗,也可能是整个预设系统的数据丢失。
外交官拿起第一颗红石子,放在桌子的最左端:XSS。
"settings.ts 里有几十处 innerHTML,"他说,"用户输入的软件名称、外部库路径、切换开关的 label——这些字符串如果被拼接进 innerHTML,就是一扇开着的门。"
AI 同行者凑过来看:"但这些数据不是都经过后端校验吗?SoftwareEntry、ExternalPath 都是 Go 那边读出来的配置文件,用户改不了啊。"
"问题不在数据从哪来,"外交官摇摇头,"问题在代码的假设。今天这些数据是安全的,明天加一个功能——比如用户可以自定义软件名称——输入路径就通了。代码不会记得自己曾经的假设。"
他用指尖敲了敲那颗红石子:"安全问题不能靠上游干净,要靠自己戴手套。"
第二颗红石子:loadPMXFile 的返回值。
"applyPresetFromLib 加载模型后,用路径去匹配模型注册表,"外交官说,"路径归一化的方式稍有不同——斜杠、反斜杠、大小写——就匹配不上。预设功能直接失效。"
"为什么不直接返回模型 ID?"
"问得好,"外交官点头,"这就是修复方案。但它的问题不在难度,在影响面。loadPMXFile 被五个地方调用——拖拽导入、模型库点击、预设应用、replaceModel、还有调试命令。改返回值要确认所有调用方都适配。漏一个就是 TypeScript 编译错误。"
第三颗红石子:材质启用状态未序列化。
"用户在材质面板里关掉了头发的显示,然后保存预设。下次加载预设,头发又出来了,"外交官拿起第三颗石子,"materialEnabled 没有进 ModelPresetFile。看起来是个小功能缺失,但用户会觉得'预设是坏的'——信任一旦没了,再好的功能也没用。"
三颗红石子一字排开。它们是本轮审计的第一批要处理的对象。
十五颗黄石子
第二排是十五颗黄石子。
黄石子是中等优先级——不致命,但膈应人。性能浪费、体验卡顿、状态不一致。用户不会说"这个软件不能用",但会说"这个软件有点糙"。
外交官把黄石子分成了三堆。
第一堆:性能 & 体验。六颗。
- 缩略图捕获时机太早,低端 GPU 上黑屏
- 布料每帧分配 Float32Array,GC 压力
- 碰撞器每帧归一化方向向量,冗余计算
- 大量模型列表同步构建 DOM,弹窗卡顿
- 舞蹈套装 async 渲染无加载占位,空白闪烁
- 预设场景 async 渲染同样的问题
"这些都是'改了用户不一定知道,但不改用户隐约觉得慢'的东西,"外交官说,"就像鞋底进了一粒沙子——不影响走路,但每一步都不舒服。"
AI 同行者拿起"加载占位"那颗石子:"这个改动小,收益大。加一行 '加载中…' 的事。"
"所以排前面,"外交官点头,"快赢项先做,士气也高。"
第二堆:状态一致性。五颗。
- recreateCloth 在 clothEnabled=false 时静默返回,调用方误解
- 时间流转每帧调 redoEnvAutoLink,微小变化也重算
- 环境状态持久化在页面关闭前可能丢失(beforeunload 验证)
- 刷新模型库强制回顶层,用户导航深度丢失
- 变换面板滑条不随快捷键同步更新
"状态一致性是最容易被低估的,"外交官用手指轻轻划过这五颗石子,"它们每一个看起来都是'小问题',但加在一起,用户就会觉得软件'有点飘'——你不知道它现在显示的是不是真的。"
他停在阴影那一颗上:
"还有一个——阴影在方向光强度低于 0.1 时自动关闭。过渡动画中突然消失,很突兀。"
"不能渐隐吗?"AI 问。
"理论上可以,ShadowGenerator 有 intensity 属性,"外交官皱眉,"但 babylon-mmd 打包的 Babylon.js 版本不确定有没有。有些版本的 ShadowGenerator 没有 intensity。需要验证。"
"验证不通过怎么办?"
"那就去掉硬切换,让阴影随光强自然渐隐,"外交官说,"反正光强低了阴影本来就淡。效果差点,但至少不跳。"
第三堆:崩溃防护。一颗。
- model-detail.ts 中 Stage 等特殊模型的 null safety 边缘场景
"主路径已经修了,"外交官说,"但边缘场景——比如没有 runtimeBones 的模型、纯空模型——还没全覆盖。降为中优是因为主路径稳了,触发概率低。"
十五颗黄石子在桌上排成三列。它们是本轮审计的第二梯队——高风险三项处理完,就轮到它们。
三十颗绿石子
第三排是三十颗绿石子。
绿石子是低优先级——代码质量、微优化、架构整洁。不影响功能,不影响性能,甚至不影响体验。它们影响的是下一个读代码的人的心情。
外交官叹了口气。三十颗,太多了,一颗一颗摆得摆到天亮。
他干脆把三十颗绿石子分成了四堆,用手指在桌上画了四条线。
第一堆:类型与安全。八颗。
未使用导入、空引用守卫、硬编码帧时间、as any 类型断言……都是"代码洁癖"级别的问题。
"未使用导入真的有必要清吗?"AI 同行者拿起一颗,"TypeScript 编译会 tree-shake 掉的。"
"代码不是写给编译器看的,"外交官说,"是写给下一个人看的。一个文件顶部有十个 import,三个没用,下一个人读的时候就得想'这个是不是哪里用到了我没看到?'——认知负担就是这么一点点加上去的。"
第二堆:架构与重构。十颗。
- scene/ 目录 22 个文件平铺,建议按 domain 分子目录
- 焦散纹理在切换粒子类型时重复生成
- 道具面板 5 个滑块可提取为配置数组循环
- settings.ts 的自定义滑动条无拖拽,建议改用原生 range
- 性能模式切换未持久化到后端配置
"这些是'重写比修补省事'的东西,"外交官说,"但重写有风险,也需要整块时间。排到最后是对的——等核心功能都稳了,再动结构。"
第三堆:UI 细节。六颗。
- SlideMenu 键盘焦点在鼠标悬停后丢失
- 移动端 mouseenter/mouseleave 可能闪烁提示
- 过渡时间硬编码,建议统一 CSS 常量
- outfit 点击变体无 loading 反馈
- 涟漪重用逻辑极端多涟漪下可能误覆盖
- 无匹配 blink morph 时静默无调试提示
"UI 细节是那种'你不说用户不知道哪里不对,但就是感觉不精致'的东西,"AI 说。
"对,"外交官笑了笑,"精致感就是 60 个小细节加在一起。但现在还轮不到。先把骨架搭稳,再往脸上抹粉。"
第四堆:功能扩展。六颗。
- 布料仅支持 skirt 拓扑,预留了 tube/cape/rope
- LipSync 仅控制「あ」口型,不支持圆唇音「お」
- VMD 写入关键帧插值硬编码为线性,可扩展贝塞尔
- 预设未包含布料配置/物理开关
- 预设撤销仅支持单模型
"这些都是'有了更好'的功能,"外交官把最后几颗石子归拢,"不是 bug,是可能性。它们的优先级最低,但也许某一天会成为下一卷的主题。"
三十颗绿石子安静地躺在桌子的另一端。它们是联邦的"如果有时间"清单——所有开发者都知道,"如果有时间"永远不会来。但写下来至少有一个好处:你知道自己欠了多少债。
优先级的辩论
石子摆完的时候,议会的钟声敲了三下。
AI 同行者看着桌上的三排石子,忽然问了一个问题:
"你怎么决定哪颗是红的、哪颗是黄的、哪颗是绿的?"
外交官沉默了一会儿。
"有一个简单的判断标准,"他说,"如果这个问题炸了,用户会不会骂娘?"
"会骂娘 → 红。会皱眉 → 黄。会忽略 → 绿。"
"就这么简单?"
"就这么简单,"外交官点头,"优先级不是技术判断,是用户情绪判断。技术上再难的问题,如果用户感知不到,就是绿的。技术上再简单的问题,如果用户一打开就看见,就是红的。"
他指了指 XSS 那颗红石子:
"比如 XSS。真正被攻击的概率很低——毕竟这是桌面应用,没有远程网页注入。但它是红的,因为安全问题不能赌概率。赌赢了 99 次,输 1 次就是全输。"
又指了指"加载占位"那颗黄石子:
"这个是黄的,不是因为技术简单,是因为用户感知明确——'怎么点开是白的?'——但不会造成数据损失。膈应人,但不伤人。"
最后指了指"未使用导入"那堆绿石子:
"这些是绿的,因为用户永远看不见。它们的债主是开发者,不是用户。"
AI 同行者点点头,又问:
"那先做哪个?"
外交官笑了。
"高风险三项先做,这个没悬念。但具体顺序——"他用手指点了点三颗红石子,"XSS 第一。安全问题不能等。loadPMXFile 第二。预设功能是核心体验。材质启用第三。"
"然后呢?"
"然后挑快赢项——加载占位、缩略图优化、布料缓存。改动小,见效快,士气高,"外交官说,"做几个快赢项,团队状态起来了,再啃硬骨头。"
"硬骨头是哪些?"
"列表 rAF 分片、refreshLibrary 导航恢复、变换面板实时刷新——这些涉及状态管理和架构调整的,"他说,"放最后,或者放下一轮。"
报告的最后一页
外交官在审计报告的最后一页写下了这样一段话:
本次审计共发现问题 128 项,当场修复 80 项,遗留 48 项。
遗留问题中,高风险 3 项(XSS、loadPMXFile 返回值、材质启用序列化),中等优先级 15 项,低优先级 30 项。
建议下一轮优先处理高风险 3 项,然后按"快赢项 → 性能优化 → 状态一致性 → 代码质量"的顺序推进。
审计没有终点。每修一个问题,就离下一个问题更近一步。但这不是坏事——问题越少,下一个问题的位置就越清楚。
他合上笔记本,把笔帽按上。
圆厅的窗外,天开始亮了。
AI 同行者打了个哈欠:"所以……现在开始修?"
外交官站起来,伸了个懒腰,看向窗外的八座城邦。
"不急,"他说,"先吃早饭。"
然后他拿起桌上的第一颗红石子——XSS——揣进了口袋。
附录:遗留问题分布统计
| 优先级 | 数量 | 占比 | 典型问题 |
|---|---|---|---|
| 🔴 高风险 | 3 | 6% | XSS 注入、loadPMXFile 返回值脆弱、材质可见性丢失 |
| 🟡 中等 | 15 | 31% | 性能浪费、UX 闪烁、状态不一致、崩溃防护边缘 |
| 🟢 低优 | 30 | 63% | 代码质量、微优化、架构整洁、功能扩展 |
| 合计 | 48 | 100% | — |
教训:优先级不是技术难度的排序,是用户痛感的排序。最痛的最先修,不管代码好不好写。