dsh-liuying-voice 1.2.0 → 1.3.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/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * dsh-liuying-voice client:设置页。
3
3
  * 运行时格式与 DSH client 插件一致(__ModuleLoader__.load + require("react"))。
4
4
  */
@@ -113,6 +113,16 @@ window.__ModuleLoader__.load({
113
113
  return h(
114
114
  "div",
115
115
  { style: { display: "grid", gap: 12, padding: 4 } },
116
+ h(
117
+ "a",
118
+ {
119
+ href: "/liuying-voice",
120
+ target: "_blank",
121
+ rel: "noreferrer",
122
+ style: { fontSize: 13, color: "var(--dsh-accent, #4db6a0)", textDecoration: "none" },
123
+ },
124
+ "在浏览器打开独立设置页 ↗",
125
+ ),
116
126
  h(
117
127
  "label",
118
128
  { style: { display: "flex", alignItems: "center", gap: 8 } },
package/lib/index.js CHANGED
@@ -384,6 +384,26 @@ function mountSettingsPage(ctx) {
384
384
  });
385
385
  }
386
386
 
387
+ function mountPersonaPage(ctx) {
388
+ const web = ctx.get("webServer");
389
+ if (!web?.register) return null;
390
+ return web.register({
391
+ kind: "exact",
392
+ path: "/liuying-voice/persona",
393
+ handler: async (req, res) => {
394
+ try {
395
+ const html = readFileSync(path.join(PLUGIN_ROOT, "lib", "persona-page.html"), "utf8");
396
+ res.statusCode = 200;
397
+ res.setHeader("content-type", "text/html; charset=utf-8");
398
+ res.end(html);
399
+ } catch {
400
+ res.statusCode = 500;
401
+ res.end("persona-page.html 缺失");
402
+ }
403
+ },
404
+ });
405
+ }
406
+
387
407
  export async function apply(ctx, config = {}) {
388
408
  diag(`apply 开始;webServer=${ctx.get("webServer") ? "有" : "无"};tools=${ctx.get("tools") ? "有" : "无"}`);
389
409
  // 一次性把可能影响 python 子进程的环境变量记下来(排查 edge-tts 用)
@@ -425,6 +445,19 @@ export async function apply(ctx, config = {}) {
425
445
  } catch (error) {
426
446
  log(`设置页面挂载失败:${error?.message ?? error}`);
427
447
  }
448
+ try {
449
+ const dispose = mountPersonaPage(ctx);
450
+ if (typeof dispose === "function") {
451
+ const prev = disposeSettings;
452
+ disposeSettings = () => {
453
+ if (typeof prev === "function") prev();
454
+ dispose();
455
+ };
456
+ }
457
+ log("人格卡页面已挂载:/liuying-voice/persona");
458
+ } catch (error) {
459
+ log(`人格卡页面挂载失败:${error?.message ?? error}`);
460
+ }
428
461
  try {
429
462
  const dispose = mountMemory(ctx);
430
463
  disposeMemory = typeof dispose === "function" ? dispose : null;
@@ -0,0 +1,123 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>流萤 · 人格卡</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; font: 15px/1.75 "Segoe UI", "Microsoft YaHei", sans-serif; background: #0c1113; color: #d8e6e4; }
11
+ .wrap { display: flex; min-height: 100vh; }
12
+ aside { width: 220px; flex: 0 0 220px; border-right: 1px solid #1e2a2d; padding: 22px 14px; background: #0f1517; }
13
+ aside h1 { font-size: 15px; margin: 0 0 16px; color: #8fd8c8; letter-spacing: .08em; font-weight: 600; }
14
+ aside a { display: block; color: #9db5b9; text-decoration: none; font-size: 13px; padding: 5px 8px; border-radius: 5px; margin: 2px 0; }
15
+ aside a:hover { background: #16201f; color: #cfe3e6; }
16
+ aside a.on { background: #1f3a34; color: #b8f0e2; }
17
+ main { flex: 1; padding: 30px 40px 60px; max-width: 860px; }
18
+ main h2 { color: #8fd8c8; font-size: 18px; margin: 26px 0 10px; border-bottom: 1px solid #1e2a2d; padding-bottom: 6px; }
19
+ main h3 { color: #b8d8d0; font-size: 15px; margin: 20px 0 8px; }
20
+ main p, main li { color: #c4d6d4; }
21
+ main strong { color: #eafaf5; }
22
+ main ul { padding-left: 22px; }
23
+ main blockquote { margin: 12px 0; padding: 8px 14px; border-left: 3px solid #2f5a50; color: #9db5b9; background: #10191b; }
24
+ main code { background: #16201f; padding: 1px 5px; border-radius: 4px; font-size: 13px; }
25
+ .muted { color: #5f7478; font-size: 13px; }
26
+ .toolbar { margin-bottom: 18px; display: flex; gap: 10px; }
27
+ .toolbar a { color: #4db6a0; text-decoration: none; font-size: 13px; }
28
+ .loading { color: #5f7478; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <div class="wrap">
33
+ <aside>
34
+ <h1>流萤 · 人格卡</h1>
35
+ <div id="nav" class="muted">加载中…</div>
36
+ </aside>
37
+ <main>
38
+ <div class="toolbar"><a id="rawLink" href="#" target="_blank">查看原文 ↗</a></div>
39
+ <div id="content" class="loading">加载中…</div>
40
+ </main>
41
+ </div>
42
+ <script>
43
+ const API = "/liuying-voice/memory";
44
+ const $ = (id) => document.getElementById(id);
45
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
46
+
47
+ // 极简 Markdown 渲染:标题 / 列表 / 粗体 / 代码 / 引用
48
+ function render(md) {
49
+ const lines = md.split(/\r?\n/);
50
+ let out = "", inList = false, inQuote = false;
51
+ const inline = (t) =>
52
+ esc(t)
53
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
54
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
55
+ .replace(/~~([^~]+)~~/g, "<s>$1</s>");
56
+ const closeList = () => { if (inList) { out += "</ul>"; inList = false; } };
57
+ for (const line of lines) {
58
+ const t = line.trim();
59
+ if (!t) { closeList(); inQuote = false; continue; }
60
+ if (t.startsWith(">")) {
61
+ closeList();
62
+ if (!inQuote) { out += "<blockquote>"; inQuote = true; }
63
+ out += inline(t.slice(1).trim()) + "<br>";
64
+ continue;
65
+ }
66
+ if (inQuote) { out += "</blockquote>"; inQuote = false; }
67
+ const h = t.match(/^(#{1,4})\s+(.*)$/);
68
+ if (h) { closeList(); out += `<h${h[1].length + 1}>${inline(h[2])}</h${h[1].length + 1}>`; continue; }
69
+ if (/^[-*]\s+/.test(t)) {
70
+ if (!inList) { out += "<ul>"; inList = true; }
71
+ out += `<li>${inline(t.replace(/^[-*]\s+/, ""))}</li>`;
72
+ continue;
73
+ }
74
+ closeList();
75
+ out += `<p>${inline(t)}</p>`;
76
+ }
77
+ closeList();
78
+ if (inQuote) out += "</blockquote>";
79
+ return out;
80
+ }
81
+
82
+ function show(file) {
83
+ $("content").className = "loading";
84
+ $("content").textContent = "加载中…";
85
+ fetch(`${API}?p=${encodeURIComponent(file)}`, { cache: "no-store" })
86
+ .then((r) => r.text())
87
+ .then((html) => {
88
+ const m = html.match(/<pre>([\s\S]*)<\/pre>/);
89
+ const body = m ? m[1] : "";
90
+ const text = body.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"');
91
+ $("content").className = "";
92
+ $("content").innerHTML = render(text);
93
+ $("rawLink").href = `${API}?p=${encodeURIComponent(file)}`;
94
+ document.querySelectorAll("aside a").forEach((a) => a.classList.toggle("on", a.dataset.f === file));
95
+ })
96
+ .catch((e) => { $("content").textContent = "加载失败:" + e.message; });
97
+ }
98
+
99
+ fetch(API, { cache: "no-store" })
100
+ .then((r) => r.text())
101
+ .then((html) => {
102
+ const links = [...html.matchAll(/<li><a href="[^"]*?p=([^"&]+)">([^<]+)<\/a><\/li>/g)].map((m) => ({
103
+ rel: decodeURIComponent(m[1]),
104
+ name: m[2],
105
+ }));
106
+ const order = ["firefly-lore.md", "firefly-stories.md", "penacony-2.0.md", "3.8-终将实现的-台词.md", "penacony-full.md", "penacony-tasks.md", "README.md"];
107
+ links.sort((a, b) => {
108
+ const ia = order.indexOf(a.rel), ib = order.indexOf(b.rel);
109
+ return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.rel.localeCompare(b.rel);
110
+ });
111
+ $("nav").innerHTML = links
112
+ .map((l) => `<a href="#" data-f="${esc(l.rel)}">${esc(l.rel)}</a>`)
113
+ .join("");
114
+ document.querySelectorAll("aside a").forEach((a) => {
115
+ a.addEventListener("click", (ev) => { ev.preventDefault(); show(a.dataset.f); });
116
+ });
117
+ if (links.length) show(links[0].rel);
118
+ else $("content").textContent = "memory 目录为空";
119
+ })
120
+ .catch((e) => { $("nav").textContent = ""; $("content").textContent = "加载失败:" + e.message; });
121
+ </script>
122
+ </body>
123
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-liuying-voice",
3
- "version": "1.2.0",
4
- "description": "流萤语音插件 for DeepSeek Harness:本地 edge-tts + RVC / GPT-SoVITS 双引擎,自带流萤参考音频、一键环境安装脚本和独立设置页面。开源 MIT。",
3
+ "version": "1.3.0",
4
+ "description": "流萤语音插件 for DeepSeek Harness:本地 edge-tts + RVC / GPT-SoVITS 双引擎,自带流萤参考音频、一键环境安装脚本、独立设置页面与 DSH 设置入口。开源 MIT。",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",