cortico-world-hands 0.20.2 → 0.20.4

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.
Files changed (2) hide show
  1. package/dist/index.js +223 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -233,9 +233,9 @@ var HANDS_LOOK_CONFIG_GROUP = {
233
233
  };
234
234
 
235
235
  // src/world.ts
236
- import { writeFileSync as writeFileSync2 } from "node:fs";
236
+ import { writeFileSync as writeFileSync4 } from "node:fs";
237
237
  import { fileURLToPath } from "node:url";
238
- import { join as join2 } from "node:path";
238
+ import { join as join4 } from "node:path";
239
239
 
240
240
  // src/policy.ts
241
241
  var PASS_RULES = [
@@ -513,9 +513,167 @@ var Honcho = class {
513
513
  }
514
514
  };
515
515
 
516
- // src/stores.ts
517
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
516
+ // src/sync.ts
517
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
518
518
  import { join } from "node:path";
519
+ var START = "<!--yixin:sync:start-->";
520
+ var END = "<!--yixin:sync:end-->";
521
+ function readMaybe(path) {
522
+ try {
523
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
524
+ } catch {
525
+ return "";
526
+ }
527
+ }
528
+ function mergeMemorySection(file, rows) {
529
+ const body = rows.filter((r) => r && r.key && r.value).map((r) => `- **${r.key}**\uFF1A${r.value.replace(/\s+/g, " ").trim()}`).join("\n");
530
+ const section = `${START}
531
+ ## \u4ECE\u61FF\u5FC3\u540C\u6B65\u8FC7\u6765\u7684\u8BB0\u5FC6\uFF08${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\uFF09
532
+ ${body}
533
+ ${END}`;
534
+ const old = readMaybe(file);
535
+ let next;
536
+ if (old.includes(START) && old.includes(END)) {
537
+ next = old.slice(0, old.indexOf(START)) + section + old.slice(old.indexOf(END) + END.length);
538
+ } else {
539
+ next = (old ? old.trimEnd() + "\n\n" : "") + section + "\n";
540
+ }
541
+ if (next === old) return { lines: rows.length, wrote: false };
542
+ writeFileSync(file, next);
543
+ return { lines: rows.length, wrote: true };
544
+ }
545
+ function readPersona(dir) {
546
+ for (const name of ["CONSTITUTION.md", "constitution.md"]) {
547
+ const text = readMaybe(join(dir, name));
548
+ if (text.trim()) return text;
549
+ }
550
+ return "";
551
+ }
552
+
553
+ // src/skin.ts
554
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, unlinkSync, writeFileSync as writeFileSync2, copyFileSync } from "node:fs";
555
+ import { dirname, join as join2, resolve } from "node:path";
556
+ function findWhaleDir(roots) {
557
+ for (const root of roots) {
558
+ const candidates = [
559
+ join2(root, "web", "whale"),
560
+ join2(root, "pet", "web", "whale"),
561
+ join2(root, "vendor", "pet", "web", "whale")
562
+ ];
563
+ for (const dir of candidates) {
564
+ if (existsSync2(join2(dir, "model.json"))) return dir;
565
+ }
566
+ }
567
+ return null;
568
+ }
569
+ async function fetchBytes(url) {
570
+ const res = await fetch(url);
571
+ if (!res.ok) throw new Error(`\u4E0B\u8F7D\u5931\u8D25 ${res.status} ${url}`);
572
+ return Buffer.from(await res.arrayBuffer());
573
+ }
574
+ async function installSkinPack(baseUrl, roots, log) {
575
+ const base = baseUrl.replace(/\/+$/, "");
576
+ let manifest;
577
+ try {
578
+ manifest = JSON.parse((await fetchBytes(`${base}/manifest.json`)).toString("utf8"));
579
+ } catch (err) {
580
+ return { ok: false, detail: `\u62FF\u4E0D\u5230 manifest\uFF1A${err.message}` };
581
+ }
582
+ if (!manifest?.id || !Array.isArray(manifest.files)) {
583
+ return { ok: false, detail: "manifest \u683C\u5F0F\u4E0D\u5BF9\uFF1A\u8981 id \u548C files" };
584
+ }
585
+ const whaleDir = findWhaleDir(roots);
586
+ if (!whaleDir) {
587
+ return { ok: false, detail: `\u6CA1\u627E\u5230 web/whale/model.json\uFF0C\u627E\u8FC7\uFF1A${roots.join(" / ")}` };
588
+ }
589
+ try {
590
+ for (const file of manifest.files) {
591
+ const target = resolve(whaleDir, file.path);
592
+ if (!target.startsWith(resolve(whaleDir))) {
593
+ return { ok: false, detail: `\u8DEF\u5F84\u8D8A\u754C\uFF1A${file.path}` };
594
+ }
595
+ mkdirSync(dirname(target), { recursive: true });
596
+ writeFileSync2(target, await fetchBytes(`${base}/${file.path}`));
597
+ }
598
+ const modelPath = join2(whaleDir, "model.json");
599
+ const model = JSON.parse(readFileSync2(modelPath, "utf8"));
600
+ const outfits = Array.isArray(model.outfits) ? model.outfits : [];
601
+ const idx = outfits.findIndex((o) => o?.id === manifest.id);
602
+ if (idx >= 0) outfits[idx] = manifest.outfit;
603
+ else outfits.push(manifest.outfit);
604
+ model.outfits = outfits;
605
+ copyFileSync(modelPath, `${modelPath}.bak-${Date.now()}`);
606
+ writeFileSync2(modelPath, JSON.stringify(model, null, 2));
607
+ log?.info(`\u76AE\u80A4\u88C5\u597D\u4E86\uFF1A${manifest.id}\uFF08web \u76EE\u5F55 ${whaleDir}\uFF09`);
608
+ return { ok: true, detail: `\u88C5\u597D\u4E86\u300C${manifest.name || manifest.id}\u300D\uFF0C\u5171 ${manifest.files.length} \u4E2A\u6587\u4EF6`, webDir: whaleDir };
609
+ } catch (err) {
610
+ log?.warn(`\u76AE\u80A4\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
611
+ return { ok: false, detail: `\u88C5\u5230\u4E00\u534A\u5931\u8D25\uFF1A${err.message}` };
612
+ }
613
+ }
614
+ function cleanMedia(mediaDir, opts = {}) {
615
+ const keepDays = opts.keepDays ?? 1;
616
+ const maxMB = opts.maxMB ?? 0;
617
+ const out = { removed: 0, freedMB: 0, kept: 0, dir: mediaDir };
618
+ let names = [];
619
+ try {
620
+ names = readdirSync(mediaDir);
621
+ } catch {
622
+ return out;
623
+ }
624
+ const files = [];
625
+ for (const name of names) {
626
+ const full = join2(mediaDir, name);
627
+ try {
628
+ const st = statSync(full);
629
+ if (!st.isFile()) continue;
630
+ if (!/\.(jpg|jpeg|png|webp)$/i.test(name)) continue;
631
+ files.push({ name, full, size: st.size, mtime: st.mtimeMs });
632
+ } catch {
633
+ }
634
+ }
635
+ const cutoff = Date.now() - keepDays * 864e5;
636
+ let totalMB = files.reduce((sum, f) => sum + f.size, 0) / 1024 / 1024;
637
+ for (const f of files) {
638
+ if (f.mtime >= cutoff) continue;
639
+ if (!opts.dryRun) {
640
+ try {
641
+ unlinkSync(f.full);
642
+ } catch {
643
+ continue;
644
+ }
645
+ }
646
+ out.removed += 1;
647
+ out.freedMB += f.size / 1024 / 1024;
648
+ totalMB -= f.size / 1024 / 1024;
649
+ }
650
+ if (maxMB > 0 && totalMB > maxMB) {
651
+ const rest = files.filter((f) => f.mtime >= cutoff).sort((a, b) => a.mtime - b.mtime);
652
+ for (const f of rest) {
653
+ if (totalMB <= maxMB) break;
654
+ if (!opts.dryRun) {
655
+ try {
656
+ unlinkSync(f.full);
657
+ } catch {
658
+ continue;
659
+ }
660
+ }
661
+ out.removed += 1;
662
+ out.freedMB += f.size / 1024 / 1024;
663
+ totalMB -= f.size / 1024 / 1024;
664
+ }
665
+ }
666
+ try {
667
+ out.kept = readdirSync(mediaDir).filter((n) => /\.(jpg|jpeg|png|webp)$/i.test(n)).length;
668
+ } catch {
669
+ }
670
+ out.freedMB = Math.round(out.freedMB * 10) / 10;
671
+ return out;
672
+ }
673
+
674
+ // src/stores.ts
675
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
676
+ import { join as join3 } from "node:path";
519
677
  var JsonStore = class {
520
678
  file;
521
679
  fallback;
@@ -523,16 +681,16 @@ var JsonStore = class {
523
681
  constructor(dir, name, fallback) {
524
682
  this.fallback = fallback;
525
683
  try {
526
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
684
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
527
685
  } catch {
528
686
  }
529
- this.file = join(dir, name);
687
+ this.file = join3(dir, name);
530
688
  }
531
689
  read() {
532
690
  if (this.cached) return this.cached;
533
691
  try {
534
- if (existsSync(this.file)) {
535
- const raw = JSON.parse(readFileSync(this.file, "utf8"));
692
+ if (existsSync3(this.file)) {
693
+ const raw = JSON.parse(readFileSync3(this.file, "utf8"));
536
694
  this.cached = raw ?? this.fallback;
537
695
  return this.cached;
538
696
  }
@@ -544,7 +702,7 @@ var JsonStore = class {
544
702
  write(value) {
545
703
  this.cached = value;
546
704
  try {
547
- writeFileSync(this.file, JSON.stringify(value, null, 2));
705
+ writeFileSync3(this.file, JSON.stringify(value, null, 2));
548
706
  } catch {
549
707
  }
550
708
  }
@@ -552,7 +710,7 @@ var JsonStore = class {
552
710
 
553
711
  // src/hands.ts
554
712
  import { execFile } from "node:child_process";
555
- import { readFileSync as readFileSync2 } from "node:fs";
713
+ import { readFileSync as readFileSync4 } from "node:fs";
556
714
  import { get as httpGet, request as httpRequest } from "node:http";
557
715
  var ACTION_TIMEOUT_MS = 6e3;
558
716
  var PROBE_TTL_MS = 2e4;
@@ -562,12 +720,12 @@ var MAX_GESTURE_MS = 2e4;
562
720
  var HandError = class extends Error {
563
721
  };
564
722
  function get(port, path, timeoutMs) {
565
- return new Promise((resolve, reject) => {
723
+ return new Promise((resolve2, reject) => {
566
724
  const req = httpGet({ host: "127.0.0.1", port, path, timeout: timeoutMs }, (res) => {
567
725
  const chunks = [];
568
726
  res.on("data", (chunk) => chunks.push(chunk));
569
727
  res.on("end", () => {
570
- resolve({
728
+ resolve2({
571
729
  status: res.statusCode ?? 0,
572
730
  body: Buffer.concat(chunks),
573
731
  header: (name) => {
@@ -582,7 +740,7 @@ function get(port, path, timeoutMs) {
582
740
  });
583
741
  }
584
742
  async function post(port, path, body, timeoutMs) {
585
- return new Promise((resolve, reject) => {
743
+ return new Promise((resolve2, reject) => {
586
744
  const data = Buffer.from(body, "utf8");
587
745
  const req = httpRequest(
588
746
  {
@@ -597,7 +755,7 @@ async function post(port, path, body, timeoutMs) {
597
755
  const chunks = [];
598
756
  res.on("data", (chunk) => chunks.push(chunk));
599
757
  res.on("end", () => {
600
- resolve({
758
+ resolve2({
601
759
  status: res.statusCode ?? 0,
602
760
  body: Buffer.concat(chunks),
603
761
  header: (name) => {
@@ -690,10 +848,10 @@ var RootActuator = class {
690
848
  this.log = log;
691
849
  }
692
850
  run(argv, timeoutMs = ACTION_TIMEOUT_MS) {
693
- return new Promise((resolve) => {
851
+ return new Promise((resolve2) => {
694
852
  execFile(argv[0], argv.slice(1), { timeout: timeoutMs, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
695
853
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
696
- resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
854
+ resolve2({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
697
855
  });
698
856
  });
699
857
  }
@@ -739,10 +897,10 @@ var ShellActuator = class {
739
897
  if (!template) return { ok: false, detail: "shell \u901A\u9053\u6CA1\u914D\u6A21\u677F" };
740
898
  if (!template.includes("{cmd}")) return { ok: false, detail: "shell \u6A21\u677F\u91CC\u6CA1\u6709 {cmd}\uFF0C\u4E0D\u77E5\u9053\u628A\u547D\u4EE4\u653E\u54EA" };
741
899
  const probe = template.replace(/\{cmd\}/g, "id");
742
- const r = await new Promise((resolve) => {
900
+ const r = await new Promise((resolve2) => {
743
901
  execFile("sh", ["-c", probe], { timeout: 8e3, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
744
902
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
745
- resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
903
+ resolve2({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
746
904
  });
747
905
  });
748
906
  if (r.code !== 0) return { ok: false, detail: flat(r.err || r.out, 140) || "\u6A21\u677F\u8DD1\u4E0D\u8D77\u6765" };
@@ -751,10 +909,10 @@ var ShellActuator = class {
751
909
  }
752
910
  async apply(command) {
753
911
  const argv = this.argv(command);
754
- const r = await new Promise((resolve) => {
912
+ const r = await new Promise((resolve2) => {
755
913
  execFile(argv[0], argv.slice(1), { timeout: ACTION_TIMEOUT_MS, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
756
914
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
757
- resolve({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
915
+ resolve2({ code, out: String(stdout ?? ""), err: String(stderr ?? "") });
758
916
  });
759
917
  });
760
918
  if (r.code !== 0) throw new HandError(`\u901A\u9053\u547D\u4EE4\u6CA1\u6210\u529F\uFF08${flat(r.err || r.out, 160) || `\u9000\u51FA\u7801 ${r.code}`}\uFF09\u3002`);
@@ -858,6 +1016,20 @@ var EyesClient = class {
858
1016
  }
859
1017
  return { ok: body.ok !== false, pkg: flat(body.pkg, 120), items };
860
1018
  }
1019
+ /** 只截一块并放大:x,y,w,h 是真实像素。 */
1020
+ async zoom(x, y, w, h, maxWidth = 1080, quality = 80) {
1021
+ const cfg = this.cfg();
1022
+ const path = `/screen.jpg?x=${Math.max(0, Math.round(x))}&y=${Math.max(0, Math.round(y))}&w=${Math.max(8, Math.round(w))}&h=${Math.max(8, Math.round(h))}&max=${Math.round(maxWidth)}&q=${Math.round(quality)}`;
1023
+ const res = await get(this.handPort(), path, 12e3);
1024
+ if (res.status !== 200 || res.body.length === 0) return null;
1025
+ return {
1026
+ bytes: new Uint8Array(res.body),
1027
+ width: Number.parseInt(res.header("x-width"), 10) || 0,
1028
+ height: Number.parseInt(res.header("x-height"), 10) || 0,
1029
+ pkg: "",
1030
+ ageMs: 0
1031
+ };
1032
+ }
861
1033
  async frame(maxWidth, quality) {
862
1034
  try {
863
1035
  const w = Math.max(160, Math.min(2160, Math.round(maxWidth)));
@@ -953,7 +1125,7 @@ var Hands = class {
953
1125
  if (Date.now() - this.tokenCache.at < 3e4) return this.tokenCache.value;
954
1126
  let value = "";
955
1127
  try {
956
- const text = readFileSync2(file, "utf8").trim();
1128
+ const text = readFileSync4(file, "utf8").trim();
957
1129
  if (text && text.length <= 200) value = text;
958
1130
  } catch {
959
1131
  value = "";
@@ -1964,6 +2136,21 @@ var HANDS_TOOL_DECLS = [
1964
2136
  },
1965
2137
  required: ["content"]
1966
2138
  }
2139
+ },
2140
+ {
2141
+ name: "hand_zoom",
2142
+ tags: ["read"],
2143
+ description: "\u53EA\u622A\u5C4F\u5E55\u7684\u4E00\u5C0F\u5757\u5E76\u653E\u5927\u2014\u2014\u5C0F\u5B57\u3001\u5C0F\u6309\u94AE\u770B\u4E0D\u6E05\u65F6\u7528\u5B83\u3002\u5148 hand_see \u62FF\u5230\u5750\u6807\uFF0C\u518D\u628A\u8FD9\u5757\u7684 x/y/\u5BBD/\u9AD8 \u4EA4\u7ED9\u5B83\u3002",
2144
+ parameters: {
2145
+ type: "object",
2146
+ properties: {
2147
+ x: { type: "integer", description: "\u5DE6\u4E0A\u89D2\u6A2A\u5750\u6807\uFF08\u771F\u5B9E\u50CF\u7D20\uFF09" },
2148
+ y: { type: "integer", description: "\u5DE6\u4E0A\u89D2\u7EB5\u5750\u6807" },
2149
+ w: { type: "integer", description: "\u5BBD" },
2150
+ h: { type: "integer", description: "\u9AD8" }
2151
+ },
2152
+ required: ["x", "y", "w", "h"]
2153
+ }
1967
2154
  }
1968
2155
  ];
1969
2156
 
@@ -2085,8 +2272,8 @@ var HandsWorld = class {
2085
2272
  try {
2086
2273
  const old = readPersona(dir);
2087
2274
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2088
- if (old) writeFileSync2(join2(dir, `CONSTITUTION.bak.${stamp}.md`), old);
2089
- writeFileSync2(join2(dir, "CONSTITUTION.md"), content);
2275
+ if (old) writeFileSync4(join4(dir, `CONSTITUTION.bak.${stamp}.md`), old);
2276
+ writeFileSync4(join4(dir, "CONSTITUTION.md"), content);
2090
2277
  return `\u4EBA\u683C\u6362\u597D\u4E86\uFF08\u65E7\u7248\u5907\u4EFD\u6210 CONSTITUTION.bak.${stamp}.md\uFF09`;
2091
2278
  } catch (err) {
2092
2279
  return failed("\u5199\u4E0D\u8FDB\u53BB\uFF1A" + String(err).slice(0, 120));
@@ -2101,7 +2288,7 @@ var HandsWorld = class {
2101
2288
  try {
2102
2289
  const rows = await this.hands.memorySearch();
2103
2290
  lines = rows.length;
2104
- const target = join2(dir, "\u8BB0\u5FC6\u5E93.md");
2291
+ const target = join4(dir, "\u8BB0\u5FC6\u5E93.md");
2105
2292
  const r = mergeMemorySection(target, rows);
2106
2293
  if (r.wrote) wrote.push("\u8BB0\u5FC6\u5E93.md");
2107
2294
  const persona = readPersona(dir);
@@ -2272,7 +2459,7 @@ ${lines.join("\n")}
2272
2459
  mediaDir() {
2273
2460
  const base = this.opts.dataDir;
2274
2461
  if (!base) return null;
2275
- const dir = base.endsWith("/data") ? join2(base, "media") : join2(base, "data", "media");
2462
+ const dir = base.endsWith("/data") ? join4(base, "media") : join4(base, "data", "media");
2276
2463
  return dir;
2277
2464
  }
2278
2465
  /** 钩子 5:QQ 在后台收到的消息,原样递给她;谁来的一眼能看清。 */
@@ -2372,6 +2559,17 @@ ${lines.join("\n")}
2372
2559
  return await this.runSync(true);
2373
2560
  case "sync_status":
2374
2561
  return this.lastSync ? `\u4E0A\u6B21\u540C\u6B65 ${this.lastSync.at.slice(0, 19)}\uFF0C\u8BB0\u5FC6 ${this.lastSync.lines} \u6761\uFF0C\u4EBA\u683C ${this.lastSync.persona} \u5B57` : "\u8FD8\u6CA1\u540C\u6B65\u8FC7";
2562
+ case "hand_zoom": {
2563
+ const n = (v) => Math.round(Number(v));
2564
+ const x = n(args.x), y = n(args.y), w = n(args.w), h = n(args.h);
2565
+ if (![x, y, w, h].every(Number.isFinite) || w <= 0 || h <= 0) return failed("\u8981\u7ED9 x/y/w/h\uFF08\u771F\u5B9E\u50CF\u7D20\uFF09");
2566
+ const f = await this.hands.zoom(x, y, w, h);
2567
+ if (!f) return failed("\u8FD9\u4E00\u5757\u6CA1\u622A\u5230\uFF08\u5750\u6807\u53EF\u80FD\u8D85\u51FA\u5C4F\u5E55\uFF09");
2568
+ return {
2569
+ text: `\u653E\u5927\u770B @(${x},${y}) ${w}\xD7${h}`,
2570
+ blobs: [{ bytes: f.bytes, mime: "image/jpeg", name: "zoom.jpg", fallbackText: "\uFF08\u8FD9\u662F\u4ED6\u6307\u5B9A\u90A3\u4E00\u5757\u653E\u5927\u540E\u7684\u622A\u56FE\uFF09" }]
2571
+ };
2572
+ }
2375
2573
  case "hand_see":
2376
2574
  return await this.see();
2377
2575
  case "hand_look":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-hands",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "type": "module",
5
5
  "description": "Cortico World: hands for the desktop pet — looks at the phone screen, then taps, swipes, pinches, drags, types and presses keys through whatever actuator the device actually has",
6
6
  "license": "MIT",