dsh-agentic-pet 0.3.1 → 0.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/README.md +56 -50
- package/README.zh.md +56 -50
- package/carrier/lib/main.js +10 -6
- package/client/pet-settings.js +44 -10
- package/lib/index.js +60 -13
- package/lib/{service-Vj-lLpIv.js → service-D-b8UzM2.js} +619 -69
- package/lib/tools.js +1 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/inner-voice.d.ts +14 -0
- package/lib/types/petdata.d.ts +44 -0
- package/lib/types/registry.d.ts +2 -1
- package/package.json +2 -10
- package/presets/pet-inner-voice.patch.yml +3 -3
- package/presets/pet.patch.yml +3 -3
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import { appendFile, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
7
6
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
8
7
|
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
9
8
|
/** Return true when a value is `null` or `undefined`. */
|
|
@@ -4618,6 +4617,19 @@ function formatAvailableActions(available, cap = 24) {
|
|
|
4618
4617
|
* @module dsh-agentic-pet/inner-voice
|
|
4619
4618
|
*/
|
|
4620
4619
|
const VOICE_SESSION_PREFIX = "whale598-";
|
|
4620
|
+
/**
|
|
4621
|
+
* Saved-setting migration for preset id renames: pet-settings.json holds the
|
|
4622
|
+
* user's 会话模式 choice by id, and a rename orphans that saved value (every
|
|
4623
|
+
* ensure would first try a dead id and fall back). Map the legacy ids onto
|
|
4624
|
+
* their successors; unknown ids pass through untouched.
|
|
4625
|
+
*/
|
|
4626
|
+
const LEGACY_PRESET_IDS = {
|
|
4627
|
+
"pet-inner-voice": "whale598",
|
|
4628
|
+
"pet": "whale598-standard"
|
|
4629
|
+
};
|
|
4630
|
+
function migratePreset(preset) {
|
|
4631
|
+
return LEGACY_PRESET_IDS[preset] ?? preset;
|
|
4632
|
+
}
|
|
4621
4633
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
4622
4634
|
const finite01 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? clamp(value, 0, 1) : fallback;
|
|
4623
4635
|
/** Read `innerVoice` from pet-settings.json; anything missing or mistyped
|
|
@@ -4634,7 +4646,7 @@ async function readInnerVoiceConfig(home) {
|
|
|
4634
4646
|
}
|
|
4635
4647
|
return {
|
|
4636
4648
|
enabled: v.enabled !== false,
|
|
4637
|
-
preset: typeof v.preset === "string" && v.preset !== "" ? v.preset : "
|
|
4649
|
+
preset: migratePreset(typeof v.preset === "string" && v.preset !== "" ? v.preset : "whale598"),
|
|
4638
4650
|
wakeIntervalMinutes: typeof v.wakeIntervalMinutes === "number" && Number.isFinite(v.wakeIntervalMinutes) ? clamp(Math.round(v.wakeIntervalMinutes), 1, 60) : 2,
|
|
4639
4651
|
speakProbability: finite01(v.speakProbability, .5),
|
|
4640
4652
|
touchReplyProbability: finite01(v.touchReplyProbability, .3),
|
|
@@ -4644,7 +4656,7 @@ async function readInnerVoiceConfig(home) {
|
|
|
4644
4656
|
} catch {
|
|
4645
4657
|
return {
|
|
4646
4658
|
enabled: true,
|
|
4647
|
-
preset: "
|
|
4659
|
+
preset: "whale598",
|
|
4648
4660
|
wakeIntervalMinutes: 2,
|
|
4649
4661
|
speakProbability: .5,
|
|
4650
4662
|
touchReplyProbability: .3,
|
|
@@ -4731,10 +4743,44 @@ function mintVoiceSessionId(petId, generation, isArchived) {
|
|
|
4731
4743
|
function buildGreetPrompt(facts) {
|
|
4732
4744
|
return [
|
|
4733
4745
|
"[启动问好]",
|
|
4734
|
-
`用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)}
|
|
4746
|
+
`用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)};羁绊 ${facts.bondTier},一起过了 ${String(facts.bondDays)} 天)。`,
|
|
4735
4747
|
"主动打个招呼(pet_say)——按当下的时辰自然问好,一两句就好,别问问题等回答。"
|
|
4736
4748
|
].join(" ");
|
|
4737
4749
|
}
|
|
4750
|
+
/** B 层 · 羁绊档位:days-with-interaction → tier name. Only grows, never
|
|
4751
|
+
* decays; the names are the display contract (settings page keeps its own
|
|
4752
|
+
* copy of the thresholds). */
|
|
4753
|
+
const BOND_TIERS = [
|
|
4754
|
+
{
|
|
4755
|
+
since: 0,
|
|
4756
|
+
name: "陌生"
|
|
4757
|
+
},
|
|
4758
|
+
{
|
|
4759
|
+
since: 5,
|
|
4760
|
+
name: "认识"
|
|
4761
|
+
},
|
|
4762
|
+
{
|
|
4763
|
+
since: 15,
|
|
4764
|
+
name: "熟了"
|
|
4765
|
+
},
|
|
4766
|
+
{
|
|
4767
|
+
since: 40,
|
|
4768
|
+
name: "亲近"
|
|
4769
|
+
},
|
|
4770
|
+
{
|
|
4771
|
+
since: 90,
|
|
4772
|
+
name: "离不开"
|
|
4773
|
+
},
|
|
4774
|
+
{
|
|
4775
|
+
since: 180,
|
|
4776
|
+
name: "只有你"
|
|
4777
|
+
}
|
|
4778
|
+
];
|
|
4779
|
+
function bondTier(days) {
|
|
4780
|
+
let tier = BOND_TIERS[0]?.name ?? "陌生";
|
|
4781
|
+
for (const t of BOND_TIERS) if (days >= t.since) tier = t.name;
|
|
4782
|
+
return tier;
|
|
4783
|
+
}
|
|
4738
4784
|
/** Shared mood vocabulary (pet_state computes the same from the ledger). */
|
|
4739
4785
|
function moodOf(affection) {
|
|
4740
4786
|
return affection >= 80 ? "happy" : affection >= 40 ? "content" : "neutral";
|
|
@@ -4752,7 +4798,7 @@ function buildWakePrompt(facts) {
|
|
|
4752
4798
|
const user = facts.userActive ? `用户最近正在会话中工作(${String(facts.minutesSinceUserActivity ?? 0)} 分钟内有动静)` : "用户最近没有活动";
|
|
4753
4799
|
return [
|
|
4754
4800
|
"[自主唤醒]",
|
|
4755
|
-
`好感 ${String(facts.affection)}(${moodOf(facts.affection)}
|
|
4801
|
+
`好感 ${String(facts.affection)}(${moodOf(facts.affection)});羁绊 ${facts.bondTier}(一起过了 ${String(facts.bondDays)} 天);${lastSpoke};${user}。`,
|
|
4756
4802
|
"醒来想到什么就说一句(pet_say),想配动作就 pet_do;也可以只是安静待着(两者都不调,正文写「…」)。"
|
|
4757
4803
|
].join(" ");
|
|
4758
4804
|
}
|
|
@@ -4771,19 +4817,132 @@ function buildChatPrompt(text) {
|
|
|
4771
4817
|
return `[用户对你说] ${text.slice(0, 2e3)}`;
|
|
4772
4818
|
}
|
|
4773
4819
|
//#endregion
|
|
4820
|
+
//#region lib/types/petdata.js
|
|
4821
|
+
/**
|
|
4822
|
+
* The per-pet data store: everything the DRIVER persists about a pet lives
|
|
4823
|
+
* under `~/.dsh/whale598/<petId>/` — affection ledger, display name, event
|
|
4824
|
+
* history, tool-call history. One directory per pet, small files, single
|
|
4825
|
+
* writer (the driver), JSONL for append-only streams with bounded retention.
|
|
4826
|
+
*
|
|
4827
|
+
* Legacy layouts (`pet-affection.d/<id>.json`, the single legacy electron
|
|
4828
|
+
* book, names inside pet-settings.json) are READ as fallbacks and migrate to
|
|
4829
|
+
* the new layout on the next write — lazy migration, no data-loss window.
|
|
4830
|
+
* @module dsh-agentic-pet/petdata
|
|
4831
|
+
*/
|
|
4832
|
+
/** The consolidated per-pet data root under the dsh home. */
|
|
4833
|
+
const WHALE_DIR = "whale598";
|
|
4834
|
+
function petDataDir(home, petId) {
|
|
4835
|
+
return join(home, WHALE_DIR, petId);
|
|
4836
|
+
}
|
|
4837
|
+
/** New-layout path first; legacy layouts as read fallbacks (per-pet legacy
|
|
4838
|
+
* dir → the single legacy electron book). The ledger SHAPE is the driver's
|
|
4839
|
+
* (TouchLedger); this module only owns paths and raw IO. */
|
|
4840
|
+
function affectionPaths(home, petId) {
|
|
4841
|
+
return {
|
|
4842
|
+
current: join(petDataDir(home, petId), "pet-affection.json"),
|
|
4843
|
+
legacy: [join(home, "pet-affection.d", `${petId}.json`), ...petId === "electron" ? [join(home, "pet-affection.json")] : []]
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
/** Read the affection ledger raw: the current file wins; legacy files answer
|
|
4847
|
+
* only while the current one is absent (first write migrates the full
|
|
4848
|
+
* ledger). Returns null when no layout holds data. */
|
|
4849
|
+
async function readAffectionRaw(home, petId) {
|
|
4850
|
+
const { current, legacy } = affectionPaths(home, petId);
|
|
4851
|
+
for (const file of [current, ...legacy]) try {
|
|
4852
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
4853
|
+
if (parsed !== null && typeof parsed === "object" && Object.keys(parsed).length > 0) return parsed;
|
|
4854
|
+
} catch {}
|
|
4855
|
+
return null;
|
|
4856
|
+
}
|
|
4857
|
+
async function writeAffectionRaw(home, petId, ledger) {
|
|
4858
|
+
const dir = petDataDir(home, petId);
|
|
4859
|
+
await mkdir(dir, { recursive: true });
|
|
4860
|
+
await writeFile(join(dir, "pet-affection.json"), `${JSON.stringify(ledger)}\n`, "utf8");
|
|
4861
|
+
}
|
|
4862
|
+
function petNameFile(home, petId) {
|
|
4863
|
+
return join(petDataDir(home, petId), "pet-name.json");
|
|
4864
|
+
}
|
|
4865
|
+
/** The user's custom display name: new-layout file → legacy
|
|
4866
|
+
* pet-settings.json `names` map → undefined (caller applies the built-in
|
|
4867
|
+
* default). */
|
|
4868
|
+
async function readPetName(home, petId) {
|
|
4869
|
+
try {
|
|
4870
|
+
const parsed = JSON.parse(await readFile(petNameFile(home, petId), "utf8"));
|
|
4871
|
+
if (typeof parsed.name === "string" && parsed.name.trim() !== "") return parsed.name.trim();
|
|
4872
|
+
} catch {}
|
|
4873
|
+
try {
|
|
4874
|
+
const settings = JSON.parse(await readFile(join(home, "pet-settings.json"), "utf8"));
|
|
4875
|
+
const label = (typeof settings.names === "object" && settings.names !== null ? settings.names : {})[petId];
|
|
4876
|
+
if (typeof label === "string" && label.trim() !== "") return label.trim();
|
|
4877
|
+
} catch {}
|
|
4878
|
+
}
|
|
4879
|
+
/** Persist a custom name; `null` reverts to the built-in default (drops the
|
|
4880
|
+
* override file). Also strips the petId key from the legacy pet-settings
|
|
4881
|
+
* names map so the old layout cannot resurrect a cleared name. */
|
|
4882
|
+
async function writePetName(home, petId, name) {
|
|
4883
|
+
await mkdir(petDataDir(home, petId), { recursive: true });
|
|
4884
|
+
const file = petNameFile(home, petId);
|
|
4885
|
+
if (name === null || name.trim() === "") await rm(file, { force: true });
|
|
4886
|
+
else await writeFile(file, `${JSON.stringify({ name: name.trim() })}\n`, "utf8");
|
|
4887
|
+
try {
|
|
4888
|
+
const settingsPath = join(home, "pet-settings.json");
|
|
4889
|
+
const settings = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
4890
|
+
if (typeof settings.names === "object" && settings.names !== null && petId in settings.names) {
|
|
4891
|
+
delete settings.names[petId];
|
|
4892
|
+
if (Object.keys(settings.names).length === 0) delete settings.names;
|
|
4893
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
4894
|
+
}
|
|
4895
|
+
} catch {}
|
|
4896
|
+
}
|
|
4897
|
+
const JSONL_MAX_LINES = 600;
|
|
4898
|
+
const JSONL_KEEP_LINES = 500;
|
|
4899
|
+
function eventsFile(home, petId) {
|
|
4900
|
+
return join(petDataDir(home, petId), "events.jsonl");
|
|
4901
|
+
}
|
|
4902
|
+
function toolCallsFile(home, petId) {
|
|
4903
|
+
return join(petDataDir(home, petId), "toolcalls.jsonl");
|
|
4904
|
+
}
|
|
4905
|
+
/** Append one record; bounded retention trims the file when it outgrows the
|
|
4906
|
+
* cap (best-effort — a failed trim never loses the append). */
|
|
4907
|
+
async function appendJsonl(file, entry) {
|
|
4908
|
+
await mkdir(dirname(file), { recursive: true });
|
|
4909
|
+
await appendFile(file, `${JSON.stringify(entry)}\n`, "utf8");
|
|
4910
|
+
try {
|
|
4911
|
+
const lines = (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "");
|
|
4912
|
+
if (lines.length > JSONL_MAX_LINES) await writeFile(file, `${lines.slice(lines.length - JSONL_KEEP_LINES).join("\n")}\n`, "utf8");
|
|
4913
|
+
} catch {}
|
|
4914
|
+
}
|
|
4915
|
+
/** The last `limit` records (oldest last); malformed lines degrade to
|
|
4916
|
+
* `{raw}` instead of dropping the file. */
|
|
4917
|
+
async function readJsonl(file, limit) {
|
|
4918
|
+
try {
|
|
4919
|
+
return (await readFile(file, "utf8")).split("\n").filter((line) => line.trim() !== "").slice(-limit).map((line) => {
|
|
4920
|
+
try {
|
|
4921
|
+
return JSON.parse(line);
|
|
4922
|
+
} catch {
|
|
4923
|
+
return { raw: line };
|
|
4924
|
+
}
|
|
4925
|
+
});
|
|
4926
|
+
} catch {
|
|
4927
|
+
return [];
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
//#endregion
|
|
4774
4931
|
//#region lib/types/registry.js
|
|
4775
4932
|
/**
|
|
4776
4933
|
* The pet registry: a directory of per-pet control manifests, replacing the
|
|
4777
4934
|
* last-writer-wins single files whose races caused repeated swap incidents
|
|
4778
4935
|
* (a dying carrier's late write burying a live pet's registration). Each pet
|
|
4779
|
-
* implementation owns its own file under pet-http.d/
|
|
4936
|
+
* implementation owns its own file under whale598/pet-http.d/ (legacy
|
|
4937
|
+
* pet-http.d/ still scanned) and re-asserts it on a
|
|
4780
4938
|
* cadence; entries whose publisher pid is dead expire by omission. The
|
|
4781
4939
|
* driver reads the whole table and routes per the multi-pet design: the
|
|
4782
4940
|
* session's bound pet if alive, else the default pet (first alive entry),
|
|
4783
4941
|
* plus an all-notify broadcast mode for interactions.
|
|
4784
4942
|
* @module dsh-agentic-pet/registry
|
|
4785
4943
|
*/
|
|
4786
|
-
const REGISTRY_DIR =
|
|
4944
|
+
const REGISTRY_DIR = `${WHALE_DIR}/pet-http.d`;
|
|
4945
|
+
const LEGACY_REGISTRY_DIR = "pet-http.d";
|
|
4787
4946
|
/**
|
|
4788
4947
|
* A live pet is one whose control plane ANSWERS, not merely one whose pid
|
|
4789
4948
|
* exists — and not merely one whose port accepts TCP: a frozen pet (stuck
|
|
@@ -4818,27 +4977,37 @@ async function probeControlPlane(port) {
|
|
|
4818
4977
|
* they expire by omission the next time their file is rewritten — and so are
|
|
4819
4978
|
* silent entries (pid alive, control plane unresponsive). */
|
|
4820
4979
|
async function readLivePets(home) {
|
|
4980
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4981
|
+
for (const dir of [join(home, REGISTRY_DIR), join(home, LEGACY_REGISTRY_DIR)]) {
|
|
4982
|
+
let names = [];
|
|
4983
|
+
try {
|
|
4984
|
+
names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
|
|
4985
|
+
} catch {
|
|
4986
|
+
continue;
|
|
4987
|
+
}
|
|
4988
|
+
for (const name of names) {
|
|
4989
|
+
const id = name.slice(0, -5);
|
|
4990
|
+
if (id !== "" && !candidates.has(id)) candidates.set(id, join(dir, name));
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4821
4993
|
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 {}
|
|
4994
|
+
for (const [id, file] of candidates) try {
|
|
4995
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
4996
|
+
const port = typeof raw.port === "number" ? raw.port : NaN;
|
|
4997
|
+
const pid = typeof raw.pid === "number" ? raw.pid : NaN;
|
|
4998
|
+
if (!Number.isFinite(port) || !Number.isFinite(pid)) continue;
|
|
4999
|
+
try {
|
|
5000
|
+
process.kill(pid, 0);
|
|
5001
|
+
} catch {
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
if (!await probeControlPlane(port)) continue;
|
|
5005
|
+
live.push({
|
|
5006
|
+
id,
|
|
5007
|
+
port,
|
|
5008
|
+
pid,
|
|
5009
|
+
since: typeof raw.since === "number" ? raw.since : 0
|
|
5010
|
+
});
|
|
4842
5011
|
} catch {}
|
|
4843
5012
|
return live;
|
|
4844
5013
|
}
|
|
@@ -4920,15 +5089,35 @@ const EMPTY_LEDGER = {
|
|
|
4920
5089
|
lastTouchAt: void 0,
|
|
4921
5090
|
recent: []
|
|
4922
5091
|
};
|
|
4923
|
-
|
|
5092
|
+
const ADJUST_MAX_DELTA = 3;
|
|
5093
|
+
const ADJUST_COOLDOWN_MS = 5 * 6e4;
|
|
5094
|
+
const ADJUST_DAILY_LIMIT = 8;
|
|
5095
|
+
const ADJUST_DAILY_SUM = 10;
|
|
5096
|
+
const DECAY_START_DAYS = 3;
|
|
5097
|
+
const DECAY_STEP_DAYS = 3;
|
|
5098
|
+
async function readLedger(home, petId) {
|
|
4924
5099
|
try {
|
|
4925
|
-
const raw =
|
|
5100
|
+
const raw = await readAffectionRaw(home, petId);
|
|
5101
|
+
if (raw === null) return {
|
|
5102
|
+
...EMPTY_LEDGER,
|
|
5103
|
+
byPart: {},
|
|
5104
|
+
recent: []
|
|
5105
|
+
};
|
|
4926
5106
|
return {
|
|
4927
5107
|
affection: typeof raw.affection === "number" ? raw.affection : 0,
|
|
4928
5108
|
total: typeof raw.total === "number" ? raw.total : 0,
|
|
4929
5109
|
byPart: raw.byPart ?? {},
|
|
4930
5110
|
lastTouchAt: raw.lastTouchAt,
|
|
4931
|
-
recent: raw.recent ?? []
|
|
5111
|
+
recent: raw.recent ?? [],
|
|
5112
|
+
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,
|
|
5113
|
+
quota: typeof raw.quota === "object" && raw.quota !== null ? raw.quota : void 0,
|
|
5114
|
+
lastDecayAt: typeof raw.lastDecayAt === "number" ? raw.lastDecayAt : void 0,
|
|
5115
|
+
bond: typeof raw.bond === "object" && raw.bond !== null && typeof raw.bond.lastCountedDate === "string" ? {
|
|
5116
|
+
value: typeof raw.bond.value === "number" ? raw.bond.value : 0,
|
|
5117
|
+
days: typeof raw.bond.days === "number" ? raw.bond.days : 0,
|
|
5118
|
+
lastCountedDate: raw.bond.lastCountedDate
|
|
5119
|
+
} : void 0,
|
|
5120
|
+
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
5121
|
};
|
|
4933
5122
|
} catch {
|
|
4934
5123
|
return {
|
|
@@ -4938,12 +5127,6 @@ async function readLedger(ledgerFile) {
|
|
|
4938
5127
|
};
|
|
4939
5128
|
}
|
|
4940
5129
|
}
|
|
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
5130
|
/** Local-time log stamp: the driver log is the user's primary debugging
|
|
4948
5131
|
* tool, so timestamps show the USER's wall clock, not UTC (toISOString's Z
|
|
4949
5132
|
* form read 8h off and cost a confusion round). Sortable, zero-padded. */
|
|
@@ -4995,6 +5178,23 @@ const voiceLastSpokeAt = /* @__PURE__ */ new Map();
|
|
|
4995
5178
|
const voiceLastTouchReplyAt = /* @__PURE__ */ new Map();
|
|
4996
5179
|
const voiceNextWakeAt = /* @__PURE__ */ new Map();
|
|
4997
5180
|
const voiceGreetedOnce = /* @__PURE__ */ new Set();
|
|
5181
|
+
const voiceNames = /* @__PURE__ */ new Map();
|
|
5182
|
+
const DEFAULT_PET_NAME = { electron: "鲸鱼娘" };
|
|
5183
|
+
/** B-layer bond day counting: the FIRST interaction of a calendar day
|
|
5184
|
+
* (touch / user chat / model adjustment) marks that day as shared. Called
|
|
5185
|
+
* at the interaction points, never on reads — reading the ledger does not
|
|
5186
|
+
* make a day meaningful. Mutates in place; the caller persists. Module-level
|
|
5187
|
+
* because both the driver closures AND the model tools (registerPetTools)
|
|
5188
|
+
* count bond days. */
|
|
5189
|
+
const countBondDay = (ledger) => {
|
|
5190
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
5191
|
+
if (ledger.bond?.lastCountedDate === today) return;
|
|
5192
|
+
ledger.bond = {
|
|
5193
|
+
value: ledger.bond?.value ?? 0,
|
|
5194
|
+
days: (ledger.bond?.days ?? 0) + 1,
|
|
5195
|
+
lastCountedDate: today
|
|
5196
|
+
};
|
|
5197
|
+
};
|
|
4998
5198
|
const voiceLastPrompt = /* @__PURE__ */ new Map();
|
|
4999
5199
|
const voiceBoundSessions = /* @__PURE__ */ new Map();
|
|
5000
5200
|
const voiceWakeTimer = { timer: void 0 };
|
|
@@ -5024,6 +5224,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5024
5224
|
voiceLastTouchReplyAt.clear();
|
|
5025
5225
|
voiceNextWakeAt.clear();
|
|
5026
5226
|
voiceGreetedOnce.clear();
|
|
5227
|
+
voiceNames.clear();
|
|
5027
5228
|
voiceLastPrompt.clear();
|
|
5028
5229
|
voiceBoundSessions.clear();
|
|
5029
5230
|
if (voiceWakeTimer.timer !== void 0) clearTimeout(voiceWakeTimer.timer);
|
|
@@ -5056,7 +5257,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5056
5257
|
if (!latestArchived.has(entry.sessionId)) continue;
|
|
5057
5258
|
const generation = (voiceStore.generations[petId] ?? 0) + 1;
|
|
5058
5259
|
voiceStore.generations[petId] = generation;
|
|
5059
|
-
|
|
5260
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5060
5261
|
const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
|
|
5061
5262
|
if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
|
|
5062
5263
|
voicePendingUntil.delete(petId);
|
|
@@ -5082,13 +5283,15 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5082
5283
|
}
|
|
5083
5284
|
};
|
|
5084
5285
|
const attachedSessions = /* @__PURE__ */ new Set();
|
|
5286
|
+
const petWorkspaceDir = () => join(homedir(), "whale598");
|
|
5085
5287
|
const attachVoiceSession = async (sessionId) => {
|
|
5086
5288
|
if (attachedSessions.has(sessionId)) return;
|
|
5087
5289
|
attachedSessions.add(sessionId);
|
|
5088
5290
|
try {
|
|
5089
5291
|
const registry = ctx.get("workspaceRegistry");
|
|
5090
5292
|
if (registry === void 0) return;
|
|
5091
|
-
await (
|
|
5293
|
+
await mkdir(petWorkspaceDir(), { recursive: true });
|
|
5294
|
+
await (await registry.create(petWorkspaceDir()))?.attachSession?.(sessionId);
|
|
5092
5295
|
driverLog(home, `VOICE session attached to workspace (${sessionId})`);
|
|
5093
5296
|
} catch (error) {
|
|
5094
5297
|
attachedSessions.delete(sessionId);
|
|
@@ -5210,7 +5413,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5210
5413
|
const petId = voiceOwners.get(sessionId);
|
|
5211
5414
|
voiceOwners.delete(sessionId);
|
|
5212
5415
|
if (petId !== void 0) {
|
|
5213
|
-
|
|
5416
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5214
5417
|
voicePendingUntil.delete(petId);
|
|
5215
5418
|
saveInnerVoiceStore(home, voiceStore);
|
|
5216
5419
|
driverLog(home, `VOICE session removed by user; pet=${petId} will re-create on next wake`);
|
|
@@ -5222,6 +5425,22 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5222
5425
|
boundPetId = void 0;
|
|
5223
5426
|
pushState("idle");
|
|
5224
5427
|
});
|
|
5428
|
+
const refreshNames = async () => {
|
|
5429
|
+
voiceNames.clear();
|
|
5430
|
+
try {
|
|
5431
|
+
for (const petId of await readdir(join(home, WHALE_DIR))) {
|
|
5432
|
+
const name = await readPetName(home, petId);
|
|
5433
|
+
if (name !== void 0) voiceNames.set(petId, name);
|
|
5434
|
+
}
|
|
5435
|
+
} catch {}
|
|
5436
|
+
try {
|
|
5437
|
+
const raw = JSON.parse(await readFile(join(home, "pet-settings.json"), "utf8"));
|
|
5438
|
+
if (typeof raw.names === "object" && raw.names !== null) {
|
|
5439
|
+
for (const [petId, label] of Object.entries(raw.names)) if (petId !== "" && typeof label === "string" && label.trim() !== "" && !voiceNames.has(petId)) voiceNames.set(petId, label.trim());
|
|
5440
|
+
}
|
|
5441
|
+
} catch {}
|
|
5442
|
+
};
|
|
5443
|
+
refreshNames();
|
|
5225
5444
|
const refreshBoundSessions = async () => {
|
|
5226
5445
|
const config = await readInnerVoiceConfig(home);
|
|
5227
5446
|
voiceBoundSessions.clear();
|
|
@@ -5229,7 +5448,31 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5229
5448
|
};
|
|
5230
5449
|
refreshBoundSessions();
|
|
5231
5450
|
events.on("pet-settings/changed", () => {
|
|
5232
|
-
|
|
5451
|
+
const prevNames = new Map(voiceNames);
|
|
5452
|
+
(async () => {
|
|
5453
|
+
await refreshNames();
|
|
5454
|
+
refreshBoundSessions();
|
|
5455
|
+
const changedPets = new Set([...prevNames.keys(), ...voiceNames.keys()]);
|
|
5456
|
+
for (const petId of changedPets) {
|
|
5457
|
+
if (prevNames.get(petId) === voiceNames.get(petId)) continue;
|
|
5458
|
+
const nextName = voiceNames.get(petId);
|
|
5459
|
+
const label = nextName ?? DEFAULT_PET_NAME[petId] ?? petId;
|
|
5460
|
+
const entry = voiceStore.sessions[petId];
|
|
5461
|
+
if (entry !== void 0) {
|
|
5462
|
+
const title = `${label}的内心戏`;
|
|
5463
|
+
try {
|
|
5464
|
+
await voiceController?.rename({
|
|
5465
|
+
sessionId: entry.sessionId,
|
|
5466
|
+
title
|
|
5467
|
+
});
|
|
5468
|
+
driverLog(home, `VOICE session retitled pet=${petId} → ${title}`);
|
|
5469
|
+
} catch (error) {
|
|
5470
|
+
driverLog(home, `VOICE retitle skipped pet=${petId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
triggerRenameNotice(petId, nextName, label);
|
|
5474
|
+
}
|
|
5475
|
+
})();
|
|
5233
5476
|
if (currentState !== void 0) pushState(currentState);
|
|
5234
5477
|
});
|
|
5235
5478
|
const startAskPet = (pet, body, signal) => {
|
|
@@ -5403,16 +5646,19 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5403
5646
|
if (minted.hops > 0) driverLog(home, `VOICE mint skipped ${String(minted.hops)} archived id(s) pet=${petId} → ${minted.sessionId}`);
|
|
5404
5647
|
const sessionId = minted.sessionId;
|
|
5405
5648
|
try {
|
|
5649
|
+
await mkdir(petWorkspaceDir(), { recursive: true });
|
|
5406
5650
|
await voiceController.create({
|
|
5407
5651
|
sessionId,
|
|
5408
|
-
agentPreset: config.preset
|
|
5652
|
+
agentPreset: config.preset,
|
|
5653
|
+
cwd: petWorkspaceDir()
|
|
5409
5654
|
});
|
|
5410
5655
|
} catch (firstError) {
|
|
5411
5656
|
driverLog(home, `VOICE preset "${config.preset}" rejected for pet=${petId}: ${firstError instanceof Error ? firstError.message : String(firstError)}; falling back`);
|
|
5412
5657
|
try {
|
|
5413
5658
|
await voiceController.create({
|
|
5414
5659
|
sessionId,
|
|
5415
|
-
agentPreset: "
|
|
5660
|
+
agentPreset: "whale598",
|
|
5661
|
+
cwd: petWorkspaceDir()
|
|
5416
5662
|
});
|
|
5417
5663
|
} catch (secondError) {
|
|
5418
5664
|
driverLog(home, `VOICE session create failed for pet=${petId}: ${secondError instanceof Error ? secondError.message : String(secondError)}`);
|
|
@@ -5420,9 +5666,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5420
5666
|
}
|
|
5421
5667
|
}
|
|
5422
5668
|
try {
|
|
5669
|
+
const customName = voiceNames.get(petId);
|
|
5423
5670
|
await voiceController.rename({
|
|
5424
5671
|
sessionId,
|
|
5425
|
-
title:
|
|
5672
|
+
title: `${customName ?? DEFAULT_PET_NAME[petId] ?? petId}的内心戏`
|
|
5426
5673
|
});
|
|
5427
5674
|
} catch {}
|
|
5428
5675
|
voiceStore.sessions[petId] = {
|
|
@@ -5447,6 +5694,9 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5447
5694
|
const promptVoice = async (petId, text, opts = {}) => {
|
|
5448
5695
|
const { retried = false, userDriven = false } = opts;
|
|
5449
5696
|
if (latestArchived.has(voiceStore.sessions[petId]?.sessionId ?? "")) reconcileArchivedVoiceSessions();
|
|
5697
|
+
const customName = voiceNames.get(petId);
|
|
5698
|
+
if (customName !== void 0 && !text.startsWith("[名字]")) text = `[名字] 你叫「${customName}」。
|
|
5699
|
+
${text}`;
|
|
5450
5700
|
const sessionId = await ensureVoiceSession(petId);
|
|
5451
5701
|
if (sessionId === void 0 || voiceController === void 0) return false;
|
|
5452
5702
|
try {
|
|
@@ -5460,7 +5710,24 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5460
5710
|
}]
|
|
5461
5711
|
}, new AbortController().signal);
|
|
5462
5712
|
} catch (error) {
|
|
5463
|
-
|
|
5713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5714
|
+
driverLog(home, `VOICE prompt failed pet=${petId}: ${message}`);
|
|
5715
|
+
if (/Unknown agent preset/i.test(message) && !retried) {
|
|
5716
|
+
if (voiceStore.sessions[petId] !== void 0) {
|
|
5717
|
+
const generation = (voiceStore.generations[petId] ?? 0) + 1;
|
|
5718
|
+
voiceStore.generations[petId] = generation;
|
|
5719
|
+
Reflect.deleteProperty(voiceStore.sessions, petId);
|
|
5720
|
+
const ownerEntry = [...voiceOwners.entries()].find(([, owner]) => owner === petId);
|
|
5721
|
+
if (ownerEntry !== void 0) voiceOwners.delete(ownerEntry[0]);
|
|
5722
|
+
voiceLastPrompt.delete(petId);
|
|
5723
|
+
saveInnerVoiceStore(home, voiceStore);
|
|
5724
|
+
driverLog(home, `VOICE session retired pet=${petId} (recorded preset no longer exists — id renamed) — fresh -g${generation} mounts the current preset`);
|
|
5725
|
+
return promptVoice(petId, text, {
|
|
5726
|
+
retried: true,
|
|
5727
|
+
userDriven
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5464
5731
|
return false;
|
|
5465
5732
|
}
|
|
5466
5733
|
voicePendingUntil.set(petId, Date.now() + 25e3);
|
|
@@ -5507,11 +5774,25 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5507
5774
|
* greeting (pet offline) releases the flag for the next sweep to retry. */
|
|
5508
5775
|
const triggerGreeting = async (petId) => {
|
|
5509
5776
|
if (!await voiceGateOpen(petId, false, "greeting")) return;
|
|
5510
|
-
|
|
5777
|
+
const ledger = await readLedger(home, petId);
|
|
5778
|
+
if (await promptVoice(petId, buildGreetPrompt({
|
|
5779
|
+
affection: ledger.affection,
|
|
5780
|
+
bondValue: ledger.bond?.value ?? 0,
|
|
5781
|
+
bondDays: ledger.bond?.days ?? 0,
|
|
5782
|
+
bondTier: bondTier(ledger.bond?.value ?? 0)
|
|
5783
|
+
}))) {
|
|
5511
5784
|
voiceGreetedOnce.add(petId);
|
|
5512
5785
|
driverLog(home, `VOICE boot greeting pet=${petId}`);
|
|
5513
5786
|
} else voiceGreetedOnce.delete(petId);
|
|
5514
5787
|
};
|
|
5788
|
+
/** Rename notice (user request): fired when the user's custom name actually
|
|
5789
|
+
* changes — she acknowledges out loud, once. The per-turn short name line
|
|
5790
|
+
* stays as the compaction-proof invariant; a skipped notice (pet offline /
|
|
5791
|
+
* turn in flight) loses nothing because that line still carries the name. */
|
|
5792
|
+
const triggerRenameNotice = async (petId, nextName, label) => {
|
|
5793
|
+
if (!await voiceGateOpen(petId, false, "rename")) return;
|
|
5794
|
+
if (await promptVoice(petId, nextName !== void 0 ? `[改名] 用户给你改名叫「${label}」——以后自我介绍、气泡里自称,都用这个名字。` : `[改名] 用户把你改回了「${label}」。`)) driverLog(home, `VOICE rename notice pet=${petId} → ${label}`);
|
|
5795
|
+
};
|
|
5515
5796
|
/** Random-wake trigger (docs/inner-voice.md §6.3). The gate decides whether
|
|
5516
5797
|
* this wake speaks — never the model. */
|
|
5517
5798
|
const triggerWake = async (petId) => {
|
|
@@ -5523,11 +5804,14 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5523
5804
|
return;
|
|
5524
5805
|
}
|
|
5525
5806
|
if (!await voiceGateOpen(petId, true, "wake")) return;
|
|
5526
|
-
const ledger = await readLedger(
|
|
5807
|
+
const ledger = await readLedger(home, petId);
|
|
5527
5808
|
const now = Date.now();
|
|
5528
5809
|
const lastSpoke = voiceLastSpokeAt.get(petId);
|
|
5529
5810
|
if (await promptVoice(petId, buildWakePrompt({
|
|
5530
5811
|
affection: ledger.affection,
|
|
5812
|
+
bondValue: ledger.bond?.value ?? 0,
|
|
5813
|
+
bondDays: ledger.bond?.days ?? 0,
|
|
5814
|
+
bondTier: bondTier(ledger.bond?.value ?? 0),
|
|
5531
5815
|
minutesSinceLastSpoke: lastSpoke === void 0 ? void 0 : Math.round((now - lastSpoke) / 6e4),
|
|
5532
5816
|
minutesSinceUserActivity: voiceUserActivityAt === void 0 ? void 0 : Math.round((now - voiceUserActivityAt) / 6e4),
|
|
5533
5817
|
userActive: voiceUserActivityAt !== void 0 && now - voiceUserActivityAt < 30 * 6e4
|
|
@@ -5555,7 +5839,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5555
5839
|
return;
|
|
5556
5840
|
}
|
|
5557
5841
|
if (!await voiceGateOpen(petId, false, "touch", true)) return;
|
|
5558
|
-
const ledger = await readLedger(
|
|
5842
|
+
const ledger = await readLedger(home, petId);
|
|
5559
5843
|
if (await promptVoice(petId, buildTouchPrompt({
|
|
5560
5844
|
part,
|
|
5561
5845
|
touchesToday: ledger.recent.filter((entry) => entry.part === part).length + 1,
|
|
@@ -5613,12 +5897,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5613
5897
|
wakeSweep();
|
|
5614
5898
|
const lastEventSeqs = /* @__PURE__ */ new Map();
|
|
5615
5899
|
const recordTouch = async (petId, part, at) => {
|
|
5616
|
-
const
|
|
5617
|
-
|
|
5900
|
+
const ledger = await readLedger(home, petId);
|
|
5901
|
+
countBondDay(ledger);
|
|
5618
5902
|
ledger.total += 1;
|
|
5619
5903
|
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
5904
|
ledger.lastTouchAt = at;
|
|
5623
5905
|
ledger.recent.unshift({
|
|
5624
5906
|
part,
|
|
@@ -5626,12 +5908,11 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5626
5908
|
});
|
|
5627
5909
|
ledger.recent = ledger.recent.slice(0, 10);
|
|
5628
5910
|
try {
|
|
5629
|
-
await
|
|
5630
|
-
`, "utf8");
|
|
5911
|
+
await writeAffectionRaw(home, petId, ledger);
|
|
5631
5912
|
} catch (error) {
|
|
5632
5913
|
console.error(`pet-surface driver: could not write the ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
5633
5914
|
}
|
|
5634
|
-
if (Math.floor(ledger.
|
|
5915
|
+
if (Math.floor(ledger.total / 10) > Math.floor((ledger.total - 1) / 10)) {
|
|
5635
5916
|
const target = (await pet.list()).find((pet) => pet.id === petId);
|
|
5636
5917
|
if (target !== void 0) fetch(`http://127.0.0.1:${String(target.port)}/action`, {
|
|
5637
5918
|
method: "POST",
|
|
@@ -5641,17 +5922,23 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5641
5922
|
}
|
|
5642
5923
|
triggerTouchReply(petId, part);
|
|
5643
5924
|
};
|
|
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
5925
|
const handlePetEvent = async (petId, event) => {
|
|
5926
|
+
appendJsonl(eventsFile(home, petId), {
|
|
5927
|
+
seq: event.seq,
|
|
5928
|
+
type: event.type,
|
|
5929
|
+
...event.type === "touch" ? { part: event.part } : {},
|
|
5930
|
+
at: event.at
|
|
5931
|
+
});
|
|
5650
5932
|
if (event.type === "touch") {
|
|
5651
5933
|
if (event.part !== void 0) await recordTouch(petId, event.part, event.at);
|
|
5652
5934
|
return;
|
|
5653
5935
|
}
|
|
5654
|
-
if (event.type === "chat" && event.text !== void 0 && event.text !== "")
|
|
5936
|
+
if (event.type === "chat" && event.text !== void 0 && event.text !== "") {
|
|
5937
|
+
const ledger = await readLedger(home, petId);
|
|
5938
|
+
countBondDay(ledger);
|
|
5939
|
+
await writeAffectionRaw(home, petId, ledger);
|
|
5940
|
+
triggerChat(petId, event.text);
|
|
5941
|
+
}
|
|
5655
5942
|
};
|
|
5656
5943
|
/** Per-pet long-poll loops (§6 v4): each live pet parks ONE /events
|
|
5657
5944
|
* hold=30 request; the answer — an event batch or the 30s empty one —
|
|
@@ -5774,10 +6061,14 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5774
6061
|
}]
|
|
5775
6062
|
},
|
|
5776
6063
|
async execute(args, exec) {
|
|
6064
|
+
const callStartedAt = Date.now();
|
|
6065
|
+
let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
|
|
6066
|
+
let callDetail;
|
|
5777
6067
|
const text = String(args.text).slice(0, 200);
|
|
5778
6068
|
console.log(`pet-surface driver: pet_say ${JSON.stringify(text).slice(0, 60)}`);
|
|
5779
6069
|
try {
|
|
5780
6070
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6071
|
+
callPet = entry.id;
|
|
5781
6072
|
await pet.call(entry, "/say", {
|
|
5782
6073
|
method: "POST",
|
|
5783
6074
|
headers: { "content-type": "application/json" },
|
|
@@ -5785,7 +6076,17 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5785
6076
|
});
|
|
5786
6077
|
if (markVoiceSpoke(entry.id)) driverLog(home, `VOICE pending released by say pet=${entry.id} (inner-voice turn complete)`);
|
|
5787
6078
|
} catch (error) {
|
|
6079
|
+
callDetail = error instanceof Error ? error.message : String(error);
|
|
5788
6080
|
throw classify(error);
|
|
6081
|
+
} finally {
|
|
6082
|
+
appendJsonl(toolCallsFile(home, callPet), {
|
|
6083
|
+
tool: "pet_say",
|
|
6084
|
+
petId: callPet,
|
|
6085
|
+
ok: callDetail === void 0,
|
|
6086
|
+
...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
|
|
6087
|
+
durationMs: Date.now() - callStartedAt,
|
|
6088
|
+
at: Date.now()
|
|
6089
|
+
});
|
|
5789
6090
|
}
|
|
5790
6091
|
return JSON.stringify({
|
|
5791
6092
|
shown: true,
|
|
@@ -5819,8 +6120,12 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5819
6120
|
}]
|
|
5820
6121
|
},
|
|
5821
6122
|
async execute(args, exec) {
|
|
6123
|
+
const callStartedAt = Date.now();
|
|
6124
|
+
let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
|
|
6125
|
+
let callDetail;
|
|
5822
6126
|
try {
|
|
5823
6127
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6128
|
+
callPet = entry.id;
|
|
5824
6129
|
const requested = String(args.action);
|
|
5825
6130
|
const resolution = resolveActionRequest(requested, (await pet.capabilities(entry))?.actions ?? null);
|
|
5826
6131
|
if (resolution.kind === "unknown") throw new Error(`unknown action "${requested}" for pet "${entry.id}". Available: ${formatAvailableActions(resolution.available)}`);
|
|
@@ -5863,7 +6168,17 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5863
6168
|
waitTimedOut: true
|
|
5864
6169
|
});
|
|
5865
6170
|
} catch (error) {
|
|
6171
|
+
callDetail = error instanceof Error ? error.message : String(error);
|
|
5866
6172
|
throw classify(error);
|
|
6173
|
+
} finally {
|
|
6174
|
+
appendJsonl(toolCallsFile(home, callPet), {
|
|
6175
|
+
tool: "pet_do",
|
|
6176
|
+
petId: callPet,
|
|
6177
|
+
ok: callDetail === void 0,
|
|
6178
|
+
...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
|
|
6179
|
+
durationMs: Date.now() - callStartedAt,
|
|
6180
|
+
at: Date.now()
|
|
6181
|
+
});
|
|
5867
6182
|
}
|
|
5868
6183
|
}
|
|
5869
6184
|
}));
|
|
@@ -5882,8 +6197,12 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5882
6197
|
}]
|
|
5883
6198
|
},
|
|
5884
6199
|
async execute(args, exec) {
|
|
6200
|
+
const callStartedAt = Date.now();
|
|
6201
|
+
let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
|
|
6202
|
+
let callDetail;
|
|
5885
6203
|
try {
|
|
5886
6204
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6205
|
+
callPet = entry.id;
|
|
5887
6206
|
const cursor = digestCursors.get(entry.id) ?? 0;
|
|
5888
6207
|
let payload = await readEvents(pet, entry, cursor, 0);
|
|
5889
6208
|
if (payload.latest < cursor) payload = await readEvents(pet, entry, 0, 0);
|
|
@@ -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,
|
|
@@ -5912,7 +6231,17 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5912
6231
|
cursor: newest
|
|
5913
6232
|
});
|
|
5914
6233
|
} catch (error) {
|
|
6234
|
+
callDetail = error instanceof Error ? error.message : String(error);
|
|
5915
6235
|
throw classify(error);
|
|
6236
|
+
} finally {
|
|
6237
|
+
appendJsonl(toolCallsFile(home, callPet), {
|
|
6238
|
+
tool: "pet_digest",
|
|
6239
|
+
petId: callPet,
|
|
6240
|
+
ok: callDetail === void 0,
|
|
6241
|
+
...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
|
|
6242
|
+
durationMs: Date.now() - callStartedAt,
|
|
6243
|
+
at: Date.now()
|
|
6244
|
+
});
|
|
5916
6245
|
}
|
|
5917
6246
|
}
|
|
5918
6247
|
}));
|
|
@@ -5937,9 +6266,215 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5937
6266
|
const raw = JSON.parse(await readFile(join(home, "pet-interactions.json"), "utf8"));
|
|
5938
6267
|
if (Array.isArray(raw)) list = raw;
|
|
5939
6268
|
} catch {}
|
|
6269
|
+
appendJsonl(toolCallsFile(home, "all"), {
|
|
6270
|
+
tool: "pet_interactions",
|
|
6271
|
+
petId: "all",
|
|
6272
|
+
ok: true,
|
|
6273
|
+
durationMs: 0,
|
|
6274
|
+
at: Date.now()
|
|
6275
|
+
});
|
|
5940
6276
|
return JSON.stringify({ recent: list.slice(0, limit) });
|
|
5941
6277
|
}
|
|
5942
6278
|
}));
|
|
6279
|
+
const settleDecay = (ledger, now) => {
|
|
6280
|
+
const silentDays = (now - Math.max(ledger.lastTouchAt ?? 0, ledger.adjustments?.[0]?.at ?? 0, ledger.lastDecayAt ?? 0)) / 864e5;
|
|
6281
|
+
if (silentDays <= DECAY_START_DAYS) return 0;
|
|
6282
|
+
return Math.min(ledger.affection, Math.floor((silentDays - DECAY_START_DAYS) / DECAY_STEP_DAYS) + 1);
|
|
6283
|
+
};
|
|
6284
|
+
scope.tools.register(defineTool({
|
|
6285
|
+
name: "pet_affection_get",
|
|
6286
|
+
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 any pending decay. Call this whenever you are deciding whether and how to adjust.",
|
|
6287
|
+
parameters: { pet: {
|
|
6288
|
+
type: "string",
|
|
6289
|
+
description: "Which pet: its id from pet_state. Omit for the default pet."
|
|
6290
|
+
} },
|
|
6291
|
+
output: {
|
|
6292
|
+
schema: { type: "string" },
|
|
6293
|
+
render: (_args, value) => [{
|
|
6294
|
+
type: "text",
|
|
6295
|
+
text: value
|
|
6296
|
+
}]
|
|
6297
|
+
},
|
|
6298
|
+
async execute(args, exec) {
|
|
6299
|
+
const callStartedAt = Date.now();
|
|
6300
|
+
let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
|
|
6301
|
+
let callDetail;
|
|
6302
|
+
try {
|
|
6303
|
+
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6304
|
+
callPet = entry.id;
|
|
6305
|
+
const ledger = await readLedger(home, entry.id);
|
|
6306
|
+
const pendingDecay = settleDecay(ledger, Date.now());
|
|
6307
|
+
const effective = Math.max(0, ledger.affection - pendingDecay);
|
|
6308
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6309
|
+
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
6310
|
+
date: today,
|
|
6311
|
+
count: 0,
|
|
6312
|
+
sum: 0
|
|
6313
|
+
};
|
|
6314
|
+
return JSON.stringify({
|
|
6315
|
+
pet: entry.id,
|
|
6316
|
+
affection: effective,
|
|
6317
|
+
mood: moodOf(effective),
|
|
6318
|
+
touch: {
|
|
6319
|
+
total: ledger.total,
|
|
6320
|
+
byPart: ledger.byPart,
|
|
6321
|
+
lastTouchAt: ledger.lastTouchAt ?? null
|
|
6322
|
+
},
|
|
6323
|
+
bond: {
|
|
6324
|
+
value: ledger.bond?.value ?? 0,
|
|
6325
|
+
days: ledger.bond?.days ?? 0,
|
|
6326
|
+
tier: bondTier(ledger.bond?.value ?? 0)
|
|
6327
|
+
},
|
|
6328
|
+
recentBondAdjustments: (ledger.bondAdjustments ?? []).slice(0, 3),
|
|
6329
|
+
recentAdjustments: (ledger.adjustments ?? []).slice(0, 5),
|
|
6330
|
+
quota: {
|
|
6331
|
+
used: quota.count,
|
|
6332
|
+
limit: ADJUST_DAILY_LIMIT,
|
|
6333
|
+
sum: quota.sum,
|
|
6334
|
+
sumLimit: ADJUST_DAILY_SUM
|
|
6335
|
+
},
|
|
6336
|
+
pendingDecay
|
|
6337
|
+
});
|
|
6338
|
+
} catch (error) {
|
|
6339
|
+
callDetail = error instanceof Error ? error.message : String(error);
|
|
6340
|
+
throw classify(error);
|
|
6341
|
+
} finally {
|
|
6342
|
+
appendJsonl(toolCallsFile(home, callPet), {
|
|
6343
|
+
tool: "pet_affection_get",
|
|
6344
|
+
petId: callPet,
|
|
6345
|
+
ok: callDetail === void 0,
|
|
6346
|
+
...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
|
|
6347
|
+
durationMs: Date.now() - callStartedAt,
|
|
6348
|
+
at: Date.now()
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
}));
|
|
6353
|
+
scope.tools.register(defineTool({
|
|
6354
|
+
name: "pet_affection_adjust",
|
|
6355
|
+
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.",
|
|
6356
|
+
parameters: {
|
|
6357
|
+
delta: {
|
|
6358
|
+
type: "number",
|
|
6359
|
+
required: true,
|
|
6360
|
+
description: "The change: a NON-ZERO integer between -3 and +3."
|
|
6361
|
+
},
|
|
6362
|
+
reason: {
|
|
6363
|
+
type: "string",
|
|
6364
|
+
required: true,
|
|
6365
|
+
description: "WHY this delta — cite the concrete interaction. Empty or generic reasons are rejected."
|
|
6366
|
+
},
|
|
6367
|
+
pet: {
|
|
6368
|
+
type: "string",
|
|
6369
|
+
description: "Which pet: its id from pet_state. Omit for the default pet."
|
|
6370
|
+
},
|
|
6371
|
+
bondDelta: {
|
|
6372
|
+
type: "number",
|
|
6373
|
+
description: "OPTIONAL change to the long-term bond: a NON-ZERO integer between -1 and +1. The slowest variable — the smallest steps."
|
|
6374
|
+
},
|
|
6375
|
+
bondReason: {
|
|
6376
|
+
type: "string",
|
|
6377
|
+
description: "Required when bondDelta is given: the concrete event that moved the bond."
|
|
6378
|
+
}
|
|
6379
|
+
},
|
|
6380
|
+
output: {
|
|
6381
|
+
schema: { type: "string" },
|
|
6382
|
+
render: (_args, value) => [{
|
|
6383
|
+
type: "text",
|
|
6384
|
+
text: value
|
|
6385
|
+
}]
|
|
6386
|
+
},
|
|
6387
|
+
async execute(args, exec) {
|
|
6388
|
+
const callStartedAt = Date.now();
|
|
6389
|
+
let callPet = typeof args.pet === "string" && args.pet !== "" ? args.pet : "default";
|
|
6390
|
+
let callDetail;
|
|
6391
|
+
try {
|
|
6392
|
+
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6393
|
+
callPet = entry.id;
|
|
6394
|
+
const ledger = await readLedger(home, entry.id);
|
|
6395
|
+
const now = Date.now();
|
|
6396
|
+
const pendingDecay = settleDecay(ledger, now);
|
|
6397
|
+
if (pendingDecay > 0) {
|
|
6398
|
+
ledger.affection = Math.max(0, ledger.affection - pendingDecay);
|
|
6399
|
+
ledger.adjustments = [{
|
|
6400
|
+
delta: -pendingDecay,
|
|
6401
|
+
reason: `system: decay(${String(pendingDecay)} 天未互动)`,
|
|
6402
|
+
at: now,
|
|
6403
|
+
system: true
|
|
6404
|
+
}, ...ledger.adjustments ?? []];
|
|
6405
|
+
ledger.lastDecayAt = now;
|
|
6406
|
+
}
|
|
6407
|
+
const delta = typeof args.delta === "number" && Number.isInteger(args.delta) ? args.delta : NaN;
|
|
6408
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
6409
|
+
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)})`);
|
|
6410
|
+
if (reason === "") throw new Error("reason 必填:没有理由的调整不予记录");
|
|
6411
|
+
const adjustments = ledger.adjustments ?? [];
|
|
6412
|
+
const cooldownLeft = (adjustments.find((a) => !a.system)?.at ?? 0) + ADJUST_COOLDOWN_MS - now;
|
|
6413
|
+
if (cooldownLeft > 0) throw new Error(`距上次调整还有 ${String(Math.ceil(cooldownLeft / 1e3))} 秒冷却`);
|
|
6414
|
+
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6415
|
+
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
6416
|
+
date: today,
|
|
6417
|
+
count: 0,
|
|
6418
|
+
sum: 0
|
|
6419
|
+
};
|
|
6420
|
+
if (quota.count >= ADJUST_DAILY_LIMIT) throw new Error(`今日调整次数已用完(${String(ADJUST_DAILY_LIMIT)})——明天再判断,或者相信当前的关系状态`);
|
|
6421
|
+
if (Math.abs(quota.sum + delta) > ADJUST_DAILY_SUM) throw new Error(`今日净变动将超过 ±${String(ADJUST_DAILY_SUM)}(当前 ${String(quota.sum)})——剩下的留给明天`);
|
|
6422
|
+
countBondDay(ledger);
|
|
6423
|
+
const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
|
|
6424
|
+
const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
|
|
6425
|
+
if (bondDelta !== 0) {
|
|
6426
|
+
if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
|
|
6427
|
+
if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
|
|
6428
|
+
}
|
|
6429
|
+
ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
|
|
6430
|
+
quota.count += 1;
|
|
6431
|
+
quota.sum += delta;
|
|
6432
|
+
ledger.quota = quota;
|
|
6433
|
+
ledger.adjustments = [{
|
|
6434
|
+
delta,
|
|
6435
|
+
reason,
|
|
6436
|
+
at: now
|
|
6437
|
+
}, ...adjustments].slice(0, 20);
|
|
6438
|
+
if (bondDelta !== 0) {
|
|
6439
|
+
ledger.bond = {
|
|
6440
|
+
value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
|
|
6441
|
+
days: ledger.bond?.days ?? 0,
|
|
6442
|
+
lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
|
|
6443
|
+
};
|
|
6444
|
+
ledger.bondAdjustments = [{
|
|
6445
|
+
delta: bondDelta,
|
|
6446
|
+
reason: bondReason,
|
|
6447
|
+
at: now
|
|
6448
|
+
}, ...ledger.bondAdjustments ?? []].slice(0, 10);
|
|
6449
|
+
}
|
|
6450
|
+
await writeAffectionRaw(home, entry.id, ledger);
|
|
6451
|
+
return JSON.stringify({
|
|
6452
|
+
pet: entry.id,
|
|
6453
|
+
affection: ledger.affection,
|
|
6454
|
+
mood: moodOf(ledger.affection),
|
|
6455
|
+
applied: delta,
|
|
6456
|
+
quota: {
|
|
6457
|
+
used: quota.count,
|
|
6458
|
+
limit: ADJUST_DAILY_LIMIT,
|
|
6459
|
+
sum: quota.sum,
|
|
6460
|
+
sumLimit: ADJUST_DAILY_SUM
|
|
6461
|
+
}
|
|
6462
|
+
});
|
|
6463
|
+
} catch (error) {
|
|
6464
|
+
callDetail = error instanceof Error ? error.message : String(error);
|
|
6465
|
+
throw classify(error);
|
|
6466
|
+
} finally {
|
|
6467
|
+
appendJsonl(toolCallsFile(home, callPet), {
|
|
6468
|
+
tool: "pet_affection_adjust",
|
|
6469
|
+
petId: callPet,
|
|
6470
|
+
ok: callDetail === void 0,
|
|
6471
|
+
...callDetail !== void 0 ? { detail: callDetail.slice(0, 120) } : {},
|
|
6472
|
+
durationMs: Date.now() - callStartedAt,
|
|
6473
|
+
at: Date.now()
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
}));
|
|
5943
6478
|
scope.tools.register(defineTool({
|
|
5944
6479
|
name: "pet_state",
|
|
5945
6480
|
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,20 +6504,35 @@ function registerPetTools(ctx, home, pet) {
|
|
|
5969
6504
|
]);
|
|
5970
6505
|
const ledgers = {};
|
|
5971
6506
|
for (const id of knownIds) {
|
|
5972
|
-
const ledger = await readLedger(
|
|
6507
|
+
const ledger = await readLedger(home, id);
|
|
5973
6508
|
const mood = moodOf(ledger.affection);
|
|
5974
6509
|
ledgers[id] = {
|
|
5975
6510
|
...ledger,
|
|
5976
|
-
mood
|
|
6511
|
+
mood,
|
|
6512
|
+
bond: {
|
|
6513
|
+
value: ledger.bond?.value ?? 0,
|
|
6514
|
+
days: ledger.bond?.days ?? 0,
|
|
6515
|
+
tier: bondTier(ledger.bond?.value ?? 0)
|
|
6516
|
+
}
|
|
5977
6517
|
};
|
|
5978
6518
|
}
|
|
5979
6519
|
const petsOut = states.map((state) => {
|
|
5980
6520
|
const actions = actionsById.get(state.id);
|
|
5981
|
-
return
|
|
5982
|
-
...state
|
|
5983
|
-
|
|
6521
|
+
return {
|
|
6522
|
+
...actions === void 0 ? state : {
|
|
6523
|
+
...state,
|
|
6524
|
+
actions
|
|
6525
|
+
},
|
|
6526
|
+
name: voiceNames.get(state.id) ?? DEFAULT_PET_NAME[state.id] ?? state.id
|
|
5984
6527
|
};
|
|
5985
6528
|
});
|
|
6529
|
+
appendJsonl(toolCallsFile(home, "all"), {
|
|
6530
|
+
tool: "pet_state",
|
|
6531
|
+
petId: "all",
|
|
6532
|
+
ok: true,
|
|
6533
|
+
durationMs: 0,
|
|
6534
|
+
at: Date.now()
|
|
6535
|
+
});
|
|
5986
6536
|
return JSON.stringify({
|
|
5987
6537
|
defaultPet: pets[0]?.id ?? null,
|
|
5988
6538
|
pets: petsOut,
|
|
@@ -6255,4 +6805,4 @@ function installPetService(ctx, home) {
|
|
|
6255
6805
|
return new PetHttpService(ctx, home);
|
|
6256
6806
|
}
|
|
6257
6807
|
//#endregion
|
|
6258
|
-
export { logStamp as a, installPetDriver as i, installPetService as n, registerPetTools as o, petHome as r, createUnregisteredPetHttp as t };
|
|
6808
|
+
export { logStamp as a, readAffectionRaw as c, toolCallsFile as d, writeAffectionRaw as f, installPetDriver as i, readJsonl as l, installPetService as n, registerPetTools as o, writePetName as p, petHome as r, eventsFile as s, createUnregisteredPetHttp as t, readPetName as u };
|