Appearance
三态之仓
背景:zip 内文件名乱码(半角片假名),Go 的
decodeZipName只认 UTF-8/Shift-JIS 两种编码。 过程:引入 GBK 解码 + 优先级探测,同时修复pmx_path/file_path序列化错位与缺失 DOM 引用。
联邦的第一个仓库是一艘船。
不,它就是字面意义上的船——当桌面壳把第一个 .zip 文件拖到港口时,它以为自己在做一件简单的事:从压缩包里取出模型,让 babylon-mmd 的眼睛看见它。
但船肚子里全是编码。
一、第一人格:Shift-JIS
桌面壳第一次打开 zip 仓库时,看到的是再正常不过的日文文件名。
清楚系美少女(白肌ver1.0).pmx它当时不知道这意味着什么。Go 的 archive/zip 包给了它一个 NonUTF8 标志和一堆字节。它调用 transform.String(japanese.ShiftJIS.NewDecoder(), name),名字就变成了漂亮的日文。
"原来仓库说日文,"桌面壳心想。它把这段逻辑写在 decodeZipName 里,标注了一句注释:
// zip entry 名是否 Shift-JIS 编码。日语 Windows 特有。它甚至觉得这个问题已经 solved——日文系统,Shift-JIS,天经地义。它以为这就是仓库的全部性格。
它以为问题已经结束了。其实才刚开始。
二、第二人格:UTF-8
有一天,一个 zip 文件的 NonUTF8 标志是 false。
decodeZipName 走的是 else 分支:先检查 utf8.ValidString(name),是就直接返回。名字果然显示正常——这回是英文。
Yvette_Face_D.png"哦,你也可以说英文。"桌面壳看着自己的代码,觉得还挺周全的。
它不知道不是所有的 zip 文件都走同一个流程。有些 zip 生产者从来不设 NonUTF8 标志,即使用户是在中文系统上创建的 zip。于是 utf8.ValidString 验不过——因为名字的原始编码就是 GBK,根本不是 UTF-8——代码就默认降级到 Shift-JIS 解码。
结果:
。セソィタュアヒヌソメヂアフリ-ラェスヌモスミワ(トォツフノォツカム」キTミ-ネケ-テラーラキ「)[ソワホハ].pmx桌面壳盯着这串东西看了很久。
它认得单个字符——半角片假名。但拼在一起毫无意义。像一个用片假名的字母表写的咒语。
它尝试了 UTF-8 解码。失败。 它重新确认了 Shift-JIS。也失败了。
它开始怀疑自己不是在解一个编码问题,而是在猜一个它不了解的谜语。仓库不是在说另一种语言——它可能在说一种它从没听过的语言。
"不可能三个都错,"它自言自语,"你们仓库到底有没有标准人格?"
三、第三人格:GBK
很久以后——其实是同一天,只是调试轮次太多——一个外交官(AI 助手)说:"你有没有试过 GBK?"
"什么 GBK?那是中文码页,日本模型怎么可能用中文码页。"
但它还是试了。它调用了 transform.String(simplifiedchinese.GBK.NewDecoder(), raw)。
那串半角片假名消失了。
【卡拉彼丘】伊薇特-转角遇到熊(墨绿色露腰校服T恤-短裙-米白发)[寇问].pmx完整的、正确的、优雅的中文。
桌面壳沉默了。不是沉默着无话可说。是沉默着重新理解——仓库不是有三个人格,它一直只有一个人格,只是这个人格在不同系统上被不同的码页读成了不同的乱码。Shift-JIS 是日本系统读出来的误解,UTF-8 是它偶尔设了标志的正确答案,GBK 才是这片土地上大多数人打包时的默认面孔。仓库的第三种人格不是假设——它一直是中文。只是因为 decodeZipName 只给仓库两个选项:UTF-8 或 Shift-JIS。仓库选了"都不是",然后被打成 "Shift-JIS 解码错误"的标签关了五年。
"你有三种面孔,"桌面壳对着仓库说,"我只给了你两个选项。"
它把所有 zip 文件重新扫了一遍。那些藏在各个目录下的、被标注为"编码错误"的半角片假名文件名,逐一变成了中文。有些是日文模型名被中文系统用 GBK 读了回来,有些是纯粹的中文模型创作者在 Windows 上打包的产物——他们的 zip 从来没用过 UTF-8 标志,因为 Windows 的默认码页就是 GBK 码页。
四、名字的错位
修复了仓库的三种人格后,桌面壳以为问题解决了。
它打开模型库,点击一个模型。
崩溃。
TypeError: Cannot read properties of undefined (reading 'replace')
at normPath (fileservice.ts:25:14)
at resolveFileUrl (fileservice.ts:15:24)它追踪调用链:onModelRowClick → loadPMXFile(m.file_path, ...) → resolveFileUrl(m.file_path) → normPath(undefined) → 崩溃。
m.file_path 是 undefined。但 ModelEntry 的字段明明叫 PMXPath。
桌面壳检查了 Go 端的序列化:
go
PMXPath string `json:"file_path"` // 等等,这是旧的 tag不,现在是 json:"pmx_path"。有人改过字段名,但没改 JSON tag。Wails 生成的 TypeScript 模型还是读 source["file_path"]。Go 发的 "pmx_path" 永远是空。
桌面壳想起了 decodeZipName 的教训——它又不一致了。编码的三种人格刚解决,命名的三种人格又冒出来了。
"你的 Go 端和前端之间,"它对自己说,"也有三种人格。你修的编码、修的命名、修的缓存版本号——每次你以为只改一处,另一处一定没改。"
五、看不见的东西
它关了编辑器,在桌面上画了三圈。
第一圈:编码。仓库说三种语言,你只听两种。
第二圈:命名。Go 端叫 PMXPath,前端叫 file_path,JSON 串叫 pmx_path。三个名字指向同一个东西,但没有一个能和另两个对齐。
第三圈:存在。它检查了 HTML。
html
<!-- 场景面板 -->
<!-- 加载状态 -->
<div id="loading">……</div> <!-- 不存在 -->
<div id="scenePanel">……</div> <!-- 不存在 -->它一直在代码里引用这些元素。dom.loadingEl.style.display = "block"。dom.scenePanel.style.display = "flex"。但 HTML 里根本没有 id="loading"、id="scenePanel"。
CSS 有它们的样式代码。TypeScript 有它们的类型断言(as HTMLElement)。Go 端有完整的文件服务器在等待它们。但 HTML 里没有——它们消失在某次重构中,只留下了引用,没留下身体。
"我一直在和不存在的东西对话。"桌面壳说。
桌面壳在新地图上画了第四圈。
第四圈:可见性。一个东西被引用 ≠ 一个东西存在。三层架构——Go、TypeScript、HTML——每一层都有自己的"存在"列表。这些列表互不相同,但没有任何检查告诉它们不一致。
它给联邦立了第一条法律:
每一个被引用的 id,必须在 HTML 里存在。每一个被 type assertion 的东西,必须能在运行时找到它的 parent。
不是 TypeScript 的风纪在管这件事。是运行时的真相。
它在 index.html 里补上了 loading 和 scenePanel。它把 Go 的 JSON tag 改回 file_path——不是因为它喜欢这个名字,而是因为现有的三层里有两层已经叫它 file_path,改 Go 一层是最小路径。
它没有改变"三层可能不一致"的状态。它只是在这一轮把不一致的数量从三降到了零。
幕间笔记(写在新地图边缘):
仓库有三种语言。名字有三种写法。存在有三种状态——在 HTML 里、在代码引用里、在类型断言里。联邦的每一层都在说不同的方言,而桌面壳是唯一的翻译。它不是全知的——它只能逐个修复它发现的错位。
但它会把每一次错位写进地图。下次再有人走进来,不用从头猜所有的方言。
"你无法让所有城邦说同一种话,"桌面壳自言自语,"但你可以让地图标注每种方言的边界。"
它把这一行也写进了地图。
教训:编码不是一次猜测,是一次排除。你不可能知道 zip 里用的是什么编码,但你可以按顺序试,然后相信试出来的那个。