dsh-live2dwaifu 0.8.1 → 1.0.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/README.md CHANGED
@@ -1,173 +1,330 @@
1
- # dsh-live2dwaifu
2
-
3
- 把 **Live2D(Cubism 3 / 4)** 模型请进 DSH 网页的右下角。她会眨眼、会呼吸、头发会被风吹,说话时嘴一张一合,心情变了眉毛跟着动。
4
-
5
- 模型**不是内置的**——谁装谁放自己的。点卡片上的 ⚙,把模型文件夹整个选进去就行,文件留在这台机器上(`~/.dsh/dsh-live2d/models/`),一根网线都没往外走。
6
-
7
- > 给它一个模型,它给你一个会呼吸的角落。
8
-
9
- ![两种状态](docs/preview.png)
10
-
11
- | 样子 | 说明 |
12
- |---|---|
13
- | 展开 | 一张小卡片,上半身,底下一条气泡装台词 |
14
- | 收起 | 缩成一条,只剩名字和一个 ⌃ |
15
-
16
- 按住标题栏能把她拖到屏幕任何地方,右下角那个小三角拖的是大小;标题栏的 **◐** 一键换背景(透明 / 深色 / 浅色 / 纸质)。位置、大小、背景、开合状态都记在本机,下次还是这样。
17
-
18
- **她会看你**:鼠标在窗口里走,眼珠跟着转,脑袋也偏一点。
19
-
20
- **能戳**:点她的头会害羞,戳身上会瞪你,点底下就应你一声。
21
-
22
- **会自己动**:模型里带 `.motion3.json` 的,隔十几秒自己播一个动作。气泡能拖着挪地方,说完过一阵自己淡下去。
23
-
24
- **会出声**:一个字一声很短的电音,标点停一拍,句尾沉一下——不是朗读,是“哔哔”那种。标题栏的 **♪** 打开调音页,放个真模型在旁边,边调边听;每只模型各存一份音色,换人换声。音量拉到 0 就安静,只剩气泡和口型。
25
-
26
- **她也会甩图(测试功能)**:⚙ 里有一根滑轴,四档——不使用 / 只使用自带 / 均开启 / 只用 dsh-meme。自带那套是夏铃自己的 Q 版贴纸,二十张,随插件分发,版权归本插件;dsh-meme 那套只是借用你本机已经装的图库。默认关着,功能还在试。
27
-
28
- **她还会问你要选项**:话说到了需要你挑的地方,气泡下面会冒出两三个按钮,点一个就等于你回了她。
29
-
30
- **开关都在 ⚙ 里**:抽屉底下分两栏——「这台机器上」管眼睛跟随、点她、自己播动作、出声、跟着聊天念;「她那边」管没人理就自己开口、甩表情包、给选项、记性。前几项存本机,后几项存 `~/.dsh/dsh-live2d/config.json`。
31
-
32
- **跟着聊**:她挂在聊天流上(`conversation.chat.node` 插槽),助手每说一句她就念一句,情绪按话里的词换。
33
-
34
- **也能直接跟她说**:卡片底下有个小框,打一句回车,她回你。这条线**不进你的聊天记录**——后台直接问模型,往来只存在 `models/<id>/chat.json` 里。她说的话按人设卡现场写,不是复读;人设卡就是 `models/<id>/meta.json` 里的 `persona` 字段,没写就用默认那份(若妍)。
35
-
36
- **她记得住,而且分两层记**:一层是最近二十来句原文,一层是攒出来的「关于你的事实」——名字、作息、口味、忌讳、答应过的事,聊得越久越攒得住,不用你重新讲一遍。攒满三十句就把前面压成小结,不丢。
37
-
38
- **她说的话会过一道筛子**:套话会被删掉——“好的”“我理解你的感受”“希望帮到你”“还有什么需要”这些,出来就没。最多留两三句,允许她只回两个字。
39
-
40
- **她知道几点、也知道自己此刻在干什么**:提示词里会让她按人设推断这个时间点的处境——早上在实验室、半夜还没睡——话里自然带着那个味道,不用你提醒。
41
-
42
- **手机上一页就够**:手机连同一个地址,打开 `/dsh-live2d/chat`,就是她的脸加一个输入框,不用看整个 DSH。
43
-
44
- **她会自己开口**:十几分钟没人理她,她就找个话头说一句。嫌吵就把 `~/.dsh/dsh-live2d/config.json` 里的 `proactive` 设成 `false`。
45
-
46
- **给别人留的口子**:im 机器人、别的插件、随手写的脚本,都能让她说话。
47
-
48
- ```bash
49
- # 她说一句(image 可省,传了就在气泡里贴出来)
50
- curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
51
- -H 'content-type: application/json' \
52
- -d '{"text":"锅里有汤。","mood":"平静","from":"im"}'
53
-
54
- # 她说一句并要回话
55
- curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/chat \
56
- -H 'content-type: application/json' -d '{"text":"今天累不累"}'
57
- ```
58
-
59
- 卡片每 2.5 秒拉一次 `/dsh-live2d/api/poll`,所以外面推进来的话,几秒内就演出来。
60
-
61
- ## 三十秒上手
62
-
63
- ```bash
64
- dsh plugin --profile desktop add dsh-live2dwaifu
65
- ```
66
-
67
- 重启 DSH,右下角出现一张小卡片 → 点 ⚙ → 点「导入模型」→ 选那个装着 `.model3.json` 的文件夹 → 完事。
68
-
69
- 导入的时候浏览器会一个一个把文件搬进去,模型大点就多等几秒,进度在底下会说话。
70
-
71
- ## 放自己的模型
72
-
73
- 1. 卡片右上角 ⚙ → 「导入模型」
74
- 2. 选**整个文件夹**(里面要有 `.model3.json`、`.moc3`、贴图,最好还有 `.physics3.json`)
75
- 3. 读完它会自己认出哪个是模型,并切过去
76
-
77
- 想放好几个也行,列表里点「用这个」换人,「删」送走。
78
-
79
- **能吃什么**:Cubism 3 / 4(`.model3.json` + `.moc3`)。
80
- **吃不了什么**:Cubism 2 的老格式(`.model.json` + `.moc`)。
81
-
82
- **自定义表情**:模型文件夹里带 `.exp3.json` 的,导入后在 ⚙ 面板里会列出来,点一下就叠上去(Add / Multiply / Overwrite 三种混合照原样执行)。Cubism 4 的 `.exp3.json` 直接从原画软件导出即可。
83
-
84
- **没有动作文件也能活**:眨眼靠 EyeBlink 参数组,嘴动靠 `ParamMouthOpenY`,头发晃动是 `physics3.json` 自带的物理;表情是本插件拿标准参数(`ParamEyeLSmile`、`ParamMouthForm`、`ParamBrow*`、`ParamAngle*`)现摆的,一共七种:平静、开心、委屈、生气、害羞、无语、困。
85
-
86
- ## 让它说点什么
87
-
88
- 卡片里是个 iframe(`/dsh-live2d/viewer`)。别的插件、脚本、控制台都能指挥它:
89
-
90
- ```js
91
- const frame = document.querySelector('iframe.dsh-l2d-body');
92
-
93
- // 说话:逐字蹦出来,嘴跟着动
94
- frame.contentWindow.postMessage({ type: 'dsh-live2d/say', text: '窗台那盆番茄红了。', mood: '开心' }, '*');
95
-
96
- // 只换脸
97
- frame.contentWindow.postMessage({ type: 'dsh-live2d/mood', mood: '委屈' }, '*');
98
-
99
- // 打开模型管理
100
- frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
101
- ```
102
-
103
- 同源的 HTTP 接口,给不服 iframe 的人:
104
-
105
- | 方法 | 路径 | 作用 |
106
- |---|---|---|
107
- | GET | `/dsh-live2d/api/models` | 列出模型和当前选中的那只 |
108
- | POST | `/dsh-live2d/api/select` | `{id}` 换人 |
109
- | POST | `/dsh-live2d/api/import?model=<id>&path=<相对路径>` | 请求体就是文件字节 |
110
- | POST | `/dsh-live2d/api/commit` | `{id,name}` 扫一遍 model3 并选中 |
111
- | POST | `/dsh-live2d/api/delete` | `{id}` 删掉 |
112
-
113
- ## 写人设卡,请写细一点
114
-
115
- 她回得像不像人,全看那段字。别只写“一个可爱的女孩”——那样只会得到一段没味道的话。
116
-
117
- 值得写进去的:她是谁、几岁、做什么的、说话什么调子(短句还是长句、用不用标点)、有没有口头禅、怕什么、喜欢什么、跟你是什么关系、平时怎么称呼你、这个时间点通常在干嘛。
118
-
119
- `models/<id>/meta.json` 里的 `persona` 想写多长写多长,写不写都行,不写就是默认那份。也可以点标题栏的 **✎** 在网页里改。
120
-
121
- ## 把她的表情包给 dsh-meme 用
122
-
123
- 自带那二十张是标准图库格式,导一次就能丢给 `dsh-meme`:
124
-
125
- ```bash
126
- python tools/export-dsh-meme.py
127
- ```
128
-
129
- 产物在 `dsh-live2d/dsh-meme-pack/xialing-001/`(含 `manifest.json`、`memes/<tag>/`、`index.db`)。整个目录丢进 `~/.dsh/profiles/desktop/node_modules/dsh-meme/memes/` 下面就认了。
130
-
131
- ## 东西都放哪了
132
-
133
- ```
134
- ~/.dsh/dsh-live2d/
135
- config.json # 现在轮到谁上场
136
- models/<id>/ # 模型文件,相对结构原样保留
137
- meta.json # { name, model3 }
138
- ```
139
-
140
- ## 一些常见的小脾气
141
-
142
- **右下角什么都没有?**
143
- 先重启一次 DSH。还不来,看 profile 的 `package.json`:`dsh.profile.bundles` 里得有 `dsh-live2dwaifu`,`dependencies` 里也得有。用 `dsh plugin add` 装的会自动写进去;手动解压到 `node_modules` 的不会——DSH 只加载列在 `bundles` 里的包,名字漏了就当没装,`/dsh-live2d/api/models` 也会 404。
144
-
145
- **导入了却不认得?**
146
- 多半是文件夹选得太靠上或太靠下,`.model3.json` 就在里面那一层,选它隔壁那层。
147
-
148
- **她只有半张脸?**
149
- 这个模型的画布比例跟面板不一样。面板是竖的,太宽或太扁的模型会被裁两边。
150
-
151
- **能放动作吗?**
152
- 暂时不能编排 `.motion3.json`,表情是固定的七种。想要更多情绪,给它发 `mood`,等以后的版本。
153
-
154
- ## 关于别人的插件
155
-
156
- 她的图、她的耳朵,都是从你**已经装好的**插件里读的。这个包里不带任何第三方素材,也一行没改别人的代码:
157
-
158
- - **表情图**:从 `dsh-meme` 的图库目录里挑一张,只在本机显示,不上传、不分发。没装 `dsh-meme` 就没有图,其它照常。
159
- - **麦克风**:交给 `dsh-ears`,本插件不碰录音,只是把转写好的字送进对话。
160
- - **对话**:走 DSH 自己的模型接口(`ctx.llm`),不经过任何人的会话,也不写你的聊天记录。
161
-
162
- 不喜欢这些联动,⚙ 里都能单独关掉。
163
-
164
- ## 已知限制
165
-
166
- - 只支持 Cubism 3 / 4(表情只认 `.exp3.json`,不认 `.motion3.json` 里的表情轨)
167
- - 面板是浮层(`shell.overlay`),不是聊天区分栏;会盖住右下角,能收起
168
- - 内置七种情绪;自定义表情靠模型自带的 `.exp3.json`,一次只能叠一个
169
- - 内置的 Cubism Core 是 Live2D Inc. 的 Redistributable Code,随包分发,使用受其 EULA 约束
170
-
171
- ---
172
-
173
- MIT © LFR · npm: [`dsh-live2dwaifu`](https://www.npmjs.com/package/dsh-live2dwaifu)
1
+ # dsh-live2dwaifu
2
+
3
+ 把一只 **Live2D(Cubism 3 / 4)** 角色请进 DSH 网页的右下角。她会眨眼、会呼吸、头发会被风吹,说话时嘴一张一合,眼珠跟着鼠标走,心情变了眉毛跟着动。
4
+
5
+ 她还能跟你说话——不用经过聊天框,不用进聊天记录。有自己的记忆、自己的作息、自己的声音(那种“哔哔”的电子音),高兴了甩你一张贴图。
6
+
7
+ 模型**不是内置的**:谁装谁放自己的。文件全留在这台机器上,一根网线都不往外走。
8
+
9
+ > 给她一个模型,她给你一个会呼吸的角落。
10
+
11
+ ![界面](docs/ui.png)
12
+
13
+ | 状态 | 说明 |
14
+ |---|---|
15
+ | 展开 | 一张竖卡片:上半身、气泡、底下一个输入框 |
16
+ | 收起 | 缩成一条,只剩名字和一个 ⌃ |
17
+
18
+ ---
19
+
20
+ ## 装
21
+
22
+ ```bash
23
+ dsh plugin --profile desktop add dsh-live2dwaifu
24
+ ```
25
+
26
+ 重启 DSH。右下角出现一张小卡片 → 点 ⚙ → 「导入模型」→ 选装着 `.model3.json` 的那个文件夹 → 完事。
27
+
28
+ 桌面端也可以在**设置 → 插件**里搜名字安装。
29
+
30
+ **别手动解压到 `node_modules`。** DSH 只加载列在 profile `package.json` 的 `dsh.profile.bundles` 里的包。手动扔进去的名字不会自动登记,表现就是“装了但右下角什么都没有”,`/dsh-live2d/api/models` 也返回 404。真遇到了,把 `dsh-live2dwaifu` 补进 `bundles` 和 `dependencies` 再重启。
31
+
32
+ ---
33
+
34
+ ## 卡片
35
+
36
+ ### 位置和大小
37
+
38
+ 按住**标题栏**能拖到屏幕任何地方;右下角那个小斜纹拖的是大小(宽 200–720,高 220–视口高−80)。位置、大小、背景、开合状态都记在本机,下次打开还是这样。
39
+
40
+ 拖到一半跑到屏幕外了?关掉重开一次就回到默认位置。
41
+
42
+ ### 标题栏按钮
43
+
44
+ | 按钮 | 干什么 |
45
+ |---|---|
46
+ | **⚙** | 抽屉:模型列表、自定义表情、全部开关 |
47
+ | **◐** | 换背景:透明 / 深色 / 浅色 / 纸质 |
48
+ | **♪** | 音色调色页(每只模型各存一份音色) |
49
+ | **✎** | 人设卡编辑页 |
50
+ | **↻** | 重载 iframe(模型卡住时点这个) |
51
+ | **⌄ / ⌃** | 收起 / 展开 |
52
+
53
+ ---
54
+
55
+ ## 放自己的模型
56
+
57
+ 1. ⚙ → 「导入模型」
58
+ 2. 选**整个文件夹**,里面要有 `.model3.json`、`.moc3`、贴图,最好带 `.physics3.json`
59
+ 3. 读完它自己认出模型并切过去
60
+
61
+ 想放好几个也行:列表里「用这个」换人,「删」送走。
62
+
63
+ **吃**:Cubism 3 / 4(`.model3.json` + `.moc3`)。
64
+ **不吃**:Cubism 2 老格式(`.model.json` + `.moc`)。
65
+
66
+ **模型自带的动作**:有 `.motion3.json` 的,隔十几秒自己播一个;没有也能活——眨眼靠 EyeBlink 参数组,嘴动靠 `ParamMouthOpenY`,头发晃动是 `physics3.json` 的物理。
67
+
68
+ **模型自带的表情**:带 `.exp3.json` 的会在抽屉里列出来,点一下叠上去(Add / Multiply / Overwrite 照原样执行)。没带的话,内置七种:平静、开心、委屈、生气、害羞、无语、困。
69
+
70
+ **导入卡住或只导入一半**:模型大(贴图 4K)时浏览器要一张张传,慢是正常的,进度在抽屉底下显示。传一半断了就再来一次,同名文件会覆盖。
71
+
72
+ ---
73
+
74
+ ## 她是谁:人设卡
75
+
76
+ 她说话像不像人,全看这一段字。别只写“一个可爱的女孩”——那样只会得到一段没味道的话。
77
+
78
+ 人设卡存在 `~/.dsh/dsh-live2d/models/<id>/meta.json` 的 `persona` 字段里,也可以点标题栏 **✎** 在网页里改,改完她一重载就按新的说。
79
+
80
+ **值得写进去的**:
81
+
82
+ - 她是谁、几岁、做什么工作的
83
+ - 说话什么调子:短句还是长句、用不用标点、爱不爱反问
84
+ - 口头禅、习惯性的小动作、称呼你什么
85
+ - 怕什么、喜欢什么、有什么毛病
86
+ - 跟你是什么关系、你们在哪儿过日子
87
+ - 这个时间点她通常在干嘛
88
+
89
+ 写得越具体越像人,写一句“性格开朗”没什么用。写完可以自己念一遍,像不像一个人在发消息。
90
+
91
+ **世界隔离**(人设卡页底下一个开关):开着的时候,她不知道什么是程序、界面、插件、模型,问起来就当你在她身边;关掉她就老老实实承认自己是插件里的角色。默认开。
92
+
93
+ 不写 `persona` 也行,会用默认那份(若妍)。
94
+
95
+ ---
96
+
97
+ ## 跟她说话
98
+
99
+ ### 三条路
100
+
101
+ **一、卡片底下的输入框**:打一句、回车。这条线**不进你的聊天记录**,往来只存在她自己的 `chat.json` 里。
102
+
103
+ **二、手机**:手机连同一个地址,打开 `/dsh-live2d/chat`,全屏就她的脸加一个输入框,不用看整个 DSH。
104
+
105
+ > 端口每次启动可能不一样。在 DSH 设置里能看到当前地址,把它换成 `/dsh-live2d/chat` 就行。手机和电脑得在同一个局域网。
106
+
107
+ **三、别的插件推**(见下面「给别的插件用」)。
108
+
109
+ ### 她记得住
110
+
111
+ 记忆分两层:
112
+
113
+ - **最近二十来句**:原话,最近说了什么都在。
114
+ - **关于你的事实**:名字、作息、口味、忌讳、答应过的事——从对话里攒出来的,长期挂着。聊天时只挑跟你这句话对得上的几条带上,不会一股脑塞进提示词。
115
+
116
+ 攒满三十句,前面二十句会压成小结,不丢。嫌费 token,就在 ⚙ 里把「记住之前说的」关掉。
117
+
118
+ ### 她说的话会过一道筛子
119
+
120
+ 模型写出来的东西先过一遍:套话会被删掉——“好的”“我理解你的感受”“希望帮到你”“还有什么需要我帮忙的吗”这些,出来就没有。最多留两三句,也允许她只回两个字。
121
+
122
+ 提示词里还会明确禁止:不总结、不分点、不讲道理、不解释心情、你没问就不给建议。
123
+
124
+ > 还是觉得哪句像 AI?记下来告诉作者,往规则里加。
125
+
126
+ ### 她知道现在几点,也知道自己该在干嘛
127
+
128
+ 提示词里会让她按人设推断这个时间点的处境——早上在实验室、半夜还没睡——话里自然带着那个味道。不用你提醒,也不用写死时间表。
129
+
130
+ ### 她偶尔自己开口
131
+
132
+ 十几分钟没人理她,她就找个话头说一句,通过卡片的气泡冒出来。嫌吵:⚙ 里关掉「没人理就自己开口」。
133
+
134
+ **开了却从不出现**?她只在页面开着的时候说,而且说出来到显示有几秒延迟(卡片每 2.5 秒拉一次)——等一会。
135
+
136
+ ---
137
+
138
+ ## 声音
139
+
140
+ 她说话时一个字一声很短的电音,标点停一拍,句尾沉一下。不是朗读,是“哔哔”那种。
141
+
142
+ **调色**:标题栏 **♪** 打开调音页,左边是这只模型真身,右边四个预设(柔和 / 清脆 / 机械 / 游戏)加五个滑块(基频、抖动、长度、音量、语速)。边调边听,满意了点「存到这只模型」。**每只模型各存一份**,换人换声。
143
+
144
+ **安静**:⚙ 里关掉「出声」,或把音色调到音量 0。关掉之后只剩气泡和口型。
145
+
146
+ **第一次没有声音**?浏览器要求用户先点一下页面才允许出声。点她一下,或者点一下调音页的按钮,再试。
147
+
148
+ ---
149
+
150
+ ## 表情包(测试功能)
151
+
152
+ 她说话时可能甩一张贴图,贴在气泡里。
153
+
154
+ ⚙ 里有一根四档滑轴:
155
+
156
+ | 档位 | 意思 |
157
+ |---|---|
158
+ | 不使用 | 不说话配图 |
159
+ | 只使用自带 | 只用插件自带的那套 |
160
+ | 均开启 | 自带和 dsh-meme 混着来 |
161
+ | 只用 dsh-meme | 只借你本机已装的 dsh-meme 图库 |
162
+
163
+ **自带那套**是作者 OC 的 Q 版贴纸(效果不是很好其实)。
164
+
165
+ **怎么挑图**:她点名叫哪张就用哪张(提示词里给了名字清单);没点名的话,按她这句话的情绪随机抽一张,大约三成概率。
166
+
167
+ **给 dsh-meme 用**:自带那二十张是标准图库格式,导一次就能丢给它。
168
+
169
+ ```bash
170
+ python tools/export-dsh-meme.py
171
+ ```
172
+
173
+ 产物在 `dsh-live2d/dsh-meme-pack/xialing-001/`,整个目录丢进 `~/.dsh/profiles/desktop/node_modules/dsh-meme/memes/` 下面就认。
174
+
175
+ **图不贴题**?这功能还在试。滑到「不用」最干净。
176
+
177
+ ---
178
+
179
+ ## 全部开关
180
+
181
+ ⚙ 抽屉底下分两栏。「这台机器上」存 localStorage,只管这一台浏览器;「她那边」存 `~/.dsh/dsh-live2d/config.json`,跟着插件走。
182
+
183
+ | 开关 | 位置 | 默认 | 说明 |
184
+ |---|---|---|---|
185
+ | 眼睛跟着鼠标 | 本机 | 开 | 关掉她就不看鼠标了 |
186
+ | 点她有反应 | 本机 | 开 | 摸头害羞、戳身上瞪你 |
187
+ | 自己播动作 | 本机 | 开 | 有动作文件的模型才会动 |
188
+ | 出声 | 本机 | 开 | 电子音 |
189
+ | 跟着聊天念 | 本机 | 开 | 助手每说一句她念一句 |
190
+ | 没人理就自己开口 | 她那边 | 开 | 十几分钟没人说话她说一句 |
191
+ | 给选项让他挑 | 她那边 | 开 | 气泡上方冒出可点的按钮 |
192
+ | 记住之前说的 | 她那边 | 开 | 关掉就不带记忆、不压缩 |
193
+ | 表情包(四档) | 她那边 | 不使用 | 测试功能,见上 |
194
+
195
+ ---
196
+
197
+ ## 给别的插件用
198
+
199
+ 卡片里是个 iframe(`/dsh-live2d/viewer`)。同源脚本可以直接指挥它:
200
+
201
+ ```js
202
+ const frame = document.querySelector('iframe.dsh-l2d-body');
203
+
204
+ // 说话:逐字蹦出来,嘴跟着动,可选配图
205
+ frame.contentWindow.postMessage({
206
+ type: 'dsh-live2d/say',
207
+ text: '窗台那盆番茄红了。',
208
+ mood: '开心',
209
+ image: '/dsh-live2d/ownmeme/deyi.jpg',
210
+ }, '*');
211
+
212
+ // 只换脸
213
+ frame.contentWindow.postMessage({ type: 'dsh-live2d/mood', mood: '委屈' }, '*');
214
+
215
+ // 打开模型抽屉
216
+ frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
217
+ ```
218
+
219
+ HTTP 接口,给不想碰 iframe 的:
220
+
221
+ | 方法 | 路径 | 作用 |
222
+ |---|---|---|
223
+ | GET | `/dsh-live2d/api/models` | 模型列表 + 当前选中 + 每只的音色 |
224
+ | POST | `/dsh-live2d/api/select` | `{id}` 换人 |
225
+ | POST | `/dsh-live2d/api/import?model=<id>&path=<相对路径>` | 请求体是文件字节 |
226
+ | POST | `/dsh-live2d/api/commit` | `{id,name}` 扫一遍并选中 |
227
+ | POST | `/dsh-live2d/api/delete` | `{id}` 删掉 |
228
+ | GET | `/dsh-live2d/api/voice?id=` | 读这只模型的音色 |
229
+ | POST | `/dsh-live2d/api/voice` | `{id,voice}` 存音色 |
230
+ | GET/POST | `/dsh-live2d/api/persona` | 读 / 存人设卡 |
231
+ | GET/POST | `/dsh-live2d/api/settings` | 读 / 存开关 |
232
+ | POST | `/dsh-live2d/api/chat` | `{text}` 跟她说一句,返回 `{reply,image,choices}` |
233
+ | POST | `/dsh-live2d/api/push` | `{text,image,mood}` 让她说一句(外部推送口) |
234
+ | GET | `/dsh-live2d/api/poll?since=` | 卡片每 2.5 秒拉一次,取待播的话 |
235
+
236
+ im 机器人接进来就一句话:
237
+
238
+ ```bash
239
+ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
240
+ -H 'content-type: application/json' \
241
+ -d '{"text":"锅里有汤。","mood":"平静","from":"im"}'
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 数据存在哪
247
+
248
+ ```
249
+ ~/.dsh/dsh-live2d/
250
+ config.json 现在轮到谁上场 + 各项开关
251
+ models/<id>/
252
+ meta.json { name, model3, persona, isolation, voice }
253
+ chat.json { messages, summary, facts } 跟她私聊的记录和记忆
254
+ <模型文件…>
255
+ ```
256
+
257
+ 插件自己的文件在 `~/.dsh/profiles/desktop/node_modules/dsh-live2dwaifu/`,自带的二十张贴图在 `assets/memes/`。
258
+
259
+ **想清空她对你记的事**:删掉对应模型的 `chat.json`。**想彻底重来**:整个 `~/.dsh/dsh-live2d/` 删掉,模型得重新导入。
260
+
261
+ ---
262
+
263
+ ## 常见问题
264
+
265
+ **右下角什么都没有**
266
+ 先重启。还不来,看 profile 的 `package.json`:`dsh.profile.bundles` 和 `dependencies` 里都得有 `dsh-live2dwaifu`。手动拷进 `node_modules` 的不会自动登记。
267
+
268
+ **她只有半张脸 / 位置偏上**
269
+ 模型画布比例跟卡片不一样。卡片是竖的,太宽或太扁的模型会被裁两边。拖右下角改形状试试。
270
+
271
+ **导入了却不认得**
272
+ `.model3.json` 的层级选错了。选它隔壁那一层。
273
+
274
+ **她不动、不眨眼**
275
+ 点一次 **↻** 重载。还不行就看浏览器控制台——多半是模型缺 `.moc3` 或缺贴图。
276
+
277
+ **跟她说话没反应**
278
+ - 确认 ⚙ 里「记住之前说的」不是全关(不影响回复,只影响记忆)
279
+ - 后台直接调 DSH 的模型接口,所以 DSH 自己得能正常对话
280
+ - 报“模型接口不可用”就是当前没有可用模型,去设置里选一个
281
+
282
+ **她说的话还是像 AI**
283
+ 规则在 `lib/index.js` 的 `AI_WORDS` 和提示词那段。可以自己加词,改完重启。
284
+
285
+ **手机打不开 `/dsh-live2d/chat`**
286
+ 手机和电脑得在同一个局域网;端口用 DSH 设置里显示的那个(每次启动会变);防火墙可能挡了局域网访问。
287
+
288
+ **表情包一直不出现**
289
+ 滑轴滑到「不使用」了,或者她是随机三成概率——多聊几句。只使用自带的话,确认 `assets/memes/` 里有那二十张。
290
+
291
+ **动作播得很怪 / 抽搐**
292
+ 有些模型的 motion 会覆盖插件在算的参数。⚙ 里关掉「自己播动作」。
293
+
294
+ **她记得太多了 / 词太占 token**
295
+ ⚙ 里关掉「记住之前说的」。或者删 `chat.json` 里不要的条目。
296
+
297
+ ---
298
+
299
+ ## 已知限制
300
+
301
+ - 只支持 Cubism 3 / 4,不吃 Cubism 2
302
+ - 表情只认 `.exp3.json`,不认 `.motion3.json` 里的表情轨
303
+ - 卡片是浮层(`shell.overlay`),不是聊天区分栏,会盖住右下角——能拖走、能收起
304
+ - 内置七种情绪;自定义表情一次只能叠一个
305
+ - 跟她的对话不经过 DSH 会话,所以没有工具调用、不能上网、不读文件,只说话
306
+ - 内置的 Cubism Core 是 Live2D Inc. 的 Redistributable Code,随包分发,使用受其 EULA 约束
307
+
308
+ ---
309
+
310
+ ## 关于别人的插件
311
+
312
+ 她的图、她的耳朵,都是从你**已经装好的**插件里读的。这个包里不带任何第三方素材,也一行没改别人的代码:
313
+
314
+ - **表情图**:从 `dsh-meme` 的图库目录里挑一张,只在本机显示,不上传、不分发。没装就没有图,其它照常。
315
+ - **麦克风**:交给 `dsh-ears`,本插件不碰录音,只是把转写好的字送进对话。
316
+ - **对话**:走 DSH 自己的模型接口(`ctx.llm`),不经过任何人的会话,也不写你的聊天记录。
317
+
318
+ 不喜欢这些联动,⚙ 里都能单独关掉。
319
+
320
+ ---
321
+
322
+ ## 致谢
323
+
324
+ - Live2D Cubism Core © Live2D Inc.
325
+ - `pixi-live2d-display` 与 `pixi.js`
326
+ - DeepSeek Harness 大肥鱼
327
+
328
+ ---
329
+
330
+ MIT © LFR · npm: [`dsh-live2dwaifu`](https://www.npmjs.com/package/dsh-live2dwaifu) · [GitHub](https://github.com/FujiwaraReisen/dsh-live2dwaifu)
@@ -1,80 +1,92 @@
1
- <!DOCTYPE html>
2
- <html lang="zh">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Live2D</title>
6
- <style>
7
- html, body { margin: 0; height: 100%; background: transparent; overflow: hidden;
8
- font: 12px/1.5 "Microsoft YaHei", system-ui, sans-serif; color: #d8dbe2; }
9
- body.standalone { background: #14161a; }
10
- #stage { display: block; width: 100%; height: 100%; }
11
- #bubble { position: absolute; left: 12px; right: 12px; bottom: 10px; min-height: 30px; box-sizing: border-box;
12
- background: rgba(24,27,33,.94); border: 1px solid #3a4049; border-radius: 10px; padding: 7px 10px;
13
- color: #dfe4ec; font-size: 13px; line-height: 1.45;
14
- opacity: 0; transition: opacity .35s ease; cursor: grab; user-select: none;
15
- max-height: 40%; overflow: hidden; touch-action: none; }
16
- #bubble:active { cursor: grabbing; }
17
- #hint { position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
18
- text-align: center; padding: 20px; color: #98a0ad; }
19
- #hint.show { display: flex; }
20
- #drawer { position: absolute; inset: 0; background: rgba(18,20,24,.97); display: none;
21
- flex-direction: column; padding: 12px; box-sizing: border-box; }
22
- #drawer.show { display: flex; }
23
- #drawer h2 { font-size: 12px; font-weight: 600; margin: 0 0 8px; color: #cfd4dd; letter-spacing: .5px; }
24
- #list { flex: 1; overflow-y: auto; margin-bottom: 8px; }
25
- .row { display: flex; align-items: center; gap: 6px; padding: 5px 6px; border-radius: 8px; }
26
- .row.cur { background: #2b3038; }
27
- .row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
28
- .row button { background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c; border-radius: 6px;
29
- padding: 2px 7px; cursor: pointer; font: inherit; }
30
- .row button:hover { background: #3a4049; }
31
- #exps { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
32
- #exps button { background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
33
- border-radius: 6px; padding: 3px 8px; cursor: pointer; font: inherit; }
34
- #exps button.on { background: #3a5f8a; border-color: #4c7bb0; color: #eaf2ff; }
35
- #exps .tipx { color: #6f7783; font-size: 11px; align-self: center; }
36
- #choices { display: none; position: absolute; left: 12px; right: 12px; bottom: 96px; flex-wrap: wrap; gap: 6px; }
37
- #choices.show { display: flex; }
38
- #choices button { background: rgba(38,48,62,.96); border: 1px solid #4a6a92; border-radius: 14px;
39
- color: #dfe9f6; font: inherit; font-size: 12px; padding: 5px 11px; cursor: pointer; }
40
- #choices button:hover { background: #2f4a68; }
41
- #settings { max-height: 42%; overflow-y: auto; margin-bottom: 8px; }
42
- #settings h3 { font-size: 10px; letter-spacing: .6px; color: #6f7783; font-weight: 600; margin: 8px 0 4px; }
43
- .opt { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: 12px; color: #c3c9d3; }
44
- .opt input { accent-color: #6aa9ff; }
45
- .opt-col { padding: 4px 2px 2px; }
46
- .opt-col input[type=range] { width: 100%; accent-color: #6aa9ff; }
47
- .opt-lab { font-size: 12px; color: #dfe4ec; margin-top: 2px; }
48
- .opt-note { font-size: 10.5px; line-height: 1.6; color: #6f7783; margin-top: 4px; }
49
- .bub-img { display: block; max-width: 100%; max-height: 120px; border-radius: 8px; margin-bottom: 6px; object-fit: contain; }
50
- #actions { display: flex; gap: 6px; }
51
- #actions button { flex: 1; background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
52
- border-radius: 8px; padding: 6px; cursor: pointer; font: inherit; }
53
- #actions button:hover { background: #3a4049; }
54
- #tip { color: #7f8794; font-size: 11px; margin: 6px 2px 0; }
55
- </style>
56
- </head>
57
- <body>
58
- <canvas id="stage"></canvas>
59
- <div id="bubble"></div>
60
- <div id="choices"></div>
61
- <div id="hint"><div>还没有模型。<br />点下面的「导入模型」,选中你的 Live2D 文件夹(含 .model3.json 那一层)。</div></div>
62
- <div id="drawer">
63
- <h2>模型</h2>
64
- <div id="list"></div>
65
- <div id="exps"></div>
66
- <div id="settings"></div>
67
- <div id="actions">
68
- <button id="import">导入模型</button>
69
- <button id="close">关掉</button>
70
- </div>
71
- <div id="tip">选整个文件夹,浏览器会自己把里面所有文件读进来。</div>
72
- <input id="files" type="file" webkitdirectory multiple style="display:none" />
73
- </div>
74
- <script>
75
- if (!location.search.includes('embed')) document.body.classList.add('standalone');
76
- </script>
77
- <script src="/dsh-live2d/assets/live2dcubismcore.min.js"></script>
78
- <script src="/dsh-live2d/assets/viewer.js"></script>
79
- </body>
80
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Live2D</title>
6
+ <style>
7
+
8
+ html, body { margin: 0; height: 100%; background: transparent; overflow: hidden;
9
+ font: 12px/1.55 "Microsoft YaHei", system-ui, sans-serif; color: #dde2ea; }
10
+ body.standalone { background: #14161a; }
11
+ #stage { display: block; width: 100%; height: 100%; }
12
+ #bottom { position: absolute; left: 12px; right: 12px; bottom: 12px; display: flex; flex-direction: column;
13
+ gap: 8px; pointer-events: none; }
14
+ #bottom > * { pointer-events: auto; }
15
+ #bubble { position: relative; min-height: 34px; box-sizing: border-box;
16
+ padding: 9px 12px; border-radius: 14px;
17
+ background: linear-gradient(180deg, rgba(32,37,45,.96), rgba(22,25,31,.97));
18
+ border: 1px solid rgba(255,255,255,.09); box-shadow: 0 10px 28px rgba(0,0,0,.5);
19
+ backdrop-filter: blur(8px); color: #e8edf4; font-size: 13px; line-height: 1.6;
20
+ opacity: 0; transition: opacity .35s ease; cursor: grab; user-select: none;
21
+ max-height: 42%; overflow: hidden; touch-action: none; }
22
+ #bubble:active { cursor: grabbing; }
23
+ .bub-img { display: block; max-width: 70%; max-height: 88px; border-radius: 10px; margin-bottom: 7px; object-fit: contain; }
24
+ #hint { position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
25
+ text-align: center; padding: 24px; color: #98a0ad; }
26
+ #hint.show { display: flex; }
27
+ #drawer { position: absolute; inset: 0; display: none; flex-direction: column; padding: 12px; box-sizing: border-box;
28
+ background: linear-gradient(180deg, rgba(18,20,26,.97), rgba(14,16,20,.99)); backdrop-filter: blur(12px); }
29
+ #drawer.show { display: flex; }
30
+ #drawer h2 { font-size: 11px; letter-spacing: .8px; font-weight: 600; margin: 0 0 8px; color: #8b94a3; }
31
+ #list { flex: 0 1 auto; max-height: 32%; overflow-y: auto; margin-bottom: 8px; }
32
+ .row { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 10px; transition: background .15s; }
33
+ .row:hover { background: rgba(255,255,255,.04); }
34
+ .row.cur { background: rgba(106,169,255,.14); border: 1px solid rgba(106,169,255,.3); }
35
+ .row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
36
+ .row button { background: rgba(255,255,255,.07); color: #cfd4dd; border: 1px solid rgba(255,255,255,.1);
37
+ border-radius: 8px; padding: 3px 9px; cursor: pointer; font: inherit; transition: background .15s; }
38
+ .row button:hover { background: rgba(255,255,255,.14); }
39
+ #exps { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
40
+ #exps button { background: rgba(255,255,255,.07); color: #cfd4dd; border: 1px solid rgba(255,255,255,.1);
41
+ border-radius: 999px; padding: 4px 10px; cursor: pointer; font: inherit; transition: all .15s; }
42
+ #exps button:hover { background: rgba(255,255,255,.14); }
43
+ #exps button.on { background: linear-gradient(180deg,#33587f,#27405f); border-color: #4c7bb0; color: #eaf2ff; }
44
+ #exps .tipx { color: #6f7783; font-size: 11px; align-self: center; }
45
+ #choices { display: none; flex-wrap: wrap; gap: 8px; }
46
+ #choices.show { display: flex; }
47
+ #choices button { background: linear-gradient(180deg, rgba(52,66,86,.97), rgba(36,48,64,.97));
48
+ border: 1px solid rgba(106,169,255,.45); border-radius: 999px; color: #e6f0fb; font: inherit; font-size: 12px;
49
+ padding: 6px 13px; cursor: pointer; box-shadow: 0 6px 18px rgba(0,0,0,.35); transition: all .15s; }
50
+ #choices button:hover { filter: brightness(1.15); }
51
+ #choices button:active { transform: translateY(1px); }
52
+ #settings { flex: 0 1 auto; overflow-y: auto; margin-bottom: 8px; }
53
+ #settings h3 { font-size: 10px; letter-spacing: .8px; color: #6f7783; font-weight: 600; margin: 10px 0 5px; }
54
+ .opt { display: flex; align-items: center; gap: 8px; padding: 4px 2px; font-size: 12px; color: #c3c9d3; cursor: pointer; }
55
+ .opt input { accent-color: #6aa9ff; }
56
+ .opt-col { padding: 4px 2px 2px; }
57
+ .opt-col input[type=range] { width: 100%; accent-color: #6aa9ff; }
58
+ .opt-lab { font-size: 12px; color: #e2e8f1; margin-top: 2px; }
59
+ .opt-note { font-size: 10.5px; line-height: 1.65; color: #6f7783; margin-top: 5px; }
60
+ #actions { display: flex; gap: 8px; flex: none; }
61
+ #actions button { flex: 1; padding: 9px; border-radius: 10px; cursor: pointer; font: inherit;
62
+ background: rgba(255,255,255,.07); color: #dfe4ec; border: 1px solid rgba(255,255,255,.1); transition: background .15s; }
63
+ #actions button:hover { background: rgba(255,255,255,.14); }
64
+ #tip { color: #6f7783; font-size: 10.5px; margin: 8px 2px 0; line-height: 1.6; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <canvas id="stage"></canvas>
69
+ <div id="bottom">
70
+ <div id="choices"></div>
71
+ <div id="bubble"></div>
72
+ </div>
73
+ <div id="hint"><div>还没有模型。<br />点下面的「导入模型」,选中你的 Live2D 文件夹(含 .model3.json 那一层)。</div></div>
74
+ <div id="drawer">
75
+ <h2>模型</h2>
76
+ <div id="list"></div>
77
+ <div id="exps"></div>
78
+ <div id="settings"></div>
79
+ <div id="actions">
80
+ <button id="import">导入模型</button>
81
+ <button id="close">关掉</button>
82
+ </div>
83
+ <div id="tip">选整个文件夹,浏览器会自己把里面所有文件读进来。</div>
84
+ <input id="files" type="file" webkitdirectory multiple style="display:none" />
85
+ </div>
86
+ <script>
87
+ if (!location.search.includes('embed')) document.body.classList.add('standalone');
88
+ </script>
89
+ <script src="/dsh-live2d/assets/live2dcubismcore.min.js"></script>
90
+ <script src="/dsh-live2d/assets/viewer.js"></script>
91
+ </body>
92
+ </html>
@@ -0,0 +1,144 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>UI 预览 — dsh-live2dwaifu 1.0.0</title>
6
+ <style>
7
+ :root { --line: rgba(255,255,255,.07); }
8
+ * { box-sizing: border-box; }
9
+ body { margin:0; min-height:100vh; background:radial-gradient(1000px 520px at 50% -10%, #1b1f27, #0f1116);
10
+ color:#dfe4ec; font:12px/1.5 "Microsoft YaHei",system-ui,sans-serif;
11
+ display:flex; align-items:center; justify-content:center; gap:30px; padding:26px; flex-wrap:wrap; }
12
+ .col { display:flex; flex-direction:column; gap:14px; }
13
+ .cap { font-size:11px; color:#6f7783; letter-spacing:.5px; }
14
+
15
+ /* ===== 卡片 ===== */
16
+ .card { position:relative; width:300px; height:430px; border-radius:16px; overflow:hidden;
17
+ border:1px solid var(--line);
18
+ background:linear-gradient(180deg,rgba(36,41,50,.95) 0%,rgba(21,24,30,.97) 100%);
19
+ box-shadow:0 18px 48px rgba(0,0,0,.5); backdrop-filter:blur(10px); display:flex; flex-direction:column; }
20
+ .card.collapsed { width:150px; height:38px; }
21
+ .head { display:flex; align-items:center; gap:7px; padding:9px 11px; color:#a8b1bf; font-size:11px;
22
+ letter-spacing:.4px; border-bottom:1px solid var(--line); user-select:none; flex:none; cursor:move;
23
+ background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,0)); }
24
+ .dot { width:7px; height:7px; border-radius:50%; background:#6aa9ff; flex:none; box-shadow:0 0 0 3px rgba(106,169,255,.16); }
25
+ .name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#d7dde7; }
26
+ .btns { margin-left:auto; display:flex; gap:2px; flex:none; }
27
+ .btns span { width:22px; height:22px; border-radius:7px; display:flex; align-items:center; justify-content:center;
28
+ color:#7d858f; }
29
+ .btns span.on { color:#eef2f8; background:rgba(255,255,255,.09); }
30
+ .body { flex:1; position:relative; display:flex; flex-direction:column; justify-content:flex-end;
31
+ gap:8px; padding-bottom:12px; overflow:hidden;
32
+ background:radial-gradient(120% 70% at 50% 100%, #232935 0%, #171b22 70%); }
33
+ .body svg { position:absolute; left:50%; bottom:0; transform:translateX(-50%); width:74%; }
34
+ .bubble { position:relative; margin:0 12px; min-height:34px; padding:9px 12px; border-radius:14px;
35
+ background:linear-gradient(180deg,rgba(32,37,45,.96),rgba(22,25,31,.97));
36
+ border:1px solid rgba(255,255,255,.09); box-shadow:0 10px 28px rgba(0,0,0,.5);
37
+ color:#e8edf4; font-size:13px; line-height:1.6; }
38
+ .bubble .pic { width:72px; height:72px; border-radius:10px; margin-bottom:7px;
39
+ background:linear-gradient(160deg,#2f3946,#1d232c); border:1px solid rgba(255,255,255,.08);
40
+ display:flex; align-items:center; justify-content:center; color:#5d6675; font-size:9px; }
41
+ .choices { position:relative; margin:0 12px; display:flex; flex-wrap:wrap; gap:8px; }
42
+ .choices span { background:linear-gradient(180deg,rgba(52,66,86,.97),rgba(36,48,64,.97));
43
+ border:1px solid rgba(106,169,255,.45); border-radius:999px; color:#e6f0fb; font-size:12px;
44
+ padding:6px 13px; box-shadow:0 6px 18px rgba(0,0,0,.35); }
45
+ .say { display:flex; gap:7px; padding:8px 10px; border-top:1px solid var(--line); flex:none; }
46
+ .say .in { flex:1; min-width:0; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.09);
47
+ border-radius:10px; color:#e6ebf2; font-size:11.5px; padding:7px 10px; color:#6d7684; }
48
+ .say .in.focus { border-color:rgba(106,169,255,.55); background:rgba(255,255,255,.08); color:#e6ebf2; }
49
+ .say .send { background:linear-gradient(180deg,#33587f,#27405f); border:1px solid #3e6b9e; border-radius:10px;
50
+ color:#eaf2ff; padding:0 13px; font-size:11.5px; display:flex; align-items:center; }
51
+ .grip { position:absolute; right:1px; bottom:1px; width:18px; height:18px; opacity:.5;
52
+ background:repeating-linear-gradient(135deg,transparent 0 2px,rgba(255,255,255,.4) 2px 3px) no-repeat right bottom/10px 10px; }
53
+
54
+ /* ===== 抽屉 ===== */
55
+ .drawer { width:300px; height:600px; border-radius:16px; overflow:hidden; border:1px solid var(--line);
56
+ background:linear-gradient(180deg,rgba(18,20,26,.97),rgba(14,16,20,.99));
57
+ box-shadow:0 18px 48px rgba(0,0,0,.5); backdrop-filter:blur(12px); padding:12px; display:flex; flex-direction:column; }
58
+ .drawer h2 { font-size:11px; letter-spacing:.8px; font-weight:600; margin:0 0 8px; color:#8b94a3; }
59
+ .row { display:flex; align-items:center; gap:6px; padding:7px 8px; border-radius:10px; }
60
+ .row.cur { background:rgba(106,169,255,.14); border:1px solid rgba(106,169,255,.3); }
61
+ .row span { flex:1; }
62
+ .row b { background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.1); border-radius:8px;
63
+ padding:3px 9px; font-weight:400; color:#cfd4dd; font-size:11px; }
64
+ .exps { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:8px; }
65
+ .exps span { background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.1); border-radius:999px;
66
+ padding:4px 10px; color:#cfd4dd; font-size:11px; }
67
+ .exps span.on { background:linear-gradient(180deg,#33587f,#27405f); border-color:#4c7bb0; color:#eaf2ff; }
68
+ .settings { flex:1; overflow:hidden; }
69
+ .settings h3 { font-size:10px; letter-spacing:.8px; color:#6f7783; font-weight:600; margin:10px 0 5px; }
70
+ .opt { display:flex; align-items:center; gap:8px; padding:3px 2px; color:#c3c9d3; }
71
+ .box { width:13px; height:13px; border-radius:4px; border:1px solid #4a5260; background:rgba(255,255,255,.05); }
72
+ .box.on { background:#6aa9ff; border-color:#6aa9ff; }
73
+ .slider { margin:6px 2px 2px; position:relative; height:4px; border-radius:2px; background:rgba(255,255,255,.12); }
74
+ .slider i { position:absolute; left:0; top:0; height:4px; width:66%; border-radius:2px; background:#6aa9ff; }
75
+ .slider b { position:absolute; top:-5px; left:66%; width:14px; height:14px; margin-left:-7px; border-radius:50%;
76
+ background:#eaf2ff; box-shadow:0 2px 6px rgba(0,0,0,.4); }
77
+ .lab { font-size:12px; color:#e2e8f1; margin-top:6px; }
78
+ .note { font-size:10.5px; line-height:1.65; color:#6f7783; margin-top:5px; }
79
+ .acts { display:flex; gap:8px; flex:none; margin-top:8px; }
80
+ .acts span { flex:1; text-align:center; padding:9px; border-radius:10px; background:rgba(255,255,255,.07);
81
+ border:1px solid rgba(255,255,255,.1); color:#dfe4ec; font-size:12px; }
82
+ </style>
83
+ </head>
84
+ <body>
85
+
86
+ <div class="col">
87
+ <div class="cap">展开(右下角)</div>
88
+ <div class="card">
89
+ <div class="head"><span class="dot"></span><span class="name">夏铃</span>
90
+ <span class="btns"><span class="on">⚙</span><span>◐</span><span>♪</span><span>✎</span><span>↻</span><span>⌄</span></span></div>
91
+ <div class="body">
92
+ <svg viewBox="0 0 200 300" aria-hidden="true">
93
+ <defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
94
+ <stop offset="0" stop-color="#46505f" /><stop offset="1" stop-color="#252b34" /></linearGradient></defs>
95
+ <ellipse cx="100" cy="286" rx="80" ry="26" fill="#14171d" />
96
+ <path d="M100 44c26 0 42 19 42 44 0 17-7 30-14 37l5 32c22 9 40 25 46 49l6 46H15l6-46c6-24 24-40 46-49l5-32c-7-7-14-20-14-37 0-25 16-44 42-44z" fill="url(#g)" />
97
+ <path d="M58 88c0-25 18-44 42-44s42 19 42 44c0 6-1 11-3 15-6-16-22-26-39-26s-33 10-39 26c-2-4-3-9-3-15z" fill="#556072" />
98
+ </svg>
99
+ <div class="choices"><span>要面</span><span>要汤</span><span>都来点</span></div>
100
+ <div class="bubble">
101
+ <div class="pic">配图</div>
102
+ 锅里还有,热的。
103
+ </div>
104
+ </div>
105
+ <div class="say"><div class="in focus">跟她说一句</div><div class="send">说</div></div>
106
+ <div class="grip"></div>
107
+ </div>
108
+
109
+ <div class="cap">收起</div>
110
+ <div class="card collapsed">
111
+ <div class="head"><span class="dot"></span><span class="name">夏铃</span>
112
+ <span class="btns"><span>⌃</span></span></div>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="col">
117
+ <div class="cap">⚙ 抽屉(模型 / 表情 / 设置 / 声音 / 人设)</div>
118
+ <div class="drawer">
119
+ <h2>模型</h2>
120
+ <div class="row cur"><span>夏铃</span><b>用这个</b><b>删</b></div>
121
+ <div class="row"><span>若妍</span><b>用这个</b><b>删</b></div>
122
+ <div class="exps"><span class="on">无</span><span>得意</span><span>害羞</span><span>无语</span><span>困</span></div>
123
+ <div class="settings">
124
+ <h3>这台机器上</h3>
125
+ <div class="opt"><span class="box on"></span>眼睛跟着鼠标</div>
126
+ <div class="opt"><span class="box on"></span>点她有反应</div>
127
+ <div class="opt"><span class="box on"></span>自己播动作</div>
128
+ <div class="opt"><span class="box on"></span>出声</div>
129
+ <div class="opt"><span class="box on"></span>跟着聊天念</div>
130
+ <h3>她那边</h3>
131
+ <div class="opt"><span class="box on"></span>没人理就自己开口</div>
132
+ <div class="opt"><span class="box on"></span>给选项让他挑</div>
133
+ <div class="opt"><span class="box on"></span>记住之前说的</div>
134
+ <h3>表情包(测试功能)</h3>
135
+ <div class="slider"><i></i><b></b></div>
136
+ <div class="lab">只使用自带</div>
137
+ <div class="note">自带那套是夏铃的 Q 版贴纸,二十张;dsh-meme 那套借用你本机已经装的图库。功能还在试验,可能会调。</div>
138
+ </div>
139
+ <div class="acts"><span>导入模型</span><span>关掉</span></div>
140
+ </div>
141
+ </div>
142
+
143
+ </body>
144
+ </html>
package/docs/ui.png ADDED
Binary file
package/lib/client.js CHANGED
@@ -10,30 +10,43 @@
10
10
  const BG_LABEL = { none: '透明', dark: '深色', light: '浅色', paper: '纸质' };
11
11
 
12
12
  const CSS = `
13
- .dsh-l2d-card{position:fixed;right:16px;bottom:16px;z-index:9000;
14
- border-radius:14px;overflow:hidden;border:1px solid #33373f;
15
- background:linear-gradient(180deg,#232830 0%,#171a20 100%);
16
- box-shadow:0 12px 40px rgba(0,0,0,.5);font:12px/1.5 "Microsoft YaHei",system-ui,sans-serif;
17
- display:flex;flex-direction:column}
18
- .dsh-l2d-card.collapsed{width:146px!important;height:36px!important}
19
- .dsh-l2d-head{display:flex;align-items:center;gap:6px;padding:8px 10px;color:#9aa3b0;font-size:11px;
20
- letter-spacing:.5px;border-bottom:1px solid #2c313a;user-select:none;flex:none;cursor:move}
21
- .dsh-l2d-dot{width:6px;height:6px;border-radius:50%;background:#6aa9ff;flex:none}
22
- .dsh-l2d-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
23
- .dsh-l2d-btns{margin-left:auto;display:flex;gap:6px;flex:none}
24
- .dsh-l2d-btns button{background:none;border:0;color:#7d858f;font:inherit;cursor:pointer;padding:0 2px;line-height:1}
25
- .dsh-l2d-btns button:hover{color:#e2e6ee}
13
+ .dsh-l2d-card{position:fixed;right:18px;bottom:18px;z-index:9000;border-radius:16px;overflow:hidden;
14
+ border:1px solid rgba(255,255,255,.07);
15
+ background:linear-gradient(180deg,rgba(36,41,50,.95) 0%,rgba(21,24,30,.97) 100%);
16
+ box-shadow:0 18px 48px rgba(0,0,0,.5);backdrop-filter:blur(10px);
17
+ font:12px/1.5 "Microsoft YaHei",system-ui,sans-serif;display:flex;flex-direction:column;
18
+ transition:box-shadow .2s ease}
19
+ .dsh-l2d-card:hover{box-shadow:0 22px 58px rgba(0,0,0,.58)}
20
+ .dsh-l2d-card.collapsed{width:150px!important;height:38px!important}
21
+ .dsh-l2d-head{display:flex;align-items:center;gap:7px;padding:9px 11px;color:#a8b1bf;font-size:11px;
22
+ letter-spacing:.4px;border-bottom:1px solid rgba(255,255,255,.06);user-select:none;flex:none;cursor:move;
23
+ background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,0))}
24
+ .dsh-l2d-dot{width:7px;height:7px;border-radius:50%;background:#6aa9ff;flex:none;
25
+ box-shadow:0 0 0 3px rgba(106,169,255,.16)}
26
+ .dsh-l2d-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d7dde7}
27
+ .dsh-l2d-btns{margin-left:auto;display:flex;gap:2px;flex:none}
28
+ .dsh-l2d-btns button{background:none;border:0;color:#7d858f;font:inherit;cursor:pointer;
29
+ width:22px;height:22px;border-radius:7px;padding:0;line-height:1;
30
+ display:flex;align-items:center;justify-content:center;
31
+ transition:background .15s ease,color .15s ease}
32
+ .dsh-l2d-btns button:hover{color:#eef2f8;background:rgba(255,255,255,.09)}
26
33
  .dsh-l2d-body{display:block;width:100%;flex:1;border:0;background:transparent}
27
- .dsh-l2d-say{display:flex;gap:6px;padding:6px 8px;border-top:1px solid #2c313a;flex:none}
28
- .dsh-l2d-say input{flex:1;min-width:0;background:#1e222a;border:1px solid #333842;border-radius:7px;
29
- color:#dfe4ec;font:inherit;font-size:11px;padding:5px 8px;outline:none}
30
- .dsh-l2d-say input:focus{border-color:#3f4a5c}
31
- .dsh-l2d-send{background:#2f3540;border:1px solid #3c424c;border-radius:7px;color:#cfd4dd;
32
- font:inherit;cursor:pointer;padding:0 10px;font-size:11px}
33
- .dsh-l2d-send:hover{background:#3a4049}
34
- .dsh-l2d-grip{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;
35
- background:linear-gradient(135deg,transparent 50%,#3c424c 50%,#3c424c 62%,transparent 62%);
36
- opacity:.9}
34
+ .dsh-l2d-say{display:flex;gap:7px;padding:8px 10px;border-top:1px solid rgba(255,255,255,.06);flex:none}
35
+ .dsh-l2d-say input{flex:1;min-width:0;background:rgba(255,255,255,.05);
36
+ border:1px solid rgba(255,255,255,.09);border-radius:10px;color:#e6ebf2;
37
+ font:inherit;font-size:11.5px;padding:7px 10px;outline:none;
38
+ transition:border-color .15s ease,background .15s ease}
39
+ .dsh-l2d-say input::placeholder{color:#6d7684}
40
+ .dsh-l2d-say input:focus{border-color:rgba(106,169,255,.55);background:rgba(255,255,255,.08)}
41
+ .dsh-l2d-send{background:linear-gradient(180deg,#33587f,#27405f);border:1px solid #3e6b9e;
42
+ border-radius:10px;color:#eaf2ff;font:inherit;cursor:pointer;padding:0 13px;font-size:11.5px;
43
+ transition:filter .15s ease}
44
+ .dsh-l2d-send:hover{filter:brightness(1.14)}
45
+ .dsh-l2d-send:active{filter:brightness(.96)}
46
+ .dsh-l2d-grip{position:absolute;right:1px;bottom:1px;width:18px;height:18px;cursor:nwse-resize;opacity:.5;
47
+ transition:opacity .15s ease;
48
+ background:repeating-linear-gradient(135deg,transparent 0 2px,rgba(255,255,255,.4) 2px 3px) no-repeat right bottom/10px 10px}
49
+ .dsh-l2d-grip:hover{opacity:1}
37
50
  `;
38
51
 
39
52
  function ensureCss() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "0.8.1",
3
+ "version": "1.0.0",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",