Appearance
衣橱的自动发现
背景:换装依赖手工指定变体,模型目录新增服装文件后 UI 不刷新;快速切换模型时旧请求覆盖新模型。 过程:loadOutfits 自动发现子目录变体 + LoadingGuard 去重 + AbortSignal 取消过期请求,换装列表随目录实时更新。
"我的衣橱呢?" 桌面壳问。
"什么衣橱?"
"这个模型的衣服。它应该有——同款模型一般都有几件衣服:默认的、A1、A2。但我打开换装菜单,里面空空如也,只有一行'无可用变体'。"
外交官走到模型目录看了一眼:
"衣服在。磁盘上有个 A1/ 子目录,里面躺着纹理。但联邦不知道它的存在。"
"联邦不去看?"
"不去看。" 外交官说,"换装系统以为变体列表是别人塞给它的——它等着被通知,从不主动去翻衣橱。衣服放进了柜子,但没人告诉它柜子开了。"
桌面壳看着那行"无可用变体",忽然明白了一个道理:
"衣橱不是拿来摆的,是拿来翻的。变体不会自己举手报到——它藏在磁盘的某个角落,等着被看见。"
一、翻衣橱
桌面壳找到了 loadOutfits。
它发现换装系统确实在翻——只是翻的方式很被动。它先尝试读一个 JSON 清单文件(LoadOutfitFile),如果清单合法(有 version、有 variants 数组),就用清单;清单不存在或损坏,就放弃,直接返回"无变体"。
"清单是捷径,不是全部。" 桌面壳说,"模型目录里明明有 A1/A2 子目录,清单里没有——联邦就装看不见?"
它加了第二条路:清单失败时,去翻目录本身。
typescript
// 第一步:读清单文件(快路径)——有 version + variants 就用
// 第二步:清单缺失/损坏 → 扫描模型目录的子目录(ListSubDirs)
// 每个子目录名就是一个变体名 → 自动发现
const subdirs = await ListSubDirs(modelDir);"衣橱的正确用法是:先翻清单,清单不在,就翻柜子。柜子里有几个格子,就有几件衣服——不需要谁预先登记。"
"为什么以前不翻柜子?"
"因为翻柜子要跑磁盘 IO——ListSubDirs 是个异步的、可能慢的调用。图省事,就只信清单。但省事的代价是:任何没有清单的模型,衣橱永远是空的。"
"省事,有时候就是偷懒。"
"是。" 桌面壳说,"衣橱会自动发现——这是'变体'两个字里'自动'的分量。你放一件新衣服进柜子,联邦就该看见它。"
二、抢衣橱
但自动发现带来一个新问题:抢。
桌面壳快速切换了两个模型——先点模型 A,再点模型 B。
"模型 A 的变体还在扫描,模型 B 的请求就来了。" 它看着控制台,"两个 loadOutfits 同时跑。先到的 A 扫完了,把结果写进……A 的实例——还好是按模型 ID 分开存的。"
"那抢在哪儿?"
"抢在同一个模型的重复请求上。" 桌面壳说,"用户手快,点了一个模型两次——两个扫描请求同时启动,同时写同一个实例。谁先完成谁赢,后完成的覆盖先完成的——浪费磁盘 IO,还可能写一半。"
它加了一把锁:
typescript
// LoadingGuard:同一模型同时只允许一个加载在跑
if (!_loadingOutfitsGuard.tryEnter(id)) {
return null; // 已有加载在进行,直接放弃本次
}"tryEnter 是衣橱的开门检查:有人已经在里面翻了,你就在外面等——不,不是等,是直接放弃。反正扫描结果会写入同一个实例,先到者负责,后到者不必重复劳动。"
"放弃不是偷懒?"
"放弃是复用。" 桌面壳说,"同一个模型,同一件衣服,翻两遍柜子没有任何意义。一扇门一次只进一个人——这是衣橱的秩序。"
三、过期的请求
锁解决了重复,但没解决"过期"。
"场景是活的。" 桌面壳说,"用户切走模型 A,切到模型 B,又切回 A——A 的衣橱已经翻过了,结果还在。但如果用户把模型 A 删了,加载还没回来呢?"
"会怎样?"
"扫描完成,modelRegistry.get(id) 返回 undefined——写进一个不存在的实例。或者更糟:请求回来时,用户已经在看模型 B 了,A 的扫描结果突然刷进 UI,把 B 的列表盖掉。"
它加了 AbortSignal:
typescript
// adr-105:允许外部取消;每个 await 点前检查取消状态
if (effectiveSignal.aborted) {
return null; // 请求已过期,及时收手
}"每个 await 之间,都看一眼信号:'我还在被需要吗?'——被取消了,就默默退出,不写、不刷、不打扰。"
"这像什么?"
"像传令兵。" 桌面壳说,"命令取消的那一刻,哪怕你已经跑到了半路,也要停下——不要把一个过期的消息送到战场。UI 是瞬息万变的,加载是慢吞吞的;慢的请求,必须学会在快的变化里认输。"
尾声
桌面壳重新打开了换装菜单。
这一次,衣橱满了。A1、A2、默认——每一个子目录都变成了一格,列在列表里。它往模型目录里丢了一件新衣服的文件夹,菜单刷新,新变体出现了。
"现在它自己会翻了。" 桌面壳说,"清单不是必须的,登记不是必须的——衣服放进柜子,联邦就看得见。这叫自动发现。"
"那快速切换呢?"
"有锁,有信号。" 它说,"同一时刻一个模型只翻一遍柜子;过期的请求自己认输。衣橱有秩序,也有礼貌。"
外交官看着那个塞满衣服的模型:
"你最开始说,衣橱是空的——其实衣服一直在,只是没人去翻。"
"对。" 桌面壳说,"'无可用变体',大多数时候不是没有变体,是没有去发现。联邦最不该说的话,就是'没有'——当它其实只是'没找'的时候。"
它关掉了换装菜单。衣橱静静地躺着,等着下一件衣服被放进去。
"自动发现," 它轻声说,"是联邦对'变化'的最低敬意——世界会变,我要看见。"
教训:「无可用变体」大多不是没有变体,是没有去发现——清单是捷径,柜子才是真相。
过期的请求要会认输:每个 await 之间,都问一声「我还在被需要吗」。