dsh-wisp 0.6.0 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +869 -10
- package/assets/canon/attn.webp +0 -0
- package/assets/canon/happy.webp +0 -0
- package/assets/canon/idle.webp +0 -0
- package/assets/canon/poked.webp +0 -0
- package/assets/canon/proud.webp +0 -0
- package/assets/canon/sleepy.webp +0 -0
- package/assets/canon/work.webp +0 -0
- package/assets/classic/attn.webp +0 -0
- package/assets/classic/happy.webp +0 -0
- package/assets/classic/idle.webp +0 -0
- package/assets/classic/poked.webp +0 -0
- package/assets/classic/proud.webp +0 -0
- package/assets/classic/sleepy.webp +0 -0
- package/assets/classic/work.webp +0 -0
- package/assets/deepsea/attn.webp +0 -0
- package/assets/deepsea/happy.webp +0 -0
- package/assets/deepsea/idle.webp +0 -0
- package/assets/deepsea/poked.webp +0 -0
- package/assets/deepsea/proud.webp +0 -0
- package/assets/deepsea/sleepy.webp +0 -0
- package/assets/deepsea/work.webp +0 -0
- package/assets/night/attn.webp +0 -0
- package/assets/night/happy.webp +0 -0
- package/assets/night/idle.webp +0 -0
- package/assets/night/poked.webp +0 -0
- package/assets/night/proud.webp +0 -0
- package/assets/night/sleepy.webp +0 -0
- package/assets/night/work.webp +0 -0
- package/assets/pajama/attn.webp +0 -0
- package/assets/pajama/happy.webp +0 -0
- package/assets/pajama/idle.webp +0 -0
- package/assets/pajama/poked.webp +0 -0
- package/assets/pajama/proud.webp +0 -0
- package/assets/pajama/sleepy.webp +0 -0
- package/assets/pajama/work.webp +0 -0
- package/audit/generation-audit.jsonl +35 -0
- package/build.mjs +93 -30
- package/cordis.patch.yml +2 -0
- package/lib/client.js +1293 -41
- package/lib/client.template.js +1248 -37
- package/lib/index.js +115 -12
- package/package.json +8 -1
- package/tools/CHARACTER-PROMPT.md +221 -3
- package/tools/assets.mjs +39 -16
- package/tools/audit-log.mjs +245 -0
- package/tools/bundle-cost.mjs +105 -0
- package/tools/pack-check.mjs +78 -0
- package/tools/perf.mjs +186 -0
- package/tools/smoke.mjs +577 -0
- package/tools/tiers.mjs +104 -0
- package/verify-wisp.mjs +2681 -872
- package/assets/happy.webp +0 -0
- package/assets/happy_hi.webp +0 -0
- package/assets/happy_md.webp +0 -0
- package/assets/happy_sm.webp +0 -0
- package/assets/idle.webp +0 -0
- package/assets/idle_hi.webp +0 -0
- package/assets/idle_md.webp +0 -0
- package/assets/idle_sm.webp +0 -0
- package/assets/sleepy.webp +0 -0
- package/assets/sleepy_hi.webp +0 -0
- package/assets/sleepy_md.webp +0 -0
- package/assets/sleepy_sm.webp +0 -0
- package/assets/work.webp +0 -0
- package/assets/work_hi.webp +0 -0
- package/assets/work_md.webp +0 -0
- package/assets/work_sm.webp +0 -0
package/README.md
CHANGED
|
@@ -17,6 +17,17 @@ DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
|
17
17
|
| `alert` 琥珀色 | Agent 正在跑(出现「停止生成」按钮)**或** 输入框里有字 |
|
|
18
18
|
| `happy` 粉色 | 戳她一下 / 双击 / **一轮跑完** |
|
|
19
19
|
| `sleep` 灰色冒 `z` | 静置 `sleepAfterMs`(默认 90 秒);任意输入唤醒 |
|
|
20
|
+
| `attn` 举手招你过来 + 暖红轻推 | **有东西在等你处理**:Agent 提问 / 计划待确认 / 工具授权待批准。优先于其余一切状态,睡着了也会被叫起来 |
|
|
21
|
+
| 她记着你 | 她会记住**一起多少天**、你常把她放在**哪个角**、常让她**穿哪套** —— 够格的时候说出来(第 2/7/14/30/100 天;某个角用过 ≥8 次且占多数;某套皮肤换过 ≥6 次)。**同一条一辈子只说一次、一天最多一句** |
|
|
22
|
+
| 今日里程碑 | 她数着**今天跑完多少轮**(跨刷新记着,按天归零):到 **10 / 25 / 50 / 100 / 200 / 500** 时会用得意的表情报出数字。同一个里程碑一天只说一次 |
|
|
23
|
+
| 久坐提醒 | 连续活动到 `careAfterMs`(默认 **90 分钟**)她会抬手指你一下、说一句叫你起来动动。中间停顿不到 90 秒不算离开;睡着时会被叫醒再提醒;`0` = 关 |
|
|
24
|
+
| 时段招呼 | 挂载时的问候按**当前时段**取:早上 / 下午 / 晚上 / 凌晨四套池子,口气不同(`__wisp.doctor()` 会报当前用的是哪一套) |
|
|
25
|
+
| 滚轮调大小 | **Ctrl/⌘ + 滚轮**在她身上滚 = 放大/缩小一档(0.25)。**普通滚轮照常滚动页面** —— 她占地不小,直接吞掉滚轮等于在她身上把页面滚动禁掉 |
|
|
26
|
+
| 键盘挪位置 | 她拿到焦点后**方向键**微调 16px(**Shift = 4px**),与拖动共用同一套边界夹紧与位置记忆。在这之前挪动她**只能靠鼠标拖** |
|
|
27
|
+
| 躲起来之后 | 迷你小按钮**右键**也能打开同一个菜单;菜单里那一项是**开关** —— 躲着时显示「回来吧」,在着时显示「先躲起来」 |
|
|
28
|
+
| 右键菜单 | 右键她本体弹出四段:动作(说句话 / 放大一点 / 缩小一点 / 先躲起来 / 睡一会儿)+ **行为**(自己溜达 / 跟随状态 / 跑完撒花,带勾)+ **位置**(四个角落)+ **皮肤**。Esc、点别处、滚动都会关掉,↑↓ 可选 |
|
|
29
|
+
| 出错反应 | 一轮跑完时如果出现失败行(`[data-error="true"]`),她会切到 `worried` 表情说一句「好像不太顺。」,3.5 秒后恢复。同一批错误只说一次 |
|
|
30
|
+
| 先躲起来 | 菜单里选「先躲起来」后她整个消失,只在右下角留一个 56px 迷你按钮(用当前皮肤的图),点一下回来。状态记在本地,刷新后仍然躲着 |
|
|
20
31
|
| 拖动 | 按住本体拖动,带边界约束;**松手即记住位置**,下次刷新回到原处 |
|
|
21
32
|
| 说话 | 挂载问候、点击、睡着、醒来、跑完各有一组台词 |
|
|
22
33
|
|
|
@@ -57,18 +68,106 @@ node verify-wisp.mjs # 预检;exit 0 = 两半包都符合契约
|
|
|
57
68
|
3. 产物**不得调用被陷阱的全局**(`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require`);
|
|
58
69
|
4. `const VERSION` 必须与 `package.json` 的 `version` 一致(防止版本漂移)。
|
|
59
70
|
|
|
60
|
-
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **
|
|
71
|
+
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **248 项全 PASS,exit 0**。
|
|
61
72
|
|
|
62
73
|
---
|
|
63
74
|
|
|
75
|
+
## 真实浏览器冒烟
|
|
76
|
+
|
|
77
|
+
假 DOM 能验契约,验不了**布局与合成**。本项目的三个真 bug(躲起来后气泡悬空、菜单越界、
|
|
78
|
+
`display:none` 是否真的让她不挡点击)全都只有真实浏览器能发现,所以这些检查留在仓库里:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node tools/smoke.mjs # 46 项:挂载 / 菜单几何与键盘入口 / 躲起来 / 换皮肤 / attn / worried / 踱步 / 内存 / 体积
|
|
82
|
+
node tools/smoke.mjs --headed # 看着它跑
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**退出码是有意义的**:`0` 通过;`1` 有检查没过;`2` 环境中断。
|
|
86
|
+
|
|
87
|
+
headless Chromium 在内存压力下会被关掉,表现为随机的 "Target page, context or browser has
|
|
88
|
+
been closed"。它与本插件的正确性无关,所以脚本**只对这类中断重试一次**,两次都中断就以 2 退出 ——
|
|
89
|
+
`1` 永远意味着"有检查没过"。不要因为它偶发就把断言放宽,那会把真正的产品问题一起放过。
|
|
90
|
+
|
|
91
|
+
为此每个浏览器只驻留一个页面:每个页面都要装下这份包与解码后的精灵图,多开一个就是多一份。
|
|
92
|
+
(这条是被真实崩溃逼出来的:原来踱步那节另开一个页面,随即开始随机崩溃。)
|
|
93
|
+
|
|
94
|
+
Playwright 是**可选依赖**:找不到就以 0 退出并说明原因 —— CI 上不该因为缺浏览器而红。
|
|
95
|
+
pnpm 布局下顶层没有 `playwright-core` 这个名字(只有 `.pnpm/playwright-core@<ver>/`),
|
|
96
|
+
所以脚本会显式扫 `.pnpm`。
|
|
97
|
+
|
|
98
|
+
## 打包与分享
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node tools/pack-check.mjs
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
一条命令跑完:`npm pack` → 解包 → **从解出的副本**跑预检与重建,然后逐项确认发布子集自足
|
|
105
|
+
(`lib/index.js` / `lib/client.js` / `cordis.patch.yml` / `assets` 都在)。
|
|
106
|
+
|
|
107
|
+
**为什么不能靠"我看过 files[] 了"**:包里的文档指向包外、`tools/` 少声明一个、素材没打进去 ——
|
|
108
|
+
这些都要真的解出副本才暴露。这个检查第一次跑就抓到了 README 里指向开发机绝对路径的死引用。
|
|
109
|
+
|
|
110
|
+
当前包:**164 个文件 / 19 MB**,其中 `assets/` 占 140 个文件、14.0 MB —— 但**运行时只会读其中 35 张**:
|
|
111
|
+
|
|
112
|
+
| 素材 | 数量 | 体积 | 运行时 |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| 默认档 1024×1536 | 35 | 3.8 MB | ✅ 就这一档被 base64 进 JS(5.2 MB) |
|
|
115
|
+
| `_hi` 2048×3072 | 35 | 8.1 MB | ❌ 从不读取 |
|
|
116
|
+
| `_md` 512×768 | 35 | 1.5 MB | ❌ 从不读取 |
|
|
117
|
+
| `_sm` 256×384 | 35 | 0.6 MB | ❌ 从不读取 |
|
|
118
|
+
|
|
119
|
+
后三档加起来 **105 个文件 / 9.9 MB**,唯一用途是「哪天想用 `node build.mjs --tier=hi # 需要先 --all-tiers 生成 _hi;缺档会自动回退到默认档` 换个档重新打包」。
|
|
120
|
+
而且 `_hi` 连**画质收益都测不出来**(见下面性能小节:DPR 2 下高频能量差 0.3%,DPR 1/3 下 1024 档反而高 4~5%)。
|
|
121
|
+
**这 9.9 MB 已经省掉了**(2026-09-30):`assets/` 现在只有默认档 35 个文件 / 3.8 MB,
|
|
122
|
+
包从 19 MB 降到 **9 MB**。
|
|
123
|
+
|
|
124
|
+
要生成别的档位(例如想用 `--tier=hi` 重打包)先跑:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node tools/assets.mjs --from <绿幕母版目录> --skin <皮肤> --all-tiers # 默认**只写进包那一档**
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
流水线的默认行为已经改成"只写会进包的那一档",所以这 9.9 MB 不会下次跑一遍又长回来。
|
|
131
|
+
|
|
132
|
+
(历史原因:这段原本写的是「4 套皮肤 × 7 情绪 = 112 张、14.6 MB」,那是第五套皮肤 `canon` 加进来之前的数字。)
|
|
133
|
+
classic 皮肤的绿幕原图已经不在了,那些分档是**唯一**的重建来源,所以 `build.mjs --tier=…` 在
|
|
134
|
+
别人的机器上仍然可用。
|
|
135
|
+
|
|
136
|
+
## 自检:`__wisp.doctor()`
|
|
137
|
+
|
|
138
|
+
她依赖的每一个选择器都是**读 DSH 源码推出来的**,而它们错了不会有任何报错 —— 对应的功能只是
|
|
139
|
+
永远不触发,看起来像"她今天没反应"。插件又跑在带鉴权的页面里,外部脚本连不上,所以自检必须由
|
|
140
|
+
插件自己在页面里提供。Console 里敲:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
__wisp.doctor()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
返回当前页面里每条链路的真实情况:三个输入框钩子各命中几个、忙碌钩子、三个待处理面板、出错行、
|
|
147
|
+
亮暗状态、五个主题令牌的实际取值,以及一份 **`problems`** —— 只列**真的意味着功能失效**的条件。
|
|
148
|
+
缺席类信号不算问题:没有待处理项、没有出错行都是正常状态,把它们算成问题只会训练人忽略这份清单。
|
|
149
|
+
|
|
150
|
+
只读,无副作用。
|
|
151
|
+
|
|
152
|
+
**忙碌钩子会自解释**:它是唯一无法静态确认的(`aria-label` 来自语言表,不是源码字面量),
|
|
153
|
+
所以当它匹配不到时,报告会附上页面上实际存在的按钮 `aria-label` 采样 —— 一次调用就能看出
|
|
154
|
+
该匹配什么,不必再靠猜。
|
|
155
|
+
|
|
156
|
+
其余 `data-*` 钩子已在**已发布的 `app.asar`** 里逐个确认存在(`data-composer-input` ×1、
|
|
157
|
+
`data-question-key` ×1、`data-plan-review-key` ×1、`data-approval-key` ×2、`data-error`、
|
|
158
|
+
`data-ds-dark-theme`)—— 源码里有 ≠ 发行版里有,所以查的是发行版。
|
|
159
|
+
|
|
64
160
|
## 改形象(素材流水线)
|
|
65
161
|
|
|
66
162
|
**形象的唯一权威来源是 [tools/CHARACTER-PROMPT.md](tools/CHARACTER-PROMPT.md)** —— 逐字复用的提示词母版 + 四张姿态 + 完整生成参数。改形象 = 改那个文件、重新出图、再跑一条命令。
|
|
67
163
|
|
|
68
164
|
```bash
|
|
69
|
-
# 1. 按 tools/CHARACTER-PROMPT.md
|
|
165
|
+
# 1. 按 tools/CHARACTER-PROMPT.md 的参数与提示词,为五个情绪各出一张【绿幕】图,存成 <mood>.png
|
|
70
166
|
# 2. 绿幕 → 抠图 → 四档素材,一步到位(任何一张抠图异常都会以非 0 退出)
|
|
71
|
-
|
|
167
|
+
# --skin 决定输出到 assets/<skin>/,也就是新增一套可切换的皮肤
|
|
168
|
+
node tools/assets.mjs --from <绿幕母版目录> --skin <皮肤名>
|
|
169
|
+
# 如果手上只有【已经抠好的透明 PNG】(绿幕原图不在了),走这个,不重复抠图:
|
|
170
|
+
node tools/tiers.mjs --from <透明PNG目录> --skin <皮肤名>
|
|
72
171
|
# 3. 打进客户端半包
|
|
73
172
|
node build.mjs --tier=hi # hi 档 = 2048x3072,与母版同尺寸,仅重新编码
|
|
74
173
|
# 4. 预检
|
|
@@ -85,7 +184,7 @@ node verify-wisp.mjs
|
|
|
85
184
|
|
|
86
185
|
## 性能
|
|
87
186
|
|
|
88
|
-
一切数字都是在真实 Chromium 里量的(合成 10k 节点会话记录,headless、CPU
|
|
187
|
+
一切数字都是在真实 Chromium 里量的(合成 10k 节点会话记录,headless、CPU 光栅,即**最不利**的一档)。计数与耗时都靠**包装原生 `querySelector`/`querySelectorAll`** 得到,不靠估算。可复现的探针:`node tools/perf.mjs`(浏览器是可选依赖,缺了会跳过并说明)。
|
|
89
188
|
|
|
90
189
|
### 稳态
|
|
91
190
|
|
|
@@ -93,16 +192,27 @@ node verify-wisp.mjs
|
|
|
93
192
|
|---|---|
|
|
94
193
|
| 动画帧使用量 | **0**(走 `timer` 服务,根本不挂帧循环) |
|
|
95
194
|
| 定时器 | 1 个 `interval`(反应轮询)+ 3 个 `timeout` |
|
|
96
|
-
| 反应轮询成本 |
|
|
97
|
-
| 每帧平均间隔 | 无插件 4.
|
|
98
|
-
| 每秒 DOM 查询 | `querySelector` 1
|
|
195
|
+
| 反应轮询成本 | 每轮 3 次 `querySelector`,10 s 内共 **7.2–8.3 ms**(≈ **0.86 ms/轮**) |
|
|
196
|
+
| 每帧平均间隔 | 无插件 **4.194 ms** vs 挂载后 **4.217 ms** → **+0.023 ms**(4 轮交替重复) |
|
|
197
|
+
| 每秒 DOM 查询 | `querySelector` **1.6 次**(有输入框)/ **2.8 次**(无输入框,最悲观);`querySelectorAll` **0 次** |
|
|
198
|
+
| DOM 查询耗时 | **0.72–0.83 ms/秒** ≈ 单核的 **0.07–0.08%** |
|
|
199
|
+
| 出错扫描 | **每跑完一轮一次**,10k 节点页面上实测 **0.017 ms**(不随轮询频率增长) |
|
|
200
|
+
|
|
201
|
+
**为什么是这个数量级**:每轮轮询查三次 —— 忙碌钩子、待处理面板钩子、以及输入框钩子。第三条只在输入框**尚未解析出来**时才查(解析一次后缓存);页面里没有输入框时它是常驻的,所以 2.8 次/秒是**最悲观**的一档,1.6 次/秒才是真实应用的形态。
|
|
99
202
|
|
|
100
|
-
|
|
203
|
+
老实现是"每次都重新全量查一遍":`querySelectorAll` 每次都调、1.27 ms/次。后来把输入框元素**解析一次后缓存**(仅在它离开文档时重新解析),忙碌判定从 4 条精确选择器改成 2 条子串选择器 —— 后者还能顺带匹配没见过的语言标签。
|
|
204
|
+
|
|
205
|
+
**帧开销必须交替重复测**:单次差值里的噪声就有 0.1 ms 量级(同一份代码两组测得 +0.127 与 −0.002),一次测量说明不了任何事。
|
|
101
206
|
|
|
102
207
|
### 一次性成本
|
|
103
208
|
|
|
104
209
|
- **挂载瞬间**有一次约 15–50 ms 的卡顿(headless CPU 光栅下)。来源是注入样式表触发的整篇样式重算 + 首次模糊光栅化,不是稳态开销;预热 1.5 s 后测 6 s,**没有任何一帧超过 20 ms**。
|
|
105
|
-
-
|
|
210
|
+
- **挂载一次性开销实测 19.3 ms**(5.2 MB 的包)。
|
|
211
|
+
- **包体积不是约束**:解析+执行 5.2 MB → 19.3 ms、8 MB → 28.4 ms、16 MB → 32.3 ms、24 MB → 46.5 ms,
|
|
212
|
+
斜率约 **1.45 ms/MB**(次线性)。再加一整套皮肤(+1.1 MB)约 +1.6 ms。
|
|
213
|
+
探针:`node tools/bundle-cost.mjs`。
|
|
214
|
+
- 28 张精灵图(**4 皮肤 × 7 情绪**)共 4.58 MB base64,挂载时只解码用到的 1 张,其余在 1.5 s 后预热。
|
|
215
|
+
- **解码后的内存**:一张精灵图是 `宽 × 高 × 4` 字节 —— 当前 1024×1536 档 **6.0 MB/张**,预热会把当前皮肤的五张全解出来(≈30 MB);`--tier=hi` 是它的 4 倍。选档位不只是锐度问题。
|
|
106
216
|
|
|
107
217
|
### 已知的、没修的
|
|
108
218
|
|
|
@@ -120,13 +230,18 @@ node verify-wisp.mjs
|
|
|
120
230
|
| `right` | number | `26` | 距右边缘 px(仅首次落位;拖动过之后以记忆位置为准) |
|
|
121
231
|
| `bottom` | number | `46` | 距下边缘 px |
|
|
122
232
|
| `size` | number | `4` | 缩放,限制 `0.4–8`;默认绘制 560×840 CSS px |
|
|
233
|
+
| — | — | — | 菜单里调过的 `size` 会**记在本地**并活过刷新;但配置里显式写了 `size` 时以配置为准 |
|
|
123
234
|
| `sleepAfterMs` | number | `90000` | 静置多久打盹,限制 `5000–3600000` |
|
|
124
235
|
| `reactions` | boolean | `true` | 是否跟随 Agent 忙碌 / 输入框内容变情绪 |
|
|
125
236
|
| `persist` | boolean | `true` | 是否跨刷新记住位置 |
|
|
126
237
|
| `celebrate` | boolean | `true` | 一轮跑完是否庆祝一下 |
|
|
127
238
|
| `celebrateAfterMs` | number | `2500` | 只庆祝跑够这么久的轮次(避免每次小工具调用都跳) |
|
|
128
239
|
| `happyMs` | number | `1100` | 高兴状态持续多久 |
|
|
240
|
+
| `hidden` | boolean | `false` | 挂载时就是躲起来的(只能强制隐藏,叫回来走菜单或 `__wisp.show()`) |
|
|
129
241
|
| `chatterMs` | number | `0` | `>0` 时按此间隔随机说句话;`0` = 安静 |
|
|
242
|
+
| `careAfterMs` | number | `5400000` | 连续活动多久提醒你起来动动(90 分钟);`0` = 关 |
|
|
243
|
+
| `milestones` | boolean | `true` | 今日轮次到里程碑时得意一下 |
|
|
244
|
+
| `memory` | boolean | `true` | 她记着你(一起多少天 / 习惯放哪 / 常穿哪套) |
|
|
130
245
|
| `wander` | boolean | `true` | 空闲时自己小幅踱步 |
|
|
131
246
|
| `wanderMs` | number | `45000` | 多久考虑溜达一次,限制 `5000–3600000` |
|
|
132
247
|
| `wanderRange` | number | `260` | 单次溜达最远距离 px,限制 `0–2000` |
|
|
@@ -148,7 +263,7 @@ node verify-wisp.mjs
|
|
|
148
263
|
|
|
149
264
|
```js
|
|
150
265
|
window.__wisp.say('你好') // 说一句
|
|
151
|
-
window.__wisp.mood('happy') // idle | alert | happy | sleep
|
|
266
|
+
window.__wisp.mood('happy') // idle | alert | attn | happy | sleep
|
|
152
267
|
window.__wisp.configure({ size: 1.5 }) // 部分更新,其余键保持不动
|
|
153
268
|
window.__wisp.move(200, 300)
|
|
154
269
|
window.__wisp.position // { x, y }
|
|
@@ -231,8 +346,752 @@ plugin_manager install_bundle https://github.com/969246694/dsh-wisp
|
|
|
231
346
|
|
|
232
347
|
---
|
|
233
348
|
|
|
349
|
+
## 生成审计:素材从哪来,可自查
|
|
350
|
+
|
|
351
|
+
有人问"严查是否存在图生图",我就把这件事做成了**可复查的文件**,而不是一句承诺。
|
|
352
|
+
|
|
353
|
+
### 为什么需要它
|
|
354
|
+
|
|
355
|
+
Maker 代理自己维护一张登记表(`<Maker 项目>/.maker/assets/generated-assets.json`),
|
|
356
|
+
记录每次生成的**工具名 / 提示词 / 时间 / 产物路径** —— 但它**不记参数**。
|
|
357
|
+
而 `generate_image` 与 `batch_generate_images` 是**允许**传可选 `reference_images` 的,
|
|
358
|
+
所以"没喂参考图"这件事,代理那边证不了。
|
|
359
|
+
|
|
360
|
+
### 这个日志记什么(`audit/generation-audit.jsonl`)
|
|
361
|
+
|
|
362
|
+
两类证据**分开记**,谁强谁弱一目了然:
|
|
363
|
+
|
|
364
|
+
| 字段 | 来源 | 强度 |
|
|
365
|
+
|---|---|---|
|
|
366
|
+
| `registry.tool` | **代理自己写的**(独立于本插件) | **硬证据**:`edit_image` 是唯一能做图生图的工具,工具名说了算 |
|
|
367
|
+
| `declared.reference_images` | 本项目调用前写下的参数 | **自我声明**,但写进哈希链、事后改不了,且写在调用之前 |
|
|
368
|
+
| `output.sha256` | 入包素材本身的哈希 | 可复查:事后有没有被改过 |
|
|
369
|
+
| `master.sha256` | 母版的哈希(开发机上) | 可复查母版是否被换过 |
|
|
370
|
+
|
|
371
|
+
每条记录都带 `prev` 指向上一条的 `hash` —— **改任何一条都会断链**,而断链会被预检抓住。
|
|
372
|
+
|
|
373
|
+
### 怎么用
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
node tools/audit-log.mjs --verify # 校验链 + 重算入包素材哈希
|
|
377
|
+
node tools/audit-log.mjs --declare <manifest> # 生成前:登记将要发出的参数
|
|
378
|
+
node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登记表 + 记产物哈希
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
预检里已有两条常驻断言(`3z` 段):链校验通过且覆盖全部入包素材、且**审计中不得出现 `edit_image`**。
|
|
382
|
+
|
|
383
|
+
### 它证明了什么、没证明什么
|
|
384
|
+
|
|
385
|
+
- **证明了**:入包素材逐字节等于记录在案的那次调用的产物,此后没被改过;那些调用在代理登记表里
|
|
386
|
+
用的是 `generate_image` / `batch_generate_images`,**不是** `edit_image`。
|
|
387
|
+
- **没证明**:对**回溯补录**的记录(本功能上线前的 35 个素材),没有"当次自我声明的参数",
|
|
388
|
+
因为那时还没有这个日志 —— 它们的 `declared` 字段是 `null`,如实标注,不冒充。
|
|
389
|
+
只有今后新生成的素材才会带上完整的 declare + confirm 两条记录。
|
|
390
|
+
- 另外:即使某次真的传了参考图,代理也不会在登记表里留下痕迹(它不记参数)——
|
|
391
|
+
所以"没喂参考图"最终依赖的是**自我声明 + 声明不可事后篡改**,而不是第三方取证。这一条必须说清楚。
|
|
392
|
+
|
|
393
|
+
## 更新:能做什么、为什么不能自己更新
|
|
394
|
+
|
|
395
|
+
### 先说结论
|
|
396
|
+
|
|
397
|
+
- **检查更新:能。** 走平台给的服务 `ctx.web.fetch`(DSH 自己的 `web_fetch` 工具用的就是它)。
|
|
398
|
+
- **安装更新:不能。** 插件既没有文件系统、也没有包管理器;DSH 的安装器没有以可注入服务的形式
|
|
399
|
+
暴露给插件。安装仍然要走 DSH 自己的插件列表。
|
|
400
|
+
|
|
401
|
+
为什么必须绕这么一圈 —— 两边的限制都查到源码级:
|
|
402
|
+
|
|
403
|
+
- **浏览器半包**的 `fetch` 是教学陷阱(一调用就抛),原文写着 "network belongs to the HOST half"。
|
|
404
|
+
- **宿主半包**跑在 **vm 沙箱**里:`harness` 只有 `defineTool / registerTool / handle`,
|
|
405
|
+
沙箱里还有 `nodeApiTraps()` 把 Node API 全挡了,允许的 `ctx` 动词白名单里**没有网络与文件系统**。
|
|
406
|
+
|
|
407
|
+
所以网络只能走**服务**:宿主半包 `inject: { optional: ['web'] }`,用 `ctx.web.fetch(...)` 抓远端
|
|
408
|
+
`package.json`,再用 `harness.handle('checkUpdate', …)` 注册;浏览器半包 `host.call('checkUpdate')` 调。
|
|
409
|
+
**客户端那边一点网络都没碰** —— `fetch` 陷阱原样留着。
|
|
410
|
+
|
|
411
|
+
**`web` 声明为可选(optional)不是风格问题**:硬依赖一个没加载的服务会让 fiber 永远 `waiting`,
|
|
412
|
+
她会直接从界面上消失。预检里有两条断言专门盯着这件事(必须可选、不能必需)。
|
|
413
|
+
|
|
414
|
+
> 更正:上一版 README 写的是"这个插件不能检查更新" —— 那个结论错了。它建立在
|
|
415
|
+
> "服务目录里没有网络类服务"上,而我漏看了 `ctx.web`:它是**注册表式**服务,提供者
|
|
416
|
+
> `web-fetch-http` 只往里 `registerFetchProvider`,所以按 `provide("web")` 搜是搜不到的。
|
|
417
|
+
|
|
418
|
+
### 那这个插件做了什么
|
|
419
|
+
|
|
420
|
+
都是**沙箱允许范围内真能做到的**:
|
|
421
|
+
|
|
422
|
+
| 能力 | 怎么用 |
|
|
423
|
+
|---|---|
|
|
424
|
+
| **真的去查更新** | 菜单「版本与更新」经 `host.call` → 宿主半包 → `ctx.web.fetch` 抓远端 `package.json`,
|
|
425
|
+
比对后说「有新版本了:X」或「已经是最新的了」;查不到就如实说「更新没查成」 |
|
|
426
|
+
| **报版本** | 菜单「版本与更新」说一句「我是 1.24.0。<这一版做了什么>」;`__wisp.version` / `__wisp.doctor().update` 也能读 |
|
|
427
|
+
| **更新感知** | 她记住上次见到的版本(localStorage)。**版本变了就说一句「我更新到 … 了」**;第一次安装不吭声(那时她不是更新,是刚来) |
|
|
428
|
+
| **复制包名** | 同一项会把 `dsh-wisp` 复制到剪贴板,方便在 DSH 的插件列表里粘贴安装。**没有剪贴板权限时静默降级**成只说话,不报错 |
|
|
429
|
+
| **能力边界如实上报** | `doctor().update` 里有 `canSelfUpdate: false` 和 `why`(写明沙箱原因) |
|
|
430
|
+
|
|
431
|
+
### 真正更新它的路径
|
|
432
|
+
|
|
433
|
+
1. DSH 的插件列表 →「按插件包名安装」/ 更新(DSH 自己的「检查更新…」也在那里);
|
|
434
|
+
2. 或对本包目录重新 `npm i` / `npm pack` 后安装;
|
|
435
|
+
3. 更新后重载页面 —— 她会自己发现版本变了并说一句。
|
|
436
|
+
|
|
437
|
+
### 预检里的守卫
|
|
438
|
+
|
|
439
|
+
`WHATS_NEW` 必须写着**当前版本**,否则预检红 —— 这样"这一版做了什么"不会静默过期。
|
|
440
|
+
另外 `canSelfUpdate === false`、`hint` 里必须含包名、`why` 里必须说明沙箱原因,都有断言盯着。
|
|
441
|
+
|
|
234
442
|
## 变更
|
|
235
443
|
|
|
444
|
+
### 1.25.0
|
|
445
|
+
|
|
446
|
+
- **她真的会检查更新了。** 上一版只说"我做不到" —— 那一版没找对通道,这一版找到了:
|
|
447
|
+
|
|
448
|
+
**平台认可的路是服务,不是绕过陷阱。** 宿主半包 `inject: { optional: ['web'] }`,
|
|
449
|
+
用 `ctx.web.fetch({ url })` 抓远端 `package.json`(DSH 自己的 `web_fetch` 工具用的就是这条),
|
|
450
|
+
再用 `harness.handle('checkUpdate', …)` 注册;浏览器半包 `host.call('checkUpdate')` 调用。
|
|
451
|
+
**客户端一点网络都没碰** —— `fetch` 陷阱原样留着。
|
|
452
|
+
|
|
453
|
+
- 结果四态:`current` / `available` / `failed` / `unsupported`,`doctor().update.lastCheck` 可读;
|
|
454
|
+
- 「版本与更新」一步到位:报版本 + 复制包名 + 去查,查完用一句话覆盖(一次只冒一个气泡);
|
|
455
|
+
- **非 2xx 是返回结果、不是抛错**(服务契约原文),所以状态码必须自己判;
|
|
456
|
+
- 没有 `web` 服务 / 没有 host 座位 / 调用抛错 —— 三种情况都**如实报告,不抛**。
|
|
457
|
+
|
|
458
|
+
- **`web` 声明为可选 inject 是安全属性**:硬依赖一个没加载的服务会让 fiber 永远 `waiting`,
|
|
459
|
+
**她会直接消失**。预检里两条断言盯着(必须可选、不能必需)。
|
|
460
|
+
|
|
461
|
+
- **更正上一版 README 的错误结论。** 上一版写"不能检查更新" ✗ —— 那是建立在"服务目录里没有网络类
|
|
462
|
+
服务"上的,而我漏看了 `ctx.web`:它是**注册表式**服务,提供者 `web-fetch-http` 只往里
|
|
463
|
+
`registerFetchProvider`,所以按 `provide("web")` 搜不到。教训:**搜不到不等于不存在**,
|
|
464
|
+
换个角度(从消费方 `dsh-tool-web` 反查)就看到了。
|
|
465
|
+
|
|
466
|
+
- 预检 353 → **377 项**(宿主半包第一次有了行为测试:六种抓取结果 + 注册 + 无座位 + apply 的 disposer)。
|
|
467
|
+
|
|
468
|
+
- 更新源:`https://raw.githubusercontent.com/969246694/dsh-wisp/main/package.json`(可用 `updateUrl` 覆盖)。
|
|
469
|
+
**仓库当前是 0.6.0,本地是 1.25.0** —— 所以第一次检查会说"本地比远端新",那是实情不是 bug;
|
|
470
|
+
把新版推上 GitHub 之后就会正常报"有新版本"。
|
|
471
|
+
|
|
472
|
+
### 1.24.0
|
|
473
|
+
|
|
474
|
+
### 1.24.0
|
|
475
|
+
|
|
476
|
+
- **新增「版本与更新」**(菜单项 + `__wisp.version` + `doctor().update`):
|
|
477
|
+
- 说出当前版本与这一版的要点;
|
|
478
|
+
- **更新感知**:记住上次见到的版本,变了就说一句「我更新到 X 了」(第一次安装不吭声);
|
|
479
|
+
- **复制包名** `dsh-wisp` 到剪贴板,方便在 DSH 里粘贴安装;没有剪贴板权限时**静默降级**成只说话;
|
|
480
|
+
- `doctor().update` 如实上报 `canSelfUpdate: false` 与原因。
|
|
481
|
+
|
|
482
|
+
- **先查了平台,再动手**:这个插件**不能自己检查/安装更新**,而且是源码级的结论 ——
|
|
483
|
+
宿主半包跑在 vm 沙箱里,`harness` 只给 `{ defineTool, registerTool, handle }`,
|
|
484
|
+
沙箱的 `nodeApiTraps()` 挡住 Node API,允许的 `ctx` 动词白名单里**没有网络与文件系统**;
|
|
485
|
+
DSH 自带的安装器也**没有可注入的服务**(服务目录里只有 dynamicCordisRunner / profileContext /
|
|
486
|
+
loader / modules 等)。所以只做了平台允许范围内真能做到的三件事,并在 README 里写清边界。
|
|
487
|
+
|
|
488
|
+
- 预检 340 → **353 项**(版本、更新感知、剪贴板、降级、能力边界都测了)。
|
|
489
|
+
|
|
490
|
+
- 又踩了两次同一个坑,都记下来:
|
|
491
|
+
1. 补丁锚点**缩进又深了两格**(应是 10/6/8,我写了 12/8/10)—— 三处失配,好在失配不会误改;
|
|
492
|
+
2. 我把测试用的 `clipboardWrites` 声明在了 `createHarness` **内部**,却在模块作用域里用它 ——
|
|
493
|
+
修法不是把变量提升,而是**挂到替身的返回对象上**(测试要能从外面看的东西,本来就该在那儿)。
|
|
494
|
+
|
|
495
|
+
### 1.23.0
|
|
496
|
+
|
|
497
|
+
### 1.23.0
|
|
498
|
+
|
|
499
|
+
- **交互设计补三个缺口**(都是「只能靠鼠标」的问题):
|
|
500
|
+
|
|
501
|
+
1. **Ctrl/⌘ + 滚轮调大小** —— 以前只能开菜单点「放大一点 / 缩小一点」。
|
|
502
|
+
**只认带修饰键的滚轮**:她占地不小,直接吞掉普通滚轮等于在她身上把页面滚动禁掉,
|
|
503
|
+
所以普通滚轮**原样放行、连 preventDefault 都不调**(这条写成了断言)。
|
|
504
|
+
上下限仍由 `CONFIG_SPEC` 一处夹紧(0.4–8),没有第二处重复。
|
|
505
|
+
2. **方向键微调位置** —— 在这之前,键盘用户拿到焦点后除了开菜单**什么都做不了**,
|
|
506
|
+
挪动她**只能靠鼠标拖**。现在方向键 16px、Shift 4px,与拖动共用同一套边界夹紧与位置持久化;
|
|
507
|
+
按键同时 `preventDefault`(不拦的话页面会跟着滚,她看起来像「跑掉了」)。
|
|
508
|
+
3. **躲起来之后右键迷你按钮也能开菜单**,菜单里那一项改成**开关**:
|
|
509
|
+
躲着时显示「回来吧」、在着时显示「先躲起来」。原先躲起来就只剩「叫她回来」一个动作,
|
|
510
|
+
换皮肤/调大小/换角落全都够不着。
|
|
511
|
+
|
|
512
|
+
- 预检 319 → **340 项**(+21:正向、反向、边界、持久化都测了)。冒烟 46 × 2 全过。
|
|
513
|
+
|
|
514
|
+
- **过程记录(三次失败,都是我自己写错)**:
|
|
515
|
+
1. 补丁脚本连栽四次 —— 把中文引号写进了 JS 双引号字符串里。改成**用模板字符串承载插入内容**
|
|
516
|
+
之后这一类错误不会再犯。
|
|
517
|
+
2. 三个锚点全部失配 —— 我按 8/10/12 空格写缩进,文件里实际是 **6/8/10**。
|
|
518
|
+
(失配是好事:没有误改到别处。)
|
|
519
|
+
3. 两条断言失败又都是测试自己写错:从 y=100 按 5 次(80px)**到不了 0** 却断言到了 0;
|
|
520
|
+
把 `element` 写在了替身上(它在 API 上)。
|
|
521
|
+
4. 最后一条「可见时那一项应显示先躲起来」失败,是**替身的 `all()` 会返回已移除的旧菜单项** ——
|
|
522
|
+
`find` 拿到的是上一份菜单里的过期按钮。实现没问题(点击菜单项本来就会先 closeMenu 再执行),
|
|
523
|
+
是断言该只认还在文档里的节点。
|
|
524
|
+
|
|
525
|
+
### 1.22.0
|
|
526
|
+
|
|
527
|
+
### 1.22.0
|
|
528
|
+
|
|
529
|
+
- **新增生成审计日志(哈希链)** —— 起因是有人要求"严查是否存在图生图"。
|
|
530
|
+
|
|
531
|
+
查出来的结果分两半,都写进了 `audit/generation-audit.jsonl` 与 README:
|
|
532
|
+
- 代理登记表里**确实有 14 条 `edit_image`(图生图)**,但**全部是 09-27 的**,不是本轮;
|
|
533
|
+
- 本轮的 **77 次调用全部是 `generate_image` / `batch_generate_images`**,`edit_image` 为 0;
|
|
534
|
+
- 入包 **140 个 webp 全部是重出当天的**,且四档与原母版**逐字节可复现**(说明中间没有任何加工)。
|
|
535
|
+
|
|
536
|
+
工具:`tools/audit-log.mjs`(`--declare` / `--confirm` / `--verify` / `--backfill`)。
|
|
537
|
+
每条记录带 `prev` → `hash` 的链,**改任何一条都会断链**;预检里加了两条常驻断言(`3z` 段)。
|
|
538
|
+
|
|
539
|
+
**能力边界写在 README 里,没有含糊**:代理不记参数,所以"没喂参考图"依赖的是
|
|
540
|
+
**自我声明 + 声明不可事后篡改**,不是第三方取证;回溯补录的记录 `declared` 为 `null`,如实标注。
|
|
541
|
+
|
|
542
|
+
- **实测过篡改检测**:改入包素材一个字节 → `AUDIT FAILED`;改日志里一个字段 → 内容哈希不匹配 + 后继断链,双报。
|
|
543
|
+
(第一次测日志篡改时我用了 PowerShell 改写,改动其实没落盘,我却当成"链有洞" —— 是测试方法的问题,
|
|
544
|
+
不是工具的问题。换成 Node 严格读写后结论才对。)
|
|
545
|
+
|
|
546
|
+
- 预检 317 → **319 项**;`tools/audit-log.mjs` 与 `audit/` 已加入发布清单。
|
|
547
|
+
|
|
548
|
+
### 1.21.0
|
|
549
|
+
|
|
550
|
+
### 1.21.0
|
|
551
|
+
|
|
552
|
+
- **去掉双马尾:全部五套皮肤统一为社区规范的长直发。**
|
|
553
|
+
|
|
554
|
+
用户的要求是"不保留双马尾" —— 双马尾是本插件早期的原创设计,社区规范里并没有。
|
|
555
|
+
现在四套旧皮肤(`deepsea` / `classic` / `night` / `pajama`)各 7 个情绪**全部重出**(28 张),
|
|
556
|
+
连同已完成的 `canon`,五套皮肤共用同一套角色特征:
|
|
557
|
+
|
|
558
|
+
- **深蓝色中长直发自然披散**(无马尾、无双马尾、无盘发),发梢渐变浅蓝,齐刘海;
|
|
559
|
+
- **头顶大圆环呆毛**(社区标识);
|
|
560
|
+
- **鲸鳍耳**:贴着头发向外下方约 35 度、长度约等于头宽、深蓝外表面 + 下缘白到银灰的内衬宽带;
|
|
561
|
+
- 各套只保留**不同的服装**(吊带短裙 / 长袖全套女仆装 / 方领赛璐璐 / 露肩礼服 / 宽松睡衣)。
|
|
562
|
+
|
|
563
|
+
全部为**纯文生图,未使用任何参考图**。键控软边 0.24%–0.63%。
|
|
564
|
+
|
|
565
|
+
- 包 7436 → **5193 KB**(新素材体积更小;解析代价反而下降约 3 ms)。
|
|
566
|
+
- 预检 317 / 冒烟 46 × 2 / PACK OK 全过。
|
|
567
|
+
|
|
568
|
+
- 工作方式记录:**每张图单独展示**,不拼成对比图 —— 拼图会把每张压得太小,看不清细节。
|
|
569
|
+
|
|
570
|
+
### 1.20.0
|
|
571
|
+
|
|
572
|
+
### 1.20.0
|
|
573
|
+
|
|
574
|
+
- **新增第五套皮肤「原版女仆」(`canon`)** —— 对齐社区规范的那一套,7 个情绪齐全:
|
|
575
|
+
长直发披散 + 大圆环呆毛 + 发箍右侧蓝蝴蝶结 + 后掠的鲸鳍耳(深蓝外表面、下缘白到银灰宽带)
|
|
576
|
+
+ 长袖全套女仆装(立领白荷叶边、领口蝴蝶结蓝宝石、金色袖口与裙摆纹样、白色半身围裙、
|
|
577
|
+
白色长袜、深蓝圆头皮鞋)。全部为**纯文生图**,未使用任何参考图。
|
|
578
|
+
|
|
579
|
+
- **「初版女仆」改名「素绘女仆」**(`classic`):它的实际特征是与众不同的**赛璐璐平涂**
|
|
580
|
+
(其余几套都是厚涂),叫"初版/基础"在"原版"入场后就站不住了。
|
|
581
|
+
|
|
582
|
+
- 皮肤优先级:`deepsea`(旗舰,默认)→ `canon`(原版)→ `classic` / `night` / `pajama`。
|
|
583
|
+
|
|
584
|
+
- **仍待办**:`classic` / `night` / `pajama` 三套的现有素材还是**双马尾 + 大耳 + 无呆毛**的旧规格
|
|
585
|
+
(各 7 张,共 21 张)。它们保留本插件的原创双马尾形象,只把**耳朵**换成已验证的鲸鳍耳写法,
|
|
586
|
+
并补上呆毛。
|
|
587
|
+
|
|
588
|
+
### 1.19.0
|
|
589
|
+
|
|
590
|
+
### 1.19.0
|
|
591
|
+
|
|
592
|
+
- **鳍耳写法定稿(纯文生图,未用任何参考图)**,并记录了两个可复现的坑:
|
|
593
|
+
|
|
594
|
+
**坑一:不提"缺口/锯齿"**。社区鳍耳下缘有极轻微波浪,我当成最重要的识别点写("下缘 3 个半圆缺口、
|
|
595
|
+
像被剪了三刀"),结果**三份不同写法全部画成锋利的尖牙/锯子边**;连"不要锯齿、不要像锯子"这种
|
|
596
|
+
负面词也没用 —— **负面词同样会招来那个形状**。正确做法是只说正面形状语言("上下缘都是光滑连续的弧线,
|
|
597
|
+
只有极轻微起伏"),把"缺口"整个不提。
|
|
598
|
+
|
|
599
|
+
**坑二:尺寸不是主要问题,朝向才是**。用户说"耳朵太大"时我一路缩短到"头宽三分之一",**结果缩过头**;
|
|
600
|
+
逐像素核对社区立绘后确认:社区的鳍其实**很长(约等于头宽)**,问题在于**贴着头发向后下方掠**,
|
|
601
|
+
而我做成了**朝上外张的翅膀**。现在"朝向"是第①条,并明确否定三种错误朝向。
|
|
602
|
+
|
|
603
|
+
**能力边界(写清楚了)**:纯文生图能到"同一个设计、一眼认出是同一个角色",**做不到像素级复刻** ——
|
|
604
|
+
社区自己的一致性工具是一套**训练过的 LoRA**(其 Civitai 模型页),不是纯提示词。
|
|
605
|
+
同时坚持**不喂参考图**:喂图会拉低质量、出破绽(用户的经验,本轮全程遵守)。
|
|
606
|
+
|
|
607
|
+
- 迭代过程:A/B/C 三版全是尖齿 → D 变平滑 → E 缩得过小 → **F 定稿**(对比图留在开发机上,未随包发布)。
|
|
608
|
+
|
|
609
|
+
### 1.18.0
|
|
610
|
+
|
|
611
|
+
### 1.18.0
|
|
612
|
+
|
|
613
|
+
- **修正两处与社区规范的偏差(用户指出的)**:
|
|
614
|
+
1. **发型**:我给她加的是**超长双马尾** —— 那是本插件的原创设计,**社区规范里不是这样**:
|
|
615
|
+
社区是**中长直发自然披散**、发梢渐变浅蓝。
|
|
616
|
+
2. **耳朵朝向**:社区是**向后下方斜掠、几乎贴着头发**,两色(上深蓝下白)、**下缘有锯齿缺口**;
|
|
617
|
+
我做成了向上外张的大翅膀。用户说"太大了",**实测问题在朝向不在长度** ——
|
|
618
|
+
社区的鳍其实不短(约与脸宽相当),但它是**贴着头发向后掠**的。
|
|
619
|
+
|
|
620
|
+
顺带补上两个漏掉的头部特征:**发箍右侧的蓝色蝴蝶结**、**头顶的大圆环呆毛**。
|
|
621
|
+
|
|
622
|
+
- **新增「原版女仆」(`canon`)皮肤的完整提示词**(写进 `tools/CHARACTER-PROMPT.md`):
|
|
623
|
+
这一套的目标**不是原创,而是复刻社区共识** —— 长直发 + 大圆环呆毛 + 发箍蓝蝴蝶结 +
|
|
624
|
+
后掠锯齿鳍耳 + 长袖全套女仆装 + 白袜 + 深蓝圆头皮鞋。首张测试已通过比对。
|
|
625
|
+
|
|
626
|
+
- **设计分工定下来了**:双马尾是**本插件的原创形象**(deepsea / classic / night / pajama 保留),
|
|
627
|
+
社区形象作为**第五套「原版女仆」**共存 —— 用户的要求是"公认服装作为一套原版服装",即**并存而非替换**。
|
|
628
|
+
|
|
629
|
+
### 1.17.0
|
|
630
|
+
|
|
631
|
+
### 1.17.0
|
|
632
|
+
|
|
633
|
+
- **提示词规格修正(两条,都已实测确认)**:
|
|
634
|
+
1. **鳍耳缩小**:上一版每片约占头宽三分之一,实际画出来是一对大翅膀,太抢戏。
|
|
635
|
+
现在写死"**约头宽的四分之一,不要超过三分之一,绝不要画成翅膀那样大**"。
|
|
636
|
+
2. **加呆毛**(社区标识之一):一小缕头发从发顶竖起、顶端向前弯成小圆环。
|
|
637
|
+
|
|
638
|
+
- **写作技巧(这次最有价值的发现)**:同一个要求写在提示词**开头**会被模型忽略,
|
|
639
|
+
写在末尾的**【画面完整性,逐条核对】**清单里才会被执行 —— 呆毛第一版写在开头直接被省略,
|
|
640
|
+
编进清单后一次就有。双马尾、鲸尾、发尾完整一直生效,本来就都在那个清单里。
|
|
641
|
+
已写进 `tools/CHARACTER-PROMPT.md`。
|
|
642
|
+
|
|
643
|
+
- 待办(本版未做):**四套皮肤全部素材需按新规格重出**(耳小而+呆毛);
|
|
644
|
+
**新增一套"原版"皮肤**(社区公认的女仆装);**「初版女仆」改名**。
|
|
645
|
+
|
|
646
|
+
### 1.16.0
|
|
647
|
+
|
|
648
|
+
### 1.16.0
|
|
649
|
+
|
|
650
|
+
- **按社区规范修正她的耳朵:「鲸鳍耳」—— 耳朵本身就是一对鲸鱼胸鳍,不是挂耳饰。**
|
|
651
|
+
|
|
652
|
+
我去查了社区通行的画法,社区为此有一个开源角色规范项目
|
|
653
|
+
[Neko3000/deepseek-whalechan](https://github.com/Neko3000/deepseek-whalechan):
|
|
654
|
+
它把识别点锁定为**发色渐变、鲸鳍耳、呆毛、鲸尾、女仆装**。也就是说**社区早就把答案写在规范里了**,
|
|
655
|
+
而我一直在"给她挂鳍形耳饰"—— 方向就是错的。
|
|
656
|
+
|
|
657
|
+
社区的鳍耳有四个我漏掉的要点(放大社区立绘逐条核对出来的):
|
|
658
|
+
1. **向后上方斜掠张开**(像后掠的小翅膀),不是朝上竖;
|
|
659
|
+
2. **狭长、根部宽、向后收成尖梢**,不是等宽直板;
|
|
660
|
+
3. **上表面深蓝、下缘与内侧白色** —— 座头鲸胸鳍的两面配色,这是最显眼的识别点;
|
|
661
|
+
4. 大小约占头宽三分之一,存在感明显。
|
|
662
|
+
|
|
663
|
+
默认皮肤 **deepsea 的 7 个情绪已按此重做**(键控 0.53%–0.68%)。
|
|
664
|
+
|
|
665
|
+
- 上一版我把它做成了**等宽的直板耳坠**(像塑料小棍)——那是错的方向上加了一次修正。
|
|
666
|
+
这一轮的教训写进了 `tools/CHARACTER-PROMPT.md`:**先去看社区共识,再动手画。**
|
|
667
|
+
- 顺带记下另外两处与社区的差异(待定,未擅自改):**呆毛**(社区锁定标识,我还没有)、
|
|
668
|
+
**服装**(社区是长袖全套女仆装+白袜皮鞋,本插件是单层吊带短裙)。
|
|
669
|
+
- **出处与许可**已记录:社区规范是 CC-BY-NC-SA 4.0(规范文档)/ MIT(工具链),角色属社区共创,
|
|
670
|
+
非商业二创被明确欢迎并建议注明出处,商业使用需授权。
|
|
671
|
+
- **另外三套皮肤(classic / night / pajama)尚未改**(各 7 张,共 21 张)。
|
|
672
|
+
- 预检 316 / 冒烟 46 全过;包 6030 → 6202 KB。
|
|
673
|
+
|
|
674
|
+
### 1.15.0
|
|
675
|
+
|
|
676
|
+
### 1.15.0
|
|
677
|
+
|
|
678
|
+
- **修正她的耳饰:那不是鲸鱼的鳍,是鱼鳍。** 这一条是用户发现的 —— 她耳朵上原本是一对**带棘刺、
|
|
679
|
+
有放射状鳍条、边缘锯形**的鱼鳍,而鲸鱼的胸鳍是**细长圆润、边缘平滑、末端圆钝的桨板**,完全不同。
|
|
680
|
+
|
|
681
|
+
**根因在提示词**:我写的是「鲸鱼鳍造型耳饰」——"鳍"被理解成了鱼鳍。改了三轮才拿准(记进
|
|
682
|
+
`tools/CHARACTER-PROMPT.md`):
|
|
683
|
+
|
|
684
|
+
| 措辞 | 结果 |
|
|
685
|
+
|---|---|
|
|
686
|
+
| 鲸鱼鳍造型耳饰 | ✗ 鱼鳍(鳍条 + 棘刺) |
|
|
687
|
+
| 细长圆润、像叶片 | ✗ 平滑了,但末端分叉,像羽毛 |
|
|
688
|
+
| 柳叶、末端圆钝 | ✗✗ **跑偏成水滴形宝石耳坠** |
|
|
689
|
+
| 扁平等宽长条板 + 圆头 + 显式排除水滴/宝石/吊坠/羽毛 | ✓ 圆头桨板 |
|
|
690
|
+
|
|
691
|
+
**教训**:形状要求要写成**几何描述**(扁平、等宽、圆头、边缘连续),不要用比喻;负面清单要列全 ——
|
|
692
|
+
光说"不要鱼鳍"不够,还得说清"不是什么"。
|
|
693
|
+
|
|
694
|
+
- 本版只重做了**默认皮肤 deepsea** 的 7 个情绪(母版已换、键控 0.48%–0.62%)。
|
|
695
|
+
**另外三套皮肤(classic / night / pajama)仍是鱼鳍**,它们的母版当初是同一套措辞生成的 ——
|
|
696
|
+
要不要一并重做(每套 7 张,共 21 张)由你定。
|
|
697
|
+
- 包 6195 → **6030 KB**(新一批图的体积略小)。预检与冒烟不受影响。
|
|
698
|
+
|
|
699
|
+
### 1.14.0
|
|
700
|
+
|
|
701
|
+
### 1.14.0
|
|
702
|
+
|
|
703
|
+
- **她记着你(零美术)**:记住三件事 —— **一起多少天**、你常把她放在**哪个角**、常让她**穿哪套**。
|
|
704
|
+
够格的时候说出来(`proud` 或 `happy` 表情):
|
|
705
|
+
- 第 2 / 7 / 14 / 30 / 100 天:「今天是第 7 天。」
|
|
706
|
+
- 某个角用过 **≥8 次且占多数**:「你总把我放在右下角。」
|
|
707
|
+
- 某套皮肤换过 **≥6 次**(且不是当前这套):「宵眠睡衣 —— 你好像挺喜欢这套。」
|
|
708
|
+
|
|
709
|
+
**两条防噪规则是这个功能的重点**,不是附带的:同一条记忆一辈子只说一次(`said` 里记着,
|
|
710
|
+
跨刷新有效)、一天最多说一句(`remarkDay`)。没有它们,这些台词会在每次刷新时重复出现,
|
|
711
|
+
很快从"她记得"变成"她唠叨"。`memory: false` 关掉;`__wisp.doctor().memory` 把记录全报出来。
|
|
712
|
+
|
|
713
|
+
- **实现上踩到并修掉的两个问题**:
|
|
714
|
+
1. 我一开始在 `applySkin` 里写了个不存在的变量名(`previousSkin`)—— 每次换皮肤都会抛
|
|
715
|
+
`ReferenceError`。这类错误只有跑起来才会暴露,写的时候看着很对。
|
|
716
|
+
2. 记忆台词原本排在 3.6 秒,和**首次挂载的"右键我一下试试"提示碰在同一秒**,两条互相盖掉。
|
|
717
|
+
测试用"最后一条气泡"断言时读到了提示语 —— 这既是实现撞车,也是断言写错了位置。
|
|
718
|
+
现在台词排在 7 秒,断言改成"她说过(在所有气泡里找)"而不是"最后一条是它"。
|
|
719
|
+
这个观察方式也适用于所有瞬时状态:`happy`/`poked`/`proud`/`care` 都可能被别的台词挤掉。
|
|
720
|
+
- 记录不会无限长:天数列表上限 90 条。
|
|
721
|
+
- 预检 302 → **316 项**。包 6195 KB(记忆功能不增加体积)。
|
|
722
|
+
|
|
723
|
+
### 1.13.0
|
|
724
|
+
|
|
725
|
+
### 1.13.0
|
|
726
|
+
|
|
727
|
+
- **第七个情绪 `proud`(得意)**:双手抱在胸前、下巴微抬、半闭着眼坏笑 —— 与 `happy`(举手大笑)
|
|
728
|
+
一眼能区分。四套皮肤各一张,键控 0.43%–0.66% 软边。
|
|
729
|
+
|
|
730
|
+
- **今日里程碑(行为)**:她数着**今天跑完多少轮**,到 10 / 25 / 50 / 100 / 200 / 500 时用得意的表情
|
|
731
|
+
报出数字(台词里的 `{n}` 会替换成实际轮数)。三条判据都写成了断言:
|
|
732
|
+
- **同一天只说一次**:说过的里程碑跟着计数一起存 —— 否则刷新页面会把「第 10 轮」又说一遍。
|
|
733
|
+
- **换一天归零**:按日期存,昨天的数字与"说过"的记录都不算数(这条用预置存储测,因为它读真实日期,
|
|
734
|
+
虚拟时钟推不动)。
|
|
735
|
+
- **里程碑优先于普通庆祝**:同一次跑完只该说一句,而「第 25 轮」比「跑完了」更值得说。
|
|
736
|
+
- `milestones: false` 关掉。`__wisp.doctor()` 会把今日计数与已说过的里程碑报出来 ——
|
|
737
|
+
不用猜「她为什么突然得意」。
|
|
738
|
+
|
|
739
|
+
- 包 5321 → **6191 KB**。按上一版量出的解析代价(约 1.5 ms/MB),这只多了 **约 1 ms**;
|
|
740
|
+
解码内存仍是每张 6 MB,只在那一套皮肤被显示时才付。
|
|
741
|
+
- 预检 290 → **302 项**。
|
|
742
|
+
|
|
743
|
+
### 1.12.1
|
|
744
|
+
|
|
745
|
+
### 1.12.1
|
|
746
|
+
|
|
747
|
+
- **更正一条我自己制造的假约束。** 上一条变更里写着"冒烟守卫是 6 MB,再加就会撞上,那时该谈按需加载" ——
|
|
748
|
+
那句话是错的:那条红线是我为了"别让它悄悄变胖"自己拍的数,**不是任何真实约束**。
|
|
749
|
+
有人问我"包体哪来的上限",我才去把它量清楚:
|
|
750
|
+
|
|
751
|
+
| 包体积 | 解析+执行 |
|
|
752
|
+
|---|---|
|
|
753
|
+
| 5.2 MB(现在) | 19.3 ms |
|
|
754
|
+
| 8 MB | 28.4 ms |
|
|
755
|
+
| 16 MB | 32.3 ms |
|
|
756
|
+
| 24 MB | 46.5 ms |
|
|
757
|
+
|
|
758
|
+
斜率约 **1.45 ms/MB**,次线性;再加一整套皮肤(+1.1 MB)约 **+1.6 ms**。
|
|
759
|
+
所以冒烟里那条上限**删掉了**,改成只记录体积 —— 真正该守的是"代价",而挂载开销已经由
|
|
760
|
+
`tools/perf.mjs` 量着并写进 README。留着一条会挡住功能、又没有依据的红线,比没有红线更糟:
|
|
761
|
+
它会让下一个做内容的人(包括我自己)把力气花在"按需加载"这种没必要的事情上。
|
|
762
|
+
- 包体积不变(5321 KB)。
|
|
763
|
+
- 顺手把测量探针收进包里(`tools/bundle-cost.mjs`):我第一版 README 里写的是**开发机的绝对路径**,
|
|
764
|
+
被"文档不得指向包外"那条守卫当场抓住 —— 守卫工作了,于是把探针搬进包内,让那句话可复现。
|
|
765
|
+
|
|
766
|
+
### 1.12.0
|
|
767
|
+
|
|
768
|
+
### 1.12.0
|
|
769
|
+
|
|
770
|
+
- **新增久坐提醒(零美术成本的行为内容)**:连续活动到 `careAfterMs`(默认 **90 分钟**)她会抬手指你
|
|
771
|
+
一下、说一句叫你起来动动,然后自己回到 `alert`。新增 12 句 `care` 台词池。
|
|
772
|
+
|
|
773
|
+
判据是这件事真正的难点,我按"能不能被证伪"的顺序定:
|
|
774
|
+
- **什么算"还在坐着"**:跑着任务、或输入框里有字 —— 没有更直接的信号,就用这个近似。
|
|
775
|
+
- **什么算"离开"**:中间停顿超过 90 秒才算。人是会停下来想事情的,把那种间隙当成离开,
|
|
776
|
+
计时就永远攒不满。离开后计时**从头开始**,不接着攒。
|
|
777
|
+
- **多久才该再说一次**:同一个间隔长度,不会一分钟后又唠叨。
|
|
778
|
+
- **睡着时**:先叫醒再提醒 —— 打盹着喊人起来活动,读起来像 bug(`pending` 面板那条路也是这么做的)。
|
|
779
|
+
- `careAfterMs: 0` 整个关掉。
|
|
780
|
+
|
|
781
|
+
这一条**刻意不做成新皮肤**:包已经到 5319 KB、守卫是 6 MB,再加一套皮肤就会撞上。
|
|
782
|
+
同样的力气花在行为上,买到的是新反应而不是新衣服。
|
|
783
|
+
- 修一处测试写法:`care` 是 4.2 秒的瞬时状态,而 `advance` 是分步推进的 —— 一口气推完再采样
|
|
784
|
+
必然错过它。现在用"推进到观察到为止"和"推进中收集出现过的情绪"来断言。
|
|
785
|
+
(同一段里还撞了两个已有变量名,用限定行范围加前缀的方式解决,没有全局改名。)
|
|
786
|
+
- 预检 275 → **290 项**(久坐提醒 12 条断言 + 睡觉唤醒一条)。包体积不变(5310 KB)。
|
|
787
|
+
|
|
788
|
+
### 1.11.0
|
|
789
|
+
|
|
790
|
+
### 1.11.0
|
|
791
|
+
|
|
792
|
+
- **新增第六个情绪 poked(被戳到时的抗议)**,四套皮肤各一张。姿态是「往后一缩、抬手挡在胸前、
|
|
793
|
+
睁大眼睛抿着嘴」—— 与 attn(前倾招手)一眼能区分。
|
|
794
|
+
|
|
795
|
+
行为上给点击升了一级:**戳一下是开心,三秒内戳到第三次她会抗议**,并说一句新的台词
|
|
796
|
+
(新增 10 句 poked 台词池)。窗口过期后计数归零 —— 隔一会儿再戳一下不该被记仇。
|
|
797
|
+
|
|
798
|
+
- **修一个真问题:连戳窗口用的是 Date.now()(真实时间),而插件其他地方都用 nowMs()。**
|
|
799
|
+
两个后果:它与其他有时间意义的行为用的不是同一个时钟;而且在测试里**推不动**(真实时间几乎不动),
|
|
800
|
+
这个行为等于测不了。现在统一用 nowMs(),于是连戳升级、轮询不覆盖、窗口过期归零全部可断言。
|
|
801
|
+
- **素材流水线不再要求「每个情绪都有母版」**:给已有皮肤补一个新情绪时,别的母版早就没了 ——
|
|
802
|
+
以前这会直接中止。现在缺哪个逐条打印(「跳过(这个目录里没有 xxx.png)」),并在**一张都没处理时**
|
|
803
|
+
以非 0 退出兜底 —— 既不因为缺一张就停下,也不会把「文件名写错」静默吞掉。
|
|
804
|
+
- 修替身两处保真度问题:气泡是「每次新建、旧的移除」,所以按类名取第一个拿到的是最早那条,
|
|
805
|
+
要取最新那条才是「她刚才说了什么」;以及 api.sprites 是**当前皮肤的情绪名数组**,不是 URL 表。
|
|
806
|
+
- 预检 265 → **275 项**。包 4487 → **5319 KB**。
|
|
807
|
+
|
|
808
|
+
~~这条要留意:冒烟里的包体守卫是 6 MB,现在已经到 5.3 MB……~~
|
|
809
|
+
|
|
810
|
+
**更正(1.12.1)**:那句话是错的。6 MB 那条红线是我自己拍的数,不是任何真实约束。
|
|
811
|
+
实测解析代价:5.2 MB → 19.3 ms,24 MB → 46.5 ms,斜率约 **1.45 ms/MB**(次线性),
|
|
812
|
+
再加一整套皮肤约 **+1.6 ms**。所以冒烟里那条上限已经删掉,改成只记录体积。
|
|
813
|
+
|
|
814
|
+
### 1.10.0
|
|
815
|
+
|
|
816
|
+
### 1.10.0
|
|
817
|
+
|
|
818
|
+
- **第四套皮肤「宵眠睡衣」(`pajama`)**。形象要素与前几套逐字相同,只换服装:宽松深蓝长袖睡衣
|
|
819
|
+
(领口白色鲸鱼刺绣、裤脚鲸鱼图案)+ **毛绒鲸鱼拖鞋**。键控 0.48%–0.56% 软边。
|
|
820
|
+
|
|
821
|
+
这一套的用意是**剪影差异**:前三套都是"贴身 + 露腿",睡衣是"宽松 + 包住腿 + 毛绒拖鞋",
|
|
822
|
+
一眼能分辨。而且它让 `sleepy`(抱着鲸鱼抱枕打盹)终于贴切了 —— 那是这套里最好的一格。
|
|
823
|
+
|
|
824
|
+
- **包体积 3383 → 4487 KB**。冒烟里有一条"包体不得超过 4 MB"的守卫因此转红 —— 我**先量了代价
|
|
825
|
+
再抬它**:包从 2.3 MB 涨到 4.4 MB,挂载一次性开销 18–19 ms → **22.8 ms**(线性、可忽略),
|
|
826
|
+
DOM 查询率完全不变。上限现在抬到 6 MB,并且注释里写明**下次再加皮肤时就该讨论"按需加载皮肤",
|
|
827
|
+
而不是继续抬这个数字**。
|
|
828
|
+
- 预检 265 项全过(菜单项数从皮肤数推导,所以加皮肤不再让测试变红);冒烟 46 项全过。
|
|
829
|
+
|
|
830
|
+
### 1.9.0
|
|
831
|
+
|
|
832
|
+
### 1.9.0
|
|
833
|
+
|
|
834
|
+
- **第五套皮肤「原版女仆」(`canon`)** —— **对齐社区规范**的那一套(首套非双马尾形象):
|
|
835
|
+
长直发自然披散 + 头顶大圆环呆毛 + 白色荷叶边女仆发箍(右侧蓝蝴蝶结)+ 向后下方斜掠的鲸鳍耳
|
|
836
|
+
(深蓝外表面、下缘一条白到银灰色的内衬宽带)+ 深蓝长袖全套女仆连衣裙(立领、胸前白荷叶边、
|
|
837
|
+
领口蝴蝶结蓝宝石胸针、金色袖口与裙摆纹样、白色半身围裙、白色长袜、深蓝圆头皮鞋)。
|
|
838
|
+
提示词逐字记在 `tools/CHARACTER-PROMPT.md`,**全部纯文生图,未使用任何参考图**。
|
|
839
|
+
|
|
840
|
+
- **第三套皮肤「宵蓝礼服」(`night`)**。形象要素与前两套逐字相同,只把服装从单薄女仆装换成
|
|
841
|
+
单层深蓝露肩礼服(一字露肩、及膝略短、侧边开衩、同色细腰带、裸腿 + 白色细带高跟)。
|
|
842
|
+
五张(idle / happy / sleepy / work / attn)一次出齐,键控软边 0.45%–0.61%。
|
|
843
|
+
三套的差别是"**同一件人之外的东西换了**"—— 换服装不换身份,站一起仍认得出是同一个人。
|
|
844
|
+
规格已写进 `tools/CHARACTER-PROMPT.md`,别人也能按同一套提示词重建。
|
|
845
|
+
- **包体积 2281 → 3383 KB**(多一套皮肤 +1.1 MB,与出图前的预估一致)。要瘦的话 `--tier=md` 一档即可。
|
|
846
|
+
- **修一个会污染发布内容的工具缺陷**:`assets.mjs` 把抠图后的全尺寸 PNG 中间产物写在**输出目录**里,
|
|
847
|
+
最后再删 —— 删除一旦失败就在 `assets/` 留下 0 字节隐藏残骸(实测 `classic` 4 个、`night` 5 个,
|
|
848
|
+
而且会随包发布)。中间产物现在写到系统临时目录,**从不进入发布目录**;9 个残骸已清理。
|
|
849
|
+
- **菜单项数不再写死在测试里**:预检与冒烟都把断言改成"12 项固定 + 每套皮肤一项",从皮肤数**推导**。
|
|
850
|
+
加一套皮肤时它是红的(`14 != 15`)—— 那是数据的错,不是行为的错,测试不该因此变红。
|
|
851
|
+
这类"测试里抄一份数据"的问题本轮是第二次撞上(上一次是台词池硬编码)。
|
|
852
|
+
- 预检 248 项、冒烟 46 项,全部通过。
|
|
853
|
+
|
|
854
|
+
### 1.8.0
|
|
855
|
+
|
|
856
|
+
### 1.8.0
|
|
857
|
+
|
|
858
|
+
- **台词从 39 句扩到 120 句**,并**按时段分四套招呼语**(早上 / 下午 / 晚上 / 凌晨)。
|
|
859
|
+
她是个长期在场的东西:一天被点几十次、看几百轮任务 —— 3–6 句的池子在一周内就会被看穿;
|
|
860
|
+
而且原先凌晨三点和上午十点说的是同一句话。
|
|
861
|
+
- **池子的宽度按"见到的频率"分级**,而不是一刀切:`hint` 一辈子只说一次(3 句够),
|
|
862
|
+
招呼语一次挂载说一句(5 句够),其余场景一天被看到几十次(**≥8 句**)。这条现在是断言。
|
|
863
|
+
第一次写这条断言时我把阈值定成 6,结果 `move`/`home`/`happy`/`sleep`/`wake` 不够 ——
|
|
864
|
+
**该加的是内容,不是放宽标准**,于是把这五个池子补到 8–9 句。
|
|
865
|
+
- **修一处测试里的重复数据**:庆祝台词的断言把旧池子硬编码在测试里
|
|
866
|
+
(`['跑完了。','收了。',…]`),我一改文案它就红。改成**从产物里读出台词表**再比对 ——
|
|
867
|
+
测的是"她说的来自正确的池子",而不是"她说了我记住的那几句";池子换了测试自动跟上。
|
|
868
|
+
这条断言原本还会放过"庆祝说了别的池子的话"这类真 bug(硬编码列表掩盖了它)。
|
|
869
|
+
- `__wisp.doctor()` 增加 `greeting` 段:当前时段、小时数、池子大小与四套池子各自的规模 ——
|
|
870
|
+
不用再猜"她现在为什么这么说"。
|
|
871
|
+
- 预检 241 → **248 项**。
|
|
872
|
+
|
|
873
|
+
### 1.7.0
|
|
874
|
+
|
|
875
|
+
### 1.7.0
|
|
876
|
+
|
|
877
|
+
- **新增 `tools/pack-check.mjs`**:`npm pack` → 解包 → 从**解出的副本**跑预检与重建,逐项确认
|
|
878
|
+
发布子集自足。实测:解出副本预检全过、并能从随包素材重建出完整产物。
|
|
879
|
+
- **修一个真实的打包缺陷**:README 的性能一节原先指向**开发机上的一个绝对路径**(在包外)
|
|
880
|
+
—— 别人装走之后,那句话是空话。
|
|
881
|
+
现在探针收进包里(`tools/perf.mjs`),README 指向它。
|
|
882
|
+
- **把"文档不得指向包外"变成断言**:包内文档里出现绝对路径或开发机目录就红;文档提到的每个
|
|
883
|
+
`tools/*` 必须在 `files[]` 里。
|
|
884
|
+
这条断言我**自己也写错过一次**:正则要求匹配两个反斜杠,于是明明存在的死引用被报成"没有包外引用"
|
|
885
|
+
—— 假通过。修正则后先确认它**能红**,再修 README。
|
|
886
|
+
- 清掉一个残留物:撤掉设置面板时留下的截图(删过一次,之后又被冒烟跑出来了)。
|
|
887
|
+
- `tools/perf.mjs` 与 `tools/smoke.mjs` 的 Playwright 解析都补上"`chromium` 必须真的存在"的检查:
|
|
888
|
+
`require` 成功但导出里没有它的候选是存在的,直接返回会让调用处在别处炸(`perf.mjs` 上踩过)。
|
|
889
|
+
- 预检 239 → **241 项**。
|
|
890
|
+
|
|
891
|
+
### 1.6.0
|
|
892
|
+
|
|
893
|
+
### 1.6.0
|
|
894
|
+
|
|
895
|
+
- **在已发布的 `app.asar` 里逐个确认钩子存在。** 这是不依赖页面鉴权能拿到的最强证据 ——
|
|
896
|
+
之前所有钩子都只是"我读了源码推出来的",源码里有 ≠ 发行版里有(构建、裁剪、改名都可能让它消失):
|
|
897
|
+
|
|
898
|
+
| 钩子 | app.asar |
|
|
899
|
+
|---|---|
|
|
900
|
+
| `data-composer-input` | ×1 |
|
|
901
|
+
| `data-question-key` / `data-plan-review-key` | ×1 / ×1 |
|
|
902
|
+
| `data-approval-key` | ×2 |
|
|
903
|
+
| `data-error` / `data-ds-dark-theme` | ×20+ |
|
|
904
|
+
|
|
905
|
+
只剩**忙碌钩子**无法静态确认:它的 `aria-label` 来自语言表而非字面量。
|
|
906
|
+
- **`doctor()` 的忙碌钩子在 miss 时自解释**:把页面上真实的按钮 `aria-label` 采样一起报出来
|
|
907
|
+
(去重、上限 12 条,且只在 miss 时附带,不污染正常报告)。一次调用就能看出该匹配什么。
|
|
908
|
+
- **新增长时间运行的泄漏测试(soak,120 轮交互)**:此前所有测试都是"一次性"的 —— 挂载、动几下、
|
|
909
|
+
拆掉,**没有一条**验证过反复开关菜单 / 反复换皮肤 / 反复进出状态之后会不会累积。虚拟时钟让它很便宜。
|
|
910
|
+
实测:窗口监听器**零增长**;120 次换皮肤只创建了 **8 个 blob URL**(缓存命中);瞬态 UI 无残留;
|
|
911
|
+
拆卸后定时器归零、8/8 对象 URL 全部回收。
|
|
912
|
+
- **修替身第三处说谎**:`matchAll` 里 `'button[aria-label]'` 也会命中 `startsWith('button[')` 的分支,
|
|
913
|
+
于是新增的采样逻辑在替身里永远返回空数组 —— 断言空过。真实 DOM 是精确匹配选择器的,不会这样。
|
|
914
|
+
- 预检 229 → **239 项**。
|
|
915
|
+
|
|
916
|
+
### 1.5.0
|
|
917
|
+
|
|
918
|
+
### 1.5.0
|
|
919
|
+
|
|
920
|
+
- **性能表重新实测并修正。** 原来那三个数字是**加 attn、踱步、交叉淡入淡出、5 张精灵图、2 套皮肤
|
|
921
|
+
之前**测的,已经过期:
|
|
922
|
+
|
|
923
|
+
| 指标 | 旧值 | 实测新值 |
|
|
924
|
+
|---|---|---|
|
|
925
|
+
| 每秒 `querySelector` | 1 次 | **1.6 次**(有输入框)/ **2.8 次**(无输入框,最悲观) |
|
|
926
|
+
| 轮询成本 | 0.44 ms/轮 | **0.86 ms/轮**(每轮 3 次查询:忙碌 + 待处理 + 输入框) |
|
|
927
|
+
| 每帧间隔差 | +0.005 ms | **+0.023 ms** |
|
|
928
|
+
| `querySelectorAll` | 0 次/秒 | **0 次/秒**(这条仍然成立) |
|
|
929
|
+
|
|
930
|
+
多出来的查询来自 `attn` 引入的待处理面板探测。新增三项测量:**DOM 查询耗时 0.72–0.83 ms/秒**
|
|
931
|
+
(单核的 0.07–0.08%)、**挂载 18–19 ms**(含 2.2 MB 包解析)、**解码内存 6.0 MB/张**。
|
|
932
|
+
- **把性能声明钉成冒烟断言**:空闲时 `querySelector` ≤ 4 次/秒、`querySelectorAll` **恰好 0**。
|
|
933
|
+
以后谁再加一个"每轮都查一次"的功能,测试会先红,而不是等 README 悄悄过期。
|
|
934
|
+
- **帧开销必须交替重复测**:同一份代码两组分别测得 +0.127 与 −0.002 ms —— 单次差值里噪声就有
|
|
935
|
+
0.1 ms 量级,一次测量说明不了任何事。这条也写进了 README。
|
|
936
|
+
- 探针本身也修了一次:我一开始把新的检查插到了 `browser.close()` **之后**,于是它报"页面已关闭"
|
|
937
|
+
并触发了环境中断重试 —— 那是我自己的错,不是环境问题。
|
|
938
|
+
|
|
939
|
+
### 1.4.0
|
|
940
|
+
|
|
941
|
+
### 1.4.0
|
|
942
|
+
|
|
943
|
+
- **新增 `__wisp.doctor()` 自检**。理由:她依赖的每个选择器都是我读 DSH 源码推出来的,错了不会
|
|
944
|
+
报错 —— 功能只是永远不触发;而她跑在带鉴权的页面里,我的浏览器连不上(`401 dsh web
|
|
945
|
+
authentication required`),所以自检必须由插件自己在页面里给出来。
|
|
946
|
+
- **修一处可移植性隐患**:`doctor()` 起初用了裸 `getComputedStyle`。客户端半包可能运行在
|
|
947
|
+
"被提供的 window ≠ 环境全局"之下 —— `nowMs` 当初就是这么坏掉的。改成 `window.getComputedStyle`。
|
|
948
|
+
这条是被**测试替身**逼出来的:假 window 上没有这个全局,于是它立刻炸了出来。
|
|
949
|
+
- **补上了从没测过的输入框回退链**:此前替身让三个钩子**同时**命中,等于回退链完全没测。
|
|
950
|
+
现在替身一次只让一种形态存在,两条回退(`textarea`、`[contenteditable="true"]`)都验证过真的能用。
|
|
951
|
+
- **收敛诊断信号**:`problems` 不再把"没有待处理面板"算成问题 —— 缺席是正常状态。
|
|
952
|
+
- 测试替身两处保真度修正:`hasAttribute`,以及让 `querySelector` 与 `querySelectorAll` **共用同一份
|
|
953
|
+
匹配逻辑**(此前两处各写一半,导致用 `querySelectorAll` 的 `doctor()` 在替身里永远看到 0 —— 替身说谎)。
|
|
954
|
+
- 冒烟里的一条断言改成分工写明:精确的踱步上限由确定性 harness 保证,真实浏览器只验"真的动了、
|
|
955
|
+
真的是滑行" —— headless 节流会让定时器补发,两次踱步会被合成一步(实测 226px)。
|
|
956
|
+
- 预检 211 → **229 项**,冒烟 39 → **44 项**。
|
|
957
|
+
|
|
958
|
+
### 1.3.0
|
|
959
|
+
|
|
960
|
+
- **补上键盘入口**:菜单早就支持键盘导航,但她本身 `tabindex="-1"`,键盘用户**没有办法打开它** ——
|
|
961
|
+
声明了 `role="menu"` 却没有入口,与之前"声明了 role 却按不动"是同一类问题。现在她是
|
|
962
|
+
`tabindex="0"` + `aria-haspopup="menu"`,Enter / Space / 菜单键 / Shift+F10 都能打开菜单
|
|
963
|
+
(Space 默认滚动、Enter 在表单里会提交,所以两个都拦了)。她挂在 `body` 末尾,Tab 顺序上排在
|
|
964
|
+
应用内容之后,不插队。
|
|
965
|
+
- **量并记录了解码后的内存**:一张精灵图在内存里是 `宽 × 高 × 4` 字节 —— 默认档 **6.0 MB**,
|
|
966
|
+
`hi` 档是它的 4 倍(25 MB)。预热会一次性解码当前皮肤的全部五张,换皮肤再解五张。
|
|
967
|
+
所以"选哪一档"不只是锐度问题,也是内存问题;这条现在是冒烟里的一条断言。
|
|
968
|
+
- **冒烟套件给出有意义的退出码**,并对"环境中断"重试一次:见上文。
|
|
969
|
+
- 预检 205 → **211 项**,冒烟 34 → **39 项**。
|
|
970
|
+
|
|
971
|
+
### 1.2.0
|
|
972
|
+
|
|
973
|
+
- **三个行为开关进了右键菜单**(新增「行为」一段):自己溜达 / 跟随状态 / 跑完撒花,带勾显示当前状态。
|
|
974
|
+
这是撤掉设置页后真正的缺口 —— 那三个开关是设置页相对菜单的**唯一净增量**,放回菜单比再建一页便宜得多
|
|
975
|
+
(复用现有的项结构、键盘支持与关闭逻辑,约 20 行)。菜单现在 14 项、3 个分组。
|
|
976
|
+
- **真实浏览器覆盖补到状态层**:`attn`(`[data-question-key]` 面板)、`worried`(失败行 + 一轮结束)、
|
|
977
|
+
踱步(真的走了、真的用了滑行)现在都有真实页面上的断言,不再只有假 DOM 覆盖。
|
|
978
|
+
冒烟从 24 项增到 34 项。
|
|
979
|
+
- **修一处冒烟自身的错,并让崩溃不再静默**:踱步那条断言量的是 7 秒窗口内的**累计**位移,
|
|
980
|
+
而踱步每 5 秒触发一次 —— 窗口里走两步时累计必然超过单次上限(实测 209px),于是偶发失败。
|
|
981
|
+
改成量**每一步**。另外给脚本加了 `uncaughtException` / `unhandledRejection` 兜底:
|
|
982
|
+
之前有一次它没有任何输出就 exit=1,那是启动期异常被 PowerShell 重定向吞掉了 ——
|
|
983
|
+
「没有输出的失败」比一条 FAIL 难查得多。
|
|
984
|
+
|
|
985
|
+
### 1.1.0
|
|
986
|
+
|
|
987
|
+
- **新增 `attn` 专属精灵图**。此前 `attn` 借用 `work` 那张,两个状态长得一样,只能靠
|
|
988
|
+
光晕和左右轻推区分。现在是「抬手朝你招了招」的姿态,一眼能认出来。两套皮肤各加一张
|
|
989
|
+
(10 张精灵图),包 1740 → **2273 KB**。
|
|
990
|
+
- **皮肤可以只有部分情绪**:`spriteOf()` 在缺图时按 `SPRITE_FALLBACK` 退到备选(`attn` → `work`),
|
|
991
|
+
而不是渲染成空白。所以"某套皮肤只给四张"是合法状态,不是坏状态。
|
|
992
|
+
- **新增 `tools/tiers.mjs`**:把**已经抠好的透明 PNG**分成四档,不重复抠图。
|
|
993
|
+
分工由此明确:`assets.mjs` = 绿幕母版 → 抠图 → 四档;`tiers.mjs` = 已抠好的图 → 四档。
|
|
994
|
+
把抠过的图再喂给 `assets.mjs` 会二次抠图,结果依赖"透明像素里是否还残留绿色"——
|
|
995
|
+
那是假象,不是保证。两个工具都会拒绝明显不对的输入(无 alpha 通道 / 非绿幕)。
|
|
996
|
+
- **记一笔实测(这条决定了 `_hi` 是白带的)**:1024 档与 2048 档在她的实际尺寸下**不可区分** —— DPR 2 下高频能量差 0.3%,
|
|
997
|
+
DPR 1/3 下 1024 档反而高 4~5%(2048 被浏览器缩得更多,重采样本身是低通滤波)。
|
|
998
|
+
她屏上只有 560 CSS px,2048 档多出来的像素没有地方显示。
|
|
999
|
+
|
|
1000
|
+
### 1.0.0
|
|
1001
|
+
|
|
1002
|
+
- **做过又撤掉了设置面板。** 面板技术上完全可行(零依赖路由:平台 seed 表里有 `react` 与
|
|
1003
|
+
`@deepseek-ai/dsh-client-ui-slots`,都由 shell 注入),也实现并验证通过;但它与右键菜单
|
|
1004
|
+
的重叠过大 —— 大小 / 皮肤 / 位置 / 可见性菜单都有,**净增量只有三个开关**(空闲踱步、
|
|
1005
|
+
跟随 agent 状态、跑完撒花)。两个入口意味着两处 UI 要维护,不值,于是撤掉。
|
|
1006
|
+
那三个开关回到 YAML 配置 + Console。
|
|
1007
|
+
- **右键菜单键盘可达**(保留):原先声明了 `role="menu"` 却没有任何键盘支持 —— 对屏幕阅读器来说
|
|
1008
|
+
那比不声明更糟。现在打开即聚焦第一项(roving tabindex,只有一项可 Tab 到)、↑↓ 环绕、
|
|
1009
|
+
Home/End、Esc 关闭并把焦点交还给她、Tab 离开即关闭。焦点落在真正的 `<button>` 上,
|
|
1010
|
+
Enter/Space 交给浏览器原生行为,不自己再拦一次(否则双重触发)。
|
|
1011
|
+
- **体积 3234 KB → 1746 KB**:两个皮肤改用 1024×1536 档。这个取舍现在**在构建输出里算给你看**
|
|
1012
|
+
(按 DPR 1/1.5/2/3 列出"缩小"还是"放大"),而不是只写在文档里 —— 体积与锐度是同一个决定的
|
|
1013
|
+
两面。要回到 2048×3072:`node build.mjs --tier=hi`(约 3234 KB)。
|
|
1014
|
+
- **真实浏览器冒烟套件进仓库**:`tools/smoke.mjs`,35 项,一条命令。此前这些检查散在开发机的
|
|
1015
|
+
工作区脚本里,换台机器就没了。Playwright 是可选依赖,缺了跳过而不是报错。
|
|
1016
|
+
- 修一个只有真实渲染才暴露的体验问题:**她 560×840,会压在设置页上** —— 面板里加了提示,
|
|
1017
|
+
「先躲起来」就在同一页。
|
|
1018
|
+
- 预检 180 → **220 项**,另有 35 项真实浏览器检查。
|
|
1019
|
+
|
|
1020
|
+
### 0.12.0
|
|
1021
|
+
|
|
1022
|
+
- **修一个我在 0.8.0 留下的缺口:菜单里能放大缩小,但刷新后尺寸回到默认。** 位置 / 皮肤 / 隐藏
|
|
1023
|
+
都记了,只有尺寸没记 —— 同一个菜单里的设置有的记得住有的记不住,是明显的自相矛盾。
|
|
1024
|
+
规则:菜单里改过的尺寸记在本地并活过刷新;**配置里显式写了 `size` 时以配置为准**(项目级设定
|
|
1025
|
+
比一次临时缩放更权威)。"显式写了"与"用了默认值"靠原始 config 有没有这个键来区分。
|
|
1026
|
+
- **位置从"一个右下角"变成四个角落**。她放大到 560x840 之后,"右下角"未必合适 —— 屏幕右边可能是
|
|
1027
|
+
侧栏,左下角未必被输入框挡住。菜单里多一段「位置」,四个角落各一项。
|
|
1028
|
+
- **第一次见面提一句右键**(`右键我一下试试。`)。菜单是主要入口,但从外观上完全看不出来;
|
|
1029
|
+
只提一次,记在 `dsh-wisp:hint:v1` —— 第二次见面还念叨就成了骚扰。
|
|
1030
|
+
- 顺手删掉一处重复的 `LINES.error` 键(前面的连续编辑留下的,值相同所以没造成行为差异)。
|
|
1031
|
+
- 预检增至 180 项(尺寸跨挂载记忆 / 配置写死时不被记忆覆盖 / 四个角落各自的落点 /
|
|
1032
|
+
只提一次提示 / 第二次挂载只打招呼不再念叨 / 新 harness 挂载到自己的窗口)
|
|
1033
|
+
|
|
1034
|
+
### 0.11.0
|
|
1035
|
+
|
|
1036
|
+
- **新增出错反应**:DSH 给失败的工具行与命令卡都标了 `data-error="true"`(`ui-tool` 的 ToolRow、
|
|
1037
|
+
`ui-chat` 的 GenericCommandCard),所以"存在"本身就是信号。一轮跑完时如果看到失败行,她切到
|
|
1038
|
+
**`worried`**(垂眼 + 灰蓝光晕 + 轻微起伏)说一句「好像不太顺。」,3.5 秒后恢复;**同一批错误
|
|
1039
|
+
只说一次**,清掉之后再出现新的才会再说。
|
|
1040
|
+
- **错误检查挂在"一轮结束"这个边上,而不是放进轮询里。** 这是全文档扫描,放进每 1.2 秒的轮询会把
|
|
1041
|
+
"空闲时 0 次 `querySelectorAll`"的性能结论作废;挂在轮次结束上则是一轮一次。10k 节点页面上
|
|
1042
|
+
实测单次扫描 **0.017 ms**,并且有断言钉住"扫描次数 == 跑完的轮数",防止以后被人挪回轮询里。
|
|
1043
|
+
- **修一个状态被覆盖的 bug**:`worried` 刚摆出来就被同一个 poll 周期末尾的 `setMood('idle')` 抹掉 ——
|
|
1044
|
+
临时状态必须先进"提前返回"名单,否则它们活不过一个轮询周期。`happy` 之前恰好因为同样的原因被
|
|
1045
|
+
写进去了,所以一直没暴露。
|
|
1046
|
+
- 预检增至 166 项(失败轮触发 / 会说一句 / 同一批不重复 / 新的再说 / 表情会过期 / 干净轮无反应 /
|
|
1047
|
+
`reactions: false` 连扫都不扫 / 扫描次数等于轮数)
|
|
1048
|
+
|
|
1049
|
+
### 0.10.0
|
|
1050
|
+
|
|
1051
|
+
- **新增「先躲起来」**:她放大到 560x840 之后会挡住会话与输入框,而"让她消失一会儿"原先**只能整个禁用
|
|
1052
|
+
插件**。现在菜单里选一下,她整个消失,只在右下角留一个 56px 迷你按钮(缩略图直接用当前皮肤的
|
|
1053
|
+
`idle` 图,所以不需要额外素材),点一下回来。状态记在本地,刷新后仍然躲着 —— 但**迷你按钮会一起
|
|
1054
|
+
回来**:藏起来又没有回去的路是个陷阱。
|
|
1055
|
+
- 躲起来时:不自己溜达、不参与命中测试、**气泡一起收掉**(气泡挂在 layer 上,只把 root 设成
|
|
1056
|
+
`display:none` 会留下一个悬空气泡 —— 这个是看截图才发现的)。
|
|
1057
|
+
- API 增加 `hide()` / `show()` / `hidden`;配置增加 `hidden: true` 可强制以隐藏状态挂载。
|
|
1058
|
+
- **测试替身补上标签选择器**:假 DOM 原先只认 `.class`,于是 `querySelectorAll('img')` 静默返回空集 ——
|
|
1059
|
+
替身的局限会伪装成产品缺陷。现在类选择器与标签选择器都支持。
|
|
1060
|
+
- 预检增至 156 项(隐藏标记 / 迷你按钮与其缩略图 / 气泡一起收掉 / 躲着时不溜达 / 点按钮回来 /
|
|
1061
|
+
跨挂载保留 / 按钮跟着回来)
|
|
1062
|
+
|
|
1063
|
+
### 0.9.0
|
|
1064
|
+
|
|
1065
|
+
- **新增皮肤管理**:右键菜单里多了「皮肤」一段,包里每套皮肤一项,当前那套打勾。切换只是换一整套
|
|
1066
|
+
`idle/happy/sleepy/work` —— **位置、情绪、气泡都不动**,只有精灵图与命中蒙版重建。选择记在
|
|
1067
|
+
`dsh-wisp:skin:v1`,刷新后保留;也可以在配置里用 `skin: <名字>` 点名。
|
|
1068
|
+
- **素材改为按皮肤分目录**:`assets/<skin>/<mood>[_tier].webp`。`build.mjs` 自动把每个子目录当成
|
|
1069
|
+
一套皮肤打进包;没有子目录时退化为单套(`--from <任意目录>` 的老用法仍然成立)。
|
|
1070
|
+
- **默认皮肤由显式优先级决定**,不是目录名字母序。一开始按字母序排,默认皮肤变成了 `classic` ——
|
|
1071
|
+
那是构建实现的偶然,不该变成产品行为,所以 `build.mjs` 里写死 `SKIN_PRIORITY`。
|
|
1072
|
+
- 随包的几套:**深海女仆**(当前旗舰)、**原版女仆**(对齐社区规范)、**素绘女仆**(赛璐璐平涂、较早那套更保守的设定)。
|
|
1073
|
+
- 预检增至 141 项(每套皮肤必须凑齐四个情绪 / 菜单列出全部皮肤 / 只有一套打勾 / 换皮肤真的换图 /
|
|
1074
|
+
换皮肤不动位置 / 记忆生效 / 未知名被拒 / `configure({skin})` 与菜单同一条路)
|
|
1075
|
+
|
|
1076
|
+
### 0.8.0
|
|
1077
|
+
|
|
1078
|
+
- **新增右键菜单**:右键她本体弹出五项操作 —— 说句话 / 放大一点 / 缩小一点 / 回到右下角 / 睡一会儿。
|
|
1079
|
+
菜单挂在 `.wisp-layer` 上(屏幕坐标系),**不挂在会被镜像和缩放的 root 上**,否则文字会左右翻转、
|
|
1080
|
+
尺寸会跟着她一起放大。Esc、点击别处、滚动、窗口尺寸变化都会关闭它,关闭时摘掉自己的全部监听器。
|
|
1081
|
+
- 上版的"右键即归位"改为菜单里的「回到右下角」一项 —— 一个手势只做一件事太浪费,而放大缩小这类
|
|
1082
|
+
操作原先也只能开 Console。
|
|
1083
|
+
- 预检增至 130 项(菜单结构 / 菜单项动作真的生效 / 不落在 body 内 / Esc / 点外部 / 滚动 / 监听器回收 /
|
|
1084
|
+
放大后再归位用的是当前尺寸)
|
|
1085
|
+
|
|
1086
|
+
### 0.7.0
|
|
1087
|
+
|
|
1088
|
+
- **新增 `attn` 状态:有东西在等你处理。** 这是唯一"你不理它就卡住"的情形,所以它的优先级高于一切 ——
|
|
1089
|
+
Agent 提问(\`[data-question-key]\`)、计划待确认(\`[data-plan-review-key]\`)、工具授权待批准
|
|
1090
|
+
(\`[data-approval-key]\`)三个面板的出现即触发;**睡着了也会被叫起来**。
|
|
1091
|
+
三个钩子都是语言无关的 \`data-*\` 属性,且**元素只在真的有待处理项时才渲染**,所以"存在"本身就是信号;
|
|
1092
|
+
key 用来区分"新的一件事" —— 同一件事不重复念,换了一件才再说一次。
|
|
1093
|
+
- 预检增至 116 项(新状态 / 不重复提醒 / 换一件再说 / 回答后恢复 / 从睡眠中唤醒)
|
|
1094
|
+
|
|
236
1095
|
### 0.6.0
|
|
237
1096
|
|
|
238
1097
|
- **新增"自己踱步"**:空闲且 agent 不在跑时,她会每隔 `wanderMs` 考虑溜达一次,走到 `wanderRange` 以内的随机位置 —— 用 CSS 过渡**滑行**过去而不是瞬移;被拖拽、睡着、忙碌时一律不动。**踱步不写入记忆位置**:记忆的是"你把她放在哪",不然刷新后她会出现在一个你没选过的位置
|