Bundle
@dsh-external/dsh-client-ui-eva-unit01
EVA Unit-01 desktop pet for the dsh web GUI: draggable full-body Unit-01 in the frame overlay, NERV sync-rate HUD driven by live session state, idle animations, berserk mode and synthesized SFX
- Source
- AlexZhou19871030
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 14 days ago
Readme
# EVA 初号机桌宠(dsh web 常驻插件)
在 dsh Web GUI 里放一只可拖动的 **Q 版(Funko Pop 风)EVA 初号机**,带 NERV 同步率 HUD 与一套
离散动作。纯浏览器面插件:宿主半程是空 `apply`,浏览器半程通过 `package.json` 的 `dsh.client`
声明被 web 服务端发现并下发。
形体(T12 起换了表现形式):**机体不再是手写矢量块面,而是参考插画本身**。`pet-ref/slice-rig.py`
把 `pet-ref/reference.png` 沿实测装甲缝切成 16 层刚体部件(切缝取自逐行游程间隙与颜色族连通域,
不是目测),16 层并集与描出的躯体掩膜完全相等:0 像素丢失、0 像素臆造。每层按 2 倍屏幕尺寸导出并以
data URI 内嵌,插件仍是单文件、无需资源路由。viewBox `0 0 300 340`、渲染 240x272,躯体占
227.68 x 291 单位、宽高比 0.782 —— 就是参考图自己的比例。眼睛保留矢量(画在切片器补绘过的眼窝上),
情绪变色/发光/眨眼照旧;脐带电缆保留矢量,因为它必须弯折。
## 挂载位置与层级(不要随便改)
桌宠注册在 `shell.overlay` slot,但渲染内容用 `react-dom` 的 `createPortal` 投到
`document.body > div.eva01-portal`。**这不是为了好看,是必须的**:`shell.overlay` 的祖先
`overlayLayer` 是 `z-index:20`,而 `dsh-better-sidebar` 的右侧面板 `.W-zNGW_panel` 与底部面板
`.W-zNGW_bottomPanel` 都是 `z-index:50` 且 88% 不透明 —— 桌宠在那个层叠上下文里**无论把自己的
z-index 抬到多高都会被盖住**(实测 `z-index:2147483000` 同样无效),连点击事件都被面板吃掉。
层级取值 **60**,有实测依据:全页常驻层只有 `toggleCluster 55` / `panel 50` / `bottomPanel 50`,
而宿主的模态框、遮罩、toast 在 **100–1100**。所以 60 恰好压过所有常驻面板,又仍在所有模态类 UI
之下(报错弹窗永远盖在桌宠之上)。容器 `pointer-events:none` + 桌宠本体 `pointer-events:auto`,
保证这个全屏容器不吃任何页面事件。默认落点在**左下** `left:24px;bottom:84px`,避开右侧面板内容;
初始朝向即镜像(`is-left`),与「左半边镜像」规则一致,避免第一次拖拽时凭空翻面。
## 交互
| 操作 | 结果 |
| --- | --- |
| 拖动 | 移动,限制在窗口内;窗口缩放后自动钳回视口;跨过屏幕中线整体镜像 |
| 松手 | 落地压扁回弹 + 低频落地音 |
| 单击 | 按当前情绪随机台词 + 机体回弹 + blip,34% 概率点头 |
| 双击 | 暴走 8 秒:蓄力 → 咆哮(下颌张开)→ 红眼、剧烈抖动、红色 AT 场、线缆火花、同步率 400%、内部电源倒计时 |
| 悬停右上 `♪` | 开关音效(Web Audio 现场合成,无音频资源;不可用时静默降级) |
| 悬停右上 `–` | 收成小紫球,点球体恢复 |
| 什么都不做 | 每 7–18s 随机插一个 idle break 动作(见下) |
## 动作系统
两层:**情绪循环**(永不停,`float` 整体浮动 13px / `sway` 摆头 ±5° / `breathe` 呼吸 1.055 /
双臂 ±7° / 线缆 ±6° / 肩甲反相 ±2° / 头侧绿鳍 ±4°(BERSERK 下改为 0.34s steps 抽动)/
地面阴影跟随),叠加**离散一次性动作**。只有循环时,旁人
会觉得「没在动」——离散动作才是能被一眼看见的节拍。
共 **26 个离散动作**。每个动作都是「三件套」:`ACTIONS` 表项 + `@keyframes eva01-*` +
`.eva01-root.is-act-<名字>` 选择器(注意是 `is-act-` 前缀,组件里拼的是 `cls += " is-act-" + act`)。
下表的时长/优先级/门控/权重/音效 kind 均取自 `lib/client.js` 的 `ACTIONS` 表实际值。
| 动作 | 时长 | 优先级 | 情绪门控 | idle 权重 | 触发 | 音效 kind |
| --- | --- | --- | --- | --- | --- | --- |
| `wave` 挥手 | 1150ms | 2 | idle/active/pending/tool | 3 | 挂载 900ms 后打招呼;进入 STANDBY | `wave` |
| `nod` 点头 | 780ms | 2 | idle/active/pending/tool | 2 | 单击 34% 概率 | `nod` |
| `deny` 摇头 | 760ms | 3 | 任意 | 0 | 进入 ALERT | `deny` |
| `look` 左右张望 | 2400ms | 1 | idle/pending | 4 | idle break | `look` |
| `stretch` 伸展 | 1700ms | 1 | idle | 3 | idle break | `stretch` |
| `hop` 原地小跳 | 900ms | 2 | idle/active/tool | 3 | idle break | `hop` |
| `walk` 碎步 | 1100ms | 1 | idle/active | 2 | idle break | `step` |
| `stomp` 踏步 | 840ms | 3 | active/tool/berserk | 0 | 进入 ACTIVE / OPERATE | `stomp` |
| `charge` 蓄力 | 1500ms | 5 | 任意 | 0 | 进入 BERSERK | `charge` |
| `roar` 咆哮 | 1600ms | 5 | berserk | 0 | 蓄力后 1560ms 自动接上 | `roar` |
| `salute` 敬礼 | 1250ms | 2 | idle/active | 1 | 作业完成回到 IDLE | `salute` |
| `shiver` 颤抖 | 900ms | 4 | 任意 | 0 | ALERT 12%(T18) | — |
| `slash` 振动刀挥砍 | 1050ms | 4 | active/tool/berserk | 0 | 进入 ACTIVE/OPERATE 25%(T18) | `prog` |
| `unbind` 撕开口枷 | 1400ms | 5 | berserk | 0 | 暴走链第 2 拍(1560ms,T18) | `jaw` |
| `guard` A.T. 场防御 | 1200ms | 4 | 任意 | 0 | ALERT 18%(T18) | `atfield` |
| `shutdown` 电源耗尽停机 | 1800ms | 5 | 任意 | 0 | 暴走 8s 窗口结束(T18) | `lifeless` |
| `turn` 原地转身 | 900ms | 2 | idle/active | 2 | idle break | `pivot` |
| `crouch` 下蹲 | 1250ms | 2 | idle/tool | 2 | idle break | `crouch` |
| `scan` 传感器扫视 | 2200ms | 1 | idle/pending | 3 | idle break | `look` |
| `tapcore` 敲核心 | 1150ms | 2 | idle/active/tool | 1 | idle break | `poke` |
| `tiptoe` 踮脚 | 1400ms | 1 | idle | 2 | idle break | `tiptoe` |
| `rebound` 受击回弹 | 800ms | 3 | active/tool/alert/berserk | 0 | 使徒计数上涨(T18) | `thud` |
> T18 已组接:上表原本标「预留」的 6 个动作全部报上了真实触发点(见 T18 節);新增的 `audit-triggers.cjs` 闸门保证不会再出现只有关键帧、永远跑不到的动作。
互斥规则:**暴走/蓄力/撕枷/停机(5) > 颤抖/挥砍/防御(4) > 情绪反应/回弹(3) > 打招呼(2) > idle break(1) > 情绪循环**。
运行中的动作会拒绝优先级更低或相等的请求,所以两个动作永不争抢同一个 transform;拖拽中一律拒绝
(机体本身就是拖拽把手)。每条关键帧的 0% 与 100% 都回到 identity,class 摘掉时不会跳变。
调试触发钩子(供自动化验证用,非 UI 功能):
```js
window.__eva01.play('hop') // -> boolean,被情绪门控/优先级拒绝时返回 false,未知动作名返回 false
window.__eva01.actions // 动作名列表
window.__eva01.state() // { mood, act, drag }
```
## 音效
全部**现场合成**:振荡器 + 一块共享白噪 buffer 过 biquad,插件不带任何音频资源文件。
共 44 个 kind,硬约束由 `tests/audio.mjs` 断言:单层峰值 ≤ `GAIN_CAP 0.12`、单个配方
`delay + dur ≤ 2s`、同一 kind 的重叠峰值 ≤ 0.26,母线 `BUS_GAIN 0.75`。最响的仍是 `roar`
(0.24 合计 → 出口 0.18)。设计依据与逐层理由见 `SFX_RESEARCH.md`。
T8 新增 15 个 kind(`THROTTLE` 为同一 kind 两次播放的最小间隔):
| kind | 语义 | 挂载 | throttle |
| --- | --- | --- | --- |
| `prog` | 振动刀出鞘,双高频拍频即刃振 | `slash` | 500ms |
| `jaw` | 暴走前撕开口枷 | `unbind` | 900ms |
| `battery` | 内部电源倒计时双响 | `shutdown` + ALERT 周期 | 1000ms |
| `pivot` | 髋部伺服转向,含制动卡榫 | `turn` | 350ms |
| `crouch` | 液压下沉 + 落定 | `crouch` | 600ms |
| `tiptoe` | 踮步,响度仅 `step` 的一半 | `tiptoe` | 300ms |
| `pulse` | 生体心搏,S1/S2 强弱不等 | 呼吸循环重音周期 | 1400ms |
| `s2` | S² 機関驻留,55/56.5Hz 拍频=双螺旋 | BERSERK 周期 | 6000ms |
| `umbilical` | 三点式插头就位 + 送电涌入 | 待接 | 800ms |
| `unplug` | 电缆断开 → 切内部电源 | 待接 | 900ms |
| `atburst` | A.T. 场碰撞显形,3:2 双环鸣 | 待接 | 400ms |
| `lcl` | Entry Plug 注液 + 电解 | 待接 | 3000ms |
| `lifeless` | 电源耗尽停机,末声钝 240Hz | 待接 | 2000ms |
| `magi` | MAGI 三体多数决,全表最轻 | 待接 | 700ms |
| `sync` | 同步率分级建立,四级台阶 | 待接 | 1200ms |
三个**状态型**音效不走 `ACTIONS[x].sound`,而是在情绪层周期请求、靠 `THROTTLE` 自然限流:
`battery` 在 ALERT 下每 0.9s 请求一次、1s 节流 → 实际约 1.8s 一响;`s2` 在 BERSERK 下每 0.9s
请求、6s 节流 → 约 6.3s 重新起一次驻留;`pulse` 每 1.5s 请求,BERSERK 每次都过(心率飙升),
其余情绪过 35% 概率门 → 平均约 4s 一拍,ALERT 下让位给电池倒计时。
标「待接」的 7 个已实现并通过全部断言,只是还没有触发点(与早前的 `power` / `hum` 同一处理方式)。
## 状态联动
情绪读自两个来源:`shell.overlay` 标准 prop `useSessions`(会话列表:谁在跑、谁在等你),以及**当前会话的对话装配**——插件自己用客户端 `sessions` 服务把 `sessions.binding(current).session` 的 `ConversationSnapshot` 订阅下来。同步率每 0.9s 向目标值收敛:
| 情绪 | 触发 | 目标同步率 | HUD |
| --- | --- | --- | --- |
| `IDLE` | 其余 | 60 | `UMBILICAL LINKED` / `TOOL n` |
| `ACTIVE` | 当前会话 running,或存在任意 running 会话 | 88 | `ACT n` / `TOOL n` |
| `STANDBY` | 当前会话有 `pendingInteraction`(等你授权/回答) | 71 | — |
| `OPERATE` | 当前会话有正在执行的工具调用 | 94 | `OP <工具名>` |
| `ALERT` | 当前会话出现**新的** agent 错误 | 33 | `⚠ PATTERN BLUE ×n` |
| `BERSERK` | 双击 | 400 | `PWR mm:ss` |
优先级:`BERSERK` > `ALERT` > `OPERATE` > `ACTIVE` > `STANDBY` > `IDLE`。
派生规则(`readFeed`):
- `TOOL n` —— 窗口内 `tool-result` 节点数 + 当前在跑的调用数。
- `⚠ PATTERN BLUE ×n` —— 失败的 `tool-result`(`isError`)与 `turn-error` 节点计为「使徒」。
- `ALERT` —— 由 `lastAgentError` **文本变化**驱动,警报持续 8 秒后自动解除并播报「使徒歼灭」。订阅建立时读到的那条错误只作基线,不报警,所以刷新页面不会为历史失败拉响警报。
- 流式 token 不触发重算:`feedSig` 只在节点数、在跑调用、错误文本变化时才重新折叠快照。
## 安装
```
dsh plugin --profile web add link:E:/DSH/plugins/eva-unit01
```
该命令会同时把包名追加到 profile `package.json` 的 `dsh.profile.bundles`。bundle 列表在启动时
读取,**需要重启 dsh 才生效**。
验证组合树(不启动应用):
```
dsh --profile web --dump-config
```
## 测试
```
node tests/smoke.mjs
```
用假的 `window.__ModuleLoader__` 与假 `useSessions` 快照,跑真实 React 渲染。覆盖:
- 模块形状、导出、样式表只插一次、Slot 注册参数、`inject` 含 `slots` 与 `sessions`;
- `readFeed` 快照折叠 6 例(空会话、落地工具计数、失败工具与 `turn-error` 计为使徒、在跑调用取名并计数、错误文本、未知形状降级为空);
- `feedSig` 的流式抑制:同一节点集下 partial 变化不重算;
- `subscribeFeed` 真实订阅路径:首读作基线、后续变化上报、token-only 变化不重复通知、disposer 解绑、无 sessions 服务降级、`binding()` 抛错降级;
- `moodOf` 六档优先级,且标签与同步率互不重复;
- `Hud` 七种状态的实际标记(`OP <工具名>`、`TOOL n`、`ACT n`、`⚠ PATTERN BLUE ×n`、`PWR mm:ss`、进度条宽度);
- 4 种会话快照的整机渲染,以及 `apply()` 注册的 overlay 入口能真实渲染;
- 样式表里每个情绪态都有对应状态规则,避免加了状态却没有视觉。
SSR 不跑 effect,所以 `OPERATE`/`ALERT` 的验证走两条可测缝:订阅逻辑用假 session face 直接驱动,
HUD 标记用独立的 `Hud` 组件按状态渲染。
## 设计说明:为什么不走宿主事件
早期动态插件版本用宿主 `agent/error` 与 `tools/result` 事件驱动 ALERT 与 TOOL 计数。常驻插件的
浏览器半程没有动态包那种 Package 私有 `host.call` 通道,而 `@deepseek-ai/dsh-api-remotes` 的转发
白名单(`API_REMOTE_FORWARDED_EVENTS`)是构建时固定的、不含这两条事件——不改动 deployment 自带的
安装就加不进去。
但这两项能力**不需要**宿主通道:浏览器本来就持有当前会话的完整装配。`ConversationSnapshot` 直接带
`nodes`(含 `tool-result` / `turn-error`)、`runningCalls` 与 `lastAgentError`,所以改成从客户端
快照派生,反而比事件驱动更好:
- 刷新页面后计数不丢(快照是权威,不依赖曾经收到过哪条事件);
- 能显示**当前正在执行的工具名**,事件计数做不到;
- 零宿主侧改动,宿主半程仍是空 `apply`。
代价:计数限于当前加载窗口内的节点(`loadOlder` 之前的历史不计入)。
## T12:表现形式换轨 + 通知层(本轮)
### 1. 为什么换成贴图骨架
手写矢量追插画追不上:换轨前用同一像素管线量测,桌宠 vs 参考图是 aspect 0.912/0.782、头高占比
45.0%/35.5%、头宽高比 1.656/1.874、颈线 0.449/0.354 —— 四项全偏,而且每改一次都要重打上百个
路径坐标。改成「切参考图」后,比例不再需要调:等比贴图天然带着参考图的比例。
### 2. 验收数据(pet-ref/compare-fidelity.py,双图同一管线,不用硬编码常量)
| 指标 | 参考图 | 桌宠渲染 | 判定 |
| --- | --- | --- | --- |
| 宽高比 | 0.782 | 0.782 | OK |
| 头宽占比 | 85.14% | 85.24% | OK |
| 头高占比 | 35.55% | 35.67% | OK |
| 头宽/头高 | 1.874 | 1.869 | OK |
| 颈线位置 | 0.354 | 0.355 | OK |
| **轮廓 IoU** | — | **0.9908** | 形状即参考图 |
`measure-pet.py` 里那组 `REF` 常量已被证伪:它的 `legsplit_frac 0.744`,用同一管线量参考图自己
得到的是 0.998 —— 该指标本身不可用,已改由 `compare-fidelity.py` 现场量测双图。
### 3. 关节表(由切片 bbox 导出,CSS transform-origin 与之一致)
| 部件 | 关节 | 部件 | 关节 |
| --- | --- | --- | --- |
| head | 150,122.7 | jaw | 162.8,122.7 |
| chest | 168.6,241.6 | inner | 150,317 |
| armL | 112,166 | armR | 227,166 |
| legL | 104,241.6 | legR | 228,241.6 |
| pylonL | 105,142 | pylonR | 220,142 |
| finL | 115.2,69.5 | finR | 228.3,69 |
| greaveL | 100,267.2 | greaveR | 233,269.1 |
| footL | 98,293.3 | footR | 240,304 |
| core | 165,166.2 | — | — |
### 4. 气泡与提示音(三态 + 两声)
| 时机 | 气泡 | 音效 | 说明 |
| --- | --- | --- | --- |
| 工作中(进入 ACTIVE) | `LINES.active` 随机 | `up` + `sync` | 原有 |
| 工作中(进入 OPERATE 工具调用) | `LINES.tool` 随机 | `magi` | **新增**:此前直接进工具态时桌宠一言不发 |
| 需要授权(STANDBY) | `LINES.pending` 随机,5.2s | **`authreq`** | 新音效:G6→C7 上行四度问句,问两遍 + G5 尾音 |
| 任务结束(回到 IDLE) | **`LINES.done`** 新池 5 条 | **`taskdone`** | 新音效:C6-G5-C5 下行终止式 + 八度铃 |
| 闲时 | `LINES[mood]` | `blip` | 定时器改为 26-46s 自重排(原先固定 46s 像节拍器) |
任务结束的动作节拍改为 45% `cheer` / 55% `salute`,避免连续完成时一直敬礼。`down` 音重新接到
警报解除(下降双音正是「降级」语义),音效接线审计因此回到 44 kinds / 44 已接线。
### 5. 暴走态色彩处理(依据第二张参考图的实测配色)
第二张暴走参考图是满幅插画(白底仅 0.3%,血红背景 `#da3533` 占 12.7%),**抠不出干净的角色掩膜**
(试得的掩膜填充率仅 0.52 且贴到画面底边),因此本轮不切第二套贴图,只取它的权威配色做色彩分级:
角色主紫实测 `#393172`(平时 `#5e369c`)、灰 `#616371`、绿 `#4a8850`、暗部 `#201e2a`。落地为
`.eva01-root.is-berserk .eva01-inner{filter:brightness(.76) saturate(.88) contrast(1.14)`
`drop-shadow(0 0 9px rgba(216,53,51,.55))}` —— brightness(.76) 把 `#5e369c` 压到约 `#472a76`,
红色投影补上背光;眼色与 A.T. 场原本就走 `--eva-*` 变量切换。
### 6. 新增 4 个动作 / 3 个音效
`finflare`(鳍片外张,1300ms,音 `plate`)、`armcross`(双臂交叠,1400ms,音 `latch`)、
`lookback`(回望:眼先动 → 头转 → 鳍延迟 → 电缆最后跟,2000ms,音 `pivot`)、`cheer`(跃起举臂,
1300ms,音 `comms`)。三个新音效按 `SFX_RESEARCH.md` 的分层规则(瞬态 → 本体 → 尾音)推导。
### 7. 本轮的诚实边界
- 本机 `web_search` 未配置 `DEEPSEEK_API_KEY`,**本轮没有任何新的联网来源**;新音效是声学推导而非引用。
- 本机模型无图像输入(`describe_image` 插件 baseURL 无效、modlens 超时),所有形象判断都是像素统计,
不含任何「我看了一眼觉得像」。视觉终审仍需你本人看一眼 GUI。
- 暴走形象目前只是色彩分级 + 既有开颌/獠牙/抖动,不是第二套贴图;要复刻那张图的姿态,需要一张
白底或透明底的暴走立绘。
## T13:切片没扣干净的修复(用户报告)
用户截图指出机体带白斑/白边。查证后是切片器 v1 的两个真实缺陷,不是渲染层问题:
| 缺陷 | 成因 | 证据 | 修复 |
| --- | --- | --- | --- |
| 胸口一块白斑 | v1 对躯体掩膜用了 `binary_fill_holes`,把「近臂与躯干之间被围住的白色背景空隙」当前景填了进去 | `chest.png` 内含 54x71 纯白实心块,2189 px,均值 RGB(254,254,254) | 不再填洞;只补 <=8px 针孔,6 处真实空隙保持透明 |
| 一圈白毛边 | 白底判定 `min(rgb)>238` 太窄,白页与黑轮廓之间的反锯齿过渡行被算成躯体 | 贴图边缘近白像素占 4.8%(`legL` 11.6%、`eyeL` 11%) | 近白判定放宽到 `min(rgb)>225` 且按连通域判定页面,再对 alpha 内缩 1px 丢掉混合行 |
修复后量测(`slice-rig2.py` 自带审计):
| 指标 | v1 | v2 |
| --- | --- | --- |
| 边缘近白像素占比 | 4.8%(650/13422) | **0.1%(18/18942)** |
| 单层最大白斑 | 2189 px | **0 px** |
| 半透明边缘中的近白 | — | **0.47%(19/4053)** |
| 不透明近白像素 | — | **0** |
| 合成到 GUI 面板 #081840 后的亮边像素 | — | **16 层全部为 0** |
| 轮廓 IoU | 0.9908 | 0.9880(1px 内缩的代价,5 项比例仍全 OK) |
体积同时下降:base64 278KB -> 265KB,client.js 408,698 -> 395,758 字节。
回归:`tests/audio.mjs` 3597 断言、`tests/smoke.mjs`、`audit-classes`、`t7-crosscheck.mjs` 902 断言全过。
### T13b:脸上被啃出透明缺口(用户第二次报告)
用户报告头部出现应为黑色却透明的区域。查证:v2 掩膜共 6 个洞,其中 **5 个在脸上**
(最大 985 px,x209-244 / y255-297,原图均值 RGB(142,54,116) —— 实心美术),只有 1 个
(x380-468,RGB 249,249,249)是真背景空隙。
成因是 v2 里那段颜色键剔除红色 CJK 文字块的代码:`(R>200)&(G<70)&(B<70)` 同时命中了角色脸上的
亮红高光(`#c8262c` 的 R 正好 200),再经 5x5 两次膨胀,就在高光四周啃掉约 4px 深的一圈。
而红字块本身是独立连通域,取最大连通域时天然被排除 —— 那段剔除逻辑纯属多余。同时移除了
`binary_opening`,它会啃细线条美术。
新增不变量(这条断言本可以第一时间拦住上面的 bug,现已内建):**每个保留为透明的洞,其原图颜色
中位数必须是近白页面(min 通道 > 225),否则直接 `SystemExit` 拒绝导出**。
| 指标 | v2 | v2b |
| --- | --- | --- |
| 掩膜中的洞 | 6(5 个是美术) | **1(全部为背景,通过颜色不变量)** |
| 头部内部「暗色美术被透明」 | 存在 | **0 px** |
| 躯体像素 | 139,358 | 141,568(回补 2,210) |
| 边缘近白像素 | 0.1% | 0.1%(未回退) |
| 轮廓 IoU | 0.9880 | 0.9881 |
回归:audio 3597 断言、smoke、audit-classes、t7-crosscheck 902 断言全过;线上 bundle 400,421 字节。
## T14:暴走正典动作集(AT 力场 / 持枪 / 振动小刀 / 出击 / 殴打使徒)
### 1. 五个新动作
| 动作 | 时长 | 优先级 | 情绪门控 | 触发 | 音效 | 动作要点 |
| --- | --- | --- | --- | --- | --- | --- |
| `atopen` A.T. 力场展开 | 1800ms | 4 | idle/active/tool/alert/berserk | 暴走链第 3 拍;`play()` | `atfield` | 双臂前推、六边形力场 0.74→1.04 张开、机体后撑、眼闪 1.2x、鳍片外张 18 度 |
| `gunfire` 持枪射击 | 2000ms | 4 | alert/berserk/active/tool | ALERT 40% | `rifle` | 举枪→三连发(枪口焰 + 机体后座 5px + 右腿撑步)→收枪 |
| `knifedraw` 抽振动小刀 | 1700ms | 4 | alert/berserk/active/tool | ALERT 35% | `deploy` | 右肩甲开启 13 度 → 手伸向肩后 → 刀现 → 一记斩击 |
| `sortie` 出击 | 1600ms | 3 | idle/active/tool | 进入 ACTIVE/OPERATE 35% | `launch` | 深蹲蓄力(压扁 1.06/0.9)→ 弹射上冲 26px(拉伸 0.93/1.09)→ 两段落地 |
| `beatdown` 殴打使徒 | 2200ms | 4 | alert/berserk | 暴走链第 4 拍 | `punch` | 四记交替重拳,髋部先转带动出拳,命中帧地面火花 |
暴走链改为正典顺序:`charge`(0) → `roar`(1560) → `atopen`(3160) → `beatdown`(4960),末尾 7160ms,
仍在 8 秒暴走窗口内;每次交接都等上一动作结束,不会有两个动作抢同一个 transform。
ALERT 不再只是摇头:40% 持枪 / 35% 抽刀 / 25% 保留原摇头。
### 2. 两件手绘矢量道具
参考图里没有武器,所以枪与刀是手绘矢量(不是切片),挂在 `eva01-armR` 组内随臂运动,
平时 `opacity:0`,只在自己的动作里显形:
- `eva01-rifle` 托盘式步枪:机匣 + 顶部受光棱 + 枪口块 + 弹匣,另有 `eva01-flash` 枪口焰
- `eva01-knife` 振动小刀:刀身 + 刃口高光 + 刀柄
- `eva01-hit` 命中火花两片,放在 `inner` 层而非手臂组,所以不会随拳头一起旋转
### 3. 声画对齐(这轮特意校过,不是凑巧)
| 事件 | 动画命中帧 | 音效层延迟 |
| --- | --- | --- |
| 枪火第 1 发 | 12% × 2000ms = 240ms | `rifle` 层 delay 0.24 |
| 枪火第 2 发 | 26% = 520ms | delay 0.52 |
| 枪火第 3 发 | 40% = 800ms | delay 0.80 |
| 重拳第 1 击 | 20% × 2200ms = 440ms | `punch` 层 delay 0.44 |
| 重拳第 2 击 | 50% = 1100ms | delay 1.10 |
| 重拳第 3 击 | 82% = 1804ms | delay 1.804(+0.16 时长,刚好卡进 2s 上限)|
### 4. 四个新音效(合成参数)
| kind | 语义 | 配方要点 | throttle |
| --- | --- | --- | --- |
| `rifle` | 托盘枪三连发 | 每发 = 90→45Hz 方波 0.12s(0.075)+ 300Hz 低通噪声(0.05)+ 4.2kHz 高通脆响;间隔 280ms,是慢速重炮而非连发 | 900ms |
| `launch` | 弹射轨道 | 40→120Hz 正弦 0.9s(attack 0.2)+ 600→1800Hz 低通噪声 0.85s + 卡榫方波 + 风噪尾 | 1600ms |
| `punch` | 三记重拳 | 130→50Hz 正弦 0.16s(0.085)+ 420Hz 低通(0.045)+ 5kHz 高通爆点,逐击衰减 | 180ms |
| `deploy` | 抽刀 | 5kHz 高通瞬态(锁扣)+ 300→1200Hz 锯齿滑出(刀身)+ 2400/2412Hz 双正弦驻留 —— 12Hz 拍频即刃振 | 600ms |
### 5. 闸门抓到并修掉的两个真问题
1. `eva01-atHex` 与 `eva01-beatHit` 的 0%/100% 不是 identity(`scale(.72)`/`scale(.6)`),违反
「class 摘掉不得跳变」的不变量 —— 已把缩放挪到内部帧,首尾恢复 `scale(1)`。
2. `atopen` 起初给了 idle 权重且门控含 alert/berserk,使**警报态与暴走态混进了闲时随机池**
(危机中不该摸鱼)—— 改为 `idle: 0`,仍可由暴走链与 `window.__eva01.play()` 显式触发。
回归:`audio.mjs` **4054 断言 / 48 kinds**、`smoke.mjs`、`audit-classes`、`audit-sfx-wiring`、
`t7-crosscheck.mjs` **1133 断言 / 31 动作 / 180 组关键帧** 全过;轮廓 IoU 0.9881 未受影响
(渲染 harness 已同步剥离武器与火花图层,否则会污染轮廓量测);线上 bundle 415,482 字节。
### 6. 仍未做到的
- 暴走形态依旧是**色彩分级 + 开颌獠牙 + 抖动**,不是第二套贴图:那张暴走图是满幅插画,抠不出
干净掩膜(见 T12 第 5 节)。要真正复刻其姿态,需要白底或透明底立绘。
- 音效仍是按声学与既有 `SFX_RESEARCH.md` 体例推导;本机 `web_search` 无凭据,**本轮依然没有新的
联网正典来源**,不要把上表当作引用。
## T15:补上联网检索能力,并把正典音效调研补做完
### 1. 能力从哪来(没装插件也能用)
根因是 dsh 进程带着一个已死的代理(`127.0.0.1:7897`):`api.github.com` 走它 rc=7 拒连、
走 `33210` 或直连都是 200。所以 `web_search`(还缺 key)与 `find_dsh_plugin`(返回空列表)
都不是坏了,是被掐住。
新增 `E:\DSH\tools\websearch.py`:自动探测可用路由(33210 → 7897 → 7890 → 直连),
走 DuckDuckGo HTML 端点,**零 API key、零第三方依赖(纯标准库)**。三个子命令:
```
python E:\DSH\tools\websearch.py search "query" [n]
python E:\DSH\tools\websearch.py fetch "https://..." [chars]
python E:\DSH\tools\websearch.py github "topic query" [n]
```
### 2. 可选插件(真实检索结果,按星数)
| 插件 | 星 | 要点 |
| --- | --- | --- |
| `liustack/modsearch` | 107 | 一条命令接管 dsh 内置 `web_search`;默认引擎 Antigravity CLI **免密钥**(浏览器登录),Tavily/Exa/Firecrawl 免费额度做失效转移 |
| `anysearch-team/anysearch-dsh` | 54 | AnySearch 搜索 provider + 进阶检索工具 |
| `anweat/dsh-web-search-pro` | 12 | 多引擎 + SQLite 持久缓存 |
| `gxpppp/dsh-search-mcp` | 10 | 用 MCP 服务器(Tavily/Brave/Exa/Perplexity/DuckDuckGo)替换内置搜索 |
**注意**:装任何一个都改不了根因 —— 插件跑在 dsh 进程里,同样会走那个死代理。
顺序应为:先跑 `E:\DSH\fix-uv\apply-uv-fix.ps1` 修代理端口 → 重启 dsh → 再装插件。
### 3. 调研成果与对音效的实际改动
详见 `SFX_SOURCES_T15.md`(每条结论带来源链接)。三处实现改动:
| 改动 | 依据 | 具体参数 |
| --- | --- | --- |
| `roar` 重做为「放慢的人声尖叫」 | 初号机嘶吼实为林原めぐみ的尖叫放慢(EvaGeeks 13988 / Reddit 多源) | 删掉 1400→300Hz 合成器扫频层,改为两条同步下行高 Q 带通(760→420Hz Q6、1250→700Hz Q8)叠在 300→78Hz 锯齿上 + 呼吸尾音;合计峰值 0.24 → **0.187** |
| 新增 `plugsuit` 皮革褶皱音 | Khara 官方揭露驾驶服音效来自一只 Madison Square Garden 皮质运动包 | 1500→700Hz 带通 + 3.8kHz 高通颗粒 + 2600→1400Hz 二次揉捏;接在拖拽起手,`grab` 之后 |
| `deploy` 刃振提频 | EvaWiki:刀身以极高频振动,收纳于左肩甲 | 驻留双音 2400/2412Hz → **3200/3214Hz**(拍频 14Hz,从「嗡」变「嘶」);并确认 `knifedraw` 用右侧肩甲即 Eva 的左肩,方向与正典一致 |
仍无权威来源的项(A.T. 力场音、弹射音、托盘枪音)继续标注为声学推导,不冒充引用。
> T16 更新:其中 **A.T. 力场音** 与 **托盘枪音** 已在 T16 找到来源并落地(见下一節与 `SFX_SOURCES_T16.md`);弹射音仍无来源。
回归:`audio.mjs` **4132 断言 / 49 kinds**、`smoke.mjs`、`audit-sfx-wiring`、
`t7-crosscheck.mjs` **1133 断言** 全过。
## T16:把「无来源」的音效追到底 + 第一次真正的视觉终审
### 1. 先修能力,再干活
上一轮的两个硬限制本轮都没了:**代理**从死掉的 `127.0.0.1:7897` 改到 `33210`(用户级
环境变量已改,dsh 重启后继承),`websearch.py` 因此能真的联网;**图片理解**(`describe_image`)
配好了 Ark `/api/v3` + `doubao-seed-1-6-250615`,实测可读图 —— T12/T14 反复声明的
「本机模型无图像输入,视觉终审仍需你本人看一眼」这条限制,本轮第一次可以自己做了。
### 2. 视觉终审:确认无瑕疵,但也确认了「不能听视觉模型讲细节」
把 `pet-ref/pet-shot.png`(桌宠渲染)与 `pet-ref/reference.png`(参考插画)分别交给视觉模型,
同一套提问(对象数、姿态、比例、颜色分区、瑕疵、面部细节):
| 项目 | 结论 |
| --- | --- |
| 白斑 / 白毛边 / 透明缺口 / 断肢 / 贴图接缝 | **两图都报「未观察到」** —— T13/T13b 的修复第一次得到独立于像素统计的确认 |
| 姿态与颜色分区 | 两图描述几乎逐条重合(紫色主体、绿色鳍/肩/膝/小腿内侧、红色眼上三角、黄色眼与膝块) |
| 头身比 | 都判为 Q 版、头约占全高 1/3 —— 与 `compare-fidelity.py` 的 35.55% / 35.67% 一致 |
但视觉模型对**细节**给出了两条矛盾描述:说桌宠眼睛是「圆形、无瞳孔」、参考图是「横向椭圆、有黑色瞳孔」,
还说桌宠嘴是「咧嘴」、参考图是「闭合」。这两条**被像素测量证伪**(黄色连通域 bbox 与域内暗像素占比):
| 眼部黄色连通域 | 参考图 | 桌宠渲染 |
| --- | --- | --- |
| 左眼 w/h | 0.840 | 0.812 |
| 左眼域内暗像素占比(瞳孔) | 0.390 | 0.380 |
| 右眼 w/h | 1.278 | 1.364 |
| 右眼域内暗像素占比 | 0.214 | 0.210 |
两图的眼形比例与瞳孔占比几乎相同,**桌宠是有瞳孔的**,也没有哪一只眼睛是「横向椭圆 vs 圆形」的差别。
所以本轮**没有**按那两条描述改任何形状 —— 视觉模型可用于「有没有明显瑕疵」这类整体判断,
它的细节措辞不能当量测用。
### 3. 音效来源:一条真改动,一条认输
完整来源与摘录见 `SFX_SOURCES_T16.md`。
| kind | 本轮结果 |
| --- | --- |
| `atfield` | 补上来源(EvaGeeks 13974 的「像 binaural beats」听感)→ 加 220/226Hz 一对正弦,**6Hz 拍频**;标注为社区听感,非制作来源 |
| `rifle` | **有正典依据的重做**:EvaWiki 给出托盘枪 = Type MM-99 大口径**自动**步枪、造型取自 **Steyr ACR**、ACR 用 **flechette 钢镖弹** → 三连发间隔 280ms → **110ms**、本体 0.12s → **0.09s**、每发各带 5.2kHz 钢镖瞬态(原先只有首发一次 4.2kHz);低频本体保留(「大口径」是正典) |
| `roar` | 拿到**第三个**独立佐证(thread 13574:「录一个人尖叫再把频率放慢」;且咆哮被放在**对白轨**,英配需重录)—— T15 的实现无需再改 |
| `launch` / `punch` / `atfield` 的制作来源 | **没查到**。两条专问弹射音的查询被 DuckDuckGo 限流返回空,EvaWiki 无音源说明 → 继续标注为声学推导,不冒充引用 |
新的工艺定位(写死在文档里):论坛一手结论是 NGE 的 TV 版音效「以现成音效库 + 混音为主」,
音响制作方为 "Audio Tanaka"。原作没录真物、我们也没录真物,所以本插件的音效**永远只能是声学推导**,
任何时候都不写成「复刻正典音源」。
声画对齐随之 retime(T14 第 3 节的规矩):枪火三帧 12% / 17.5% / 23%(240 / 350 / 460ms)
= 音效层 delay 0.24 / 0.35 / 0.46;`eva01-gunFlash` / `eva01-gunBody` / `eva01-gunArmR`
三组关键帧一并改,首尾仍是 identity。
### 4. 顺手修掉的文档缺陷
README 里 **T12 章节整段重复了两次**(2590 字符逐字相同,应是上一轮 patch 的编辑事故)。
本轮删掉第二份,删除前先断言两段完全相同才动手。
### 5. 回归
`audio.mjs` **4196 断言 / 49 kinds**、`smoke.mjs`、`audit-classes`(180 组关键帧、0 处缺
0%/100%)、`audit-sfx-wiring`、`t7-crosscheck.mjs` **1133 断言 / 31 动作** 全过。
rifle 新增两层后单层峰值仍 ≤ 0.12、三发窗口互不重叠(0.24–0.33 / 0.35–0.44 / 0.46–0.55)。
### 6. 仍未做到的
- **暴走第二套贴图**依旧没做:那张暴走插画是满幅背景(白底仅 0.3%),抠不出干净掩膜(T12 第 5 节),
现在图片理解虽然能用了,但它给不出掩膜,只能给描述 —— 缺的是一张白底/透明底暴走立绘。
- `launch` / `punch` 的正典来源仍缺;下一轮可追 "Audio Tanaka" 与 EvaGeeks thread 21607
(本轮抓到的正文只有提问,没有答复)。
- 6 个「预留」动作(`shiver` / `slash` / `unbind` / `guard` / `shutdown` / `rebound`)与
5 个「待接」音效(`umbilical` / `unplug` / `atburst` / `lcl` / `lifeless`)仍没有自动触发点。
## T17:按两张参考图改暴走与 A.T. 力场(音效跟着动画走)
用户给了两张图:**图 A** 是初号机 Funko 手办实拍(深蓝近黑背景占 80%,手持红色长枪),
**图 B** 是暴走形态插画(红底)。
### 1. 先说两个如实结论
**两张图里都没有 A.T. 力场。** 视觉模型对两图分别独立回答"未出现力场",图 A 的颜色直方图前 12 位
全是 `#000818`/`#081020` 一类近黑(背景),也没有任何几何叠加的迹象。所以本轮 A.T. 力场的改法
**依据不是这两张图**,而是荧幕上的多层六边形障壁表现,这一点必须写清楚。
**图 B 依然抠不出干净掩膜,第二套贴图继续不做。** 这次是量出来的,不是感觉:
| 量测项 | 值 | 含义 |
| --- | --- | --- |
| 红背景像素占比 | 38.9% | 视觉模型说的"纯红背景 100%"是错的 |
| 颜色键后最大前景连通域 | 占全图 61.1% | 不是角色,是"非红色的所有绘画" |
| 掩膜 bbox | 710x734=整幅 | 上/下/左三条边命中率 **100%** |
| 掩膜填充率 | 0.611 | 与 T12 那张(0.52)同类,满幅插画 |
| 掩膜内的洞 | 27 个,最大几个中位色 RGB(216,56,57) | 洞是背景,颜色不变量这条仍然守住 |
所以暴走形态改成**矢量叠加**(贴在既有 16 层贴图骨架上),不是切第二套图。
### 2. 图 B 落地成 5 个新部件
掩膜内实测配色(这次量的是掩膜内部,不是整幅):紫 **#463c75**、绿 **#559e57**、
黑色外露组织 **#15151a 占身体 28%**、红 **#892426**、白 5.6%(獠牙与高光)。
| 部件 | 依据(图 B) | 实现 |
| --- | --- | --- |
| `eva01-horn` 头顶独角 | "紫色,尖端蓝色,垂直向上" | 头切片顶边 y=26、脸部中线 x=164.7(双眼中点),独角根部 y=35 → 尖端 y=3;主体 #463c75 + 蓝尖 #3244b8;进入暴走时 `hornrise` 从 scaleY(.05) 弹出 |
| `eva01-fangs` 双排獠牙 | "上颚 4 颗突出白色獠牙,下颚 6 颗较短,内侧细密锯齿" | 原先是**一条**锯齿路径(表达不了两排不同长度)→ 改为上排 4 长牙 + 下排 6 短牙 + 一条 45% 透明的细锯齿带 |
| `eva01-eyeslit` + `eva01-eyevein` | "狭长三角形眼、亮黄、眼内红色竖纹" | 圆眼窝保留(它持有发光与眨眼 transform),CSS 在暴走下压到 opacity .35,让狭长三角 + 红竖纹主导 |
| `eva01-tail` 尾巴 | "从躯干后方延伸、向下弯曲约 120°、黑色鳞片组织" | 填充矢量 #15151a + 虚线鳞纹,根部 (203,250) 为 transform-origin,`tailsway` ±8° 摆动 |
| `eva01-hunch` 前倾深蹲 | "躯干前倾约 25°、膝约 90°、压迫感" | **新建包裹组**承载这个姿态,因为 shake / roar / breathe / 各动作已经各自占用了 figure / head / chest / stage 的 transform —— 新类零冲突,不违反"两个动作不争抢同一 transform" |
暴走色彩分级也按实测重标:`brightness(.76) saturate(.88)` → **`brightness(.8) saturate(.93)`**
(#5e369c → #463c75 的亮度比实测为 0.81),红色投影加到 10px。开颌幅度从 translateY(12px)/8° 加到
**19px/13°**,配合两排獠牙。
### 3. A.T. 力场:三层障壁 + 边缘发光
从 2 层六边形加到 **3 层**(新增内环 `h3`,顶点为外环对中心 (150,170) 的 0.72 倍缩放),
三环点火间隔 **180ms**,`is-act-atopen` 下给六边形加 `drop-shadow(0 0 7px var(--eva-field))`。
一开始我把两个新环写成 `animation-delay` 修饰规则,**被 `t7-crosscheck` 拦下**:它要求每条
`is-act-*` 规则自己声明 animation(否则可能是永不生效的空规则)。这条闸门是对的——改成完整
`animation:eva01-atHex 1.8s ease-out .18s/.36s 1 both` 简写,语义更明确,闸门也不用放水。
### 4. 音效跟着动画改
| 改动 | 对齐关系 |
| --- | --- |
| `atfield` 新增两条 2.6kHz 障壁锁定瞬态,delay **0.23 / 0.41** | 三环点火帧=各自 `atHex` 的 3% × 1800ms = 54ms,加上 .18s / .36s 的层延迟 |
| 进入暴走时除 `pulse` 外追加 `plate` | 独角弹出(`hornrise` 0.62s)需要一声装甲板锁定 |
| `jawchomp` 幅度加大 | 咬合音 `jaw` 的既有节奏不变,但视觉张口幅度与之匹配 |
### 5. 验证(含一条以前做不到的)
5 道闸门:`audio.mjs` **4230 断言 / 49 kinds**、`smoke.mjs`、`audit-classes`(**183** 组关键帧、
0 处 CSS 引用未赋值、0 处缺 0%/100%)、`audit-sfx-wiring`、`t7-crosscheck.mjs` **1145 断言** 全过。
**暴走分支第一次真被渲染并看过**:`smoke.mjs` 覆盖的 4 种情绪都不含暴走(它由双击驱动,SSR 派发不了
事件)。本轮新增 `pet-ref/render-berserk.mjs`:在**内存中的 bundle 副本**里把
`react.useState(false)` 翻成 `true`(磁盘文件不动),渲染出 `berserk-render.html`,用
`vision_html_screenshot` 栅格化,再交给视觉模型逐项确认:
> 独角在、主体紫尖端蓝;嘴张开、两排牙且上排明显更长;眼睛狭长黄色、中间有红色竖纹;
> 右下方有向下弯曲的黑色尾巴;**无部件错位、无漂浮碎块、无穿模、无截断**。
### 6. 仍未做到的
- 图 B 的**姿态**(前倾 25°/深蹲 90°)只能用一层包裹组的轻微倾斜+压缩近似,Q 版贴图骨架没有髋膝的
真实关节自由度;要真复刻那个姿势仍然要第二套贴图,而第二套贴图要一张**白底或透明底**的暴走立绘。
- A.T. 力场仍无正典**音源**来源(T16 只拿到"像 binaural beats"的社区听感),视觉上的多层六边形也
只是荧幕表现的复述,没有设定集出处。
## T18:6 个「预留」动作全部接上真实触发点(并加一道防复发闸门)
从 T7 起,`shiver` / `slash` / `unbind` / `guard` / `shutdown` / `rebound` 六个动作
**表项、关键帧、选择器、音效全都齐了,但组件里没有任何自动触发点**,idle 权重也是 0 ——
只能靠 `window.__eva01.play()` 手动放。本轮全部按会话语义接上。
| 动作 | 新触发点 | 为什么是这个语义 |
| --- | --- | --- |
| `unbind` 撕开口枷 | **暴走链第 2 拍**(1560ms) | 正典顺序是先崩开束缚再嘶吼。链改为 charge(0) → unbind(1560) → roar(2960) → atopen(4560),末尾 6360ms,仍在 8s 暴走窗口内 |
| `beatdown` 殴打使徒 | 从链尾移出 → **暴走中挨打时反击** | 把它塞回链里会到 8560ms,超窗;而"挨一下打回去"本来就是它的语义 |
| `rebound` 受击回弹 | **使徒计数上涨**(失败工具/turn-error 增加),非暴走态 | 挨打就该回弹;门控补上 `alert`,因为新错误通常正好把情绪推进警报态 |
| `guard` A.T. 场防御 | **ALERT 18%** | 警报里除了开枪抽刀,也该有防御 |
| `shiver` 颤抖 | **ALERT 12%** | 驾驶员十四岁 |
| `slash` 振动刀挥砍 | **进入 ACTIVE/OPERATE 25%** | 一次工具调用就是机体切进去干活 |
| `shutdown` 电源耗尽停机 | **暴走 8 秒窗口结束的那一刻** | 窗口到期=内部电源耗尽,正是这个动作画的东西(音效 `lifeless`) |
ALERT 的答复分配从「40/35/25」改为 **34% 持枪 / 28% 抽刀 / 18% 防御 / 12% 颤抖 / 8% 摇头**;
进入作业态从「35% sortie / 65% stomp」改为 **30% sortie / 45% stomp / 25% slash**。
### 新闸门:`pet-ref/audit-triggers.cjs`
只补触发点不够——这种"有关键帧、永远跑不到"的状态必须变成**装不进去**的。新审计解析 `ACTIONS`
表,要求每个动作要么有 idle 权重,要么在组件里出现显式触发(`playAction("x")` 或三元里的动作名),
否则退出码 1。调试钩子 `window.__eva01` 那段被排除在外:它是测试缝,不是桌宠自己的行为。
**这道闸门自己也被验证过(负例测试)**:把 T18 的四处触发改回旧写法后跑同一个审计,退出码 1,
并且精确点名:
```
TRIGGERS FAILED: unreachable actions (keyframes but no way to run): shiver, slash, unbind, guard, shutdown, rebound
```
一个不会失败的闸门等于没有闸门,所以这条负例是本轮交付的一部分。
### 回归(现在是 6 道)
| 闸门 | 结果 |
| --- | --- |
| `audio.mjs` | 4230 断言 / 49 kinds |
| `smoke.mjs` | 4 情绪整机渲染 + 6 例折叠 + 7 种 HUD |
| `audit-classes` | 183 组关键帧,0 处 CSS 引用未赋值 |
| `audit-sfx-wiring` | 49 kinds 全接线 |
| `t7-crosscheck.mjs` | 1145 断言 / 31 动作 |
| `audit-triggers.cjs`(新) | **31 个动作全部可达:17 个 idle 驱动 + 14 个显式触发** |
上表也把 README 前面那张动作表里 6 行「预留」改成了真实触发点——那张表声明"取自 `ACTIONS`
表实际值",就不能留着过期的话。
### 仍未做到
- 图 A 里那把**红色长枪**(朗基努斯之枪)还没做:现有手绘道具只有托盘步枪与振动小刀,加第三件道具
是独立一轮的活。
- `launch` / `punch` 的正典音源仍缺(T16 已认输,两条待追的线索没变)。
- 暴走第二套贴图仍缺白底/透明底立绘(T17 已量测证明现有两张图都抠不出干净掩膜)。
## T19:朗基努斯之枪(第三件手绘道具)
图 A 里初号机手上那把红色长兵器就是**朗基努斯之枪**。这轮把它做成第三件道具,正典事实来自
EvaWiki《Spear of Longinus》<https://wiki.evageeks.org/Lance_of_Longinus>:
| 正典原文 | 落地 |
| --- | --- |
| "an extraterrestrial artifact with the power to **penetrate any A.T. Field**" | 优先级给 **5**(和暴走各拍同级):能穿透任何 A.T. 场的东西,不该被日常动作打断 |
| "resembles a long red **bident** on a scale matching the size of an Evangelion" | **双叉**,不是单尖;杆长 175 单位(viewBox 0 0 300 340 里从拳心到尖端) |
| "Its structure is **double-helical**" | 主杆两侧各一条明/暗螺旋股(#d98a80 / #4a0f1c) |
| "When thrown at ... Arael, the tines wrap together" | 投掷是它的正典用法 → 动作是一记长突刺,不是挥砍 |
红色不是猜的:从图 A 的红色连通域里量出来的 —— 中位 **#7b2130**、85 分位亮部 **#ab5b55**
(图 A 是暗背景实拍,所以整体偏暗)。视觉模型说"枪头内侧有绿色宝石",但量出来的绿色连通域都在
机体的绿装甲位置,**没有证据**,所以没做。
### 动作与声画对齐
`lance` 2000ms / 优先级 5 / 门控 alert+berserk+active+tool。举枪(14%) → 螺旋起转 → **突刺(31%)**
→ 收势。音效 `spear` 新增:5.4kHz 锁定瞬态 + **1180/1194Hz 双音(14Hz 拍频=双螺旋)** +
900→2100Hz 低通挥动 + **命中层 delay 0.62** = 31% × 2000ms = 620ms,与 T14 定的规矩一致。
触发点:**第三只使徒还站着的时候**(`feed.angels >= 3`)投枪 —— 依据是"穿透任何 A.T. 场、
别的都不管用时才拿出来";暴走态仍然用拳头(`beatdown`)。
### 视觉复核抓到并修掉的两个真问题
新道具第一版通过了全部 6 道闸门,但**渲染出来一看就是错的**:
1. **枪尖和螺旋股整段看不见。** 原因不是画错了,是**图层顺序**:`inner` 组里 `head` 排在
`armR` 之后,而我把枪指向左上 (x110-143, y30-66),正好落在头部切片的 bbox(x68-261,
y26-122)里 —— 被头盖住了。视觉模型报"单尖、没有螺旋"就是这个原因。改为指向**右侧空白边距**
(头右缘 x=261,枪尖落在 x272-298)后,双叉立刻出现。
2. **螺旋股仍然读不出来。** 这是几何问题,不是审美问题:二次贝塞尔离弦的最大偏移约等于控制点
垂距的**一半**,原来 ±10 的控制偏移只让股线离弦 ~5 单位,而主杆半宽 3.5 —— 基本埋在杆下。
弦 (252,258)→(285,86) 的单位法向是 (0.983,0.189),取 ±24 偏移把控制点放到 (292,176) 与
(245,167),每条股线离杆约 8 单位,再把线宽提到 3、亮股提亮到 #d98a80。
第三次渲染后视觉逐项确认:**右侧向上的红色长杆、顶端两个分叉、杆身两条深浅不同且向两侧弯出的
细线、与身体无穿模、未被画面截断**。
### 回归
6 道闸门全过:`audio.mjs` **4384 断言 / 50 kinds**、`smoke.mjs`、`audit-classes`、
`audit-sfx-wiring`、`t7-crosscheck.mjs` **1185 断言 / 32 动作**、`audit-triggers.cjs`
**32/32 可达**。
### 仍未做到
- `launch` / `punch` 的正典**音源**仍缺(T16 已认输);`spear` 的造型有正典出处,声学仍是推导。
- 暴走第二套贴图仍缺白底/透明底立绘。
- 枪的"投出去"没有真的脱手飞出(那需要一条离开机体坐标系的飞行轨迹与命中目标物),目前是突刺。
## T20:改用真实的初号机音效与音乐(合成退为兜底)
用户要的是**真音**:任务处理、点击、完成提醒都用初号机自己的音效/音乐,而不是听起来"像"的合成音。
本轮加的是**采样层**,不是替换掉合成器——两者同时在线,文件有就用文件,没有就用合成。
### 1. 宿主半程第一次有了行为
`lib/index.js` 以前是空 `apply`(纯浏览器插件)。现在它注册一条**只读**路由,因为浏览器自己读不了磁盘:
| 路由 | 作用 |
| --- | --- |
| `GET /eva01-audio/manifest` | 返回 `{ dir, writable, files:[{name,bytes,type}] }` |
| `GET /eva01-audio/file/<name>` | 返回单个文件的字节(`content-type` 按扩展名,缓存 300s) |
**没有上传路由**,这是故意的:文件由用户用资源管理器放,页面侧写不了任何东西。安全性是实测过的:
`resolveAudio` 只认"出现在目录列表里的纯文件名",所以 `../../settings.yaml`、
`..\\..\\settings.yaml`、`sub/click.mp3`、`.`、空串、非音频的 `notes.txt` 全部返回 undefined
(逐个跑过);`apply()` 在没有 `webServer` 服务时静默返回,不抛。
投放目录:**`%USERPROFILE%\.dsh\eva01-audio`**(可用环境变量 `EVA01_AUDIO_DIR` 覆盖),
首次读 manifest 时自动创建;已在里面放了一份 `放这里.txt` 说明。
### 2. 文件名决定它替换哪个事件
| 文件名 | 替换 | 事件 |
| --- | --- | --- |
| `click.*` | `blip` | 单击桌宠 |
| `task-start.*` / `start.*` | `up` | 开始处理任务(进入 ACTIVE) |
| `task-done.*` / `done.*` / `complete.*` | `taskdone` | 任务完成提醒(回到 IDLE) |
| `alert.*` | `alarm` | 使徒警报(出现错误) |
| `berserk.*` | `roar` | 双击暴走 |
| `bgm.*` / `music.*` | —— | **任务处理期间循环播放的音乐** |
进阶:**文件名等于任意一个内置 kind(现有 50 个)就覆盖那个 kind** —— 放个 `atfield.mp3`,
A.T. 力场展开就用你的文件,其余仍走合成。
### 3. 工程细节
- 采样播放挂在 `playSfx` 里、**节流检查之后、合成层循环之前**:采样同样受 `THROTTLE` 约束
(不会连发),命中采样时一个合成节点都不建。
- 首次触发某个 kind 时缓冲区还在解码(`decodeAudioData` 是异步的),那一次仍由合成音应答,
之后都是真音。BGM 会在 1.2s 后重试一次,避免开局那一次错过就整场没音乐。
- 增益:音效 `SAMPLE_GAIN 0.5`、音乐 `BGM_GAIN 0.2`,**音乐永远压在音效之下**;淡入 0.6s、
淡出 0.7s;`♪` 静音开关会一并停掉音乐;`active ↔ tool` 切换**不会**重启音乐(effect 故意不写
cleanup),卸载时停。
- 插件**不附带任何音频资源**:包体积没变,仓库里没有一个音频文件。
### 4. 新闸门 `audit-samples.cjs` 第一次运行就抓到一个真 bug
这道审计断言 5 条:槽位目标必须是真实 kind、`bgm`/`music` 不能同时是 kind、采样钩子必须在节流之后
合成之前、`BGM_GAIN < SAMPLE_GAIN ≤ 1`、两个半程的路由常量必须对齐。
第一次跑就红了:`SAMPLE_ALIAS.berserk` 指向 `"growl"`,而 `growl` 只是 `SFX_ALIAS` 里
`roar` 的**历史别名**。`playSfx` 先解析别名再查采样表,所以采样表的键必须是**解析后**的 kind ——
照原样发出去,`berserk.mp3` 永远不会响。改为 `"roar"`,并给审计加了一条"槽位不得指向别名"的断言。
### 5. 回归(7 道闸门)
`audio.mjs` 4384 断言 / 50 kinds · `smoke.mjs` · `audit-classes` · `audit-sfx-wiring` ·
`t7-crosscheck.mjs` 1185 断言 / 32 动作 · `audit-triggers.cjs` 32/32 可达 ·
**`audit-samples.cjs` 8 槽位全部命中真实 kind**。两个半程都过 `node --check`。
### 6. 需要注意
- **宿主半程改了 → 必须重启 dsh** 那条路由才会挂上(profile 只在启动时加载)。实测证据:当前进程
访问 `/eva01-audio/manifest` 返回的是 SPA 的 HTML 而不是 JSON。客户端半程(采样层本身)刷新
页面即生效,只是在重启前 manifest 永远拿不到,于是全部回落到合成音——**降级是干净的**。
- 音频文件的版权归你自己处理:插件只播放你放进那个目录的文件,不下载、不附带。
## T21:真的把素材找来装好了(8 个文件已就位)
用户要我自己去找素材。搜索路径:`websearch.py` → myinstants 的 Evangelion 搜索页(29 个直链
`.mp3`)。101soundboards 返回 403 拒绝抓取,voicy/soundboards.gg 未尝试。下载走
`127.0.0.1:33210` 代理,带 Referer;每个文件都验过 MP3 magic bytes,并用 `ffprobe` 量了真实时长。
### 装好的 8 个文件(`~/.dsh/eva01-audio`)
| 文件 | 素材 | 时长 | 为什么是它 |
| --- | --- | --- | --- |
| `click.mp3` | evangelion-laser | **0.81s** | 全场最短、干脆,点击音不能拖尾 |
| `task-start.mp3` | evangelion-external-power-disconnect | 2.27s | 「外部电源切断」=转内部电源出击,正是"开始干活" |
| `task-done.mp3` | omedetou-1(おめでとう) | 1.73s | 完成提醒要短;18s 的 congratulations 版被否掉 |
| `bgm.mp3` | decisive-battle(決戦) | **25.26s** | 作战 BGM,够长,循环不刺耳 |
| `alert.mp3` | ramiel-sound-clean | 5.30s | 第五使徒的驻留音,本身就是"使徒接近"的听觉符号 |
| `berserk.mp3` | shinji-scream | 4.92s | **有 T15 依据**:初号机嘶吼的正典做法就是把人声尖叫放慢 |
| `atfield.mp3` | at-field-impact | 2.09s | 覆盖内置 `atfield` kind(A.T. 力场展开) |
| `unplug.mp3` | 同 task-start 素材 | 2.27s | 覆盖内置 `unplug` kind(暴走切内部电源) |
备选素材留在 `pet-ref/audio-candidates/`(congratulations 18.23s、cruel-angel 11.53s、
okaerinasai 3.96s、ramiel-attack-scream 5.28s、evangelion 21.45s),想换哪个直接改名覆盖即可。
### 审计随之扩了一节
`audit-samples.cjs` 现在还会读投放目录,把每个音频文件**实际会触发什么**列出来,并且**如果某个音频
文件映射不到任何事件就直接失败** —— 防止 `taskstart.mp3`(少了连字符)这种"看着装好了、其实永远
不响"的情况。当前 8 个文件全部命中:alarm / atfield / roar / 循环音乐 / blip / taskdone / up / unplug。
### 版权
这些是 myinstants 上用户上传的动画片段,版权属于 Khara/GAINAX。放在你自己机器上给桌宠用属于私人
使用;**不要再分发**,插件仓库里也仍然不含任何音频文件(素材只在你的用户目录)。想换成你自己抓轨的
OST,直接覆盖同名文件即可。
Install
dsh plugin --profile web add github:AlexZhou19871030/dsh-plugin-eva-unit01
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-external-dsh-client-ui-eva-unit01 from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.