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 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) => fetch(ROUTE + '/theme-import', {
1723
- method: 'POST',
1724
- headers: { 'content-type': 'application/zip' },
1725
- body: buffer,
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.64';
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
- const body = await readBody(req);
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
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pighub",
3
- "version": "0.9.64",
3
+ "version": "0.9.66",
4
4
  "description": "猪猪库 PigHub 的 DeepSeek Harness 插件:侧边栏猪圈面板、每日一只猪猪、重复获得升星、可拖出的置顶桌宠,数据落在 ~/.PigHub。不内嵌任何上游图片。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",