dsh-agentic-pet 0.3.2 → 0.4.1
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/README.md +17 -2
- package/README.zh.md +17 -2
- package/carrier/dist-renderer/assets/{index-C61Q_dyJ.js → index-BMH7auTh.js} +1 -1
- package/carrier/dist-renderer/index.html +1 -1
- package/carrier/lib/main.js +48 -26
- package/carrier/lib/preload.js +3 -1
- package/carrier/lib/tts.js +212 -0
- package/client/pet-settings.js +145 -10
- package/lib/index.js +126 -27
- package/lib/{service-Vj-lLpIv.js → service-BjTawwUn.js} +574 -86
- package/lib/tools.js +2 -2
- package/lib/types/index.d.ts +2 -2
- package/lib/types/inner-voice.d.ts +14 -0
- package/lib/types/petdata.d.ts +59 -0
- package/lib/types/registry.d.ts +2 -1
- package/package.json +3 -3
- package/presets/pet-inner-voice.patch.yml +3 -3
- package/presets/pet.patch.yml +3 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { appendFile, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
7
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
8
8
|
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
9
9
|
/** Return true when a value is `null` or `undefined`. */
|
|
@@ -4608,23 +4608,182 @@ function formatAvailableActions(available, cap = 24) {
|
|
|
4608
4608
|
return rest > 0 ? `${shown} …(+${String(rest)} more — pet_state {actions:true} lists all)` : shown;
|
|
4609
4609
|
}
|
|
4610
4610
|
//#endregion
|
|
4611
|
-
//#region lib/types/
|
|
4611
|
+
//#region lib/types/petdata.js
|
|
4612
4612
|
/**
|
|
4613
|
-
* The
|
|
4614
|
-
*
|
|
4615
|
-
*
|
|
4616
|
-
*
|
|
4617
|
-
*
|
|
4618
|
-
*
|
|
4613
|
+
* The per-pet data store: everything the DRIVER persists about a pet lives
|
|
4614
|
+
* under `~/.dsh/whale598/<petId>/` — affection ledger, display name, event
|
|
4615
|
+
* history, tool-call history. One directory per pet, small files, single
|
|
4616
|
+
* writer (the driver), JSONL for append-only streams with bounded retention.
|
|
4617
|
+
*
|
|
4618
|
+
* Legacy layouts (`pet-affection.d/<id>.json`, the single legacy electron
|
|
4619
|
+
* book, names inside pet-settings.json) are READ as fallbacks and migrate to
|
|
4620
|
+
* the new layout on the next write — lazy migration, no data-loss window.
|
|
4621
|
+
* @module dsh-agentic-pet/petdata
|
|
4619
4622
|
*/
|
|
4623
|
+
/** The consolidated per-pet data root under the dsh home. */
|
|
4624
|
+
const WHALE_DIR = "whale598";
|
|
4625
|
+
function petDataDir(home, petId) {
|
|
4626
|
+
return join(home, WHALE_DIR, petId);
|
|
4627
|
+
}
|
|
4628
|
+
/** New-layout path first; legacy layouts as read fallbacks (per-pet legacy
|
|
4629
|
+
* dir → the single legacy electron book). The ledger SHAPE is the driver's
|
|
4630
|
+
* (TouchLedger); this module only owns paths and raw IO. */
|
|
4631
|
+
function affectionPaths(home, petId) {
|
|
4632
|
+
return {
|
|
4633
|
+
current: join(petDataDir(home, petId), "pet-affection.json"),
|
|
4634
|
+
legacy: [join(home, "pet-affection.d", `${petId}.json`), ...petId === "electron" ? [join(home, "pet-affection.json")] : []]
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
/** Read the affection ledger raw: the current file wins; legacy files answer
|
|
4638
|
+
* only while the current one is absent (first write migrates the full
|
|
4639
|
+
* ledger). Returns null when no layout holds data. */
|
|
4640
|
+
async function readAffectionRaw(home, petId) {
|
|
4641
|
+
const { current, legacy } = affectionPaths(home, petId);
|
|
4642
|
+
for (const file of [current, ...legacy]) try {
|
|
4643
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
4644
|
+
if (parsed !== null && typeof parsed === "object" && Object.keys(parsed).length > 0) return parsed;
|
|
4645
|
+
} catch {}
|
|
4646
|
+
return null;
|
|
4647
|
+
}
|
|
4648
|
+
async function writeAffectionRaw(home, petId, ledger) {
|
|
4649
|
+
const dir = petDataDir(home, petId);
|
|
4650
|
+
await mkdir(dir, { recursive: true });
|
|
4651
|
+
await writeFile(join(dir, "pet-affection.json"), `${JSON.stringify(ledger)}\n`, "utf8");
|
|
4652
|
+
}
|
|
4653
|
+
function petNameFile(home, petId) {
|
|
4654
|
+
return join(petDataDir(home, petId), "pet-name.json");
|
|
4655
|
+
}
|
|
4656
|
+
/** The user's custom display name: new-layout file → legacy
|
|
4657
|
+
* pet-settings.json `names` map → undefined (caller applies the built-in
|
|
4658
|
+
* default). */
|
|
4659
|
+
async function readPetName(home, petId) {
|
|
4660
|
+
try {
|
|
4661
|
+
const parsed = JSON.parse(await readFile(petNameFile(home, petId), "utf8"));
|
|
4662
|
+
if (typeof parsed.name === "string" && parsed.name.trim() !== "") return parsed.name.trim();
|
|
4663
|
+
} catch {}
|
|
4664
|
+
try {
|
|
4665
|
+
const settings = JSON.parse(await readFile(join(home, "pet-settings.json"), "utf8"));
|
|
4666
|
+
const label = (typeof settings.names === "object" && settings.names !== null ? settings.names : {})[petId];
|
|
4667
|
+
if (typeof label === "string" && label.trim() !== "") return label.trim();
|
|
4668
|
+
} catch {}
|
|
4669
|
+
}
|
|
4670
|
+
/** Persist a custom name; `null` reverts to the built-in default (drops the
|
|
4671
|
+
* override file). Also strips the petId key from the legacy pet-settings
|
|
4672
|
+
* names map so the old layout cannot resurrect a cleared name. */
|
|
4673
|
+
async function writePetName(home, petId, name) {
|
|
4674
|
+
await mkdir(petDataDir(home, petId), { recursive: true });
|
|
4675
|
+
const file = petNameFile(home, petId);
|
|
4676
|
+
if (name === null || name.trim() === "") await rm(file, { force: true });
|
|
4677
|
+
else await writeFile(file, `${JSON.stringify({ name: name.trim() })}\n`, "utf8");
|
|
4678
|
+
try {
|
|
4679
|
+
const settingsPath = join(home, "pet-settings.json");
|
|
4680
|
+
const settings = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
4681
|
+
if (typeof settings.names === "object" && settings.names !== null && petId in settings.names) {
|
|
4682
|
+
Reflect.deleteProperty(settings.names, petId);
|
|
4683
|
+
if (Object.keys(settings.names).length === 0) delete settings.names;
|
|
4684
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
4685
|
+
}
|
|
4686
|
+
} catch {}
|
|
4687
|
+
}
|
|
4688
|
+
const JSONL_MAX_LINES = 600;
|
|
4689
|
+
const JSONL_KEEP_LINES = 500;
|
|
4690
|
+
function eventsFile(home, petId) {
|
|
4691
|
+
return join(petDataDir(home, petId), "events.jsonl");
|
|
4692
|
+
}
|
|
4693
|
+
/** Append one record; bounded retention trims the file when it outgrows the
|
|
4694
|
+
* cap (best-effort — a failed trim never loses the append). */
|
|
4695
|
+
async function appendJsonl(file, entry) {
|
|
4696
|
+
await mkdir(dirname(file), { recursive: true });
|
|
4697
|
+
await appendFile(file, `${JSON.stringify(entry)}\n`, "utf8");
|
|
4698
|
+
try {
|
|
4699
|
+
const lines = (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "");
|
|
4700
|
+
if (lines.length > JSONL_MAX_LINES) await writeFile(file, `${lines.slice(lines.length - JSONL_KEEP_LINES).join("\n")}\n`, "utf8");
|
|
4701
|
+
} catch {}
|
|
4702
|
+
}
|
|
4703
|
+
/** The last `limit` records (oldest last); malformed lines degrade to
|
|
4704
|
+
* `{raw}` instead of dropping the file. */
|
|
4705
|
+
async function readJsonl(file, limit) {
|
|
4706
|
+
try {
|
|
4707
|
+
return (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "").slice(-limit).map((line) => {
|
|
4708
|
+
try {
|
|
4709
|
+
return JSON.parse(line);
|
|
4710
|
+
} catch {
|
|
4711
|
+
return { raw: line };
|
|
4712
|
+
}
|
|
4713
|
+
});
|
|
4714
|
+
} catch {
|
|
4715
|
+
return [];
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
const GLOBAL_STATE_FILES = [
|
|
4719
|
+
"pet-settings.json",
|
|
4720
|
+
"pet-inner-voice.json",
|
|
4721
|
+
"pet-interactions.json",
|
|
4722
|
+
"pet-world.json",
|
|
4723
|
+
"pet-window.json"
|
|
4724
|
+
];
|
|
4725
|
+
function globalPaths(home, name) {
|
|
4726
|
+
return {
|
|
4727
|
+
current: join(home, WHALE_DIR, name),
|
|
4728
|
+
legacy: join(home, name)
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
function globalFilePath(home, name) {
|
|
4732
|
+
return globalPaths(home, name).current;
|
|
4733
|
+
}
|
|
4734
|
+
/** Rename legacy → current for every global file that hasn't moved yet.
|
|
4735
|
+
* Best-effort: a failed rename leaves the legacy copy in place and the read
|
|
4736
|
+
* fallback still answers. Called once at driver install. */
|
|
4737
|
+
async function migrateGlobals(home) {
|
|
4738
|
+
await mkdir(join(home, WHALE_DIR), { recursive: true });
|
|
4739
|
+
for (const name of GLOBAL_STATE_FILES) {
|
|
4740
|
+
const { current, legacy } = globalPaths(home, name);
|
|
4741
|
+
try {
|
|
4742
|
+
if (existsSync(current) || !existsSync(legacy)) continue;
|
|
4743
|
+
await rename(legacy, current);
|
|
4744
|
+
} catch {}
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
/** Read a global state file: current → legacy → {} (callers' defensive
|
|
4748
|
+
* field checks treat {} exactly like a missing file). */
|
|
4749
|
+
async function readGlobalJson(home, name) {
|
|
4750
|
+
const { current, legacy } = globalPaths(home, name);
|
|
4751
|
+
for (const file of [current, legacy]) try {
|
|
4752
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
4753
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
4754
|
+
} catch {}
|
|
4755
|
+
return {};
|
|
4756
|
+
}
|
|
4757
|
+
/** Write a global state file (current location only; pretty-printed — these
|
|
4758
|
+
* files are user-inspectable and hand-editable). */
|
|
4759
|
+
async function writeGlobalJson(home, name, obj) {
|
|
4760
|
+
const { current } = globalPaths(home, name);
|
|
4761
|
+
await mkdir(dirname(current), { recursive: true });
|
|
4762
|
+
await writeFile(current, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
|
|
4763
|
+
}
|
|
4764
|
+
//#endregion
|
|
4765
|
+
//#region lib/types/inner-voice.js
|
|
4620
4766
|
const VOICE_SESSION_PREFIX = "whale598-";
|
|
4767
|
+
/**
|
|
4768
|
+
* Saved-setting migration for preset id renames: pet-settings.json holds the
|
|
4769
|
+
* user's 会话模式 choice by id, and a rename orphans that saved value (every
|
|
4770
|
+
* ensure would first try a dead id and fall back). Map the legacy ids onto
|
|
4771
|
+
* their successors; unknown ids pass through untouched.
|
|
4772
|
+
*/
|
|
4773
|
+
const LEGACY_PRESET_IDS = {
|
|
4774
|
+
"pet-inner-voice": "whale598",
|
|
4775
|
+
"pet": "whale598-standard"
|
|
4776
|
+
};
|
|
4777
|
+
function migratePreset(preset) {
|
|
4778
|
+
return LEGACY_PRESET_IDS[preset] ?? preset;
|
|
4779
|
+
}
|
|
4621
4780
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
4622
4781
|
const finite01 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? clamp(value, 0, 1) : fallback;
|
|
4623
4782
|
/** Read `innerVoice` from pet-settings.json; anything missing or mistyped
|
|
4624
4783
|
* folds to its default (defensive parse, `readAutonomy` precedent). */
|
|
4625
4784
|
async function readInnerVoiceConfig(home) {
|
|
4626
4785
|
try {
|
|
4627
|
-
const raw =
|
|
4786
|
+
const raw = await readGlobalJson(home, "pet-settings.json");
|
|
4628
4787
|
const v = typeof raw.innerVoice === "object" && raw.innerVoice !== null ? raw.innerVoice : {};
|
|
4629
4788
|
const legacyMinutes = typeof v.touchReplyCooldownMinutes === "number" && Number.isFinite(v.touchReplyCooldownMinutes) ? v.touchReplyCooldownMinutes : void 0;
|
|
4630
4789
|
const secondsRaw = typeof v.touchReplyCooldownSeconds === "number" && Number.isFinite(v.touchReplyCooldownSeconds) ? v.touchReplyCooldownSeconds : legacyMinutes !== void 0 ? legacyMinutes * 60 : void 0;
|
|
@@ -4634,7 +4793,7 @@ async function readInnerVoiceConfig(home) {
|
|
|
4634
4793
|
}
|
|
4635
4794
|
return {
|
|
4636
4795
|
enabled: v.enabled !== false,
|
|
4637
|
-
preset: typeof v.preset === "string" && v.preset !== "" ? v.preset : "
|
|
4796
|
+
preset: migratePreset(typeof v.preset === "string" && v.preset !== "" ? v.preset : "whale598"),
|
|
4638
4797
|
wakeIntervalMinutes: typeof v.wakeIntervalMinutes === "number" && Number.isFinite(v.wakeIntervalMinutes) ? clamp(Math.round(v.wakeIntervalMinutes), 1, 60) : 2,
|
|
4639
4798
|
speakProbability: finite01(v.speakProbability, .5),
|
|
4640
4799
|
touchReplyProbability: finite01(v.touchReplyProbability, .3),
|
|
@@ -4644,7 +4803,7 @@ async function readInnerVoiceConfig(home) {
|
|
|
4644
4803
|
} catch {
|
|
4645
4804
|
return {
|
|
4646
4805
|
enabled: true,
|
|
4647
|
-
preset: "
|
|
4806
|
+
preset: "whale598",
|
|
4648
4807
|
wakeIntervalMinutes: 2,
|
|
4649
4808
|
speakProbability: .5,
|
|
4650
4809
|
touchReplyProbability: .3,
|
|
@@ -4653,9 +4812,6 @@ async function readInnerVoiceConfig(home) {
|
|
|
4653
4812
|
};
|
|
4654
4813
|
}
|
|
4655
4814
|
}
|
|
4656
|
-
function innerVoiceStorePath(home) {
|
|
4657
|
-
return join(home, "pet-inner-voice.json");
|
|
4658
|
-
}
|
|
4659
4815
|
/** Defensive extraction of the persisted generation counters (petId → n). */
|
|
4660
4816
|
function generationsOf(raw) {
|
|
4661
4817
|
const out = {};
|
|
@@ -4666,7 +4822,7 @@ function generationsOf(raw) {
|
|
|
4666
4822
|
}
|
|
4667
4823
|
async function loadInnerVoiceStore(home) {
|
|
4668
4824
|
try {
|
|
4669
|
-
const raw =
|
|
4825
|
+
const raw = await readGlobalJson(home, "pet-inner-voice.json");
|
|
4670
4826
|
const sessions = {};
|
|
4671
4827
|
for (const [petId, entry] of Object.entries(typeof raw.sessions === "object" && raw.sessions !== null ? raw.sessions : {})) {
|
|
4672
4828
|
const record = entry;
|
|
@@ -4688,7 +4844,7 @@ async function loadInnerVoiceStore(home) {
|
|
|
4688
4844
|
}
|
|
4689
4845
|
async function saveInnerVoiceStore(home, store) {
|
|
4690
4846
|
try {
|
|
4691
|
-
await
|
|
4847
|
+
await writeGlobalJson(home, "pet-inner-voice.json", store);
|
|
4692
4848
|
} catch (error) {
|
|
4693
4849
|
console.error(`pet-surface inner-voice: could not write the store: ${error instanceof Error ? error.message : String(error)}`);
|
|
4694
4850
|
}
|
|
@@ -4731,10 +4887,44 @@ function mintVoiceSessionId(petId, generation, isArchived) {
|
|
|
4731
4887
|
function buildGreetPrompt(facts) {
|
|
4732
4888
|
return [
|
|
4733
4889
|
"[启动问好]",
|
|
4734
|
-
`用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)}
|
|
4890
|
+
`用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)};羁绊 ${facts.bondTier},一起过了 ${String(facts.bondDays)} 天)。`,
|
|
4735
4891
|
"主动打个招呼(pet_say)——按当下的时辰自然问好,一两句就好,别问问题等回答。"
|
|
4736
4892
|
].join(" ");
|
|
4737
4893
|
}
|
|
4894
|
+
/** B 层 · 羁绊档位:days-with-interaction → tier name. Only grows, never
|
|
4895
|
+
* decays; the names are the display contract (settings page keeps its own
|
|
4896
|
+
* copy of the thresholds). */
|
|
4897
|
+
const BOND_TIERS = [
|
|
4898
|
+
{
|
|
4899
|
+
since: 0,
|
|
4900
|
+
name: "陌生"
|
|
4901
|
+
},
|
|
4902
|
+
{
|
|
4903
|
+
since: 5,
|
|
4904
|
+
name: "认识"
|
|
4905
|
+
},
|
|
4906
|
+
{
|
|
4907
|
+
since: 15,
|
|
4908
|
+
name: "熟了"
|
|
4909
|
+
},
|
|
4910
|
+
{
|
|
4911
|
+
since: 40,
|
|
4912
|
+
name: "亲近"
|
|
4913
|
+
},
|
|
4914
|
+
{
|
|
4915
|
+
since: 90,
|
|
4916
|
+
name: "离不开"
|
|
4917
|
+
},
|
|
4918
|
+
{
|
|
4919
|
+
since: 180,
|
|
4920
|
+
name: "只有你"
|
|
4921
|
+
}
|
|
4922
|
+
];
|
|
4923
|
+
function bondTier(days) {
|
|
4924
|
+
let tier = BOND_TIERS[0]?.name ?? "陌生";
|
|
4925
|
+
for (const t of BOND_TIERS) if (days >= t.since) tier = t.name;
|
|
4926
|
+
return tier;
|
|
4927
|
+
}
|
|
4738
4928
|
/** Shared mood vocabulary (pet_state computes the same from the ledger). */
|
|
4739
4929
|
function moodOf(affection) {
|
|
4740
4930
|
return affection >= 80 ? "happy" : affection >= 40 ? "content" : "neutral";
|
|
@@ -4752,7 +4942,7 @@ function buildWakePrompt(facts) {
|
|
|
4752
4942
|
const user = facts.userActive ? `用户最近正在会话中工作(${String(facts.minutesSinceUserActivity ?? 0)} 分钟内有动静)` : "用户最近没有活动";
|
|
4753
4943
|
return [
|
|
4754
4944
|
"[自主唤醒]",
|
|
4755
|
-
`好感 ${String(facts.affection)}(${moodOf(facts.affection)}
|
|
4945
|
+
`好感 ${String(facts.affection)}(${moodOf(facts.affection)});羁绊 ${facts.bondTier}(一起过了 ${String(facts.bondDays)} 天);${lastSpoke};${user}。`,
|
|
4756
4946
|
"醒来想到什么就说一句(pet_say),想配动作就 pet_do;也可以只是安静待着(两者都不调,正文写「…」)。"
|
|
4757
4947
|
].join(" ");
|
|
4758
4948
|
}
|
|
@@ -4776,14 +4966,16 @@ function buildChatPrompt(text) {
|
|
|
4776
4966
|
* The pet registry: a directory of per-pet control manifests, replacing the
|
|
4777
4967
|
* last-writer-wins single files whose races caused repeated swap incidents
|
|
4778
4968
|
* (a dying carrier's late write burying a live pet's registration). Each pet
|
|
4779
|
-
* implementation owns its own file under pet-http.d/
|
|
4969
|
+
* implementation owns its own file under whale598/pet-http.d/ (legacy
|
|
4970
|
+
* pet-http.d/ still scanned) and re-asserts it on a
|
|
4780
4971
|
* cadence; entries whose publisher pid is dead expire by omission. The
|
|
4781
4972
|
* driver reads the whole table and routes per the multi-pet design: the
|
|
4782
4973
|
* session's bound pet if alive, else the default pet (first alive entry),
|
|
4783
4974
|
* plus an all-notify broadcast mode for interactions.
|
|
4784
4975
|
* @module dsh-agentic-pet/registry
|
|
4785
4976
|
*/
|
|
4786
|
-
const REGISTRY_DIR =
|
|
4977
|
+
const REGISTRY_DIR = `${WHALE_DIR}/pet-http.d`;
|
|
4978
|
+
const LEGACY_REGISTRY_DIR = "pet-http.d";
|
|
4787
4979
|
/**
|
|
4788
4980
|
* A live pet is one whose control plane ANSWERS, not merely one whose pid
|
|
4789
4981
|
* exists — and not merely one whose port accepts TCP: a frozen pet (stuck
|
|
@@ -4818,27 +5010,37 @@ async function probeControlPlane(port) {
|
|
|
4818
5010
|
* they expire by omission the next time their file is rewritten — and so are
|
|
4819
5011
|
* silent entries (pid alive, control plane unresponsive). */
|
|
4820
5012
|
async function readLivePets(home) {
|
|
5013
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
5014
|
+
for (const dir of [join(home, REGISTRY_DIR), join(home, LEGACY_REGISTRY_DIR)]) {
|
|
5015
|
+
let names = [];
|
|
5016
|
+
try {
|
|
5017
|
+
names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
|
|
5018
|
+
} catch {
|
|
5019
|
+
continue;
|
|
5020
|
+
}
|
|
5021
|
+
for (const name of names) {
|
|
5022
|
+
const id = name.slice(0, -5);
|
|
5023
|
+
if (id !== "" && !candidates.has(id)) candidates.set(id, join(dir, name));
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
4821
5026
|
const live = [];
|
|
4822
|
-
try {
|
|
4823
|
-
const
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
since: typeof raw.since === "number" ? raw.since : 0
|
|
4840
|
-
});
|
|
4841
|
-
} catch {}
|
|
5027
|
+
for (const [id, file] of candidates) try {
|
|
5028
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
5029
|
+
const port = typeof raw.port === "number" ? raw.port : NaN;
|
|
5030
|
+
const pid = typeof raw.pid === "number" ? raw.pid : NaN;
|
|
5031
|
+
if (!Number.isFinite(port) || !Number.isFinite(pid)) continue;
|
|
5032
|
+
try {
|
|
5033
|
+
process.kill(pid, 0);
|
|
5034
|
+
} catch {
|
|
5035
|
+
continue;
|
|
5036
|
+
}
|
|
5037
|
+
if (!await probeControlPlane(port)) continue;
|
|
5038
|
+
live.push({
|
|
5039
|
+
id,
|
|
5040
|
+
port,
|
|
5041
|
+
pid,
|
|
5042
|
+
since: typeof raw.since === "number" ? raw.since : 0
|
|
5043
|
+
});
|
|
4842
5044
|
} catch {}
|
|
4843
5045
|
return live;
|
|
4844
5046
|
}
|
|
@@ -4920,15 +5122,32 @@ const EMPTY_LEDGER = {
|
|
|
4920
5122
|
lastTouchAt: void 0,
|
|
4921
5123
|
recent: []
|
|
4922
5124
|
};
|
|
4923
|
-
|
|
5125
|
+
const ADJUST_MAX_DELTA = 3;
|
|
5126
|
+
const ADJUST_COOLDOWN_MS = 5 * 6e4;
|
|
5127
|
+
const ADJUST_DAILY_LIMIT = 8;
|
|
5128
|
+
const ADJUST_DAILY_SUM = 10;
|
|
5129
|
+
async function readLedger(home, petId) {
|
|
4924
5130
|
try {
|
|
4925
|
-
const raw =
|
|
5131
|
+
const raw = await readAffectionRaw(home, petId);
|
|
5132
|
+
if (raw === null) return {
|
|
5133
|
+
...EMPTY_LEDGER,
|
|
5134
|
+
byPart: {},
|
|
5135
|
+
recent: []
|
|
5136
|
+
};
|
|
4926
5137
|
return {
|
|
4927
5138
|
affection: typeof raw.affection === "number" ? raw.affection : 0,
|
|
4928
5139
|
total: typeof raw.total === "number" ? raw.total : 0,
|
|
4929
5140
|
byPart: raw.byPart ?? {},
|
|
4930
5141
|
lastTouchAt: raw.lastTouchAt,
|
|
4931
|
-
recent: raw.recent ?? []
|
|
5142
|
+
recent: raw.recent ?? [],
|
|
5143
|
+
adjustments: Array.isArray(raw.adjustments) ? raw.adjustments.filter((a) => typeof a === "object" && a !== null && typeof a.delta === "number" && typeof a.reason === "string" && typeof a.at === "number") : void 0,
|
|
5144
|
+
quota: typeof raw.quota === "object" && raw.quota !== null ? raw.quota : void 0,
|
|
5145
|
+
bond: typeof raw.bond === "object" && raw.bond !== null && typeof raw.bond.lastCountedDate === "string" ? {
|
|
5146
|
+
value: typeof raw.bond.value === "number" ? raw.bond.value : 0,
|
|
5147
|
+
days: typeof raw.bond.days === "number" ? raw.bond.days : 0,
|
|
5148
|
+
lastCountedDate: raw.bond.lastCountedDate
|
|
5149
|
+
} : void 0,
|
|
5150
|
+
bondAdjustments: Array.isArray(raw.bondAdjustments) ? raw.bondAdjustments.filter((a) => typeof a === "object" && a !== null && typeof a.delta === "number" && typeof a.reason === "string" && typeof a.at === "number") : void 0
|
|
4932
5151
|
};
|
|
4933
5152
|
} catch {
|
|
4934
5153
|
return {
|
|
@@ -4938,12 +5157,6 @@ async function readLedger(ledgerFile) {
|
|
|
4938
5157
|
};
|
|
4939
5158
|
}
|
|
4940
5159
|
}
|
|
4941
|
-
/** The per-pet ledger file. Legacy deployments' single book is folded under
|
|
4942
|
-
* the electron id so pre-registry affection history survives the upgrade. */
|
|
4943
|
-
function ledgerFileFor(home, petId) {
|
|
4944
|
-
if (petId === "electron" && !existsSync(join(home, "pet-affection.d"))) return join(home, "pet-affection.json");
|
|
4945
|
-
return join(home, "pet-affection.d", `${petId}.json`);
|
|
4946
|
-
}
|
|
4947
5160
|
/** Local-time log stamp: the driver log is the user's primary debugging
|
|
4948
5161
|
* tool, so timestamps show the USER's wall clock, not UTC (toISOString's Z
|
|
4949
5162
|
* form read 8h off and cost a confusion round). Sortable, zero-padded. */
|
|
@@ -4953,7 +5166,7 @@ const logStamp = () => {
|
|
|
4953
5166
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
4954
5167
|
};
|
|
4955
5168
|
const driverLog = (home, line) => {
|
|
4956
|
-
appendFile(join(home, "pet-driver.log"), `${logStamp()} ${line}
|
|
5169
|
+
appendFile(join(home, WHALE_DIR, "pet-driver.log"), `${logStamp()} ${line}
|
|
4957
5170
|
`, "utf8").catch(() => {});
|
|
4958
5171
|
};
|
|
4959
5172
|
/** The one /events read both consumers share (§6 v4): the ledger loop
|
|
@@ -4995,6 +5208,23 @@ const voiceLastSpokeAt = /* @__PURE__ */ new Map();
|
|
|
4995
5208
|
const voiceLastTouchReplyAt = /* @__PURE__ */ new Map();
|
|
4996
5209
|
const voiceNextWakeAt = /* @__PURE__ */ new Map();
|
|
4997
5210
|
const voiceGreetedOnce = /* @__PURE__ */ new Set();
|
|
5211
|
+
const voiceNames = /* @__PURE__ */ new Map();
|
|
5212
|
+
const DEFAULT_PET_NAME = { electron: "鲸鱼娘" };
|
|
5213
|
+
/** B-layer bond day counting: the FIRST interaction of a calendar day
|
|
5214
|
+
* (touch / user chat / model adjustment) marks that day as shared. Called
|
|
5215
|
+
* at the interaction points, never on reads — reading the ledger does not
|
|
5216
|
+
* make a day meaningful. Mutates in place; the caller persists. Module-level
|
|
5217
|
+
* because both the driver closures AND the model tools (registerPetTools)
|
|
5218
|
+
* count bond days. */
|
|
5219
|
+
const countBondDay = (ledger) => {
|
|
5220
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
5221
|
+
if (ledger.bond?.lastCountedDate === today) return;
|
|
5222
|
+
ledger.bond = {
|
|
5223
|
+
value: ledger.bond?.value ?? 0,
|
|
5224
|
+
days: (ledger.bond?.days ?? 0) + 1,
|
|
5225
|
+
lastCountedDate: today
|
|
5226
|
+
};
|
|
5227
|
+
};
|
|
4998
5228
|
const voiceLastPrompt = /* @__PURE__ */ new Map();
|
|
4999
5229
|
const voiceBoundSessions = /* @__PURE__ */ new Map();
|
|
5000
5230
|
const voiceWakeTimer = { timer: void 0 };
|
|
@@ -5018,12 +5248,14 @@ function markVoiceSpoke(petId) {
|
|
|
5018
5248
|
}
|
|
5019
5249
|
function installPetDriver(ctx, home, pet) {
|
|
5020
5250
|
driverLog(home, "installPetDriver entered");
|
|
5251
|
+
migrateGlobals(home);
|
|
5021
5252
|
voiceOwners.clear();
|
|
5022
5253
|
voicePendingUntil.clear();
|
|
5023
5254
|
voiceLastSpokeAt.clear();
|
|
5024
5255
|
voiceLastTouchReplyAt.clear();
|
|
5025
5256
|
voiceNextWakeAt.clear();
|
|
5026
5257
|
voiceGreetedOnce.clear();
|
|
5258
|
+
voiceNames.clear();
|
|
5027
5259
|
voiceLastPrompt.clear();
|
|
5028
5260
|
voiceBoundSessions.clear();
|
|
5029
5261
|
if (voiceWakeTimer.timer !== void 0) clearTimeout(voiceWakeTimer.timer);
|
|
@@ -5056,7 +5288,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5056
5288
|
if (!latestArchived.has(entry.sessionId)) continue;
|
|
5057
5289
|
const generation = (voiceStore.generations[petId] ?? 0) + 1;
|
|
5058
5290
|
voiceStore.generations[petId] = generation;
|
|
5059
|
-
|
|
5291
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5060
5292
|
const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
|
|
5061
5293
|
if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
|
|
5062
5294
|
voicePendingUntil.delete(petId);
|
|
@@ -5082,13 +5314,15 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5082
5314
|
}
|
|
5083
5315
|
};
|
|
5084
5316
|
const attachedSessions = /* @__PURE__ */ new Set();
|
|
5317
|
+
const petWorkspaceDir = () => join(homedir(), "whale598");
|
|
5085
5318
|
const attachVoiceSession = async (sessionId) => {
|
|
5086
5319
|
if (attachedSessions.has(sessionId)) return;
|
|
5087
5320
|
attachedSessions.add(sessionId);
|
|
5088
5321
|
try {
|
|
5089
5322
|
const registry = ctx.get("workspaceRegistry");
|
|
5090
5323
|
if (registry === void 0) return;
|
|
5091
|
-
await (
|
|
5324
|
+
await mkdir(petWorkspaceDir(), { recursive: true });
|
|
5325
|
+
await (await registry.create(petWorkspaceDir()))?.attachSession?.(sessionId);
|
|
5092
5326
|
driverLog(home, `VOICE session attached to workspace (${sessionId})`);
|
|
5093
5327
|
} catch (error) {
|
|
5094
5328
|
attachedSessions.delete(sessionId);
|
|
@@ -5135,7 +5369,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5135
5369
|
*/
|
|
5136
5370
|
const readAutonomy = async () => {
|
|
5137
5371
|
try {
|
|
5138
|
-
const p =
|
|
5372
|
+
const p = (await readGlobalJson(home, "pet-settings.json")).proactivity;
|
|
5139
5373
|
const gap = typeof p?.idleAnimationGapMinutes === "number" && Number.isFinite(p.idleAnimationGapMinutes) ? Math.min(120, Math.max(1, Math.round(p.idleAnimationGapMinutes))) : 10;
|
|
5140
5374
|
return {
|
|
5141
5375
|
idleAnimation: p?.idleAnimation !== false,
|
|
@@ -5210,7 +5444,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5210
5444
|
const petId = voiceOwners.get(sessionId);
|
|
5211
5445
|
voiceOwners.delete(sessionId);
|
|
5212
5446
|
if (petId !== void 0) {
|
|
5213
|
-
|
|
5447
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5214
5448
|
voicePendingUntil.delete(petId);
|
|
5215
5449
|
saveInnerVoiceStore(home, voiceStore);
|
|
5216
5450
|
driverLog(home, `VOICE session removed by user; pet=${petId} will re-create on next wake`);
|
|
@@ -5222,6 +5456,22 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5222
5456
|
boundPetId = void 0;
|
|
5223
5457
|
pushState("idle");
|
|
5224
5458
|
});
|
|
5459
|
+
const refreshNames = async () => {
|
|
5460
|
+
voiceNames.clear();
|
|
5461
|
+
try {
|
|
5462
|
+
for (const petId of await readdir(join(home, WHALE_DIR))) {
|
|
5463
|
+
const name = await readPetName(home, petId);
|
|
5464
|
+
if (name !== void 0) voiceNames.set(petId, name);
|
|
5465
|
+
}
|
|
5466
|
+
} catch {}
|
|
5467
|
+
try {
|
|
5468
|
+
const raw = await readGlobalJson(home, "pet-settings.json");
|
|
5469
|
+
if (typeof raw.names === "object" && raw.names !== null) {
|
|
5470
|
+
for (const [petId, label] of Object.entries(raw.names)) if (petId !== "" && typeof label === "string" && label.trim() !== "" && !voiceNames.has(petId)) voiceNames.set(petId, label.trim());
|
|
5471
|
+
}
|
|
5472
|
+
} catch {}
|
|
5473
|
+
};
|
|
5474
|
+
refreshNames();
|
|
5225
5475
|
const refreshBoundSessions = async () => {
|
|
5226
5476
|
const config = await readInnerVoiceConfig(home);
|
|
5227
5477
|
voiceBoundSessions.clear();
|
|
@@ -5229,7 +5479,31 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5229
5479
|
};
|
|
5230
5480
|
refreshBoundSessions();
|
|
5231
5481
|
events.on("pet-settings/changed", () => {
|
|
5232
|
-
|
|
5482
|
+
const prevNames = new Map(voiceNames);
|
|
5483
|
+
(async () => {
|
|
5484
|
+
await refreshNames();
|
|
5485
|
+
refreshBoundSessions();
|
|
5486
|
+
const changedPets = new Set([...prevNames.keys(), ...voiceNames.keys()]);
|
|
5487
|
+
for (const petId of changedPets) {
|
|
5488
|
+
if (prevNames.get(petId) === voiceNames.get(petId)) continue;
|
|
5489
|
+
const nextName = voiceNames.get(petId);
|
|
5490
|
+
const label = nextName ?? DEFAULT_PET_NAME[petId] ?? petId;
|
|
5491
|
+
const entry = voiceStore.sessions[petId];
|
|
5492
|
+
if (entry !== void 0) {
|
|
5493
|
+
const title = `${label}的内心戏`;
|
|
5494
|
+
try {
|
|
5495
|
+
await voiceController?.rename({
|
|
5496
|
+
sessionId: entry.sessionId,
|
|
5497
|
+
title
|
|
5498
|
+
});
|
|
5499
|
+
driverLog(home, `VOICE session retitled pet=${petId} → ${title}`);
|
|
5500
|
+
} catch (error) {
|
|
5501
|
+
driverLog(home, `VOICE retitle skipped pet=${petId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
triggerRenameNotice(petId, nextName, label);
|
|
5505
|
+
}
|
|
5506
|
+
})();
|
|
5233
5507
|
if (currentState !== void 0) pushState(currentState);
|
|
5234
5508
|
});
|
|
5235
5509
|
const startAskPet = (pet, body, signal) => {
|
|
@@ -5258,7 +5532,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5258
5532
|
abort
|
|
5259
5533
|
};
|
|
5260
5534
|
};
|
|
5261
|
-
const interactionLedgerFile =
|
|
5535
|
+
const interactionLedgerFile = globalFilePath(home, "pet-interactions.json");
|
|
5262
5536
|
const recordInteraction = (entry) => {
|
|
5263
5537
|
const at = Date.now();
|
|
5264
5538
|
(async () => {
|
|
@@ -5403,16 +5677,19 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5403
5677
|
if (minted.hops > 0) driverLog(home, `VOICE mint skipped ${String(minted.hops)} archived id(s) pet=${petId} → ${minted.sessionId}`);
|
|
5404
5678
|
const sessionId = minted.sessionId;
|
|
5405
5679
|
try {
|
|
5680
|
+
await mkdir(petWorkspaceDir(), { recursive: true });
|
|
5406
5681
|
await voiceController.create({
|
|
5407
5682
|
sessionId,
|
|
5408
|
-
agentPreset: config.preset
|
|
5683
|
+
agentPreset: config.preset,
|
|
5684
|
+
cwd: petWorkspaceDir()
|
|
5409
5685
|
});
|
|
5410
5686
|
} catch (firstError) {
|
|
5411
5687
|
driverLog(home, `VOICE preset "${config.preset}" rejected for pet=${petId}: ${firstError instanceof Error ? firstError.message : String(firstError)}; falling back`);
|
|
5412
5688
|
try {
|
|
5413
5689
|
await voiceController.create({
|
|
5414
5690
|
sessionId,
|
|
5415
|
-
agentPreset: "
|
|
5691
|
+
agentPreset: "whale598",
|
|
5692
|
+
cwd: petWorkspaceDir()
|
|
5416
5693
|
});
|
|
5417
5694
|
} catch (secondError) {
|
|
5418
5695
|
driverLog(home, `VOICE session create failed for pet=${petId}: ${secondError instanceof Error ? secondError.message : String(secondError)}`);
|
|
@@ -5420,9 +5697,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5420
5697
|
}
|
|
5421
5698
|
}
|
|
5422
5699
|
try {
|
|
5700
|
+
const customName = voiceNames.get(petId);
|
|
5423
5701
|
await voiceController.rename({
|
|
5424
5702
|
sessionId,
|
|
5425
|
-
title:
|
|
5703
|
+
title: `${customName ?? DEFAULT_PET_NAME[petId] ?? petId}的内心戏`
|
|
5426
5704
|
});
|
|
5427
5705
|
} catch {}
|
|
5428
5706
|
voiceStore.sessions[petId] = {
|
|
@@ -5447,6 +5725,9 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5447
5725
|
const promptVoice = async (petId, text, opts = {}) => {
|
|
5448
5726
|
const { retried = false, userDriven = false } = opts;
|
|
5449
5727
|
if (latestArchived.has(voiceStore.sessions[petId]?.sessionId ?? "")) reconcileArchivedVoiceSessions();
|
|
5728
|
+
const customName = voiceNames.get(petId);
|
|
5729
|
+
if (customName !== void 0 && !text.startsWith("[名字]")) text = `[名字] 你叫「${customName}」。
|
|
5730
|
+
${text}`;
|
|
5450
5731
|
const sessionId = await ensureVoiceSession(petId);
|
|
5451
5732
|
if (sessionId === void 0 || voiceController === void 0) return false;
|
|
5452
5733
|
try {
|
|
@@ -5460,7 +5741,24 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5460
5741
|
}]
|
|
5461
5742
|
}, new AbortController().signal);
|
|
5462
5743
|
} catch (error) {
|
|
5463
|
-
|
|
5744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5745
|
+
driverLog(home, `VOICE prompt failed pet=${petId}: ${message}`);
|
|
5746
|
+
if (/Unknown agent preset/i.test(message) && !retried) {
|
|
5747
|
+
if (voiceStore.sessions[petId] !== void 0) {
|
|
5748
|
+
const generation = (voiceStore.generations[petId] ?? 0) + 1;
|
|
5749
|
+
voiceStore.generations[petId] = generation;
|
|
5750
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5751
|
+
const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
|
|
5752
|
+
if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
|
|
5753
|
+
voiceLastPrompt.delete(petId);
|
|
5754
|
+
saveInnerVoiceStore(home, voiceStore);
|
|
5755
|
+
driverLog(home, `VOICE session retired pet=${petId} (recorded preset no longer exists — id renamed) — fresh -g${generation} mounts the current preset`);
|
|
5756
|
+
return promptVoice(petId, text, {
|
|
5757
|
+
retried: true,
|
|
5758
|
+
userDriven
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5464
5762
|
return false;
|
|
5465
5763
|
}
|
|
5466
5764
|
voicePendingUntil.set(petId, Date.now() + 25e3);
|
|
@@ -5507,11 +5805,25 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5507
5805
|
* greeting (pet offline) releases the flag for the next sweep to retry. */
|
|
5508
5806
|
const triggerGreeting = async (petId) => {
|
|
5509
5807
|
if (!await voiceGateOpen(petId, false, "greeting")) return;
|
|
5510
|
-
|
|
5808
|
+
const ledger = await readLedger(home, petId);
|
|
5809
|
+
if (await promptVoice(petId, buildGreetPrompt({
|
|
5810
|
+
affection: ledger.affection,
|
|
5811
|
+
bondValue: ledger.bond?.value ?? 0,
|
|
5812
|
+
bondDays: ledger.bond?.days ?? 0,
|
|
5813
|
+
bondTier: bondTier(ledger.bond?.value ?? 0)
|
|
5814
|
+
}))) {
|
|
5511
5815
|
voiceGreetedOnce.add(petId);
|
|
5512
5816
|
driverLog(home, `VOICE boot greeting pet=${petId}`);
|
|
5513
5817
|
} else voiceGreetedOnce.delete(petId);
|
|
5514
5818
|
};
|
|
5819
|
+
/** Rename notice (user request): fired when the user's custom name actually
|
|
5820
|
+
* changes — she acknowledges out loud, once. The per-turn short name line
|
|
5821
|
+
* stays as the compaction-proof invariant; a skipped notice (pet offline /
|
|
5822
|
+
* turn in flight) loses nothing because that line still carries the name. */
|
|
5823
|
+
const triggerRenameNotice = async (petId, nextName, label) => {
|
|
5824
|
+
if (!await voiceGateOpen(petId, false, "rename")) return;
|
|
5825
|
+
if (await promptVoice(petId, nextName !== void 0 ? `[改名] 用户给你改名叫「${label}」——以后自我介绍、气泡里自称,都用这个名字。` : `[改名] 用户把你改回了「${label}」。`)) driverLog(home, `VOICE rename notice pet=${petId} → ${label}`);
|
|
5826
|
+
};
|
|
5515
5827
|
/** Random-wake trigger (docs/inner-voice.md §6.3). The gate decides whether
|
|
5516
5828
|
* this wake speaks — never the model. */
|
|
5517
5829
|
const triggerWake = async (petId) => {
|
|
@@ -5523,11 +5835,14 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5523
5835
|
return;
|
|
5524
5836
|
}
|
|
5525
5837
|
if (!await voiceGateOpen(petId, true, "wake")) return;
|
|
5526
|
-
const ledger = await readLedger(
|
|
5838
|
+
const ledger = await readLedger(home, petId);
|
|
5527
5839
|
const now = Date.now();
|
|
5528
5840
|
const lastSpoke = voiceLastSpokeAt.get(petId);
|
|
5529
5841
|
if (await promptVoice(petId, buildWakePrompt({
|
|
5530
5842
|
affection: ledger.affection,
|
|
5843
|
+
bondValue: ledger.bond?.value ?? 0,
|
|
5844
|
+
bondDays: ledger.bond?.days ?? 0,
|
|
5845
|
+
bondTier: bondTier(ledger.bond?.value ?? 0),
|
|
5531
5846
|
minutesSinceLastSpoke: lastSpoke === void 0 ? void 0 : Math.round((now - lastSpoke) / 6e4),
|
|
5532
5847
|
minutesSinceUserActivity: voiceUserActivityAt === void 0 ? void 0 : Math.round((now - voiceUserActivityAt) / 6e4),
|
|
5533
5848
|
userActive: voiceUserActivityAt !== void 0 && now - voiceUserActivityAt < 30 * 6e4
|
|
@@ -5555,7 +5870,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5555
5870
|
return;
|
|
5556
5871
|
}
|
|
5557
5872
|
if (!await voiceGateOpen(petId, false, "touch", true)) return;
|
|
5558
|
-
const ledger = await readLedger(
|
|
5873
|
+
const ledger = await readLedger(home, petId);
|
|
5559
5874
|
if (await promptVoice(petId, buildTouchPrompt({
|
|
5560
5875
|
part,
|
|
5561
5876
|
touchesToday: ledger.recent.filter((entry) => entry.part === part).length + 1,
|
|
@@ -5613,12 +5928,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5613
5928
|
wakeSweep();
|
|
5614
5929
|
const lastEventSeqs = /* @__PURE__ */ new Map();
|
|
5615
5930
|
const recordTouch = async (petId, part, at) => {
|
|
5616
|
-
const
|
|
5617
|
-
|
|
5931
|
+
const ledger = await readLedger(home, petId);
|
|
5932
|
+
countBondDay(ledger);
|
|
5618
5933
|
ledger.total += 1;
|
|
5619
5934
|
ledger.byPart[part] = (ledger.byPart[part] ?? 0) + 1;
|
|
5620
|
-
const before = ledger.affection;
|
|
5621
|
-
ledger.affection = Math.min(100, ledger.affection + (part === "head" ? 2 : 1));
|
|
5622
5935
|
ledger.lastTouchAt = at;
|
|
5623
5936
|
ledger.recent.unshift({
|
|
5624
5937
|
part,
|
|
@@ -5626,12 +5939,11 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5626
5939
|
});
|
|
5627
5940
|
ledger.recent = ledger.recent.slice(0, 10);
|
|
5628
5941
|
try {
|
|
5629
|
-
await
|
|
5630
|
-
`, "utf8");
|
|
5942
|
+
await writeAffectionRaw(home, petId, ledger);
|
|
5631
5943
|
} catch (error) {
|
|
5632
5944
|
console.error(`pet-surface driver: could not write the ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
5633
5945
|
}
|
|
5634
|
-
if (Math.floor(ledger.
|
|
5946
|
+
if (Math.floor(ledger.total / 10) > Math.floor((ledger.total - 1) / 10)) {
|
|
5635
5947
|
const target = (await pet.list()).find((pet) => pet.id === petId);
|
|
5636
5948
|
if (target !== void 0) fetch(`http://127.0.0.1:${String(target.port)}/action`, {
|
|
5637
5949
|
method: "POST",
|
|
@@ -5641,17 +5953,23 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5641
5953
|
}
|
|
5642
5954
|
triggerTouchReply(petId, part);
|
|
5643
5955
|
};
|
|
5644
|
-
/** Event dispatch for ONE pet event. touch → the affection ledger;
|
|
5645
|
-
* chat → STRAIGHT to the pet's inner-voice session, bypassing the touch
|
|
5646
|
-
* path's probability/cooldown/user-busy gates entirely (a user reaching
|
|
5647
|
-
* out to talk is never throttled — §6 routing constraint); unknown types
|
|
5648
|
-
* are ignored while the cursor still advances. */
|
|
5649
5956
|
const handlePetEvent = async (petId, event) => {
|
|
5957
|
+
appendJsonl(eventsFile(home, petId), {
|
|
5958
|
+
seq: event.seq,
|
|
5959
|
+
type: event.type,
|
|
5960
|
+
...event.type === "touch" ? { part: event.part } : {},
|
|
5961
|
+
at: event.at
|
|
5962
|
+
});
|
|
5650
5963
|
if (event.type === "touch") {
|
|
5651
5964
|
if (event.part !== void 0) await recordTouch(petId, event.part, event.at);
|
|
5652
5965
|
return;
|
|
5653
5966
|
}
|
|
5654
|
-
if (event.type === "chat" && event.text !== void 0 && event.text !== "")
|
|
5967
|
+
if (event.type === "chat" && event.text !== void 0 && event.text !== "") {
|
|
5968
|
+
const ledger = await readLedger(home, petId);
|
|
5969
|
+
countBondDay(ledger);
|
|
5970
|
+
await writeAffectionRaw(home, petId, ledger);
|
|
5971
|
+
triggerChat(petId, event.text);
|
|
5972
|
+
}
|
|
5655
5973
|
};
|
|
5656
5974
|
/** Per-pet long-poll loops (§6 v4): each live pet parks ONE /events
|
|
5657
5975
|
* hold=30 request; the answer — an event batch or the 30s empty one —
|
|
@@ -5714,12 +6032,13 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5714
6032
|
function registerPetTools(ctx, home, pet) {
|
|
5715
6033
|
ctx.inject(["tools"], (scope) => {
|
|
5716
6034
|
console.log("pet-surface driver: pet tools registered");
|
|
6035
|
+
const dshLoginAt = Date.now();
|
|
5717
6036
|
const sleep = (ms) => new Promise((resolve) => {
|
|
5718
6037
|
setTimeout(resolve, ms);
|
|
5719
6038
|
});
|
|
5720
6039
|
const readFocus = async () => {
|
|
5721
6040
|
try {
|
|
5722
|
-
const raw =
|
|
6041
|
+
const raw = await readGlobalJson(home, "pet-settings.json");
|
|
5723
6042
|
return typeof raw.focus === "string" && raw.focus !== "" ? raw.focus : void 0;
|
|
5724
6043
|
} catch {
|
|
5725
6044
|
return;
|
|
@@ -5900,7 +6219,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5900
6219
|
if (event.at > (lastTouchAt ?? 0)) lastTouchAt = event.at;
|
|
5901
6220
|
}
|
|
5902
6221
|
digestCursors.set(entry.id, newest);
|
|
5903
|
-
const ledger = await readLedger(
|
|
6222
|
+
const ledger = await readLedger(home, entry.id);
|
|
5904
6223
|
const mood = moodOf(ledger.affection);
|
|
5905
6224
|
return JSON.stringify({
|
|
5906
6225
|
pet: entry.id,
|
|
@@ -5934,12 +6253,171 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5934
6253
|
const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? Math.min(50, Math.max(1, Math.round(args.limit))) : 10;
|
|
5935
6254
|
let list = [];
|
|
5936
6255
|
try {
|
|
5937
|
-
const raw =
|
|
6256
|
+
const raw = await readGlobalJson(home, "pet-interactions.json");
|
|
5938
6257
|
if (Array.isArray(raw)) list = raw;
|
|
5939
6258
|
} catch {}
|
|
5940
6259
|
return JSON.stringify({ recent: list.slice(0, limit) });
|
|
5941
6260
|
}
|
|
5942
6261
|
}));
|
|
6262
|
+
scope.tools.register(defineTool({
|
|
6263
|
+
name: "pet_affection_get",
|
|
6264
|
+
description: "Read the FULL affection context before adjusting it: the current value you maintain (and its mood), the touch FACTS the system records (counts by part — cause, not feeling), your recent adjustments with their reasons, the remaining daily quota, and the time facts (now / lastInteractionAt / lastLoginAt, epoch ms — the system only records and reports facts, it never moves affection or bond). Judge honestly: if the silence since lastInteractionAt has made the current value no longer true, adjust it yourself with a concrete reason.",
|
|
6265
|
+
parameters: { pet: {
|
|
6266
|
+
type: "string",
|
|
6267
|
+
description: "Which pet: its id from pet_state. Omit for the default pet."
|
|
6268
|
+
} },
|
|
6269
|
+
output: {
|
|
6270
|
+
schema: { type: "string" },
|
|
6271
|
+
render: (_args, value) => [{
|
|
6272
|
+
type: "text",
|
|
6273
|
+
text: value
|
|
6274
|
+
}]
|
|
6275
|
+
},
|
|
6276
|
+
async execute(args, exec) {
|
|
6277
|
+
try {
|
|
6278
|
+
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6279
|
+
const ledger = await readLedger(home, entry.id);
|
|
6280
|
+
const now = Date.now();
|
|
6281
|
+
const lastInteractionAt = Math.max(ledger.lastTouchAt ?? 0, ledger.adjustments?.[0]?.at ?? 0);
|
|
6282
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6283
|
+
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
6284
|
+
date: today,
|
|
6285
|
+
count: 0,
|
|
6286
|
+
sum: 0
|
|
6287
|
+
};
|
|
6288
|
+
return JSON.stringify({
|
|
6289
|
+
pet: entry.id,
|
|
6290
|
+
affection: ledger.affection,
|
|
6291
|
+
mood: moodOf(ledger.affection),
|
|
6292
|
+
touch: {
|
|
6293
|
+
total: ledger.total,
|
|
6294
|
+
byPart: ledger.byPart,
|
|
6295
|
+
lastTouchAt: ledger.lastTouchAt ?? null
|
|
6296
|
+
},
|
|
6297
|
+
bond: {
|
|
6298
|
+
value: ledger.bond?.value ?? 0,
|
|
6299
|
+
days: ledger.bond?.days ?? 0,
|
|
6300
|
+
tier: bondTier(ledger.bond?.value ?? 0)
|
|
6301
|
+
},
|
|
6302
|
+
recentBondAdjustments: (ledger.bondAdjustments ?? []).slice(0, 3),
|
|
6303
|
+
recentAdjustments: (ledger.adjustments ?? []).slice(0, 5),
|
|
6304
|
+
quota: {
|
|
6305
|
+
used: quota.count,
|
|
6306
|
+
limit: ADJUST_DAILY_LIMIT,
|
|
6307
|
+
sum: quota.sum,
|
|
6308
|
+
sumLimit: ADJUST_DAILY_SUM
|
|
6309
|
+
},
|
|
6310
|
+
facts: {
|
|
6311
|
+
now,
|
|
6312
|
+
lastInteractionAt: lastInteractionAt > 0 ? lastInteractionAt : null,
|
|
6313
|
+
lastLoginAt: dshLoginAt
|
|
6314
|
+
}
|
|
6315
|
+
});
|
|
6316
|
+
} catch (error) {
|
|
6317
|
+
throw classify(error);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
}));
|
|
6321
|
+
scope.tools.register(defineTool({
|
|
6322
|
+
name: "pet_affection_adjust",
|
|
6323
|
+
description: "Adjust the affection YOU maintain toward the user: delta (-3..+3 integer) plus a SPECIFIC reason citing what happened. Touch counts are facts the system records separately — this tool is your judgment layer, and the driver never moves the value on a touch. Limits: at least 5 minutes between adjustments, 8 per day, |daily sum| <= 10 — violations are REJECTED with the remaining quota, not clamped. Judge honestly: this number is your read of the relationship, not a score to farm.",
|
|
6324
|
+
parameters: {
|
|
6325
|
+
delta: {
|
|
6326
|
+
type: "number",
|
|
6327
|
+
required: true,
|
|
6328
|
+
description: "The change: a NON-ZERO integer between -3 and +3."
|
|
6329
|
+
},
|
|
6330
|
+
reason: {
|
|
6331
|
+
type: "string",
|
|
6332
|
+
required: true,
|
|
6333
|
+
description: "WHY this delta — cite the concrete interaction. Empty or generic reasons are rejected."
|
|
6334
|
+
},
|
|
6335
|
+
pet: {
|
|
6336
|
+
type: "string",
|
|
6337
|
+
description: "Which pet: its id from pet_state. Omit for the default pet."
|
|
6338
|
+
},
|
|
6339
|
+
bondDelta: {
|
|
6340
|
+
type: "number",
|
|
6341
|
+
description: "OPTIONAL change to the long-term bond: a NON-ZERO integer between -1 and +1. The slowest variable — the smallest steps."
|
|
6342
|
+
},
|
|
6343
|
+
bondReason: {
|
|
6344
|
+
type: "string",
|
|
6345
|
+
description: "Required when bondDelta is given: the concrete event that moved the bond."
|
|
6346
|
+
}
|
|
6347
|
+
},
|
|
6348
|
+
output: {
|
|
6349
|
+
schema: { type: "string" },
|
|
6350
|
+
render: (_args, value) => [{
|
|
6351
|
+
type: "text",
|
|
6352
|
+
text: value
|
|
6353
|
+
}]
|
|
6354
|
+
},
|
|
6355
|
+
async execute(args, exec) {
|
|
6356
|
+
try {
|
|
6357
|
+
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6358
|
+
const ledger = await readLedger(home, entry.id);
|
|
6359
|
+
const now = Date.now();
|
|
6360
|
+
const delta = typeof args.delta === "number" && Number.isInteger(args.delta) ? args.delta : NaN;
|
|
6361
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
6362
|
+
if (Number.isNaN(delta) || delta === 0 || Math.abs(delta) > ADJUST_MAX_DELTA) throw new Error(`delta 必须是 -${String(ADJUST_MAX_DELTA)}..+${String(ADJUST_MAX_DELTA)} 的非零整数(收到 ${String(args.delta)})`);
|
|
6363
|
+
if (reason === "") throw new Error("reason 必填:没有理由的调整不予记录");
|
|
6364
|
+
const adjustments = ledger.adjustments ?? [];
|
|
6365
|
+
const cooldownLeft = (adjustments.find((a) => !a.system)?.at ?? 0) + ADJUST_COOLDOWN_MS - now;
|
|
6366
|
+
if (cooldownLeft > 0) throw new Error(`距上次调整还有 ${String(Math.ceil(cooldownLeft / 1e3))} 秒冷却`);
|
|
6367
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6368
|
+
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
6369
|
+
date: today,
|
|
6370
|
+
count: 0,
|
|
6371
|
+
sum: 0
|
|
6372
|
+
};
|
|
6373
|
+
if (quota.count >= ADJUST_DAILY_LIMIT) throw new Error(`今日调整次数已用完(${String(ADJUST_DAILY_LIMIT)})——明天再判断,或者相信当前的关系状态`);
|
|
6374
|
+
if (Math.abs(quota.sum + delta) > ADJUST_DAILY_SUM) throw new Error(`今日净变动将超过 ±${String(ADJUST_DAILY_SUM)}(当前 ${String(quota.sum)})——剩下的留给明天`);
|
|
6375
|
+
countBondDay(ledger);
|
|
6376
|
+
const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
|
|
6377
|
+
const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
|
|
6378
|
+
if (bondDelta !== 0) {
|
|
6379
|
+
if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
|
|
6380
|
+
if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
|
|
6381
|
+
}
|
|
6382
|
+
ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
|
|
6383
|
+
quota.count += 1;
|
|
6384
|
+
quota.sum += delta;
|
|
6385
|
+
ledger.quota = quota;
|
|
6386
|
+
ledger.adjustments = [{
|
|
6387
|
+
delta,
|
|
6388
|
+
reason,
|
|
6389
|
+
at: now
|
|
6390
|
+
}, ...adjustments].slice(0, 20);
|
|
6391
|
+
if (bondDelta !== 0) {
|
|
6392
|
+
ledger.bond = {
|
|
6393
|
+
value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
|
|
6394
|
+
days: ledger.bond?.days ?? 0,
|
|
6395
|
+
lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
|
|
6396
|
+
};
|
|
6397
|
+
ledger.bondAdjustments = [{
|
|
6398
|
+
delta: bondDelta,
|
|
6399
|
+
reason: bondReason,
|
|
6400
|
+
at: now
|
|
6401
|
+
}, ...ledger.bondAdjustments ?? []].slice(0, 10);
|
|
6402
|
+
}
|
|
6403
|
+
await writeAffectionRaw(home, entry.id, ledger);
|
|
6404
|
+
return JSON.stringify({
|
|
6405
|
+
pet: entry.id,
|
|
6406
|
+
affection: ledger.affection,
|
|
6407
|
+
mood: moodOf(ledger.affection),
|
|
6408
|
+
applied: delta,
|
|
6409
|
+
quota: {
|
|
6410
|
+
used: quota.count,
|
|
6411
|
+
limit: ADJUST_DAILY_LIMIT,
|
|
6412
|
+
sum: quota.sum,
|
|
6413
|
+
sumLimit: ADJUST_DAILY_SUM
|
|
6414
|
+
}
|
|
6415
|
+
});
|
|
6416
|
+
} catch (error) {
|
|
6417
|
+
throw classify(error);
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
}));
|
|
5943
6421
|
scope.tools.register(defineTool({
|
|
5944
6422
|
name: "pet_state",
|
|
5945
6423
|
description: "Read the desktop pets' current state: every running pet with its id, what it is doing, which animation is playing, where it sits on screen (location), where it may roam (world, when the pet supports one), its visibility, plus each pet's affection book. Use it when the user refers to a pet, to pick a target for pet_say/pet_do, or to react to a pet.",
|
|
@@ -5969,18 +6447,28 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5969
6447
|
]);
|
|
5970
6448
|
const ledgers = {};
|
|
5971
6449
|
for (const id of knownIds) {
|
|
5972
|
-
const ledger = await readLedger(
|
|
6450
|
+
const ledger = await readLedger(home, id);
|
|
5973
6451
|
const mood = moodOf(ledger.affection);
|
|
6452
|
+
const tier = bondTier(ledger.bond?.value ?? 0);
|
|
6453
|
+
const bond = {
|
|
6454
|
+
value: ledger.bond?.value ?? 0,
|
|
6455
|
+
days: ledger.bond?.days ?? 0,
|
|
6456
|
+
tier
|
|
6457
|
+
};
|
|
5974
6458
|
ledgers[id] = {
|
|
5975
6459
|
...ledger,
|
|
5976
|
-
mood
|
|
6460
|
+
mood,
|
|
6461
|
+
bond
|
|
5977
6462
|
};
|
|
5978
6463
|
}
|
|
5979
6464
|
const petsOut = states.map((state) => {
|
|
5980
6465
|
const actions = actionsById.get(state.id);
|
|
5981
|
-
return
|
|
5982
|
-
...state
|
|
5983
|
-
|
|
6466
|
+
return {
|
|
6467
|
+
...actions === void 0 ? state : {
|
|
6468
|
+
...state,
|
|
6469
|
+
actions
|
|
6470
|
+
},
|
|
6471
|
+
name: voiceNames.get(state.id) ?? DEFAULT_PET_NAME[state.id] ?? state.id
|
|
5984
6472
|
};
|
|
5985
6473
|
});
|
|
5986
6474
|
return JSON.stringify({
|
|
@@ -6255,4 +6743,4 @@ function installPetService(ctx, home) {
|
|
|
6255
6743
|
return new PetHttpService(ctx, home);
|
|
6256
6744
|
}
|
|
6257
6745
|
//#endregion
|
|
6258
|
-
export { logStamp as a, installPetDriver as i, installPetService as n, registerPetTools as o, petHome as r, createUnregisteredPetHttp as t };
|
|
6746
|
+
export { logStamp as a, globalFilePath as c, readJsonl as d, readPetName as f, writePetName as h, installPetDriver as i, readAffectionRaw as l, writeGlobalJson as m, installPetService as n, registerPetTools as o, writeAffectionRaw as p, petHome as r, eventsFile as s, createUnregisteredPetHttp as t, readGlobalJson as u };
|