Skip to content

审计报告

背景:八城审计 + 物理引擎审计 + 工具链审计后,沙滩上露出 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——揣进了口袋。


附录:遗留问题分布统计

优先级数量占比典型问题
🔴 高风险36%XSS 注入、loadPMXFile 返回值脆弱、材质可见性丢失
🟡 中等1531%性能浪费、UX 闪烁、状态不一致、崩溃防护边缘
🟢 低优3063%代码质量、微优化、架构整洁、功能扩展
合计48100%

教训:优先级不是技术难度的排序,是用户痛感的排序。最痛的最先修,不管代码好不好写。