dsh-wisp 0.6.0
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/LICENSE +32 -0
- package/NOTICE.md +151 -0
- package/README.md +285 -0
- 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/build.mjs +169 -0
- package/cordis.patch.yml +51 -0
- package/lib/client.js +1091 -0
- package/lib/client.template.js +1086 -0
- package/lib/index.js +19 -0
- package/package.json +72 -0
- package/tools/CHARACTER-PROMPT.md +88 -0
- package/tools/assets.mjs +139 -0
- package/tools/keyout.mjs +104 -0
- package/verify-wisp.mjs +872 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-wisp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
--------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
THIS LICENCE COVERS THE CODE ONLY.
|
|
26
|
+
|
|
27
|
+
The character artwork under assets/ — and the same images inlined as base64
|
|
28
|
+
inside lib/client.js — is licensed separately, under CC BY-NC-SA 4.0.
|
|
29
|
+
Read NOTICE.md before redistributing either part.
|
|
30
|
+
|
|
31
|
+
"DeepSeek" and related marks belong to their respective owners. This is an
|
|
32
|
+
unofficial fan project and is not affiliated with or endorsed by DeepSeek.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# NOTICE — 来源、署名与许可
|
|
2
|
+
|
|
3
|
+
本插件包含**两部分不同来源的内容**,适用**不同许可**。分享或再分发前请读完。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 代码
|
|
8
|
+
|
|
9
|
+
`lib/`、`cordis.patch.yml`、`build.mjs`、`verify-wisp.mjs` 等全部源码。
|
|
10
|
+
|
|
11
|
+
**许可:MIT**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. 角色形象与图片
|
|
16
|
+
|
|
17
|
+
`assets/` 下的四个精灵图(`idle` / `happy` / `sleepy` / `work`),以及内嵌在
|
|
18
|
+
`lib/client.js` 里的同源 base64 数据。
|
|
19
|
+
|
|
20
|
+
### 这是什么
|
|
21
|
+
|
|
22
|
+
DeepSeek 娘是 **DeepSeek 社区共同创作的拟人化同人形象**,
|
|
23
|
+
不是 DeepSeek 官方角色,也没有任何官方设定集。
|
|
24
|
+
|
|
25
|
+
社区对其形象的公认描述(引自一个公开的社区桌宠项目):
|
|
26
|
+
|
|
27
|
+
> 「可爱温柔、工作认真,失败时有点慌张,以鲸尾表达情绪的**蓝发鲸鱼女仆**。」
|
|
28
|
+
|
|
29
|
+
本插件使用的四张图**全部由 AI 生成,且全部生成于本项目自己名下的 TapTap Maker 工程**
|
|
30
|
+
(`孤屿`,`assets/image/`)。制作过程分两步:
|
|
31
|
+
|
|
32
|
+
1. 初版由 AI 生成(683×1024);
|
|
33
|
+
2. 此后每一次改版——提升分辨率到 2048×3072、更换服装——都是**在上一版自己生成的图上、
|
|
34
|
+
用 Maker 的图生图(改图)接口重绘**的。生成时使用 4K 渲染分辨率,即细节是在 4K 下
|
|
35
|
+
重新合成后缩放到 2048×3072,而不是把小图放大。
|
|
36
|
+
|
|
37
|
+
**关键一点:底图始终是本项目自己生成的图,不是任何第三方插画。** 因此这些图片
|
|
38
|
+
**不构成对任何现有插画作品的复制、描摹或改图**;本插件作者也未曾以他人作品作为输入。
|
|
39
|
+
|
|
40
|
+
### 图片生成规则(硬性)
|
|
41
|
+
|
|
42
|
+
**只用文生图,一条图只放一个角色,且必须自己做抠图。**
|
|
43
|
+
|
|
44
|
+
1. **禁用 `edit_image`**(改图接口)及任何"以现有图片为生成底图"的方式;**禁用 `reference_images`**。
|
|
45
|
+
形象只能靠提示词锚定。采用 `model: "gpt"`、`resolution: "4K"`、`target_size: "2048x3072"`。
|
|
46
|
+
2. **一条图里只放一个角色**:提示词必须写明「画面中只有一个角色」,禁止拼图、多视图、三视图、分镜。
|
|
47
|
+
3. **不要用服务端的透明背景**(`transparent: false`)。改在**纯绿幕**上出图,再用本地
|
|
48
|
+
`keyout` 抠图。原因见下。
|
|
49
|
+
|
|
50
|
+
#### 为什么必须自己做抠图
|
|
51
|
+
|
|
52
|
+
服务端的自动透明背景**会把不该抠的抠掉**:角色是深蓝/浅蓝/白配色,浅蓝渐变的**发梢**与**鲸尾**
|
|
53
|
+
最接近浅色底,于是被当成背景切掉 —— 表现是"生成结果变成了短发、鲸尾消失",极易误判成模型跑形。
|
|
54
|
+
|
|
55
|
+
实测:同一批提示词,走服务端透明背景时四张全部丢失发梢与鲸尾;改成绿幕 + 本地绿幕抠图后,
|
|
56
|
+
只有 0.43%–0.53% 的边缘像素需要做半透明处理,**发丝一根没丢,鲸尾完整**。
|
|
57
|
+
|
|
58
|
+
#### 画风措辞的坑
|
|
59
|
+
|
|
60
|
+
写「高质量二次元插画」会明显把模型推向**韩系半写实**(头发变短、体型成熟化、加胸部),角色设定随之
|
|
61
|
+
崩掉。必须写成「干净的动漫赛璐璐上色,平涂色块加简单明暗,日系动画角色设定图风格,人物为少女体型,
|
|
62
|
+
不要写实、不要韩系半写实、不要成熟化」。
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### 关键声明
|
|
67
|
+
|
|
68
|
+
- **非官方**:本插件与 DeepSeek(深度求索)官方无任何关系,未获其授权、认可或赞助。
|
|
69
|
+
名称中出现的 "DeepSeek" 仅用于**说明所模仿的对象**。
|
|
70
|
+
- **同人性质**:角色属于社区同人创作范畴。若权利人提出异议,应停止分发图片部分。
|
|
71
|
+
- **AI 生成**:图片由 AI 生成,可能包含不准确或非预期的细节;按「现状」提供,不作任何保证。
|
|
72
|
+
- **无所有权主张**:本插件作者**不对该角色形象主张任何权利**。此处不授予你任何
|
|
73
|
+
你原本不拥有的权利。
|
|
74
|
+
|
|
75
|
+
### 图片许可
|
|
76
|
+
|
|
77
|
+
**CC BY-NC-SA 4.0**(署名—非商业性使用—相同方式共享)
|
|
78
|
+
|
|
79
|
+
- **署名**:需注明「角色形象源自 DeepSeek 社区同人创作,图片由 AI 生成」并链回本项目
|
|
80
|
+
- **非商业**:不得用于商业用途
|
|
81
|
+
- **相同方式共享**:再分发图片须沿用同一许可
|
|
82
|
+
|
|
83
|
+
之所以不把图片一并放进 MIT:MIT 是代码许可,作者并无权用它替图片授权。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. 署名建议
|
|
88
|
+
|
|
89
|
+
再分发时,请在你的 README 或关于页保留类似文字:
|
|
90
|
+
|
|
91
|
+
> dsh-wisp —— DeepSeek Harness 的 DeepSeek娘桌宠插件。
|
|
92
|
+
> 角色形象「DeepSeek娘」源自 DeepSeek 社区同人创作,非官方角色。
|
|
93
|
+
> 插件内图片由 AI 依据社区公认特征生成(CC BY-NC-SA 4.0),代码以 MIT 许可发布。
|
|
94
|
+
> 本项目与 DeepSeek 官方无关。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. 构建陷阱(给维护者)
|
|
99
|
+
|
|
100
|
+
### 4.1 占位符不能出现在注释里
|
|
101
|
+
|
|
102
|
+
`build.mjs` 把四张图注入 `lib/client.template.js` 时,占位符**绝不能出现在注释里**。
|
|
103
|
+
|
|
104
|
+
曾经踩过的坑:占位符写成 `/*__SPRITES__*/{}`,替换后残留 `/*`,**整个注入的对象变成块注释**,`SPRITES` 静默求值为 `{}`,图片源变成 `undefined` —— 表现是插件"挂载成功"但显示碎图。这个 bug 极难从症状反推。
|
|
105
|
+
|
|
106
|
+
现在 `build.mjs` 有四道防线:
|
|
107
|
+
|
|
108
|
+
1. 占位符必须**恰好出现一次**,替换后不得残留
|
|
109
|
+
2. **真的去求值** `const SPRITES = {…}`,确认四个 key 都是 `data:image/` 开头,否则拒绝写出文件
|
|
110
|
+
3. 产物**不得调用被陷阱的全局**(`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require`)——在客户端半包里这些是抛异常的形参,调用即崩,而打包器不会报
|
|
111
|
+
4. `const VERSION` 必须与 `package.json` 的 `version` 一致
|
|
112
|
+
|
|
113
|
+
`verify-wisp.mjs` 另有两道**回归守卫**:断言 `img.src` 是 `blob:` 而不是 `data:` URI;并把六个被陷阱的全局以抛异常的形式注入后**真跑一遍**。
|
|
114
|
+
|
|
115
|
+
### 4.2 为什么用 Blob 而不是直接给 data URI
|
|
116
|
+
|
|
117
|
+
初版直接把 base64 data URI 赋给 `img.src`。插件加载正常、DOM 正确、光晕也渲染了,**但精灵图显示为碎图**。内嵌字节经校验无误(RIFF/WEBP magic,256x384 RGBA),所以问题在于本页面拒绝 `data:` 作为图片源——该界面通过自定义 `dsh-app://` 协议提供服务,会拦截资源加载。
|
|
118
|
+
|
|
119
|
+
因此现在把 base64 解码为 `Blob` 再取 object URL;拆解时 `revokeObjectURL` 释放。
|
|
120
|
+
|
|
121
|
+
**并且要连缓存键一起删。** blob 缓存是**模块级**的(模块只被 import 一次),而挂载是**每次 `apply` 一次**。只 revoke 不删键,下一次挂载就会拿到已吊销的 URL —— 又变成碎图,而且只在"HMR 重挂 / 重复行"这类路径上出现。
|
|
122
|
+
|
|
123
|
+
### 4.3 时间基准必须只有一个
|
|
124
|
+
|
|
125
|
+
`(window.performance && performance.now) ? performance.now() : Date.now()` 看着无害,实际混用了两个引用:判定条件读 `window.performance`,取值读裸全局 `performance`。在页面里两者是同一个对象,但在自带 `window` 的壳里可能不是 —— 于是**截止时间用一个时间基准计算、用另一个比较,所有定时器永不触发**:她不会睡着、不会庆祝、气泡不会消失,而控制台一行错误都没有。
|
|
126
|
+
|
|
127
|
+
现在只经 `window.performance` 读取。`verify-wisp.mjs` 的虚拟时钟会把这个混用直接暴露成一条失败。
|
|
128
|
+
|
|
129
|
+
### 4.4 反应轮询是唯一按秒发生的成本
|
|
130
|
+
|
|
131
|
+
忙碌判定与输入框检测跑在一个 1.2 s 的定时器上,只要应用开着就一直跑,所以它的形状必须便宜。在真实 Chromium(10k 节点会话记录,headless CPU 光栅,即最不利的一档)实测每次 tick:
|
|
132
|
+
|
|
133
|
+
| 写法 | 每次成本 |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `querySelectorAll('[data-composer-input],[contenteditable="true"],textarea')` | 0.33 ms(40k 节点 1.42 ms),每次 tick 全文档收集 |
|
|
136
|
+
| 4 条精确 `button[aria-label="…"]` | 0.45 ms(1.86 ms),空手而归也要走 4 遍 —— 而空闲是常态 |
|
|
137
|
+
| **2 条子串 `button[aria-label*="…"]`** | **0.27 ms(1.10 ms)**,且能匹配没见过的翻译 |
|
|
138
|
+
|
|
139
|
+
再加上"输入框元素只解析一次后缓存",整个 tick 从 **1.27 ms 降到 0.44 ms**,每秒的 `querySelectorAll` 调用数从 1 变成 **0**。`verify-wisp.mjs` 把这三件事都钉成了断言:已解析的输入框不得每 tick 重查、一次 tick 不得收集 NodeList、无输入框时每 tick 最多一次探测。
|
|
140
|
+
|
|
141
|
+
顺带一条渲染侧的教训:`will-change: transform` 不要常驻 —— 它为一个几乎不发生的事件(拖动)永久保留一个合成层;现在只在真的拖动期间挂上。
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 5. 参考
|
|
146
|
+
|
|
147
|
+
- 萌娘百科「DeepSeek娘」:https://mzh.moegirl.org.cn/DeepSeek%E5%A8%98
|
|
148
|
+
- 社区桌宠项目(`pet.json` 中的角色描述):https://github.com/xpy12367/codex-pet-DeepSeek-girl
|
|
149
|
+
|
|
150
|
+
上述链接仅作为**形象来源的说明**,其中的内容版权归各自作者所有。
|
|
151
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# dsh-wisp
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
4
|
+
|
|
5
|
+
**当前版本 `0.6.0`** · 零依赖 · 单文件客户端半包(精灵图内嵌为 data URI)
|
|
6
|
+
|
|
7
|
+
> 非官方插件,与 DeepSeek(深度求索)官方无关。角色形象与图片许可见 [NOTICE.md](NOTICE.md)。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 它做什么
|
|
12
|
+
|
|
13
|
+
| 行为 | 触发条件 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 漂浮与呼吸 | 常驻(`prefers-reduced-motion` 下停止) |
|
|
16
|
+
| `idle` 青色 | 默认 |
|
|
17
|
+
| `alert` 琥珀色 | Agent 正在跑(出现「停止生成」按钮)**或** 输入框里有字 |
|
|
18
|
+
| `happy` 粉色 | 戳她一下 / 双击 / **一轮跑完** |
|
|
19
|
+
| `sleep` 灰色冒 `z` | 静置 `sleepAfterMs`(默认 90 秒);任意输入唤醒 |
|
|
20
|
+
| 拖动 | 按住本体拖动,带边界约束;**松手即记住位置**,下次刷新回到原处 |
|
|
21
|
+
| 说话 | 挂载问候、点击、睡着、醒来、跑完各有一组台词 |
|
|
22
|
+
|
|
23
|
+
外层 `pointer-events:none`,只有本体可点 —— 它不会挡住你对应用的操作。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 文件结构
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
dsh-wisp-plugin/
|
|
31
|
+
├── package.json dsh.bundle.patch + dsh.client.platform,零依赖
|
|
32
|
+
├── cordis.patch.yml 插入一行 host 入口(指向 dsh-wisp,绝不能是 dsh-wisp/client)
|
|
33
|
+
├── lib/
|
|
34
|
+
│ ├── index.js host 半包:刻意为空的 ES 模块
|
|
35
|
+
│ ├── client.template.js 浏览器半包源码(唯一需要编辑的文件)
|
|
36
|
+
│ └── client.js 由 build.mjs 生成,勿手改
|
|
37
|
+
├── assets/ 四张 256x384 透明 WebP(idle / happy / sleepy / work)
|
|
38
|
+
├── build.mjs 注入精灵图 + 三道构建防线
|
|
39
|
+
├── verify-wisp.mjs 预检:两套契约 + 真实执行
|
|
40
|
+
├── NOTICE.md 角色来源与许可
|
|
41
|
+
└── README.md 本文件
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 构建与预检
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node build.mjs # 把 assets/*.webp 注入 lib/client.template.js → lib/client.js
|
|
48
|
+
node verify-wisp.mjs # 预检;exit 0 = 两半包都符合契约
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**改了 `client.template.js` 就必须重新 `build.mjs`** —— `client.js` 是生成物。
|
|
52
|
+
|
|
53
|
+
`build.mjs` 现在有四道防线,任何一道失败都拒绝写出文件:
|
|
54
|
+
|
|
55
|
+
1. 占位符 `__SPRITES_LITERAL__` 必须恰好出现一次,且替换后不得残留;
|
|
56
|
+
2. 注入后的 `SPRITES` 表要**真的被求值**,四个 key 都必须是 `data:image/` 开头;
|
|
57
|
+
3. 产物**不得调用被陷阱的全局**(`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require`);
|
|
58
|
+
4. `const VERSION` 必须与 `package.json` 的 `version` 一致(防止版本漂移)。
|
|
59
|
+
|
|
60
|
+
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **98 项全 PASS,exit 0**。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 改形象(素材流水线)
|
|
65
|
+
|
|
66
|
+
**形象的唯一权威来源是 [tools/CHARACTER-PROMPT.md](tools/CHARACTER-PROMPT.md)** —— 逐字复用的提示词母版 + 四张姿态 + 完整生成参数。改形象 = 改那个文件、重新出图、再跑一条命令。
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 1. 按 tools/CHARACTER-PROMPT.md 的参数与提示词,为四个情绪各出一张【绿幕】图,存成 <mood>.png
|
|
70
|
+
# 2. 绿幕 → 抠图 → 四档素材,一步到位(任何一张抠图异常都会以非 0 退出)
|
|
71
|
+
node tools/assets.mjs --from <绿幕母版目录>
|
|
72
|
+
# 3. 打进客户端半包
|
|
73
|
+
node build.mjs --tier=hi # hi 档 = 2048x3072,与母版同尺寸,仅重新编码
|
|
74
|
+
# 4. 预检
|
|
75
|
+
node verify-wisp.mjs
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
三条硬约束(实测依据见 CHARACTER-PROMPT.md 与 NOTICE.md):
|
|
79
|
+
|
|
80
|
+
1. **不要用服务端的透明背景。** 它会把浅蓝渐变的发梢与鲸尾当背景切掉,表现成"模型跑形",几乎无法从结果反推。出绿幕、自己抠图 —— 只有 0.4%–0.7% 的边缘像素需要处理,发丝一根不丢。
|
|
81
|
+
2. **不要传参考图。** 形象靠提示词锚定;一条提示词只放一个角色。
|
|
82
|
+
3. **画风别写"高质量二次元插画"这种笼统说法**,它会推向韩系半写实并崩掉设定。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 性能
|
|
87
|
+
|
|
88
|
+
一切数字都是在真实 Chromium 里量的(合成 10k 节点会话记录,headless、CPU 光栅,即**最不利**的一档)。可复现的探针脚本在 `F:\deepseek对话\_perf\`。
|
|
89
|
+
|
|
90
|
+
### 稳态
|
|
91
|
+
|
|
92
|
+
| 指标 | 数值 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| 动画帧使用量 | **0**(走 `timer` 服务,根本不挂帧循环) |
|
|
95
|
+
| 定时器 | 1 个 `interval`(反应轮询)+ 3 个 `timeout` |
|
|
96
|
+
| 反应轮询成本 | **0.44 ms / 次**(每 1.2 s 一次 ≈ 0.04% 单核) |
|
|
97
|
+
| 每帧平均间隔 | 无插件 4.202 ms vs 挂载后 4.207 ms(4 次重复,交替测量) |
|
|
98
|
+
| 每秒 DOM 查询 | `querySelector` 1 次;`querySelectorAll` **0 次** |
|
|
99
|
+
|
|
100
|
+
前三个数字来自"每次都重新全量查一遍"的老实现时是:`querySelectorAll` 每次都调、1.27 ms/次。现在把输入框元素**解析一次后缓存**(仅在它离开文档时重新解析),忙碌判定从 4 条精确选择器改成 2 条子串选择器 —— 后者还能顺带匹配没见过的语言标签。
|
|
101
|
+
|
|
102
|
+
### 一次性成本
|
|
103
|
+
|
|
104
|
+
- **挂载瞬间**有一次约 15–50 ms 的卡顿(headless CPU 光栅下)。来源是注入样式表触发的整篇样式重算 + 首次模糊光栅化,不是稳态开销;预热 1.5 s 后测 6 s,**没有任何一帧超过 20 ms**。
|
|
105
|
+
- 4 张精灵图共 134 KB base64,挂载时只解码用到的 1 张,其余在 1.5 s 后预热。
|
|
106
|
+
|
|
107
|
+
### 已知的、没修的
|
|
108
|
+
|
|
109
|
+
- **rAF 兜底路径**在没有 `timer` 服务的壳里,只要还有任务待触发就会保持帧循环(拿不到 `setTimeout`,没有别的唤醒源)。真实 DSH 壳不走这条路 —— `window.__wisp.clock` 若不是 `timer-service` 就是它。
|
|
110
|
+
- 常驻的呼吸动画在 headless CPU 光栅下会让偶发单帧变长(4 次重复里最差 37 ms,平均值不受影响)。GPU 合成的真实窗口里这类 `filter` 通常不花钱。`prefers-reduced-motion` 会关掉全部动画。
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 配置
|
|
115
|
+
|
|
116
|
+
配置写在 profile 的 bundle patch 里(`~/.dsh/profiles/<profile>/cordis.patch.yml`,或本包 `cordis.patch.yml` 的 `insert` 行上),作为 `apply(ctx, config)` 的第二个参数。**全部可选,全部会被夹到合法区间**——写错只会退化成默认值,不会做出一个找不到或点不动的桌宠。
|
|
117
|
+
|
|
118
|
+
| 键 | 类型 | 默认 | 说明 |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `right` | number | `26` | 距右边缘 px(仅首次落位;拖动过之后以记忆位置为准) |
|
|
121
|
+
| `bottom` | number | `46` | 距下边缘 px |
|
|
122
|
+
| `size` | number | `4` | 缩放,限制 `0.4–8`;默认绘制 560×840 CSS px |
|
|
123
|
+
| `sleepAfterMs` | number | `90000` | 静置多久打盹,限制 `5000–3600000` |
|
|
124
|
+
| `reactions` | boolean | `true` | 是否跟随 Agent 忙碌 / 输入框内容变情绪 |
|
|
125
|
+
| `persist` | boolean | `true` | 是否跨刷新记住位置 |
|
|
126
|
+
| `celebrate` | boolean | `true` | 一轮跑完是否庆祝一下 |
|
|
127
|
+
| `celebrateAfterMs` | number | `2500` | 只庆祝跑够这么久的轮次(避免每次小工具调用都跳) |
|
|
128
|
+
| `happyMs` | number | `1100` | 高兴状态持续多久 |
|
|
129
|
+
| `chatterMs` | number | `0` | `>0` 时按此间隔随机说句话;`0` = 安静 |
|
|
130
|
+
| `wander` | boolean | `true` | 空闲时自己小幅踱步 |
|
|
131
|
+
| `wanderMs` | number | `45000` | 多久考虑溜达一次,限制 `5000–3600000` |
|
|
132
|
+
| `wanderRange` | number | `260` | 单次溜达最远距离 px,限制 `0–2000` |
|
|
133
|
+
|
|
134
|
+
例:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
- id: wisp
|
|
138
|
+
name: "dsh-wisp"
|
|
139
|
+
config:
|
|
140
|
+
bottom: 90
|
|
141
|
+
size: 1.2
|
|
142
|
+
chatterMs: 300000
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 运行时 API
|
|
146
|
+
|
|
147
|
+
页面 Console 里 `window.__wisp` 可用:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
window.__wisp.say('你好') // 说一句
|
|
151
|
+
window.__wisp.mood('happy') // idle | alert | happy | sleep
|
|
152
|
+
window.__wisp.configure({ size: 1.5 }) // 部分更新,其余键保持不动
|
|
153
|
+
window.__wisp.move(200, 300)
|
|
154
|
+
window.__wisp.position // { x, y }
|
|
155
|
+
window.__wisp.resetPosition() // 清掉记忆位置,回到右下角
|
|
156
|
+
window.__wisp.clock // 'timer-service' | 'animation-frame'
|
|
157
|
+
window.__wisp.destroy()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`window.__wisp.clock` 直接告诉你这台机器走了哪条调度路径,排查时先看它。
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 平台契约(踩过的坑都在这)
|
|
165
|
+
|
|
166
|
+
### 1. 两个文件、两套契约
|
|
167
|
+
|
|
168
|
+
宿主 **import** `cordis.patch.yml` 里 `name:` 指向的包 —— 必须是合法 ESM,即 `lib/index.js`。
|
|
169
|
+
浏览器半包是**经典脚本**,由 `package.json` 的 `exports["./client"]` 定位、`/plugins` 路由投递,自己调用 `window.__ModuleLoader__.load({ id, factory })` 注册。
|
|
170
|
+
|
|
171
|
+
**patch 那一行写成 `dsh-wisp/client` 会导致 `failed to import`** ——宿主无法 import 经典脚本。
|
|
172
|
+
|
|
173
|
+
### 2. 六个全局是抛异常的陷阱
|
|
174
|
+
|
|
175
|
+
`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require` 在客户端半包里被**抛异常的形参**遮蔽。所有时序因此来自 Client `timer` 服务:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
ctx.get('timer').timeout(fn, ms) // → disposer
|
|
179
|
+
ctx.get('timer').interval(fn, ms) // → disposer
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`timer` 服务取不到时(老壳 / 无头壳)退回 `requestAnimationFrame` 截止时间表,**且只在有任务待触发时才挂帧**——原来那种常驻 60fps 帧循环会让桌面进程每秒白醒 60 次。
|
|
183
|
+
|
|
184
|
+
`ctx.get('theme')` 与 `ctx.on('theme/change')` 用来跟随明暗;`body[data-ds-dark-theme]` 是兜底(这是壳真的会写的属性)。
|
|
185
|
+
|
|
186
|
+
### 3. 精灵图必须走 `blob:`,不能直接给 `data:`
|
|
187
|
+
|
|
188
|
+
初版把 base64 data URI 直接赋给 `img.src`:插件"挂载成功"、DOM 正确、光晕正常,**图是碎的**。字节经校验无误,是这个壳拒绝 `data:` 作图片源。现在解码成 `Blob` 再取 object URL,拆解时 `revokeObjectURL` 释放,**并把缓存条目一起删掉**——blob 缓存是模块级的,而挂载是每次 apply 一次,只 revoke 不删键会让下一次挂载拿到已吊销的 URL,又变成碎图。
|
|
189
|
+
|
|
190
|
+
### 4. 一次性实例
|
|
191
|
+
|
|
192
|
+
`apply` 会先销毁已存在的 `window.__wisp`。HMR 重挂、重复行、以及注入版残留都会走到这条路径,否则会叠出第二只。
|
|
193
|
+
|
|
194
|
+
### 5. 反应信号取自 DOM 事实
|
|
195
|
+
|
|
196
|
+
- 忙碌:`button[aria-label*="停止"],button[aria-label*="Stop"]` —— 壳自己的「正在跑」按钮(zh 标签是"停止生成",en 是 "Stop generating")。用子串而不是枚举精确标签,是因为标签会被翻译,而且实测 2 条子串选择器比 4 条精确选择器便宜 40%,覆盖反而更宽;
|
|
197
|
+
- 输入框有字:`[data-composer-input]`(依次回退到 `textarea`、`[contenteditable="true"]`)——**解析一次后缓存**,只在元素离开文档时重新解析。
|
|
198
|
+
|
|
199
|
+
两者都是壳为自家用户渲染的东西,比内部服务契约更抗版本升级。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 安装
|
|
204
|
+
|
|
205
|
+
**从 npm 装(推荐)** —— 这是**唯一不需要访问 GitHub** 的方式。在「添加插件」对话框里,把**安装源**设为 **中国大陆镜像源**(`registry.npmmirror.com`),然后填**包名**:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
dsh-wisp
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**从 GitHub 装** —— 需要能访问 github.com:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
plugin_manager install_bundle https://github.com/969246694/dsh-wisp
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**从本地路径装** —— 桌面版 profile 归 Electron 独占管理,`dsh plugin --profile desktop` 会被拒绝,所以走 GUI:**插件管理页 → 添加插件 → 填本目录绝对路径**。本地路径安装不下载任何包。
|
|
218
|
+
|
|
219
|
+
> **为什么镜像源只对 npm 那条路有用**:GitHub 地址被归类为 **git 规格**,由 `git` 拉取,**完全绕过 npm**——「安装源」那个设置对它无效。
|
|
220
|
+
|
|
221
|
+
装完**刷新页面**(`Ctrl+Shift+R`)即可拿到浏览器半包——`/plugins` 路由的 `rev` 由文件 `mtime` 驱动,必然变化。宿主半包(`lib/index.js`)的改动才需要完全退出托盘进程重启。
|
|
222
|
+
|
|
223
|
+
### 装好后这样验
|
|
224
|
+
|
|
225
|
+
- [ ] 右下角出现发光桌宠
|
|
226
|
+
- [ ] Console 有 `[wisp] mounted — DeepSeek娘 v0.5.0 (timers: …)`,且括号里是 `timer-service`
|
|
227
|
+
- [ ] 打字 → 变琥珀色;清空 → 回青色
|
|
228
|
+
- [ ] 让她跑一轮 → 结束时会庆祝一下
|
|
229
|
+
- [ ] 拖到别处 → 硬刷新 → **还在原处**
|
|
230
|
+
- [ ] `window.__wisp` 存在
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 变更
|
|
235
|
+
|
|
236
|
+
### 0.6.0
|
|
237
|
+
|
|
238
|
+
- **新增"自己踱步"**:空闲且 agent 不在跑时,她会每隔 `wanderMs` 考虑溜达一次,走到 `wanderRange` 以内的随机位置 —— 用 CSS 过渡**滑行**过去而不是瞬移;被拖拽、睡着、忙碌时一律不动。**踱步不写入记忆位置**:记忆的是"你把她放在哪",不然刷新后她会出现在一个你没选过的位置
|
|
239
|
+
- **新增右键归位**:她放大到 560×840 之后很容易被拖到看不见的地方,而"恢复位置"原先只能开 Console —— 现在右键她本体即可回到右下角默认位
|
|
240
|
+
- 被拖放后会随机说一句(35% 概率)
|
|
241
|
+
- 预检增至 108 项(踱步范围/滑行类名/不覆盖记忆位置/睡着不动/右键归位/关掉就不动)
|
|
242
|
+
|
|
243
|
+
### 0.5.0
|
|
244
|
+
|
|
245
|
+
- **素材流水线收进插件**(`tools/`):`CHARACTER-PROMPT.md` 成为形象的唯一权威来源(逐字复用提示词 + 四姿态 + 生成参数),`assets.mjs` 一条命令完成「绿幕 → 抠图 → 四档素材」,任何一张抠图异常都会以非 0 退出而不是悄悄混进包里;`keyout.mjs` 可单独抠一张调试
|
|
246
|
+
- **情绪切换改为交叉淡入淡出**:原来直接换 `src`,观感像闪一下;现在旧精灵保留独立图层淡出后再移除
|
|
247
|
+
- **每个情绪有自己的动作**:happy 改成 1.15s 小跳,alert 呼吸加快到 4.6s,sleep 仍是 9s 慢呼吸
|
|
248
|
+
- 预检增至 98 项(新增交叉淡入淡出的两层/回收断言)
|
|
249
|
+
|
|
250
|
+
### 0.4.0 – 0.4.4
|
|
251
|
+
|
|
252
|
+
- 尺寸从 140×210 放大到 560×840(`size: 4`,上限提到 8)
|
|
253
|
+
- **修复 `transform` 位移写错导致的"挪不动"**:位移是原点无关的,之前按"重心原点"补了半格导致绘制位置与命中区错开半个身位;同时补上行内关键样式,避免样式表未生效时把她所在的层变成文档流元素、把页面撑出滚动条(实测旧行为可达 3292px)
|
|
254
|
+
- 抠图质量:弃用服务端透明背景,改绿幕 + 本地抠图(发梢与鲸尾不再被切掉)
|
|
255
|
+
- 形象更新:深蓝超长双马尾 + 鲸鱼鳍耳饰 + 单薄性感女仆装 + 商业级高精度二次元画风
|
|
256
|
+
|
|
257
|
+
### 0.3.1
|
|
258
|
+
|
|
259
|
+
- **反应轮询成本降到 1/3**(实测 1.27 ms → 0.44 ms 每次):输入框元素改为**解析一次后缓存**,不再每次全量 `querySelectorAll`(现在每秒 0 次 NodeList 收集);忙碌判定由 4 条精确选择器改为 2 条子串选择器(顺带覆盖未预见的语言标签)
|
|
260
|
+
- **去掉常驻 `will-change: transform`**:只在真的拖动期间挂合成提示,不再为几乎不发生的事件常驻一个合成层
|
|
261
|
+
- `paint()` 不再每次重写永不变化的 `transform-origin`
|
|
262
|
+
- 新增 3 项预检守卫:已解析的输入框不得每帧重查、一次 tick 不得收集 NodeList、无输入框时每 tick 最多一次探测
|
|
263
|
+
|
|
264
|
+
### 0.3.0
|
|
265
|
+
|
|
266
|
+
- **调度改用 Client `timer` 服务**,rAF 降级为兜底且只在有待触发任务时挂帧 —— 去掉常驻帧循环与每次 `pointermove` 重建定时器的开销
|
|
267
|
+
- **修复 `nowMs` 混用两个时钟源**:`window.performance && performance.now()` 在壳自带 `window` 的场景下会用两个时间基准算/比截止时间,导致**任何定时器都永不触发**;现在只经 `window.performance` 读取
|
|
268
|
+
- **修复重挂载碎图**:blob 缓存改为吊销时一并清键(模块级缓存 × 每次 apply 挂载的交叉 bug)
|
|
269
|
+
- 新增:位置记忆、跑完庆祝、`configure()` 部分更新、`resetPosition()`、`clock` / `position` / `currentMood` 只读查询
|
|
270
|
+
- 新增:`theme` 服务订阅,气泡与文字改用 `--dsw-alias-*` 主题 token(带手调兜底)
|
|
271
|
+
- 新增:忙碌判定改用壳真实的「停止生成」按钮标签
|
|
272
|
+
- 加固:配置区间夹取(坏配置退化为默认值而非 NaN 几何)、`document.body` 未就绪时有限重试、存储不可用(不透明源)时优雅降级
|
|
273
|
+
- `build.mjs` 增加陷阱全局与版本漂移两道防线;`verify-wisp.mjs` 重写为六陷阱注入 + 虚拟时钟双路径驱动
|
|
274
|
+
|
|
275
|
+
### 0.2.1
|
|
276
|
+
|
|
277
|
+
- 精灵图内嵌;blob URL 修复碎图;`ctx.effect` 拆除钩子
|
|
278
|
+
|
|
279
|
+
### 0.2.0
|
|
280
|
+
|
|
281
|
+
- host 半包清空为零依赖;patch 指向 host 半包(修 `failed to import`)
|
|
282
|
+
|
|
283
|
+
### 0.1.0
|
|
284
|
+
|
|
285
|
+
- 初版(纯 DOM 注入版)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/idle.webp
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/work.webp
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|