dsh-live2dwaifu 1.3.0 → 2.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
@@ -380,6 +380,29 @@ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
380
380
 
381
381
  版本号怎么走:动了功能和结构就抬第二位(1.2.0),只补洞和小修就抬第三位(1.2.1)。
382
382
 
383
+ ### 2.0.0
384
+
385
+ - 修:`ISOLATION` 这个变量在 1.3.0 被误删,`/api/chat` 全部返回 500——那之后她其实一句都说不出来。这是这版最主要的问题。
386
+ - 修:补回 1.3.1 里漏掉的语料接口(之前保存语料会 404)。
387
+ - 修:调音页的下拉框改成只试听,不再顺手把正在显示的模型换掉;要换模型另点「切到这只模型」。
388
+ - 修:卡片位置存在本机,窗口缩小后可能跑到屏幕外,现在重开时自动收回可见范围。
389
+ - 修:页面切到后台时暂停轮询,不再空跑请求。
390
+ - 修:所有接受模型 id 的接口加了字符校验,避免拼出奇怪路径。
391
+ - 改:对话要求补一条——不要每句都反问,不要用「哈哈」「嗯嗯」凑字数。
392
+ - 清:删掉几个没用到的常量、开关和恒真判断。
393
+
394
+ <details>
395
+ <summary>2.0.0 之前的记录(黑历史,留着自嘲)</summary>
396
+
397
+ ### 1.3.1
398
+
399
+ - 修:语料接口整个漏掉了(页面打得开,存不进去,返回 404)。补回来。
400
+ - 修:关掉「记住聊过的内容」之后不写聊天记录了,之前还在偷偷存。
401
+ - 修:语料里的问答样例只在有当前这句话时挑,主动开口不再乱塞无关问答。
402
+ - 修:主动说话间隔滑轴遇到旧配置值(比如 12 分钟)会显示成「关闭」,现在找最接近的档位。
403
+ - 修:语料接口的模型 id 只接受安全字符。
404
+ - 加:按 Esc 关掉覆盖层。
405
+
383
406
  ### 1.3.0
384
407
 
385
408
  - 主动说话的间隔可以调了:⚙ 里一根滑轴,关闭 / 30 分钟 / 1 小时 / 2 小时 / 4 小时。默认从原来的十几分钟放宽到 30 分钟。
@@ -448,6 +471,8 @@ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
448
471
  - 人设卡编辑页、音色调色页。
449
472
  - 对外接口:`/api/chat`、`/api/push`、`/api/poll`。
450
473
 
474
+ </details>
475
+
451
476
  ## 反馈
452
477
 
453
478
  遇到问题、想要新功能、觉得哪句话还是像 AI,都可以写信到 **FujiwaraReisen@outlook.com**。
@@ -147,11 +147,10 @@
147
147
  var said = unquote(m[2]);
148
148
  if (!said) continue;
149
149
  if (USERS.test(who)) { pending = said; continue; }
150
- if (HERS.test(who) || true) {
151
- pairs.push({ user: pending || '', assistant: said });
152
- pending = null;
153
- continue;
154
- }
150
+ // 不是「他/我/用户」的说话人,一律当角色
151
+ pairs.push({ user: pending || '', assistant: said });
152
+ pending = null;
153
+ continue;
155
154
  }
156
155
  var t = unquote(line);
157
156
  if (t) lines.push(t);
@@ -71,6 +71,7 @@
71
71
  // 主动开口也收着
72
72
  var seq = null;
73
73
  setInterval(function () {
74
+ if (document.hidden) return;
74
75
  fetch('/dsh-live2d/api/poll?since=' + (seq === null ? 0 : seq))
75
76
  .then(function (r) { return r.json(); })
76
77
  .then(function (d) {
package/assets/tone.html CHANGED
@@ -91,6 +91,7 @@
91
91
 
92
92
  <div class="foot">
93
93
  <button class="act primary" id="save">保存</button>
94
+ <button class="act" id="useModel">切到这只模型</button>
94
95
  <button class="act" id="reset">恢复默认</button>
95
96
  <span class="state" id="state"></span>
96
97
  </div>
@@ -205,21 +206,27 @@
205
206
 
206
207
  sel.addEventListener('change', function () {
207
208
  modelId = sel.value;
208
- loadVoice(true);
209
+ loadVoice();
209
210
  });
210
211
 
211
- function loadVoice(alsoSwitch) {
212
+ $('useModel').addEventListener('click', function () {
212
213
  if (!modelId) return;
213
- state.textContent = '读取中';
214
- if (alsoSwitch) {
215
- fetch('/dsh-live2d/api/select', {
216
- method: 'POST',
217
- headers: { 'content-type': 'application/json' },
218
- body: JSON.stringify({ id: modelId })
219
- }).then(function () {
214
+ state.textContent = '切换中';
215
+ fetch('/dsh-live2d/api/select', {
216
+ method: 'POST',
217
+ headers: { 'content-type': 'application/json' },
218
+ body: JSON.stringify({ id: modelId })
219
+ })
220
+ .then(function () {
220
221
  frame.src = '/dsh-live2d/viewer?embed=1&t=' + Date.now();
221
- });
222
- }
222
+ state.textContent = '已切换';
223
+ })
224
+ .catch(function (e) { state.textContent = '切换失败:' + e.message; });
225
+ });
226
+
227
+ function loadVoice() {
228
+ if (!modelId) return;
229
+ state.textContent = '读取中';
223
230
  fetch('/dsh-live2d/api/voice?id=' + encodeURIComponent(modelId))
224
231
  .then(function (r) { return r.json(); })
225
232
  .then(function (d) {
@@ -249,7 +256,7 @@
249
256
  state.textContent = '尚未导入模型';
250
257
  return;
251
258
  }
252
- loadVoice(false);
259
+ loadVoice();
253
260
  })
254
261
  .catch(function (e) { state.textContent = '读取模型列表失败:' + e.message; });
255
262
 
package/assets/viewer.js CHANGED
@@ -44358,7 +44358,6 @@ void main(void)\r
44358
44358
  follow: "\u804A\u5929\u7A97\u53E3\u51FA\u73B0\u65B0\u56DE\u590D\u65F6\uFF0C\u7531\u89D2\u8272\u5FF5\u51FA\u6765"
44359
44359
  };
44360
44360
  var SERVER_HINT = {
44361
- proactive: "\u4E00\u6BB5\u65F6\u95F4\u6CA1\u4EBA\u8BF4\u8BDD\uFF0C\u89D2\u8272\u81EA\u5DF1\u5F00\u4E00\u53E5",
44362
44361
  choices: "\u89D2\u8272\u89C9\u5F97\u9700\u8981\u9009\u62E9\u65F6\uFF0C\u6C14\u6CE1\u4E0A\u65B9\u51FA\u73B0\u53EF\u70B9\u7684\u6309\u94AE",
44363
44362
  memory: "\u4FDD\u7559\u804A\u8FC7\u7684\u5185\u5BB9\uFF0C\u4E0B\u6B21\u63A5\u7740\u804A\uFF08\u5360\u66F4\u591A\u6A21\u578B\u989D\u5EA6\uFF09"
44364
44363
  };
@@ -44417,10 +44416,19 @@ void main(void)\r
44417
44416
  gapSlider.min = "0";
44418
44417
  gapSlider.max = String(GAP_VALUES.length - 1);
44419
44418
  gapSlider.step = "1";
44420
- const curGap = Math.max(
44421
- 0,
44422
- GAP_VALUES.indexOf(Number(serverSettings.proactiveInterval ?? 30))
44423
- );
44419
+ const curGap = (() => {
44420
+ const v2 = Number(serverSettings.proactiveInterval ?? 30);
44421
+ let best = 0;
44422
+ let bestDiff = Infinity;
44423
+ GAP_VALUES.forEach((g2, i2) => {
44424
+ const diff = Math.abs(g2 - v2);
44425
+ if (diff < bestDiff) {
44426
+ bestDiff = diff;
44427
+ best = i2;
44428
+ }
44429
+ });
44430
+ return best;
44431
+ })();
44424
44432
  gapSlider.value = String(curGap);
44425
44433
  const gapLab = document.createElement("div");
44426
44434
  gapLab.className = "opt-lab";
package/lib/client.js CHANGED
@@ -106,6 +106,14 @@
106
106
  window.addEventListener('message', onMsg);
107
107
  return () => window.removeEventListener('message', onMsg);
108
108
  }, []);
109
+ react.useEffect(() => {
110
+ if (!sheet) return undefined;
111
+ const onKey = (ev) => {
112
+ if (ev.key === 'Escape') setSheet(null);
113
+ };
114
+ window.addEventListener('keydown', onKey);
115
+ return () => window.removeEventListener('keydown', onKey);
116
+ }, [sheet]);
109
117
  const [nonce, setNonce] = react.useState(0);
110
118
  const ref = react.useRef(null);
111
119
 
@@ -113,6 +121,16 @@
113
121
  react.useEffect(() => localStorage.setItem('dsh-l2d-bg', bg), [bg]);
114
122
  react.useEffect(() => localStorage.setItem('dsh-l2d-size', JSON.stringify(size)), [size]);
115
123
  react.useEffect(() => localStorage.setItem('dsh-l2d-pos', JSON.stringify(pos)), [pos]);
124
+ react.useEffect(() => {
125
+ setPos((p) => {
126
+ const w = size.w;
127
+ const h = open ? size.h : 38;
128
+ const x = Math.max(-w + 90, Math.min(window.innerWidth - 90, p.x));
129
+ const y = Math.max(-h + 50, Math.min(window.innerHeight - 50, p.y));
130
+ return x === p.x && y === p.y ? p : { x, y };
131
+ });
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, []);
116
134
  react.useEffect(() => {
117
135
  frameWin = (ref.current && ref.current.contentWindow) || null;
118
136
  }, [nonce, open]);
@@ -120,7 +138,7 @@
120
138
  let seq = null;
121
139
  let stop = false;
122
140
  const tick = () => {
123
- if (stop) return;
141
+ if (stop || document.hidden) return;
124
142
  fetch(BASE + '/api/poll?since=' + (seq === null ? 0 : seq))
125
143
  .then((r) => r.json())
126
144
  .then((d) => {
package/lib/index.js CHANGED
@@ -24,7 +24,6 @@ const MEME_TAGS = {
24
24
  };
25
25
 
26
26
  const DEFAULT_SETTINGS = {
27
- proactive: true,
28
27
  /** 多久没人说话才自己开口,单位分钟;0 = 关闭 */
29
28
  proactiveInterval: 30,
30
29
  /** off | own | both | external —— 测试功能,默认关 */
@@ -175,6 +174,13 @@ const TALK_SAMPLES = [
175
174
  '他:明天要早起 —— 你:那我今晚早点关灯。',
176
175
  ].join('\n');
177
176
 
177
+ /** 世界隔离:不说破自己在哪儿 */
178
+ const ISOLATION = [
179
+ '【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
180
+ '有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
181
+ '你不知道他那边什么样,得他讲给你听。',
182
+ ].join('\n');
183
+
178
184
  /** 语料:lines = 她的台词,pairs = 一问一答 */
179
185
  function readCorpus(id) {
180
186
  try {
@@ -206,17 +212,20 @@ function writeCorpus(id, corpus) {
206
212
  /** 从语料里挑几条最贴当前对话的,拼成样例 */
207
213
  function pickCorpusSamples(corpus, text, toneN = 2) {
208
214
  const out = [];
209
- const chars = new Set(String(text || ''));
210
- const scored = (corpus.pairs || [])
211
- .map((p) => {
212
- let hit = 0;
213
- for (const ch of String(p.user || '')) if (chars.has(ch)) hit += 1;
214
- return { p, hit };
215
- })
216
- .sort((a, b) => b.hit - a.hit);
217
- for (const s of scored.slice(0, 2)) {
218
- if (s.p.user) out.push('他:' + s.p.user + ' —— 你:' + s.p.assistant);
219
- else out.push('你:' + s.p.assistant);
215
+ const query = String(text || '').trim();
216
+ if (query) {
217
+ const chars = new Set(query);
218
+ const scored = (corpus.pairs || [])
219
+ .map((p) => {
220
+ let hit = 0;
221
+ for (const ch of String(p.user || '')) if (chars.has(ch)) hit += 1;
222
+ return { p, hit };
223
+ })
224
+ .sort((a, b) => b.hit - a.hit);
225
+ for (const s of scored.slice(0, 2)) {
226
+ if (s.p.user) out.push('他:' + s.p.user + ' —— 你:' + s.p.assistant);
227
+ else out.push('你:' + s.p.assistant);
228
+ }
220
229
  }
221
230
  const pool = (corpus.lines || []).slice();
222
231
  for (let i = 0; i < toneN && pool.length; i += 1) {
@@ -318,7 +327,7 @@ function pickFacts(facts, text) {
318
327
  });
319
328
  scored.sort((a, b) => b.hit - a.hit);
320
329
  const top = scored.filter((s) => s.hit > 0).slice(0, 6).map((s) => s.f);
321
- return top.length ? top : facts.slice(-6);
330
+ return top;
322
331
  }
323
332
 
324
333
  /** 去 AI 味:删套话、砍尾巴、压重复 */
@@ -332,7 +341,8 @@ const AI_WORDS = [
332
341
  ];
333
342
 
334
343
  function humanize(input) {
335
- let t = String(input || '');
344
+ const original = String(input || '');
345
+ let t = original;
336
346
  for (const re of AI_WORDS) t = t.replace(re, '');
337
347
  t = t
338
348
  .replace(/[ \t]+/g, ' ')
@@ -346,7 +356,7 @@ function humanize(input) {
346
356
  const cut = t.match(/^[\s\S]{0,60}?[。!?!?]/);
347
357
  t = cut ? cut[0] : t.slice(0, 60);
348
358
  }
349
- return t.trim();
359
+ return t.trim() || original.trim();
350
360
  }
351
361
 
352
362
  /* ── 出口队列:谁能说话,都往这儿扔 ───────────────────────── */
@@ -400,7 +410,6 @@ async function generate(slot, prompt, limit = 900) {
400
410
  }
401
411
 
402
412
  /* ── 主动开口:没人说话,她自己开一句 ─────────────────────── */
403
- const IDLE_MS = 12 * 60 * 1000;
404
413
  let lastAutoAt = 0;
405
414
 
406
415
  async function maybeTalk(ctx) {
@@ -519,7 +528,12 @@ function findModel3(dir) {
519
528
  return out[0];
520
529
  }
521
530
 
531
+ function validId(id) {
532
+ return /^[\w.-]+$/.test(String(id || ''));
533
+ }
534
+
522
535
  function readMeta(id) {
536
+ if (!validId(id)) return null;
523
537
  try {
524
538
  return JSON.parse(readFileSync(join(MODELS, id, 'meta.json'), 'utf8'));
525
539
  } catch {
@@ -643,7 +657,7 @@ async function handle(req, res, ctx) {
643
657
  } catch {}
644
658
  }
645
659
  const facts = st.memory ? pickFacts(store.facts, text) : [];
646
- const samples = st.memory ? pickCorpusSamples(readCorpus(id), text) : [];
660
+ const samples = pickCorpusSamples(readCorpus(id), text);
647
661
  const hist = store.messages;
648
662
  const lines = hist
649
663
  .slice(-8)
@@ -698,10 +712,17 @@ async function handle(req, res, ctx) {
698
712
  ? pickImage(st.memesMode, moodNow)
699
713
  : null;
700
714
 
701
- writeChat(id, hist.concat([
702
- { role: 'user', text, at: Date.now() },
703
- { role: 'assistant', text: line, at: Date.now() },
704
- ]), store.summary, store.facts);
715
+ if (st.memory) {
716
+ writeChat(
717
+ id,
718
+ hist.concat([
719
+ { role: 'user', text, at: Date.now() },
720
+ { role: 'assistant', text: line, at: Date.now() },
721
+ ]),
722
+ store.summary,
723
+ store.facts,
724
+ );
725
+ }
705
726
  return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
706
727
  }
707
728
 
@@ -795,6 +816,30 @@ async function handle(req, res, ctx) {
795
816
  }
796
817
 
797
818
  // API
819
+
820
+ // 语料
821
+ if (path === '/api/corpus' && method === 'GET') {
822
+ const id = String(url.searchParams.get('id') ?? '');
823
+ if (id && !/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
824
+ const c = id ? readCorpus(id) : { lines: [], pairs: [] };
825
+ return sendJson(res, 200, { id, lines: c.lines, pairs: c.pairs, total: c.lines.length + c.pairs.length });
826
+ }
827
+
828
+ if (path === '/api/corpus' && method === 'POST') {
829
+ const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
830
+ const id = String(body.id ?? '');
831
+ if (!/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
832
+ const meta = readMeta(id);
833
+ if (!meta) return sendJson(res, 404, { error: 'model not found' });
834
+ const corpus = {
835
+ lines: Array.isArray(body.lines) ? body.lines.filter((x) => typeof x === 'string' && x.trim()) : [],
836
+ pairs: Array.isArray(body.pairs)
837
+ ? body.pairs.filter((p) => p && typeof p.assistant === 'string' && p.assistant.trim())
838
+ : [],
839
+ };
840
+ writeCorpus(id, corpus);
841
+ return sendJson(res, 200, { ok: true, total: corpus.lines.length + corpus.pairs.length });
842
+ }
798
843
  if (path === '/api/voice' && method === 'GET') {
799
844
  const id = String(url.searchParams.get('id') ?? '');
800
845
  const meta = id ? readMeta(id) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",