dsh-pighub 0.9.64 → 0.9.66
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 +12 -5
- package/lib/index.js +52 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1718,12 +1718,19 @@ window.__ModuleLoader__.load({
|
|
|
1718
1718
|
const file = input && input.files && input.files[0];
|
|
1719
1719
|
if (!file) return;
|
|
1720
1720
|
const finish = () => { if (input) input.value = ''; };
|
|
1721
|
+
// 走状态路由上的 action(不走单独的 /theme-import:宿主的 exact 路由按前缀先命中,
|
|
1722
|
+
// 二进制会被状态路由当 JSON 解析)。base64 多 33%,但一定命中。
|
|
1721
1723
|
file.arrayBuffer()
|
|
1722
|
-
.then((buffer) =>
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1724
|
+
.then((buffer) => {
|
|
1725
|
+
let binary = '';
|
|
1726
|
+
const bytes = new Uint8Array(buffer);
|
|
1727
|
+
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
|
|
1728
|
+
return fetch(ROUTE, {
|
|
1729
|
+
method: 'POST',
|
|
1730
|
+
headers: { 'content-type': 'application/json' },
|
|
1731
|
+
body: JSON.stringify({ action: 'theme-import', zipBase64: btoa(binary) }),
|
|
1732
|
+
});
|
|
1733
|
+
})
|
|
1727
1734
|
.then((response) => response.json())
|
|
1728
1735
|
.then((payload) => { if (payload && payload.ok) apply(payload); })
|
|
1729
1736
|
.catch(() => {})
|
package/lib/index.js
CHANGED
|
@@ -43,7 +43,7 @@ export const inject = ['tools'];
|
|
|
43
43
|
import { startDesktopPet, stopDesktopPet, syncDesktopPet, desktopPetState, sayOnDesktop, reclaimStaleSessions } from './desktop-pet.js';
|
|
44
44
|
|
|
45
45
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
const VERSION = '0.9.
|
|
46
|
+
const VERSION = '0.9.66';
|
|
47
47
|
/** 桌面桌宠的精灵边长(CSS px 基准,= manifest 里 sizeRatio 的中位数)。 */
|
|
48
48
|
const DESKTOP_PET_SIZE = 96;
|
|
49
49
|
/** 全局尺寸系数(用户口径 2026-11-13:「整体猪猪缩小到 75% 大小」)。三层一起乘。 */
|
|
@@ -490,9 +490,59 @@ export function apply(ctx, config) {
|
|
|
490
490
|
try {
|
|
491
491
|
if (req.method === 'GET') { sendJson(res, 200, snapshot(root)); return; }
|
|
492
492
|
if (req.method !== 'POST') { sendJson(res, 405, { ok: false, error: 'method_not_allowed' }); return; }
|
|
493
|
-
|
|
493
|
+
/* 状态路由的 body 上限提到 24 MB:主题包导入是 base64(738 KB 的 zip → 约 1 MB 的报文),
|
|
494
|
+
* 默认 64 KB 会把它判成 body_too_large。这是个只监听回环的本地服务,放宽是安全的。 */
|
|
495
|
+
const body = await readBody(req, 24 * 1024 * 1024);
|
|
494
496
|
const action = typeof body.action === 'string' ? body.action : 'state';
|
|
495
497
|
|
|
498
|
+
/* 主题包导入走**状态路由上的 action**(用户口径 2026-11-14)。
|
|
499
|
+
* 为什么不另开 /pighub/theme-import:宿主的 exact 路由按前缀先命中,二进制 zip 会被
|
|
500
|
+
* 这条状态路由抢走当 JSON 解析(实测报 "Unexpected token 'P'",P 就是 zip 魔数 PK)。
|
|
501
|
+
* base64 多 33% 传输,换来路由一定命中。 */
|
|
502
|
+
if (action === 'theme-import') {
|
|
503
|
+
const b64 = typeof body.zipBase64 === 'string' ? body.zipBase64 : '';
|
|
504
|
+
if (!b64) { sendJson(res, 200, { ok: false, error: 'empty_body' }); return; }
|
|
505
|
+
let raw = null;
|
|
506
|
+
try { raw = Buffer.from(b64, 'base64'); } catch { raw = null; }
|
|
507
|
+
if (!raw || !raw.length) { sendJson(res, 200, { ok: false, error: 'bad_base64' }); return; }
|
|
508
|
+
const parsed = readZip(raw);
|
|
509
|
+
if (!parsed.ok) { sendJson(res, 200, { ok: false, error: parsed.reason }); return; }
|
|
510
|
+
const jsonEntry = parsed.files.find((file) => /(^|\/)theme\.json$/i.test(file.name));
|
|
511
|
+
if (!jsonEntry) { sendJson(res, 200, { ok: false, error: 'theme_json_missing' }); return; }
|
|
512
|
+
const prefix = jsonEntry.name.replace(/theme\.json$/i, '');
|
|
513
|
+
let meta;
|
|
514
|
+
try { meta = JSON.parse(jsonEntry.data.toString('utf8')); } catch { sendJson(res, 200, { ok: false, error: 'theme_json_invalid' }); return; }
|
|
515
|
+
const themeName = String(meta && meta.name ? meta.name : prefix.replace(/\/$/, '')).trim();
|
|
516
|
+
if (!/^[a-zA-Z0-9._-]{1,48}$/.test(themeName) || themeName === CLASSIC_THEME_NAME) {
|
|
517
|
+
sendJson(res, 200, { ok: false, error: 'theme_name_invalid', name: themeName });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const targetDir = join(userThemesDir, themeName);
|
|
521
|
+
const staging = join(userThemesDir, '.' + themeName + '.tmp-' + Date.now());
|
|
522
|
+
try {
|
|
523
|
+
rmSync(staging, { recursive: true, force: true });
|
|
524
|
+
mkdirSync(staging, { recursive: true });
|
|
525
|
+
for (const file of parsed.files) {
|
|
526
|
+
const rel = file.name.startsWith(prefix) ? file.name.slice(prefix.length) : file.name;
|
|
527
|
+
if (!rel || rel.endsWith('/')) continue;
|
|
528
|
+
if (!/\.(json|png|jpg|jpeg|webp|svg)$/i.test(rel)) continue;
|
|
529
|
+
if (!isSafeZipName(rel)) { sendJson(res, 200, { ok: false, error: 'unsafe_entry_name', name: file.name }); return; }
|
|
530
|
+
const target = join(staging, rel);
|
|
531
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
532
|
+
writeFileSync(target, file.data);
|
|
533
|
+
}
|
|
534
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
535
|
+
renameSync(staging, targetDir);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
try { rmSync(staging, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
538
|
+
sendJson(res, 200, { ok: false, error: 'write_failed', detail: String((error && error.message) || error).slice(0, 120) });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const nextPrefs2 = writePrefs(root, { theme: themeName });
|
|
542
|
+
sendJson(res, 200, { ...snapshot(root), ok: true, theme: themeName, prefs: nextPrefs2 });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
496
546
|
if (action === 'state') { sendJson(res, 200, snapshot(root)); return; }
|
|
497
547
|
|
|
498
548
|
/*
|