dsh-live2dwaifu 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/lib/index.js +86 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,9 @@
33
33
 
34
34
  **也能直接跟她说**:卡片底下有个小框,打一句回车,她回你。这条线**不进你的聊天记录**——后台直接问模型,往来只存在 `models/<id>/chat.json` 里。她说的话按人设卡现场写,不是复读;人设卡就是 `models/<id>/meta.json` 里的 `persona` 字段,没写就用默认那份(若妍)。
35
35
 
36
- **她记得住**:聊够三十句,前面二十句会被压成一小段小结挂在记忆里,不会一过二十句就把你说过的话全忘掉。嫌费 token 就在 ⚙ 里把「记住之前说的」关掉。
36
+ **她记得住,而且分两层记**:一层是最近二十来句原文,一层是攒出来的「关于你的事实」——名字、作息、口味、忌讳、答应过的事,聊得越久越攒得住,不用你重新讲一遍。攒满三十句就把前面压成小结,不丢。
37
+
38
+ **她说的话会过一道筛子**:套话会被删掉——“好的”“我理解你的感受”“希望帮到你”“还有什么需要”这些,出来就没。最多留两三句,允许她只回两个字。
37
39
 
38
40
  **她知道几点、也知道自己此刻在干什么**:提示词里会让她按人设推断这个时间点的处境——早上在实验室、半夜还没睡——话里自然带着那个味道,不用你提醒。
39
41
 
package/lib/index.js CHANGED
@@ -191,9 +191,10 @@ function readChatStore(id) {
191
191
  return {
192
192
  messages: Array.isArray(d.messages) ? d.messages : [],
193
193
  summary: typeof d.summary === 'string' ? d.summary : '',
194
+ facts: Array.isArray(d.facts) ? d.facts : [],
194
195
  };
195
196
  } catch {
196
- return { messages: [], summary: '' };
197
+ return { messages: [], summary: '', facts: [] };
197
198
  }
198
199
  }
199
200
 
@@ -201,33 +202,99 @@ function readChat(id) {
201
202
  return readChatStore(id).messages;
202
203
  }
203
204
 
204
- function writeChat(id, messages, summary) {
205
+ function writeChat(id, messages, summary, facts) {
205
206
  try {
206
207
  const prev = readChatStore(id);
207
208
  const data = {
208
209
  messages: messages.slice(-40),
209
210
  summary: summary === undefined ? prev.summary : summary,
211
+ facts: (facts === undefined ? prev.facts : facts).slice(-60),
210
212
  };
211
213
  writeFileSync(join(MODELS, id, 'chat.json'), JSON.stringify(data, null, 2), 'utf8');
212
214
  } catch {}
213
215
  }
214
216
 
215
- /** 攒够了就把前二十句压成一段小结,人物记得住更早的事 */
217
+ /** 把压缩出来的两段拆开:小结 + 关于他的事实 */
218
+ function parseDigest(raw) {
219
+ let text = String(raw || '').trim();
220
+ let summary = text;
221
+ const facts = [];
222
+ const at = text.search(/事实\s*[::]/);
223
+ if (at >= 0) {
224
+ summary = text.slice(0, at).replace(/^小结\s*[::]?\s*/, '').trim();
225
+ const body = text.slice(at).replace(/^事实\s*[::]\s*/, '');
226
+ for (const line of body.split(/\n+/)) {
227
+ const item = line.replace(/^[-*·\s]+/, '').trim();
228
+ if (item.length >= 2 && item.length <= 60) facts.push(item);
229
+ }
230
+ }
231
+ summary = summary.replace(/^小结\s*[::]?\s*/, '').trim();
232
+ return { summary, facts: facts.slice(0, 12) };
233
+ }
234
+
235
+ /** 攒够了就把前二十句压成小结 + 事实,人物记得住更早的事 */
216
236
  async function compactChat(slot, store) {
217
237
  const head = store.messages.slice(0, 20);
218
238
  const tail = store.messages.slice(20);
219
239
  if (!head.length) return store;
220
240
  const text = head.map((m) => (m.role === 'user' ? '他:' : '你:') + String(m.text ?? '')).join('\n');
221
241
  const prompt =
222
- '把下面这段对话压成一段不超过一百二十字的小结,只留事实和情绪走向,用第三人称写,不要引号,不要分点:\n' + text;
223
- let line = '';
242
+ '下面是一段聊天记录。请用两段输出:\n' +
243
+ '第一段以「小结:」开头,把这段聊了什么压成不超过一百字的第三人称概述,只留事实和情绪走向。\n' +
244
+ '第二段以「事实:」开头,逐行列出从对话里能确定的、关于「他」的稳定信息(名字、作息、口味、忌讳、答应过的事、反复提到的人或物),' +
245
+ '每条一行、以短横线开头、不超过二十字;不确定的不要编。没有就写「事实:无」。\n\n' + text;
246
+ let raw = '';
224
247
  try {
225
- line = await generate(slot, prompt, 600);
248
+ raw = await generate(slot, prompt, 900);
226
249
  } catch {
227
- line = '';
228
- }
229
- const summary = [store.summary, line].filter(Boolean).join('\n').slice(-1200);
230
- return { messages: tail, summary };
250
+ raw = '';
251
+ }
252
+ const digest = parseDigest(raw);
253
+ const summary = [store.summary, digest.summary].filter(Boolean).join('\n').slice(-1200);
254
+ const merged = [...(store.facts || [])];
255
+ for (const f of digest.facts) {
256
+ if (f === '无') continue;
257
+ if (!merged.some((x) => x === f)) merged.push(f);
258
+ }
259
+ return { messages: tail, summary, facts: merged.slice(-60) };
260
+ }
261
+
262
+ /** 跟他此刻说的话最贴的几条事实 */
263
+ function pickFacts(facts, text) {
264
+ if (!facts || !facts.length) return [];
265
+ const chars = new Set(String(text || '').replace(/[\s,。!?、,.!?]/g, ''));
266
+ const scored = facts.map((f) => {
267
+ let hit = 0;
268
+ for (const ch of String(f)) if (chars.has(ch)) hit += 1;
269
+ return { f, hit };
270
+ });
271
+ scored.sort((a, b) => b.hit - a.hit);
272
+ const top = scored.filter((s) => s.hit > 0).slice(0, 6).map((s) => s.f);
273
+ return top.length ? top : facts.slice(-6);
274
+ }
275
+
276
+ /** 去 AI 味:删套话、砍尾巴、压重复 */
277
+ const AI_WORDS = [
278
+ /^(好的|当然|没问题|明白了|收到)[,,。!!、\s]+/,
279
+ /(希望|但愿)[^。!?]{0,12}帮(到|上)你[^。!?]*[。!!]?/g,
280
+ /还有什么(需要|想要|问题|可以帮|能帮)[^。!?]*[。!?]?/g,
281
+ /(总之|总的来说|综上|先说结论|首先|其次|另外|值得一提的是)[,,::]?/g,
282
+ /作为(一个)?(AI|人工智能|语言模型)[^。!?]*[。!?]?/g,
283
+ /(?:^|[。!?])(我理解你的感受|我很抱歉听到|这一定很不容易)[^。!?]*[。!?]?/g,
284
+ ];
285
+
286
+ function humanize(input) {
287
+ let t = String(input || '');
288
+ for (const re of AI_WORDS) t = t.replace(re, '');
289
+ t = t
290
+ .replace(/[ \t]+/g, ' ')
291
+ .replace(/[。!?]{2,}/g, (m) => m[0])
292
+ .replace(/…{3,}/g, '……')
293
+ .replace(/[,,]\s*$/g, '')
294
+ .trim();
295
+ const parts = t.split(/(?<=[。!?!?~])/).filter((s) => s.trim());
296
+ if (parts.length > 3) t = parts.slice(0, 3).join('');
297
+ return t.trim();
231
298
  }
232
299
 
233
300
  /* ── 出口队列:谁能说话,都往这儿扔 ───────────────────────── */
@@ -316,7 +383,7 @@ async function maybeTalk(ctx) {
316
383
  return;
317
384
  }
318
385
  if (!text) return;
319
- const line = text.length > 120 ? text.slice(0, 120) : text;
386
+ const line = humanize(text).slice(0, 120);
320
387
  pushOut({ text: line, from: 'proactive' });
321
388
  writeChat(id, readChat(id).concat([{ role: 'assistant', text: line, at: Date.now() }]));
322
389
  }
@@ -516,9 +583,10 @@ async function handle(req, res, ctx) {
516
583
  if (st.memory && store.messages.length > 30) {
517
584
  try {
518
585
  store = await compactChat(slot, store);
519
- writeChat(id, store.messages, store.summary);
586
+ writeChat(id, store.messages, store.summary, store.facts);
520
587
  } catch {}
521
588
  }
589
+ const facts = st.memory ? pickFacts(store.facts, text) : [];
522
590
  const hist = store.messages;
523
591
  const lines = hist
524
592
  .slice(-8)
@@ -529,9 +597,12 @@ async function handle(req, res, ctx) {
529
597
  meta.isolation === false ? '' : ISOLATION,
530
598
  '现在是 ' + nowText() + '。按你这个人设,这个时间点你多半在做什么?让这句话带上那个处境,但不要说破。',
531
599
  store.summary ? '之前聊过的,你还记得:\n' + store.summary : '',
600
+ facts.length ? '关于他,你记得这些:\n' + facts.map((f) => '- ' + f).join('\n') : '',
532
601
  lines ? '你们之前说的:\n' + lines : '',
533
602
  '他说:' + text,
534
- '回他一句。只输出这句话本身:一到三句,短,口语,像发消息。不要解释,不要加引号,不要旁白,不要括号里的动作。',
603
+ '像人发消息那样回他:一到两句,短;不总结、不分点、不讲道理、不解释心情;' +
604
+ '不说“我理解你”“希望能帮到你”“还有什么需要”这类话;他没问就别给建议;可以只回两个字。' +
605
+ '只输出这句话本身:不要引号,不要旁白,不要括号里的动作。',
535
606
  st.choices ? '如果你想让他挑,可以在最后另起一行写 ||选项一|选项二,最多三项。' : '',
536
607
  st.memesMode === 'own' || st.memesMode === 'both'
537
608
  ? '如果你想附一张贴图,就在最后另起一行写 ||图:名字。能挑的名字有:' +
@@ -552,6 +623,7 @@ async function handle(req, res, ctx) {
552
623
  return sendJson(res, 502, { error: '生成失败:' + (e instanceof Error ? e.message : String(e)) });
553
624
  }
554
625
  LAST_ACTIVE.at = Date.now();
626
+ reply = humanize(reply);
555
627
  const parsed = splitMeta(reply || '');
556
628
  let line = parsed.text;
557
629
  let choices = st.choices ? parsed.choices : [];
@@ -573,7 +645,7 @@ async function handle(req, res, ctx) {
573
645
  writeChat(id, hist.concat([
574
646
  { role: 'user', text, at: Date.now() },
575
647
  { role: 'assistant', text: line, at: Date.now() },
576
- ]), store.summary);
648
+ ]), store.summary, store.facts);
577
649
  return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
578
650
  }
579
651
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",