dsh-live2dwaifu 0.5.0 → 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.
@@ -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>
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
@@ -100,6 +100,23 @@ const DEFAULT_PERSONA = [
100
100
  '不解释心情,不总结,不升华,不写括号里的动作。',
101
101
  ].join('\n');
102
102
 
103
+ /** 没说清要让你挑什么的时候,按情绪给两个短答 */
104
+ const FALLBACK_CHOICES = {
105
+ 开心: ['好', '嗯嗯'],
106
+ 生气: ['……', '别气'],
107
+ 委屈: ['我在', '怎么了'],
108
+ 害羞: ['唔', '别看'],
109
+ 无语: ['行吧', '你说'],
110
+ 困: ['睡吧', '嗯'],
111
+ 平静: ['嗯', '在的'],
112
+ };
113
+
114
+ const ISOLATION = [
115
+ '【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
116
+ '有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
117
+ '你不知道他那边什么样,得他讲给你听。',
118
+ ].join('\n');
119
+
103
120
  function personaOf(meta) {
104
121
  const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
105
122
  return p || DEFAULT_PERSONA;
@@ -403,6 +420,7 @@ async function handle(req, res, ctx) {
403
420
  .join('\n');
404
421
  const prompt = [
405
422
  personaOf(meta),
423
+ meta.isolation === false ? '' : ISOLATION,
406
424
  '现在是 ' + nowText() + '。',
407
425
  lines ? '你们之前说的:\n' + lines : '',
408
426
  '他说:' + text,
@@ -421,8 +439,11 @@ async function handle(req, res, ctx) {
421
439
  LAST_ACTIVE.at = Date.now();
422
440
  const parsed = splitChoices(reply || '');
423
441
  let line = parsed.text;
424
- const choices = st.choices ? parsed.choices : [];
442
+ let choices = st.choices ? parsed.choices : [];
425
443
  if (line.length > 240) line = line.slice(0, 240);
444
+ if (st.choices && !choices.length && line) {
445
+ choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
446
+ }
426
447
  if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
427
448
  const image = st.memes && guessMood(line) && Math.random() < 0.35 ? pickMeme(guessMood(line)) : null;
428
449
 
@@ -437,7 +458,12 @@ async function handle(req, res, ctx) {
437
458
  if (path === '/api/persona' && method === 'GET') {
438
459
  const id = String(url.searchParams.get('id') ?? '');
439
460
  const meta = id ? readMeta(id) : null;
440
- return sendJson(res, 200, { id, persona: personaOf(meta), custom: typeof meta?.persona === 'string' && !!meta.persona.trim() });
461
+ return sendJson(res, 200, {
462
+ id,
463
+ persona: personaOf(meta),
464
+ custom: typeof meta?.persona === 'string' && !!meta.persona.trim(),
465
+ isolation: meta?.isolation !== false,
466
+ });
441
467
  }
442
468
 
443
469
  if (path === '/api/persona' && method === 'POST') {
@@ -445,9 +471,10 @@ async function handle(req, res, ctx) {
445
471
  const id = String(body.id ?? '');
446
472
  const meta = id ? readMeta(id) : null;
447
473
  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 }) });
474
+ const persona = typeof body.persona === 'string' ? body.persona : meta.persona;
475
+ const isolation = typeof body.isolation === 'boolean' ? body.isolation : meta.isolation !== false;
476
+ writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona, isolation }, null, 2), 'utf8');
477
+ return sendJson(res, 200, { ok: true, persona: personaOf({ persona }), isolation });
451
478
  }
452
479
 
453
480
  // 开关:哪些功能开着
@@ -490,6 +517,11 @@ async function handle(req, res, ctx) {
490
517
  return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
491
518
  }
492
519
 
520
+ // 人设卡页
521
+ if (path === '/persona' || path === '/persona.html') {
522
+ return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
523
+ }
524
+
493
525
  // 音色调音页
494
526
  if (path === '/tone' || path === '/tone.html') {
495
527
  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.6.0",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",