dsh-live2dwaifu 0.5.0 → 0.7.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
@@ -23,7 +23,7 @@
23
23
 
24
24
  **会出声**:一个字一声很短的电音,标点停一拍,句尾沉一下——不是朗读,是“哔哔”那种。标题栏的 **♪** 打开调音页,放个真模型在旁边,边调边听;每只模型各存一份音色,换人换声。音量拉到 0 就安静,只剩气泡和口型。
25
25
 
26
- **她也会甩图**:图库直接吃 `dsh-meme` 那套,说话时按情绪挑一张,贴在气泡里。
26
+ **她也会甩图(测试功能)**:⚙ 里有一根滑轴,四档——不使用 / 只使用自带 / 均开启 / 只用 dsh-meme。自带那套是夏铃自己的 Q 版贴纸,二十张,随插件分发,版权归本插件;dsh-meme 那套只是借用你本机已经装的图库。默认关着,功能还在试。
27
27
 
28
28
  **她还会问你要选项**:话说到了需要你挑的地方,气泡下面会冒出两三个按钮,点一个就等于你回了她。
29
29
 
@@ -125,6 +125,16 @@ frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
125
125
  **能放动作吗?**
126
126
  暂时不能编排 `.motion3.json`,表情是固定的七种。想要更多情绪,给它发 `mood`,等以后的版本。
127
127
 
128
+ ## 关于别人的插件
129
+
130
+ 她的图、她的耳朵,都是从你**已经装好的**插件里读的。这个包里不带任何第三方素材,也一行没改别人的代码:
131
+
132
+ - **表情图**:从 `dsh-meme` 的图库目录里挑一张,只在本机显示,不上传、不分发。没装 `dsh-meme` 就没有图,其它照常。
133
+ - **麦克风**:交给 `dsh-ears`,本插件不碰录音,只是把转写好的字送进对话。
134
+ - **对话**:走 DSH 自己的模型接口(`ctx.llm`),不经过任何人的会话,也不写你的聊天记录。
135
+
136
+ 不喜欢这些联动,⚙ 里都能单独关掉。
137
+
128
138
  ## 已知限制
129
139
 
130
140
  - 只支持 Cubism 3 / 4(表情只认 `.exp3.json`,不认 `.motion3.json` 里的表情轨)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "xialing-001",
3
+ "name": "夏铃",
4
+ "version": "1.0.0",
5
+ "license": "随本插件以 MIT 分发;图由 LFR 用 Seedream 生成",
6
+ "note": "Q 版三头身,白大褂 + 红高领毛衣 + 黑裤,淡灰虹膜,细黑框眼镜。",
7
+ "items": [
8
+ { "file": "deyi.jpg", "label": "得意", "moods": ["开心"] },
9
+ { "file": "shengqi.jpg", "label": "生气", "moods": ["生气"] },
10
+ { "file": "haixiu.jpg", "label": "害羞", "moods": ["害羞"] },
11
+ { "file": "kun.jpg", "label": "困", "moods": ["困"] },
12
+ { "file": "wuyu.jpg", "label": "无语", "moods": ["无语"] },
13
+ { "file": "jingya.jpg", "label": "惊讶", "moods": ["惊讶"] },
14
+ { "file": "kaixin.jpg", "label": "开心", "moods": ["开心"] },
15
+ { "file": "sikao.jpg", "label": "想事情", "moods": ["平静"] },
16
+ { "file": "xianqi.jpg", "label": "嫌弃", "moods": ["无语", "生气"] },
17
+ { "file": "diantou.jpg", "label": "点头", "moods": ["平静"] },
18
+ { "file": "zhaoshou.jpg", "label": "打招呼", "moods": ["开心"] },
19
+ { "file": "bunaifan.jpg", "label": "别烦我", "moods": ["生气"] },
20
+ { "file": "xin-ing.jpg", "label": "心动", "moods": ["害羞"] },
21
+ { "file": "xin-cui.jpg", "label": "手指抵唇", "moods": ["害羞"] },
22
+ { "file": "xin-peng.jpg", "label": "崩溃", "moods": ["惊讶"] },
23
+ { "file": "xin-tiao.jpg", "label": "挑衅", "moods": ["生气"] },
24
+ { "file": "xin-xiao.jpg", "label": "看好戏", "moods": ["开心"] },
25
+ { "file": "xin-daxiao.jpg", "label": "大笑", "moods": ["开心"] },
26
+ { "file": "xin-hei.jpg", "label": "暗笑", "moods": ["无语"] },
27
+ { "file": "xin-yao.jpg", "label": "馋", "moods": ["害羞"] }
28
+ ]
29
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,149 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>人设卡 — dsh-live2dwaifu</title>
6
+ <style>
7
+ :root { --bg:#12141a; --card:#171a20; --line:#2c313a; --txt:#dfe4ec; --dim:#7d858f; --acc:#6aa9ff; }
8
+ * { box-sizing: border-box; }
9
+ body { margin:0; min-height:100vh; background:radial-gradient(1100px 560px at 50% -10%, #1b1f27, var(--bg));
10
+ color:var(--txt); font:13px/1.6 "Microsoft YaHei", system-ui, sans-serif;
11
+ display:flex; align-items:flex-start; justify-content:center; gap:26px; padding:26px; flex-wrap:wrap; }
12
+ .left { display:flex; flex-direction:column; gap:10px; width:330px; }
13
+ .card { width:330px; height:440px; background:var(--card); border:1px solid var(--line); border-radius:14px;
14
+ overflow:hidden; box-shadow:0 18px 50px rgba(0,0,0,.45); }
15
+ .card iframe { display:block; width:100%; height:100%; border:0; }
16
+ .panel { width:460px; }
17
+ .panel h1 { font-size:15px; margin:0 0 3px; font-weight:600; }
18
+ .panel p.sub { margin:0 0 14px; color:var(--dim); font-size:12px; }
19
+ .group { border:1px solid var(--line); border-radius:12px; padding:12px; margin-bottom:12px; background:#15181e; }
20
+ .group h2 { font-size:11px; letter-spacing:.6px; color:var(--dim); margin:0 0 10px; font-weight:600; }
21
+ select { width:100%; padding:8px 10px; border-radius:9px; border:1px solid #3c424c; background:#181c22;
22
+ color:var(--txt); font:inherit; font-size:13px; }
23
+ textarea { width:100%; height:300px; resize:vertical; padding:10px 12px; border-radius:10px;
24
+ border:1px solid #3c424c; background:#181c22; color:var(--txt);
25
+ font:12px/1.75 "Microsoft YaHei", system-ui, sans-serif; outline:none; }
26
+ textarea:focus { border-color:#3f4a5c; }
27
+ .switch { display:flex; align-items:flex-start; gap:10px; padding:4px 0; cursor:pointer; }
28
+ .switch input { margin-top:3px; accent-color:var(--acc); }
29
+ .switch b { font-weight:600; font-size:12.5px; }
30
+ .switch em { display:block; font-style:normal; color:var(--dim); font-size:11.5px; line-height:1.6; }
31
+ .foot { display:flex; align-items:center; gap:10px; }
32
+ button.act { padding:9px 16px; border-radius:9px; border:1px solid #3c424c; background:#232830;
33
+ color:#dfe4ec; cursor:pointer; font:inherit; font-size:12px; }
34
+ button.act.primary { background:#27405f; border-color:#3e6b9e; color:#eaf2ff; }
35
+ button.act:hover { filter:brightness(1.12); }
36
+ .state { color:var(--dim); font-size:11px; margin-left:auto; }
37
+ .tip { color:#6f7783; font-size:11px; margin-top:10px; line-height:1.7; }
38
+ </style>
39
+ </head>
40
+ <body>
41
+
42
+ <div class="left">
43
+ <div class="card"><iframe id="live" src="/dsh-live2d/viewer?embed=1"></iframe></div>
44
+ <div class="tip">左边就是她本人。改完存一下,她下一句就按新的人设说。</div>
45
+ </div>
46
+
47
+ <div class="panel">
48
+ <h1>人设卡</h1>
49
+ <p class="sub">她是谁、怎么说话,全在这段字里。</p>
50
+
51
+ <div class="group">
52
+ <h2>配给哪个模型</h2>
53
+ <select id="models"></select>
54
+ </div>
55
+
56
+ <div class="group">
57
+ <h2>人设</h2>
58
+ <textarea id="persona" spellcheck="false"></textarea>
59
+ </div>
60
+
61
+ <div class="group">
62
+ <h2>世界隔离</h2>
63
+ <label class="switch">
64
+ <input type="checkbox" id="isolation" checked />
65
+ <span>
66
+ <b>让她待在自己的世界里</b>
67
+ <em>打开后,她不知道什么是程序、界面、插件、模型。问起来就当你在她身边,不提“系统”“设置”“网页”。关掉她就会老老实实承认自己是插件里的角色。</em>
68
+ </span>
69
+ </label>
70
+ </div>
71
+
72
+ <div class="foot">
73
+ <button class="act primary" id="save">存</button>
74
+ <button class="act" id="reload">撤销改动</button>
75
+ <span class="state" id="state"></span>
76
+ </div>
77
+ <div class="tip">存在 <code>~/.dsh/dsh-live2d/models/&lt;id&gt;/meta.json</code> 的 <code>persona</code> 字段里,想手改也行。</div>
78
+ </div>
79
+
80
+ <script>
81
+ (function () {
82
+ var frame = document.getElementById('live');
83
+ var sel = document.getElementById('models');
84
+ var area = document.getElementById('persona');
85
+ var iso = document.getElementById('isolation');
86
+ var state = document.getElementById('state');
87
+ var modelId = '';
88
+
89
+ function $(id) { return document.getElementById(id); }
90
+
91
+ function load() {
92
+ if (!modelId) return;
93
+ state.textContent = '读…';
94
+ fetch('/dsh-live2d/api/persona?id=' + encodeURIComponent(modelId))
95
+ .then(function (r) { return r.json(); })
96
+ .then(function (d) {
97
+ area.value = d.persona || '';
98
+ iso.checked = d.isolation !== false;
99
+ state.textContent = d.custom ? '这份是手写的' : '还是默认那份';
100
+ })
101
+ .catch(function (e) { state.textContent = '读不了:' + e.message; });
102
+ }
103
+
104
+ function save() {
105
+ if (!modelId) return;
106
+ state.textContent = '存…';
107
+ fetch('/dsh-live2d/api/persona', {
108
+ method: 'POST',
109
+ headers: { 'content-type': 'application/json' },
110
+ body: JSON.stringify({ id: modelId, persona: area.value, isolation: iso.checked })
111
+ })
112
+ .then(function (r) { return r.json(); })
113
+ .then(function (d) {
114
+ state.textContent = d.ok ? '存好了' : '没存上:' + (d.error || '?');
115
+ if (d.ok) {
116
+ frame.src = '/dsh-live2d/viewer?embed=1&t=' + Date.now();
117
+ }
118
+ })
119
+ .catch(function (e) { state.textContent = '没存上:' + e.message; });
120
+ }
121
+
122
+ $('save').addEventListener('click', save);
123
+ $('reload').addEventListener('click', load);
124
+ sel.addEventListener('change', function () {
125
+ modelId = sel.value;
126
+ load();
127
+ });
128
+
129
+ fetch('/dsh-live2d/api/models')
130
+ .then(function (r) { return r.json(); })
131
+ .then(function (d) {
132
+ var list = d.models || [];
133
+ sel.innerHTML = '';
134
+ list.forEach(function (m) {
135
+ var o = document.createElement('option');
136
+ o.value = m.id;
137
+ o.textContent = (m.name || m.id) + (m.id === d.selected ? '(正在显示)' : '');
138
+ sel.appendChild(o);
139
+ });
140
+ modelId = d.selected || (list[0] && list[0].id) || '';
141
+ if (modelId) sel.value = modelId;
142
+ if (!list.length) { state.textContent = '还没有模型'; return; }
143
+ load();
144
+ })
145
+ .catch(function (e) { state.textContent = '读不了模型表:' + e.message; });
146
+ })();
147
+ </script>
148
+ </body>
149
+ </html>
@@ -42,6 +42,10 @@
42
42
  #settings h3 { font-size: 10px; letter-spacing: .6px; color: #6f7783; font-weight: 600; margin: 8px 0 4px; }
43
43
  .opt { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: 12px; color: #c3c9d3; }
44
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; }
45
49
  .bub-img { display: block; max-width: 100%; max-height: 120px; border-radius: 8px; margin-bottom: 6px; object-fit: contain; }
46
50
  #actions { display: flex; gap: 6px; }
47
51
  #actions button { flex: 1; background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
package/assets/viewer.js CHANGED
@@ -44346,10 +44346,11 @@ void main(void)\r
44346
44346
  ];
44347
44347
  var SERVER_OPTS = [
44348
44348
  ["proactive", "\u6CA1\u4EBA\u7406\u5C31\u81EA\u5DF1\u5F00\u53E3"],
44349
- ["memes", "\u4F1A\u7529\u8868\u60C5\u5305"],
44350
44349
  ["choices", "\u7ED9\u9009\u9879\u8BA9\u4ED6\u6311"],
44351
44350
  ["memory", "\u8BB0\u4F4F\u4E4B\u524D\u8BF4\u7684"]
44352
44351
  ];
44352
+ var MEME_MODES = ["off", "own", "both", "external"];
44353
+ var MEME_LABELS = ["\u4E0D\u4F7F\u7528", "\u53EA\u4F7F\u7528\u81EA\u5E26", "\u5747\u5F00\u542F", "\u53EA\u7528 dsh-meme"];
44353
44354
  var serverSettings = {};
44354
44355
  function renderSettings() {
44355
44356
  const box = $2("settings");
@@ -44391,6 +44392,41 @@ void main(void)\r
44391
44392
  row.appendChild(document.createTextNode(label));
44392
44393
  box.appendChild(row);
44393
44394
  }
44395
+ const h3 = document.createElement("h3");
44396
+ h3.textContent = "\u8868\u60C5\u5305\uFF08\u6D4B\u8BD5\u529F\u80FD\uFF09";
44397
+ box.appendChild(h3);
44398
+ const wrap = document.createElement("div");
44399
+ wrap.className = "opt-col";
44400
+ const slider = document.createElement("input");
44401
+ slider.type = "range";
44402
+ slider.min = "0";
44403
+ slider.max = String(MEME_MODES.length - 1);
44404
+ slider.step = "1";
44405
+ const cur = Math.max(0, MEME_MODES.indexOf(serverSettings.memesMode || "off"));
44406
+ slider.value = String(cur);
44407
+ const lab = document.createElement("div");
44408
+ lab.className = "opt-lab";
44409
+ lab.textContent = MEME_LABELS[cur];
44410
+ slider.oninput = () => {
44411
+ lab.textContent = MEME_LABELS[Number(slider.value)];
44412
+ };
44413
+ slider.onchange = () => {
44414
+ const mode = MEME_MODES[Number(slider.value)];
44415
+ serverSettings.memesMode = mode;
44416
+ fetch(BASE + "/api/settings", {
44417
+ method: "POST",
44418
+ headers: { "content-type": "application/json" },
44419
+ body: JSON.stringify({ settings: { memesMode: mode } })
44420
+ }).catch(() => {
44421
+ });
44422
+ };
44423
+ const note = document.createElement("div");
44424
+ note.className = "opt-note";
44425
+ note.textContent = "\u81EA\u5E26\u90A3\u5957\u662F\u590F\u94C3\u7684 Q \u7248\u8D34\u7EB8\uFF0C\u4E8C\u5341\u5F20\uFF1Bdsh-meme \u90A3\u5957\u501F\u7528\u4F60\u672C\u673A\u5DF2\u7ECF\u88C5\u7684\u56FE\u5E93\u3002\u529F\u80FD\u8FD8\u5728\u8BD5\u9A8C\uFF0C\u53EF\u80FD\u4F1A\u8C03\u3002";
44426
+ wrap.appendChild(slider);
44427
+ wrap.appendChild(lab);
44428
+ wrap.appendChild(note);
44429
+ box.appendChild(wrap);
44394
44430
  }
44395
44431
  function loadSettings() {
44396
44432
  fetch(BASE + "/api/settings").then((r2) => r2.json()).then((d2) => {
package/lib/client.js CHANGED
@@ -177,6 +177,7 @@
177
177
 
178
178
  const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
179
179
  const openTone = () => window.open(BASE + '/tone', '_blank', 'width=1140,height=780');
180
+ const openPersona = () => window.open(BASE + '/persona', '_blank', 'width=1080,height=740');
180
181
 
181
182
  const send = () => {
182
183
  const t = draft.trim();
@@ -222,6 +223,7 @@
222
223
  open && h('button', { title: '模型管理', onPointerDown: noDrag, onClick: manage }, '⚙'),
223
224
  open && h('button', { title: '背景:' + BG_LABEL[bg], onPointerDown: noDrag, onClick: cycleBg }, '◐'),
224
225
  open && h('button', { title: '音色', onPointerDown: noDrag, onClick: openTone }, '♪'),
226
+ open && h('button', { title: '人设卡', onPointerDown: noDrag, onClick: openPersona }, '✎'),
225
227
  open && h('button', { title: '刷新', onPointerDown: noDrag, onClick: () => setNonce((n) => n + 1) }, '↻'),
226
228
  h('button', { title: open ? '收起' : '展开', onPointerDown: noDrag, onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
227
229
  ),
package/lib/index.js CHANGED
@@ -25,7 +25,8 @@ const MEME_TAGS = {
25
25
 
26
26
  const DEFAULT_SETTINGS = {
27
27
  proactive: true,
28
- memes: true,
28
+ /** off | own | both | external —— 测试功能,默认关 */
29
+ memesMode: 'off',
29
30
  choices: true,
30
31
  memory: true,
31
32
  follow: true,
@@ -35,6 +36,45 @@ const DEFAULT_SETTINGS = {
35
36
  sound: true,
36
37
  };
37
38
 
39
+ const MEMES_MODES = ['off', 'own', 'both', 'external'];
40
+ const OWN_MEME_DIR = join(ASSETS, 'memes');
41
+
42
+ function readOwnMemes() {
43
+ try {
44
+ const m = JSON.parse(readFileSync(join(OWN_MEME_DIR, 'manifest.json'), 'utf8'));
45
+ return Array.isArray(m.items) ? m.items : [];
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ /** 从自带那套里按情绪挑一张,返回文件名 */
52
+ function pickOwnMeme(mood) {
53
+ const items = readOwnMemes();
54
+ if (!items.length) return null;
55
+ const hit = items.filter((it) => Array.isArray(it.moods) && it.moods.includes(mood));
56
+ const pool = hit.length ? hit : items;
57
+ const it = pool[Math.floor(Math.random() * pool.length)];
58
+ return it && it.file ? it.file : null;
59
+ }
60
+
61
+ /** 按模式挑一张配图,返回可直接用的 URL */
62
+ function pickImage(mode, mood) {
63
+ const m = MEMES_MODES.includes(mode) ? mode : 'off';
64
+ if (m === 'off') return null;
65
+ const pool = [];
66
+ if (m === 'own' || m === 'both') {
67
+ const f = pickOwnMeme(mood);
68
+ if (f) pool.push('/dsh-live2d/ownmeme/' + encodeURIComponent(f));
69
+ }
70
+ if (m === 'external' || m === 'both') {
71
+ const f = pickMeme(mood);
72
+ if (f) pool.push('/dsh-live2d/media/' + f.split('/').map(encodeURIComponent).join('/'));
73
+ }
74
+ if (!pool.length) return null;
75
+ return pool[Math.floor(Math.random() * pool.length)];
76
+ }
77
+
38
78
  function settingsOf(cfg) {
39
79
  return { ...DEFAULT_SETTINGS, ...(cfg?.settings || {}) };
40
80
  }
@@ -100,6 +140,23 @@ const DEFAULT_PERSONA = [
100
140
  '不解释心情,不总结,不升华,不写括号里的动作。',
101
141
  ].join('\n');
102
142
 
143
+ /** 没说清要让你挑什么的时候,按情绪给两个短答 */
144
+ const FALLBACK_CHOICES = {
145
+ 开心: ['好', '嗯嗯'],
146
+ 生气: ['……', '别气'],
147
+ 委屈: ['我在', '怎么了'],
148
+ 害羞: ['唔', '别看'],
149
+ 无语: ['行吧', '你说'],
150
+ 困: ['睡吧', '嗯'],
151
+ 平静: ['嗯', '在的'],
152
+ };
153
+
154
+ const ISOLATION = [
155
+ '【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
156
+ '有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
157
+ '你不知道他那边什么样,得他讲给你听。',
158
+ ].join('\n');
159
+
103
160
  function personaOf(meta) {
104
161
  const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
105
162
  return p || DEFAULT_PERSONA;
@@ -366,6 +423,12 @@ async function handle(req, res, ctx) {
366
423
  return file ? sendFile(res, file) : send(res, 403, 'forbidden');
367
424
  }
368
425
 
426
+ // 自带的 Q 版表情包
427
+ if (path.startsWith('/ownmeme/')) {
428
+ const file = safeJoin(OWN_MEME_DIR, decodeURIComponent(path.slice('/ownmeme/'.length)));
429
+ return file ? sendFile(res, file) : send(res, 403, 'forbidden');
430
+ }
431
+
369
432
  // 表情图库
370
433
  if (path.startsWith('/media/')) {
371
434
  const file = safeJoin(MEMES_ROOT, decodeURIComponent(path.slice('/media/'.length)));
@@ -403,6 +466,7 @@ async function handle(req, res, ctx) {
403
466
  .join('\n');
404
467
  const prompt = [
405
468
  personaOf(meta),
469
+ meta.isolation === false ? '' : ISOLATION,
406
470
  '现在是 ' + nowText() + '。',
407
471
  lines ? '你们之前说的:\n' + lines : '',
408
472
  '他说:' + text,
@@ -421,10 +485,14 @@ async function handle(req, res, ctx) {
421
485
  LAST_ACTIVE.at = Date.now();
422
486
  const parsed = splitChoices(reply || '');
423
487
  let line = parsed.text;
424
- const choices = st.choices ? parsed.choices : [];
488
+ let choices = st.choices ? parsed.choices : [];
425
489
  if (line.length > 240) line = line.slice(0, 240);
490
+ if (st.choices && !choices.length && line) {
491
+ choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
492
+ }
426
493
  if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
427
- const image = st.memes && guessMood(line) && Math.random() < 0.35 ? pickMeme(guessMood(line)) : null;
494
+ const moodNow = guessMood(line);
495
+ const image = st.memesMode !== 'off' && Math.random() < 0.35 ? pickImage(st.memesMode, moodNow) : null;
428
496
 
429
497
  writeChat(id, hist.concat([
430
498
  { role: 'user', text, at: Date.now() },
@@ -437,7 +505,12 @@ async function handle(req, res, ctx) {
437
505
  if (path === '/api/persona' && method === 'GET') {
438
506
  const id = String(url.searchParams.get('id') ?? '');
439
507
  const meta = id ? readMeta(id) : null;
440
- return sendJson(res, 200, { id, persona: personaOf(meta), custom: typeof meta?.persona === 'string' && !!meta.persona.trim() });
508
+ return sendJson(res, 200, {
509
+ id,
510
+ persona: personaOf(meta),
511
+ custom: typeof meta?.persona === 'string' && !!meta.persona.trim(),
512
+ isolation: meta?.isolation !== false,
513
+ });
441
514
  }
442
515
 
443
516
  if (path === '/api/persona' && method === 'POST') {
@@ -445,9 +518,10 @@ async function handle(req, res, ctx) {
445
518
  const id = String(body.id ?? '');
446
519
  const meta = id ? readMeta(id) : null;
447
520
  if (!meta) return sendJson(res, 404, { error: 'model not found' });
448
- const persona = typeof body.persona === 'string' ? body.persona : '';
449
- writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona }, null, 2), 'utf8');
450
- return sendJson(res, 200, { ok: true, persona: personaOf({ persona }) });
521
+ const persona = typeof body.persona === 'string' ? body.persona : meta.persona;
522
+ const isolation = typeof body.isolation === 'boolean' ? body.isolation : meta.isolation !== false;
523
+ writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona, isolation }, null, 2), 'utf8');
524
+ return sendJson(res, 200, { ok: true, persona: personaOf({ persona }), isolation });
451
525
  }
452
526
 
453
527
  // 开关:哪些功能开着
@@ -464,6 +538,9 @@ async function handle(req, res, ctx) {
464
538
  for (const k of Object.keys(DEFAULT_SETTINGS)) {
465
539
  if (typeof patch?.[k] === 'boolean') next[k] = patch[k];
466
540
  }
541
+ if (typeof patch?.memesMode === 'string' && MEMES_MODES.includes(patch.memesMode)) {
542
+ next.memesMode = patch.memesMode;
543
+ }
467
544
  writeConfig({ ...cfg, settings: next });
468
545
  return sendJson(res, 200, { ok: true, settings: next });
469
546
  }
@@ -490,6 +567,11 @@ async function handle(req, res, ctx) {
490
567
  return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
491
568
  }
492
569
 
570
+ // 人设卡页
571
+ if (path === '/persona' || path === '/persona.html') {
572
+ return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
573
+ }
574
+
493
575
  // 音色调音页
494
576
  if (path === '/tone' || path === '/tone.html') {
495
577
  return sendFile(res, join(ASSETS, 'tone.html'), MIME['.html']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",