dsh-folia 4.3.1 → 4.4.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 +163 -12
- package/lib/routes/pets.js +67 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -17212,12 +17212,21 @@ window.__ModuleLoader__.load({
|
|
|
17212
17212
|
const [state, setState] = React.useState(null);
|
|
17213
17213
|
const [busy, setBusy] = React.useState("");
|
|
17214
17214
|
const [note, setNote] = React.useState(null);
|
|
17215
|
+
const [importable, setImportable] = React.useState([]);
|
|
17215
17216
|
|
|
17216
17217
|
const load = React.useCallback(async (force) => {
|
|
17217
17218
|
try {
|
|
17218
17219
|
const resp = await apiJson("/pets/list" + (force ? "?refresh=1" : ""), { parse: "always" });
|
|
17219
17220
|
const doc = (resp && resp.data) || null;
|
|
17220
|
-
if (resp && resp.ok && doc && doc.ok)
|
|
17221
|
+
if (resp && resp.ok && doc && doc.ok) {
|
|
17222
|
+
setState(doc);
|
|
17223
|
+
// 顺带问一下本机其它宠物插件里有哪些可导入的(失败不影响主列表)。
|
|
17224
|
+
try {
|
|
17225
|
+
const im = await apiJson("/pets/importable", { parse: "always" });
|
|
17226
|
+
const imDoc = (im && im.data) || null;
|
|
17227
|
+
if (imDoc && imDoc.ok) setImportable(imDoc.pets || []);
|
|
17228
|
+
} catch { /* 无外部目录是正常情况 */ }
|
|
17229
|
+
}
|
|
17221
17230
|
else setNote({ ok: false, text: weT("读取失败:") + ((resp && resp.error) || "unknown") });
|
|
17222
17231
|
} catch (e) {
|
|
17223
17232
|
setNote({ ok: false, text: weT("请求失败:") + ((e && e.message) || weT("网络错误")) });
|
|
@@ -17235,6 +17244,7 @@ window.__ModuleLoader__.load({
|
|
|
17235
17244
|
if (resp && resp.ok && r && r.ok) {
|
|
17236
17245
|
if (kind === "install") setNote({ ok: true, text: weT("已安装并启用:") + (label || "") });
|
|
17237
17246
|
if (kind === "remove") setNote({ ok: true, text: weT("已卸载:") + (label || "") });
|
|
17247
|
+
if (kind === "import") setNote({ ok: true, text: weT("已导入并启用:") + (label || "") });
|
|
17238
17248
|
if (kind === "config") setNote({ ok: true, text: weT("已保存") });
|
|
17239
17249
|
await load(true);
|
|
17240
17250
|
} else {
|
|
@@ -17369,6 +17379,26 @@ window.__ModuleLoader__.load({
|
|
|
17369
17379
|
: React.createElement("div", { className: "we-picker__hint" }, weT("还没有安装任何桌宠 —— 从下面的站点清单里挑一只。")),
|
|
17370
17380
|
));
|
|
17371
17381
|
|
|
17382
|
+
// 本机其它宠物插件里可导入的(dsh-pet 等下载的宠物就落在 $DSH_HOME/pets)
|
|
17383
|
+
if (importable.length) {
|
|
17384
|
+
children.push(React.createElement("div", { key: "impHead", className: "we-picker__section-head", style: { marginTop: "14px" } },
|
|
17385
|
+
React.createElement("span", { className: "we-picker__section-label" },
|
|
17386
|
+
weT("本机可导入") + " · " + importable.length),
|
|
17387
|
+
));
|
|
17388
|
+
children.push(React.createElement("div", { key: "imp", style: PET_STYLE.grid },
|
|
17389
|
+
importable.map((it) => {
|
|
17390
|
+
const isBusy = busy === "import:" + it.id;
|
|
17391
|
+
return React.createElement("div", { key: "imp:" + it.id, style: PET_STYLE.card },
|
|
17392
|
+
React.createElement("div", { style: PET_STYLE.title }, it.name || it.id),
|
|
17393
|
+
React.createElement("div", { style: PET_STYLE.meta },
|
|
17394
|
+
weT("来自其它宠物插件 · ") + petFmtBytes(it.bytes)),
|
|
17395
|
+
React.createElement("div", { style: PET_STYLE.row },
|
|
17396
|
+
btn(isBusy ? weT("导入中…") : weT("导入"), () => act("import", { id: it.id }, it.name || it.id), Boolean(busy))),
|
|
17397
|
+
);
|
|
17398
|
+
}),
|
|
17399
|
+
));
|
|
17400
|
+
}
|
|
17401
|
+
|
|
17372
17402
|
// 站点:未装的可安装
|
|
17373
17403
|
const available = site.filter((s) => !installedIds.has(s.id));
|
|
17374
17404
|
children.push(React.createElement("div", { key: "siteHead", className: "we-picker__section-head", style: { marginTop: "14px" } },
|
|
@@ -17430,6 +17460,25 @@ window.__ModuleLoader__.load({
|
|
|
17430
17460
|
|
|
17431
17461
|
const PET_LAYER_ID = "we-pet-layer";
|
|
17432
17462
|
const PET_ROW = { idle: 0, runRight: 1, runLeft: 2, wave: 3, jump: 4, fail: 5, wait: 6, work: 7, done: 8 };
|
|
17463
|
+
/**
|
|
17464
|
+
* 行号 → 轨道名的**候选表**。
|
|
17465
|
+
*
|
|
17466
|
+
* ⚠️ dsh-pet 的 manifest 用的是 kebab 命名(`running-right` / `waving` / `failed` …),
|
|
17467
|
+
* 我最初按驼峰(`runRight` / `wave`)去查,结果**一次都没命中** —— 每帧都退回默认 120ms,
|
|
17468
|
+
* 于是所有动作的节奏都是同一个硬速度,看起来就像"动作不对/没几个动作"。
|
|
17469
|
+
* 这里按「标准名 → 驼峰别名 → 其它常见写法」依次找,命中即用。
|
|
17470
|
+
*/
|
|
17471
|
+
const PET_TRACK_ALIASES = [
|
|
17472
|
+
['idle'],
|
|
17473
|
+
['running-right', 'runRight', 'right'],
|
|
17474
|
+
['running-left', 'runLeft', 'left'],
|
|
17475
|
+
['waving', 'wave'],
|
|
17476
|
+
['jumping', 'jump'],
|
|
17477
|
+
['failed', 'fail'],
|
|
17478
|
+
['waiting', 'wait'],
|
|
17479
|
+
['running', 'working', 'work'],
|
|
17480
|
+
['review', 'done', 'completed'],
|
|
17481
|
+
];
|
|
17433
17482
|
const PET_DEFAULT_FRAME_MS = 120;
|
|
17434
17483
|
/**
|
|
17435
17484
|
* 活动检测:DSH 的客户端 `ctx` 只暴露 effect/get/slots/surface,没有会话事件,
|
|
@@ -17476,7 +17525,7 @@ window.__ModuleLoader__.load({
|
|
|
17476
17525
|
return petNow();
|
|
17477
17526
|
}
|
|
17478
17527
|
/** 两次随机动作之间的间隔范围(毫秒)。 */
|
|
17479
|
-
const PET_IDLE_GAP_MS = [
|
|
17528
|
+
const PET_IDLE_GAP_MS = [3000, 7000];
|
|
17480
17529
|
|
|
17481
17530
|
/**
|
|
17482
17531
|
* 声音:包内 `sounds/<事件>.ogg|mp3|wav` 优先(manifest.sounds 显式声明),
|
|
@@ -17590,7 +17639,10 @@ window.__ModuleLoader__.load({
|
|
|
17590
17639
|
let walkSpeed = 0;
|
|
17591
17640
|
/** 走动剩余时长(毫秒)。 */
|
|
17592
17641
|
let walkLeft = 0;
|
|
17642
|
+
/** 单击轮换到第几号轨道。 */
|
|
17643
|
+
let clickIdx = 0;
|
|
17593
17644
|
|
|
17645
|
+
let scaleCur = Number(scale) > 0 ? Number(scale) : 1;
|
|
17594
17646
|
const cw = Math.max(1, Number(pet.cell && pet.cell.width) || 192);
|
|
17595
17647
|
const ch = Math.max(1, Number(pet.cell && pet.cell.height) || 208);
|
|
17596
17648
|
const cols = Math.max(1, Number(pet.columns) || 8);
|
|
@@ -17600,15 +17652,24 @@ window.__ModuleLoader__.load({
|
|
|
17600
17652
|
const n = Number(frames[r]);
|
|
17601
17653
|
return Number.isFinite(n) && n > 0 ? Math.min(cols, n) : cols;
|
|
17602
17654
|
}
|
|
17655
|
+
/** 该行的轨道配置(按别名表找;找不到返回 null)。 */
|
|
17656
|
+
function trackOf(r) {
|
|
17657
|
+
const names = PET_TRACK_ALIASES[r] || [];
|
|
17658
|
+
for (const n of names) {
|
|
17659
|
+
const tr = pet.tracks && pet.tracks[n];
|
|
17660
|
+
if (tr && Array.isArray(tr.durations) && tr.durations.length) return tr;
|
|
17661
|
+
}
|
|
17662
|
+
return null;
|
|
17663
|
+
}
|
|
17603
17664
|
function frameMs(r, f) {
|
|
17604
|
-
const
|
|
17605
|
-
const d =
|
|
17665
|
+
const tr = trackOf(r);
|
|
17666
|
+
const d = tr ? tr.durations[f] : null;
|
|
17606
17667
|
return Number.isFinite(Number(d)) && Number(d) > 0 ? Number(d) : PET_DEFAULT_FRAME_MS;
|
|
17607
17668
|
}
|
|
17608
17669
|
|
|
17609
17670
|
function resize() {
|
|
17610
|
-
const w = Math.round(cw *
|
|
17611
|
-
const h = Math.round(ch *
|
|
17671
|
+
const w = Math.round(cw * scaleCur);
|
|
17672
|
+
const h = Math.round(ch * scaleCur);
|
|
17612
17673
|
const dpr = Math.min(3, Math.max(1, (typeof window !== "undefined" && window.devicePixelRatio) || 1));
|
|
17613
17674
|
canvas.width = Math.round(w * dpr);
|
|
17614
17675
|
canvas.height = Math.round(h * dpr);
|
|
@@ -17627,8 +17688,8 @@ window.__ModuleLoader__.load({
|
|
|
17627
17688
|
const sx = frame * cw;
|
|
17628
17689
|
const sy = row * ch;
|
|
17629
17690
|
try {
|
|
17630
|
-
ctx.clearRect(0, 0, cw *
|
|
17631
|
-
ctx.drawImage(img, sx, sy, cw, ch, 0, 0, cw *
|
|
17691
|
+
ctx.clearRect(0, 0, cw * scaleCur, ch * scaleCur);
|
|
17692
|
+
ctx.drawImage(img, sx, sy, cw, ch, 0, 0, cw * scaleCur, ch * scaleCur);
|
|
17632
17693
|
} catch { /* 贴图还没解码完:下一帧再画 */ }
|
|
17633
17694
|
}
|
|
17634
17695
|
|
|
@@ -17734,10 +17795,10 @@ window.__ModuleLoader__.load({
|
|
|
17734
17795
|
const maxX = Math.max(0, (window.innerWidth || 1920) - layerEl.offsetWidth);
|
|
17735
17796
|
if ((dir > 0 && p0.left >= maxX - 2) || (dir < 0 && p0.left <= 2)) {
|
|
17736
17797
|
setRow(dir > 0 ? PET_ROW.runLeft : PET_ROW.runRight);
|
|
17737
|
-
walkSpeed = -dir * (0.9 *
|
|
17798
|
+
walkSpeed = -dir * (0.9 * scaleCur);
|
|
17738
17799
|
} else {
|
|
17739
17800
|
setRow(dir > 0 ? PET_ROW.runRight : PET_ROW.runLeft);
|
|
17740
|
-
walkSpeed = dir * (0.9 *
|
|
17801
|
+
walkSpeed = dir * (0.9 * scaleCur);
|
|
17741
17802
|
}
|
|
17742
17803
|
walkLeft = action.ms;
|
|
17743
17804
|
return;
|
|
@@ -17783,14 +17844,101 @@ window.__ModuleLoader__.load({
|
|
|
17783
17844
|
// 单击 = 挥手(给用户一个"它有反应"的反馈),之后自动回 idle
|
|
17784
17845
|
walkSpeed = 0;
|
|
17785
17846
|
walkLeft = 0;
|
|
17786
|
-
|
|
17847
|
+
// 单击 = 演下一个动作(按 9 条轨道轮换),让用户能逐个看到它会什么。
|
|
17848
|
+
clickIdx = (clickIdx + 1) % PET_TRACK_ALIASES.length;
|
|
17849
|
+
const nextRow = clickIdx === 0 ? PET_ROW.idle : clickIdx;
|
|
17850
|
+
setRow(nextRow);
|
|
17787
17851
|
holdUntil = petNow() + 1400;
|
|
17788
|
-
voice.play("wave");
|
|
17852
|
+
voice.play(nextRow === PET_ROW.fail ? "fail" : nextRow === PET_ROW.done ? "done" : "wave");
|
|
17789
17853
|
} else {
|
|
17790
17854
|
try { localStorage.setItem("wePetPos", JSON.stringify({ left: layerEl.style.left, top: layerEl.style.top })); } catch { /* ignore */ }
|
|
17791
17855
|
}
|
|
17792
17856
|
}
|
|
17793
17857
|
|
|
17858
|
+
/* ── 右键菜单:换宠物之外的全部互动都在这里 ────────────────────────────
|
|
17859
|
+
* 为什么自己做:宿主的页签要打开面板才能操作,而"摸它"这件事应该发生在宠物身上。
|
|
17860
|
+
* 菜单条目 = 9 条动作各一条(点一下立刻试演)+ 声音/大小/复位/隐藏。
|
|
17861
|
+
*/
|
|
17862
|
+
let menu = null;
|
|
17863
|
+
function closeMenu() {
|
|
17864
|
+
if (menu && menu.parentNode) menu.parentNode.removeChild(menu);
|
|
17865
|
+
menu = null;
|
|
17866
|
+
}
|
|
17867
|
+
function buildMenuItem(label, onClick) {
|
|
17868
|
+
const b = document.createElement("button");
|
|
17869
|
+
b.type = "button";
|
|
17870
|
+
b.textContent = label;
|
|
17871
|
+
b.style.cssText = "display:block;width:100%;padding:6px 10px;background:transparent;border:0;color:inherit;text-align:left;font-size:12px;cursor:pointer;white-space:nowrap";
|
|
17872
|
+
b.addEventListener("mouseenter", () => { b.style.background = "rgba(255,255,255,0.10)"; });
|
|
17873
|
+
b.addEventListener("mouseleave", () => { b.style.background = "transparent"; });
|
|
17874
|
+
b.addEventListener("click", (ev) => { ev.stopPropagation(); onClick(); });
|
|
17875
|
+
return b;
|
|
17876
|
+
}
|
|
17877
|
+
function openMenu(clientX, clientY) {
|
|
17878
|
+
closeMenu();
|
|
17879
|
+
menu = document.createElement("div");
|
|
17880
|
+
menu.style.cssText = [
|
|
17881
|
+
"position:fixed", "z-index:2147482001", "min-width:150px", "padding:6px 0",
|
|
17882
|
+
"border-radius:8px", "font-size:12px", "color:#eee",
|
|
17883
|
+
"background:rgba(28,32,40,0.96)", "border:1px solid rgba(255,255,255,0.16)",
|
|
17884
|
+
"box-shadow:0 10px 30px rgba(0,0,0,0.45)", "backdrop-filter:blur(8px)",
|
|
17885
|
+
].join(";");
|
|
17886
|
+
const titles = ["闲置", "向右走", "向左走", "挥手", "跳一下", "失败", "发呆", "干活", "完成"];
|
|
17887
|
+
const head = document.createElement("div");
|
|
17888
|
+
head.textContent = "试演动作";
|
|
17889
|
+
head.style.cssText = "padding:4px 10px 6px;opacity:0.55;font-size:11px";
|
|
17890
|
+
menu.appendChild(head);
|
|
17891
|
+
titles.forEach((name, r) => {
|
|
17892
|
+
menu.appendChild(buildMenuItem(name, () => {
|
|
17893
|
+
walkSpeed = 0; walkLeft = 0;
|
|
17894
|
+
setRow(r);
|
|
17895
|
+
holdUntil = petNow() + 1600;
|
|
17896
|
+
voice.play(r === PET_ROW.fail ? "fail" : r === PET_ROW.done ? "done" : "wave");
|
|
17897
|
+
closeMenu();
|
|
17898
|
+
}));
|
|
17899
|
+
});
|
|
17900
|
+
const hr = document.createElement("div");
|
|
17901
|
+
hr.style.cssText = "height:1px;margin:6px 0;background:rgba(255,255,255,0.14)";
|
|
17902
|
+
menu.appendChild(hr);
|
|
17903
|
+
menu.appendChild(buildMenuItem("再大一点", () => { applyScale(scale + 0.2); closeMenu(); }));
|
|
17904
|
+
menu.appendChild(buildMenuItem("再小一点", () => { applyScale(Math.max(0.5, scale - 0.2)); closeMenu(); }));
|
|
17905
|
+
menu.appendChild(buildMenuItem("回右下角", () => {
|
|
17906
|
+
try { localStorage.removeItem("wePetPos"); } catch { /* ignore */ }
|
|
17907
|
+
layerEl.style.left = ""; layerEl.style.top = "";
|
|
17908
|
+
layerEl.style.right = "24px"; layerEl.style.bottom = "16px";
|
|
17909
|
+
closeMenu();
|
|
17910
|
+
}));
|
|
17911
|
+
menu.appendChild(buildMenuItem("不显示它", () => {
|
|
17912
|
+
// 只隐藏本地这一层;真正的开关在面板里(下一次轮询会被尊重)。
|
|
17913
|
+
if (layerEl) layerEl.style.display = "none";
|
|
17914
|
+
closeMenu();
|
|
17915
|
+
}));
|
|
17916
|
+
document.body.appendChild(menu);
|
|
17917
|
+
const w = menu.offsetWidth || 160;
|
|
17918
|
+
const h = menu.offsetHeight || 320;
|
|
17919
|
+
const maxX = (window.innerWidth || 1920) - w - 8;
|
|
17920
|
+
const maxY = (window.innerHeight || 1080) - h - 8;
|
|
17921
|
+
menu.style.left = Math.max(8, Math.min(maxX, clientX)) + "px";
|
|
17922
|
+
menu.style.top = Math.max(8, Math.min(maxY, clientY)) + "px";
|
|
17923
|
+
}
|
|
17924
|
+
function onContextMenu(e) {
|
|
17925
|
+
e.preventDefault();
|
|
17926
|
+
openMenu(e.clientX, e.clientY);
|
|
17927
|
+
}
|
|
17928
|
+
function onDocDown(e) {
|
|
17929
|
+
if (menu && !menu.contains(e.target)) closeMenu();
|
|
17930
|
+
}
|
|
17931
|
+
/** 菜单里的缩放:改本地样式并写回配置(下一次轮询沿用)。 */
|
|
17932
|
+
function applyScale(next) {
|
|
17933
|
+
const v = Math.max(0.5, Math.min(3, Number(next) || 1));
|
|
17934
|
+
try { apiPostJson("/pets/config", { scale: v }, { parse: "always" }); } catch { /* 本地先变 */ }
|
|
17935
|
+
scaleCur = v;
|
|
17936
|
+
resize();
|
|
17937
|
+
draw();
|
|
17938
|
+
}
|
|
17939
|
+
|
|
17940
|
+
canvas.addEventListener("contextmenu", onContextMenu);
|
|
17941
|
+
if (typeof document !== "undefined" && document.addEventListener) document.addEventListener("pointerdown", onDocDown);
|
|
17794
17942
|
img.onload = () => { ready = true; resize(); draw(); };
|
|
17795
17943
|
img.onerror = () => { destroy(); };
|
|
17796
17944
|
img.src = apiUrl(pet.sheet || "/pets/sheet?v=" + encodeURIComponent(pet.dirId || pet.id));
|
|
@@ -17810,6 +17958,9 @@ window.__ModuleLoader__.load({
|
|
|
17810
17958
|
try { voice.destroy(); } catch { /* ignore */ }
|
|
17811
17959
|
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(raf); else clearTimeout(raf);
|
|
17812
17960
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
17961
|
+
canvas.removeEventListener("contextmenu", onContextMenu);
|
|
17962
|
+
if (typeof document !== "undefined" && document.removeEventListener) document.removeEventListener("pointerdown", onDocDown);
|
|
17963
|
+
closeMenu();
|
|
17813
17964
|
if (typeof window !== "undefined") {
|
|
17814
17965
|
window.removeEventListener("pointermove", onPointerMove);
|
|
17815
17966
|
window.removeEventListener("pointerup", onPointerUp);
|
package/lib/routes/pets.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { createHash } from 'node:crypto';
|
|
23
23
|
import { inflateRawSync } from 'node:zlib';
|
|
24
|
-
import { createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { cpSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { basename, dirname, extname, join } from 'node:path';
|
|
27
27
|
|
|
@@ -50,6 +50,35 @@ function siteBase() {
|
|
|
50
50
|
/** 站点上的桌宠分类 slug(站点 `/api/public/categories` 里就有这一条)。 */
|
|
51
51
|
const PET_CATEGORY = 'desktop-pet';
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 别的宠物插件把用户下载的宠物放在 `$DSH_HOME/pets/<id>/`(dsh-pet 的公开契约)。
|
|
55
|
+
*
|
|
56
|
+
* 为什么提供"导入"而不是"共用目录":共用会让两边的生命周期纠缠在一起(对方删一只、
|
|
57
|
+
* 我们这边跟着消失)。**复制一份进我们自己的目录**,两边互不影响,代价只是多占几 MB。
|
|
58
|
+
*/
|
|
59
|
+
function externalPetsDir() {
|
|
60
|
+
const home = process.env.DSH_HOME || join(process.env.APPDATA || homedir(), 'dsh-desktop', 'harness');
|
|
61
|
+
return join(home, 'pets');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 本机其它宠物插件目录里、我们还没装的那些(只读扫描,不写对方目录)。 */
|
|
65
|
+
function listImportable() {
|
|
66
|
+
const src = externalPetsDir();
|
|
67
|
+
const out = [];
|
|
68
|
+
let names = [];
|
|
69
|
+
try { names = readdirSync(src); } catch { return out; }
|
|
70
|
+
const mine = new Set(listInstalled().map((p) => p.dirId));
|
|
71
|
+
for (const name of names) {
|
|
72
|
+
if (mine.has(name)) continue;
|
|
73
|
+
const dir = join(src, name);
|
|
74
|
+
try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
75
|
+
const pet = readPet(dir, name);
|
|
76
|
+
if (!pet) continue;
|
|
77
|
+
out.push({ id: name, name: pet.name, bytes: pet.bytes, columns: pet.columns, cell: pet.cell });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
function ensureDir(p) {
|
|
54
83
|
if (!existsSync(p)) mkdirSync(p, { recursive: true });
|
|
55
84
|
return p;
|
|
@@ -355,6 +384,43 @@ export function registerPetsRoutes(webServer, c) {
|
|
|
355
384
|
},
|
|
356
385
|
}));
|
|
357
386
|
|
|
387
|
+
// ── GET /pets/importable(本机其它宠物插件里可导入的) ────────────────────
|
|
388
|
+
disposers.push(webServer.register({
|
|
389
|
+
kind: 'exact',
|
|
390
|
+
path: `${BASE}/pets/importable`,
|
|
391
|
+
handler: async (req, res) => {
|
|
392
|
+
let pets = [];
|
|
393
|
+
try { pets = listImportable(); } catch (e) { sendJson(res, 200, { ok: true, pets: [], error: String((e && e.message) || e) }); return; }
|
|
394
|
+
sendJson(res, 200, { ok: true, pets, dir: externalPetsDir() });
|
|
395
|
+
},
|
|
396
|
+
}));
|
|
397
|
+
|
|
398
|
+
// ── POST /pets/import(把本机另一只宠物复制进来) ─────────────────────────
|
|
399
|
+
disposers.push(webServer.register({
|
|
400
|
+
kind: 'exact',
|
|
401
|
+
path: `${BASE}/pets/import`,
|
|
402
|
+
handler: async (req, res) => {
|
|
403
|
+
if (req.method !== 'POST') { sendJson(res, 405, { ok: false, error: 'method' }); return; }
|
|
404
|
+
const text = await readBodyText(req);
|
|
405
|
+
let id = '';
|
|
406
|
+
try { id = String((JSON.parse(text || '{}') || {}).id || '').trim(); } catch { id = ''; }
|
|
407
|
+
if (!id || /[\\/]/.test(id) || id.includes('..') || id.length > 120) { sendJson(res, 400, { ok: false, error: 'bad-id' }); return; }
|
|
408
|
+
const src = join(externalPetsDir(), id);
|
|
409
|
+
if (!existsSync(src)) { sendJson(res, 404, { ok: false, error: 'not-found' }); return; }
|
|
410
|
+
if (!readPet(src, id)) { sendJson(res, 422, { ok: false, error: 'invalid-pet' }); return; }
|
|
411
|
+
const dst = join(dataRoot(), id);
|
|
412
|
+
try {
|
|
413
|
+
ensureDir(dirname(dst));
|
|
414
|
+
cpSync(src, dst, { recursive: true, force: true });
|
|
415
|
+
} catch (e) { sendJson(res, 500, { ok: false, error: 'copy-failed', detail: String((e && e.message) || e) }); return; }
|
|
416
|
+
const pet = readPet(dst, id);
|
|
417
|
+
if (!pet) { sendJson(res, 422, { ok: false, error: 'install-invalid' }); return; }
|
|
418
|
+
const cfg = writeConfig({ enabled: true, petId: id });
|
|
419
|
+
if (typeof log === 'function') log('[pets] imported ' + id + ' from ' + externalPetsDir());
|
|
420
|
+
sendJson(res, 200, { ok: true, id, pet, config: cfg });
|
|
421
|
+
},
|
|
422
|
+
}));
|
|
423
|
+
|
|
358
424
|
// ── POST /pets/config(开关 / 选中 / 缩放) ───────────────────────────────
|
|
359
425
|
disposers.push(webServer.register({
|
|
360
426
|
kind: 'exact',
|