dsh-agentic-pet 0.4.7 → 0.4.9
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/client/pet-settings.js +19 -0
- package/lib/index.js +35 -1
- package/lib/{service-ZRl_L6yW.js → service-BZbhT92u.js} +475 -85
- package/lib/tools.js +3 -3
- package/lib/types/driver.d.ts +9 -0
- package/lib/types/inner-voice.d.ts +73 -0
- package/lib/types/registry.d.ts +4 -2
- package/lib/types/service.d.ts +4 -0
- package/lib/types/tools.d.ts +2 -2
- package/package.json +1 -1
- package/presets/pet-inner-voice.patch.yml +1 -0
package/client/pet-settings.js
CHANGED
|
@@ -279,6 +279,25 @@ window.__ModuleLoader__.load({
|
|
|
279
279
|
if (policy !== 'defer') update({ innerVoice: { selfWakeBusyPolicy: 'defer' } })
|
|
280
280
|
} }, '忙时顺延'))
|
|
281
281
|
})()),
|
|
282
|
+
h('div', { style: row }, '宠物间消息投递',
|
|
283
|
+
(function () {
|
|
284
|
+
var seg = function (active, side) {
|
|
285
|
+
return { padding: '4px 12px', fontSize: 12, cursor: 'pointer',
|
|
286
|
+
border: '1px solid ' + (active ? '#22c55e' : '#c9d4e0'),
|
|
287
|
+
background: active ? '#22c55e' : '#f4f7fa',
|
|
288
|
+
color: active ? '#ffffff' : '#1c2733',
|
|
289
|
+
borderRadius: side === 'left' ? '4px 0 0 4px' : '0 4px 4px 0',
|
|
290
|
+
marginLeft: side === 'right' ? -1 : 0 }
|
|
291
|
+
}
|
|
292
|
+
var mode = iv.petToPetDelivery === 'immediate' ? 'immediate' : 'queue'
|
|
293
|
+
return h('div', { style: { display: 'inline-flex' } },
|
|
294
|
+
h('button', { style: seg(mode === 'queue', 'left'), onClick: function () {
|
|
295
|
+
if (mode !== 'queue') update({ innerVoice: { petToPetDelivery: 'queue' } })
|
|
296
|
+
} }, '排队等她醒'),
|
|
297
|
+
h('button', { style: seg(mode === 'immediate', 'right'), onClick: function () {
|
|
298
|
+
if (mode !== 'immediate') update({ innerVoice: { petToPetDelivery: 'immediate' } })
|
|
299
|
+
} }, '立即叫醒'))
|
|
300
|
+
})()),
|
|
282
301
|
h('div', { style: row }, '会话模式',
|
|
283
302
|
(function () {
|
|
284
303
|
var seg = function (active, side) {
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as globalFilePath, d as readJsonl, f as readPetName, h as writePetName, i as installPetDriver, l as readAffectionRaw, m as writeGlobalJson, n as installPetService, p as writeAffectionRaw, r as petHome, s as eventsFile, u as readGlobalJson } from "./service-
|
|
1
|
+
import { c as globalFilePath, d as readJsonl, f as readPetName, h as writePetName, i as installPetDriver, l as readAffectionRaw, m as writeGlobalJson, n as installPetService, p as writeAffectionRaw, r as petHome, s as eventsFile, u as readGlobalJson } from "./service-BZbhT92u.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -37,6 +37,13 @@ const DEFAULT_SETTINGS = {
|
|
|
37
37
|
touchReplyProbability: .3,
|
|
38
38
|
touchReplyCooldownSeconds: 30,
|
|
39
39
|
selfWakeBusyPolicy: "fire",
|
|
40
|
+
petToPetDelivery: "queue",
|
|
41
|
+
petMessageLimits: {
|
|
42
|
+
minutes5: 5,
|
|
43
|
+
minutes10: 8,
|
|
44
|
+
minutes30: 10,
|
|
45
|
+
minutes60: 15
|
|
46
|
+
},
|
|
40
47
|
boundSessions: {}
|
|
41
48
|
},
|
|
42
49
|
tts: { enabled: false },
|
|
@@ -132,6 +139,33 @@ function mergeSettings(current, patch) {
|
|
|
132
139
|
const replyCooldown = clampNumber(v.touchReplyCooldownSeconds, 0, 600);
|
|
133
140
|
if (replyCooldown !== void 0) innerVoice.touchReplyCooldownSeconds = replyCooldown;
|
|
134
141
|
if (v.selfWakeBusyPolicy === "defer" || v.selfWakeBusyPolicy === "fire") innerVoice.selfWakeBusyPolicy = v.selfWakeBusyPolicy;
|
|
142
|
+
if (v.petToPetDelivery === "immediate" || v.petToPetDelivery === "queue") innerVoice.petToPetDelivery = v.petToPetDelivery;
|
|
143
|
+
const patchLimits = v.petMessageLimits;
|
|
144
|
+
if (typeof patchLimits === "object" && patchLimits !== null) {
|
|
145
|
+
const merged = {
|
|
146
|
+
minutes5: 5,
|
|
147
|
+
minutes10: 8,
|
|
148
|
+
minutes30: 10,
|
|
149
|
+
minutes60: 15,
|
|
150
|
+
...innerVoice.petMessageLimits
|
|
151
|
+
};
|
|
152
|
+
const base = {
|
|
153
|
+
minutes5: merged.minutes5 ?? 5,
|
|
154
|
+
minutes10: merged.minutes10 ?? 8,
|
|
155
|
+
minutes30: merged.minutes30 ?? 10,
|
|
156
|
+
minutes60: merged.minutes60 ?? 15
|
|
157
|
+
};
|
|
158
|
+
const clampLimit = (value, fallback) => {
|
|
159
|
+
const parsed = clampNumber(value, 0, 999);
|
|
160
|
+
return parsed === void 0 ? fallback : parsed;
|
|
161
|
+
};
|
|
162
|
+
innerVoice.petMessageLimits = {
|
|
163
|
+
minutes5: clampLimit(patchLimits.minutes5, base.minutes5),
|
|
164
|
+
minutes10: clampLimit(patchLimits.minutes10, base.minutes10),
|
|
165
|
+
minutes30: clampLimit(patchLimits.minutes30, base.minutes30),
|
|
166
|
+
minutes60: clampLimit(patchLimits.minutes60, base.minutes60)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
135
169
|
const bound = v.boundSessions;
|
|
136
170
|
if (typeof bound === "object" && bound !== null) {
|
|
137
171
|
const record = {};
|
|
@@ -5044,6 +5044,7 @@ const GLOBAL_STATE_FILES = [
|
|
|
5044
5044
|
"pet-settings.json",
|
|
5045
5045
|
"pet-inner-voice.json",
|
|
5046
5046
|
"pet-wake-plans.json",
|
|
5047
|
+
"pet-messages.json",
|
|
5047
5048
|
"pet-interactions.json",
|
|
5048
5049
|
"pet-world.json",
|
|
5049
5050
|
"pet-window.json"
|
|
@@ -5124,6 +5125,8 @@ async function readInnerVoiceConfig(home) {
|
|
|
5124
5125
|
touchReplyProbability: finite01(v.touchReplyProbability, .3),
|
|
5125
5126
|
touchReplyCooldownSeconds: secondsRaw === void 0 ? 30 : clamp(Math.round(secondsRaw), 0, 600),
|
|
5126
5127
|
selfWakeBusyPolicy: v.selfWakeBusyPolicy === "defer" ? "defer" : "fire",
|
|
5128
|
+
petToPetDelivery: v.petToPetDelivery === "immediate" ? "immediate" : "queue",
|
|
5129
|
+
petMessageLimits: parsePetMessageLimits(v.petMessageLimits),
|
|
5127
5130
|
boundSessions
|
|
5128
5131
|
};
|
|
5129
5132
|
} catch {
|
|
@@ -5135,6 +5138,8 @@ async function readInnerVoiceConfig(home) {
|
|
|
5135
5138
|
touchReplyProbability: .3,
|
|
5136
5139
|
touchReplyCooldownSeconds: 30,
|
|
5137
5140
|
selfWakeBusyPolicy: "fire",
|
|
5141
|
+
petToPetDelivery: "queue",
|
|
5142
|
+
petMessageLimits: { ...DEFAULT_PET_MESSAGE_LIMITS },
|
|
5138
5143
|
boundSessions: {}
|
|
5139
5144
|
};
|
|
5140
5145
|
}
|
|
@@ -5222,6 +5227,127 @@ function takeNearDuePlan(list, now, windowMs = 6e4) {
|
|
|
5222
5227
|
function missedPlans(due, bootAt) {
|
|
5223
5228
|
return due.filter((entry) => entry.at < bootAt);
|
|
5224
5229
|
}
|
|
5230
|
+
function messagesOf(raw) {
|
|
5231
|
+
if (!Array.isArray(raw)) return [];
|
|
5232
|
+
const out = [];
|
|
5233
|
+
for (const item of raw) {
|
|
5234
|
+
if (typeof item !== "object" || item === null) continue;
|
|
5235
|
+
const rec = item;
|
|
5236
|
+
if (typeof rec.from !== "string" || rec.from === "" || typeof rec.text !== "string" || rec.text === "" || typeof rec.at !== "number" || !Number.isFinite(rec.at)) continue;
|
|
5237
|
+
out.push({
|
|
5238
|
+
from: rec.from,
|
|
5239
|
+
text: rec.text,
|
|
5240
|
+
at: rec.at
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
return out;
|
|
5244
|
+
}
|
|
5245
|
+
/** Load every pet's unread inbox: current → legacy → {} (defensive like the
|
|
5246
|
+
* other stores). */
|
|
5247
|
+
async function loadPetMessages(home) {
|
|
5248
|
+
try {
|
|
5249
|
+
const raw = await readGlobalJson(home, "pet-messages.json");
|
|
5250
|
+
const byPet = typeof raw.messages === "object" && raw.messages !== null ? raw.messages : {};
|
|
5251
|
+
const out = {};
|
|
5252
|
+
for (const [petId, list] of Object.entries(byPet)) {
|
|
5253
|
+
if (petId === "") continue;
|
|
5254
|
+
const messages = messagesOf(list);
|
|
5255
|
+
if (messages.length > 0) out[petId] = messages;
|
|
5256
|
+
}
|
|
5257
|
+
return out;
|
|
5258
|
+
} catch {
|
|
5259
|
+
return {};
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
async function savePetMessages(home, messages) {
|
|
5263
|
+
try {
|
|
5264
|
+
await writeGlobalJson(home, "pet-messages.json", { messages });
|
|
5265
|
+
} catch (error) {
|
|
5266
|
+
console.error(`pet-surface inner-voice: could not write pet messages: ${error instanceof Error ? error.message : String(error)}`);
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
/** Append one message to a pet's inbox, oldest dropped past the cap (a
|
|
5270
|
+
* neglected inbox cannot grow without bound). */
|
|
5271
|
+
function queuePetMessage(list, message) {
|
|
5272
|
+
return [...list, message].slice(-20);
|
|
5273
|
+
}
|
|
5274
|
+
/** Consume the WHOLE inbox (pet_inbox 取文即清空; the wake prompt only
|
|
5275
|
+
* announces counts). Returns the taken messages and the emptied tail. */
|
|
5276
|
+
function takePetMessages(list) {
|
|
5277
|
+
return {
|
|
5278
|
+
taken: [...list],
|
|
5279
|
+
next: []
|
|
5280
|
+
};
|
|
5281
|
+
}
|
|
5282
|
+
const DEFAULT_PET_MESSAGE_LIMITS = {
|
|
5283
|
+
minutes5: 5,
|
|
5284
|
+
minutes10: 8,
|
|
5285
|
+
minutes30: 10,
|
|
5286
|
+
minutes60: 15
|
|
5287
|
+
};
|
|
5288
|
+
const PET_MESSAGE_WINDOW_MINUTES = {
|
|
5289
|
+
minutes5: 5,
|
|
5290
|
+
minutes10: 10,
|
|
5291
|
+
minutes30: 30,
|
|
5292
|
+
minutes60: 60
|
|
5293
|
+
};
|
|
5294
|
+
/** Defensive per-key parse: a finite non-negative number passes (rounded),
|
|
5295
|
+
* anything else folds to the default for that tier. */
|
|
5296
|
+
function parsePetMessageLimits(raw) {
|
|
5297
|
+
const record = typeof raw === "object" && raw !== null ? raw : {};
|
|
5298
|
+
const limitOf = (key) => {
|
|
5299
|
+
const value = record[key];
|
|
5300
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : DEFAULT_PET_MESSAGE_LIMITS[key];
|
|
5301
|
+
};
|
|
5302
|
+
return {
|
|
5303
|
+
minutes5: limitOf("minutes5"),
|
|
5304
|
+
minutes10: limitOf("minutes10"),
|
|
5305
|
+
minutes30: limitOf("minutes30"),
|
|
5306
|
+
minutes60: limitOf("minutes60")
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
/** The tiered sliding-window check for one from→to pair. `timestamps` are the
|
|
5310
|
+
* pair's recent send times; the hour window prunes them (静默一小时后自然
|
|
5311
|
+
* 重新开始), then each tier rejects when its window already holds the cap.
|
|
5312
|
+
* The first (narrowest) violated tier is reported, with the earliest moment
|
|
5313
|
+
* a slot frees up. `recent` on success = the pruned list — the caller appends
|
|
5314
|
+
* its own timestamp after the send succeeds. */
|
|
5315
|
+
function checkPetMessageRate(timestamps, now, limits) {
|
|
5316
|
+
const horizon = PET_MESSAGE_WINDOW_MINUTES.minutes60 * 6e4;
|
|
5317
|
+
const pruned = timestamps.filter((at) => now - at < horizon);
|
|
5318
|
+
for (const key of Object.keys(PET_MESSAGE_WINDOW_MINUTES)) {
|
|
5319
|
+
const windowMs = PET_MESSAGE_WINDOW_MINUTES[key] * 6e4;
|
|
5320
|
+
const cap = limits[key];
|
|
5321
|
+
const inWindow = pruned.filter((at) => now - at < windowMs);
|
|
5322
|
+
if (inWindow.length >= cap) {
|
|
5323
|
+
const oldest = Math.min(...inWindow);
|
|
5324
|
+
return {
|
|
5325
|
+
ok: false,
|
|
5326
|
+
windowMinutes: PET_MESSAGE_WINDOW_MINUTES[key],
|
|
5327
|
+
cap,
|
|
5328
|
+
count: inWindow.length,
|
|
5329
|
+
retryAfterSeconds: Math.max(1, Math.ceil((oldest + windowMs - now) / 1e3))
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
return {
|
|
5334
|
+
ok: true,
|
|
5335
|
+
recent: pruned
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
/** The immediate-delivery prompt (petToPetDelivery='immediate'): the target
|
|
5339
|
+
* is woken right away with the sender's name and id — the id because the
|
|
5340
|
+
* reply path is pet_say audience="<fromId>". The target's current affection
|
|
5341
|
+
* toward the sender rides along so the reply carries history, not a blank
|
|
5342
|
+
* slate. Responding or ignoring is the target's own call, exactly like any
|
|
5343
|
+
* other wake. */
|
|
5344
|
+
function buildCrossPetPrompt(facts) {
|
|
5345
|
+
return [
|
|
5346
|
+
`[来自另外桌宠 ${facts.fromName}]`,
|
|
5347
|
+
`${facts.text.slice(0, 200)}`,
|
|
5348
|
+
`(你對 ${facts.fromName} 的好感 ${String(facts.affection)},${moodOf(facts.affection)})想回应就 pet_say 带 audience="${facts.fromId}"(同样 ≤40 字,气泡上会有 to 前缀);不想理就安静待着。`
|
|
5349
|
+
].join(" ");
|
|
5350
|
+
}
|
|
5225
5351
|
/** Defensive extraction of the persisted generation counters (petId → n). */
|
|
5226
5352
|
function generationsOf(raw) {
|
|
5227
5353
|
const out = {};
|
|
@@ -5297,6 +5423,7 @@ function mintVoiceSessionId(petId, generation, isArchived) {
|
|
|
5297
5423
|
function buildGreetPrompt(facts) {
|
|
5298
5424
|
return [
|
|
5299
5425
|
"[启动问好]",
|
|
5426
|
+
`你的名字是「${facts.name}」。`,
|
|
5300
5427
|
`用户刚打开 DeepSeek,你的世界刚亮起来(好感 ${String(facts.affection)},${moodOf(facts.affection)};羁绊 ${facts.bondTier},一起过了 ${String(facts.bondDays)} 天)。`,
|
|
5301
5428
|
"主动打个招呼(pet_say)——按当下的时辰自然问好,一两句就好,别问问题等回答。"
|
|
5302
5429
|
].join(" ");
|
|
@@ -5373,6 +5500,9 @@ function buildWakePrompt(facts) {
|
|
|
5373
5500
|
}
|
|
5374
5501
|
const pending = facts.pendingPlans ?? [];
|
|
5375
5502
|
if (pending.length > 0) lines.push(`你还有 ${String(pending.length)} 个待执行计划:${pending.map((plan) => `${wakePlanClock(plan.at)}「${plan.intent}」`).join(";")}。继续等待、改时间或取消由你决定(pet_schedule_wake 同名覆盖即改期,pet_cancel_wake 撤销)。`);
|
|
5503
|
+
const unread = facts.unread ?? [];
|
|
5504
|
+
const unreadTotal = unread.reduce((sum, entry) => sum + entry.count, 0);
|
|
5505
|
+
if (unreadTotal > 0) lines.push(`你有 ${String(unreadTotal)} 条未读消息(来自${unread.map((entry) => ` ${entry.fromName}×${String(entry.count)}`).join("、")}),想看就调 pet_inbox,不理也行。`);
|
|
5376
5506
|
lines.push("醒来想到什么就说一句(pet_say),想配动作就 pet_do;也可以只是安静待着(两者都不调,正文写「…」)。");
|
|
5377
5507
|
return lines.join(" ");
|
|
5378
5508
|
}
|
|
@@ -5441,7 +5571,7 @@ async function probeControlPlane(port) {
|
|
|
5441
5571
|
* must never outrank a publisher that stated its boot time. This turns the
|
|
5442
5572
|
* multi-pet design's de-facto rule ("the default pet is the first STARTED")
|
|
5443
5573
|
* into an actual guarantee: discovery output IS boot order, so every
|
|
5444
|
-
* `pets[0]` consumer (
|
|
5574
|
+
* `pets[0]` consumer (the driver's unnamed-target fallback) reads the
|
|
5445
5575
|
* same pet. Stable sort: equal `since` values keep registry listing order.
|
|
5446
5576
|
*/
|
|
5447
5577
|
function bootOrder(pets) {
|
|
@@ -5449,7 +5579,9 @@ function bootOrder(pets) {
|
|
|
5449
5579
|
}
|
|
5450
5580
|
/** Every live pet in boot order. Dead-pid entries are skipped — they expire
|
|
5451
5581
|
* by omission the next time their file is rewritten — and so are silent
|
|
5452
|
-
* entries (pid alive, control plane unresponsive).
|
|
5582
|
+
* entries (pid alive, control plane unresponsive). Probes run CONCURRENTLY:
|
|
5583
|
+
* wall time per pass is the slowest pet (≤800ms frozen budget), not the
|
|
5584
|
+
* serial sum. */
|
|
5453
5585
|
async function readAllPets(home) {
|
|
5454
5586
|
let names = [];
|
|
5455
5587
|
try {
|
|
@@ -5457,30 +5589,30 @@ async function readAllPets(home) {
|
|
|
5457
5589
|
} catch {
|
|
5458
5590
|
return [];
|
|
5459
5591
|
}
|
|
5460
|
-
|
|
5461
|
-
for (const name of names) {
|
|
5592
|
+
return bootOrder((await Promise.all(names.map(async (name) => {
|
|
5462
5593
|
const id = name.slice(0, -5);
|
|
5463
|
-
if (id === "")
|
|
5594
|
+
if (id === "") return null;
|
|
5464
5595
|
try {
|
|
5465
5596
|
const raw = JSON.parse(await readFile(join(home, REGISTRY_DIR, name), "utf8"));
|
|
5466
5597
|
const port = typeof raw.port === "number" ? raw.port : NaN;
|
|
5467
5598
|
const pid = typeof raw.pid === "number" ? raw.pid : NaN;
|
|
5468
|
-
if (!Number.isFinite(port) || !Number.isFinite(pid))
|
|
5599
|
+
if (!Number.isFinite(port) || !Number.isFinite(pid)) return null;
|
|
5469
5600
|
try {
|
|
5470
5601
|
process.kill(pid, 0);
|
|
5471
5602
|
} catch {
|
|
5472
|
-
|
|
5603
|
+
return null;
|
|
5473
5604
|
}
|
|
5474
|
-
if (!await probeControlPlane(port))
|
|
5475
|
-
|
|
5605
|
+
if (!await probeControlPlane(port)) return null;
|
|
5606
|
+
return {
|
|
5476
5607
|
id,
|
|
5477
5608
|
port,
|
|
5478
5609
|
pid,
|
|
5479
5610
|
since: typeof raw.since === "number" ? raw.since : 0
|
|
5480
|
-
}
|
|
5481
|
-
} catch {
|
|
5482
|
-
|
|
5483
|
-
|
|
5611
|
+
};
|
|
5612
|
+
} catch {
|
|
5613
|
+
return null;
|
|
5614
|
+
}
|
|
5615
|
+
}))).filter((entry) => entry !== null));
|
|
5484
5616
|
}
|
|
5485
5617
|
/**
|
|
5486
5618
|
* The default pet for UNNAMED targets: the earliest-booted live entry
|
|
@@ -5547,7 +5679,25 @@ async function readLedger(home, petId) {
|
|
|
5547
5679
|
days: typeof raw.bond.days === "number" ? raw.bond.days : 0,
|
|
5548
5680
|
lastCountedDate: raw.bond.lastCountedDate
|
|
5549
5681
|
} : void 0,
|
|
5550
|
-
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
|
|
5682
|
+
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,
|
|
5683
|
+
petAffection: typeof raw.petAffection === "object" && raw.petAffection !== null ? Object.fromEntries(Object.entries(raw.petAffection).flatMap(([id, pair]) => {
|
|
5684
|
+
if (typeof pair !== "object" || pair === null) return [];
|
|
5685
|
+
const rec = pair;
|
|
5686
|
+
if (typeof rec.value !== "number" || !Number.isFinite(rec.value)) return [];
|
|
5687
|
+
return [[id, {
|
|
5688
|
+
value: rec.value,
|
|
5689
|
+
adjustments: Array.isArray(rec.adjustments) ? rec.adjustments.filter((a) => typeof a === "object" && a !== null && typeof a.delta === "number" && typeof a.reason === "string" && typeof a.at === "number") : []
|
|
5690
|
+
}]];
|
|
5691
|
+
})) : void 0,
|
|
5692
|
+
petFacts: typeof raw.petFacts === "object" && raw.petFacts !== null ? Object.fromEntries(Object.entries(raw.petFacts).flatMap(([id, fact]) => {
|
|
5693
|
+
if (typeof fact !== "object" || fact === null) return [];
|
|
5694
|
+
const rec = fact;
|
|
5695
|
+
if (typeof rec.total !== "number" || !Number.isFinite(rec.total)) return [];
|
|
5696
|
+
return [[id, {
|
|
5697
|
+
total: rec.total,
|
|
5698
|
+
lastAt: typeof rec.lastAt === "number" ? rec.lastAt : 0
|
|
5699
|
+
}]];
|
|
5700
|
+
})) : void 0
|
|
5551
5701
|
};
|
|
5552
5702
|
} catch {
|
|
5553
5703
|
return {
|
|
@@ -5608,7 +5758,10 @@ const voiceLastSpokeAt = /* @__PURE__ */ new Map();
|
|
|
5608
5758
|
const voiceLastTouchReplyAt = /* @__PURE__ */ new Map();
|
|
5609
5759
|
const voiceNextWakeAt = /* @__PURE__ */ new Map();
|
|
5610
5760
|
const voiceWakePlans = /* @__PURE__ */ new Map();
|
|
5761
|
+
const voicePetMessages = /* @__PURE__ */ new Map();
|
|
5762
|
+
const crossPetLog = /* @__PURE__ */ new Map();
|
|
5611
5763
|
let selfWakeSender;
|
|
5764
|
+
let crossPetDeliverer;
|
|
5612
5765
|
/** Write-through the in-memory plan map to disk (empty lists dropped — the
|
|
5613
5766
|
* absence of a pet key IS an empty schedule). */
|
|
5614
5767
|
const persistWakePlans = (home) => {
|
|
@@ -5616,6 +5769,16 @@ const persistWakePlans = (home) => {
|
|
|
5616
5769
|
for (const [petId, list] of voiceWakePlans) if (list.length > 0) snapshot[petId] = list;
|
|
5617
5770
|
saveWakePlans(home, snapshot);
|
|
5618
5771
|
};
|
|
5772
|
+
/** Write-through the in-memory pet-to-pet inbox to disk (same empty-key rule
|
|
5773
|
+
* as persistWakePlans). */
|
|
5774
|
+
const persistPetMessages = (home) => {
|
|
5775
|
+
const snapshot = {};
|
|
5776
|
+
for (const [petId, list] of voicePetMessages) if (list.length > 0) snapshot[petId] = list;
|
|
5777
|
+
savePetMessages(home, snapshot);
|
|
5778
|
+
};
|
|
5779
|
+
/** The user's naming for a pet, falling back to the built-in defaults, then
|
|
5780
|
+
* the raw id — used everywhere the model or a bubble needs a friendly name. */
|
|
5781
|
+
const petDisplayName = (petId) => voiceNames.get(petId) ?? DEFAULT_PET_NAME[petId] ?? petId;
|
|
5619
5782
|
const voiceGreetedOnce = /* @__PURE__ */ new Set();
|
|
5620
5783
|
const voiceNames = /* @__PURE__ */ new Map();
|
|
5621
5784
|
const DEFAULT_PET_NAME = { electron: "鲸鱼娘" };
|
|
@@ -5664,7 +5827,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5664
5827
|
voiceLastTouchReplyAt.clear();
|
|
5665
5828
|
voiceNextWakeAt.clear();
|
|
5666
5829
|
voiceWakePlans.clear();
|
|
5830
|
+
voicePetMessages.clear();
|
|
5667
5831
|
selfWakeSender = void 0;
|
|
5832
|
+
crossPetDeliverer = void 0;
|
|
5833
|
+
crossPetLog.clear();
|
|
5668
5834
|
voiceGreetedOnce.clear();
|
|
5669
5835
|
voiceNames.clear();
|
|
5670
5836
|
voiceLastPrompt.clear();
|
|
@@ -5771,6 +5937,11 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5771
5937
|
const count = Object.values(plans).reduce((sum, list) => sum + list.length, 0);
|
|
5772
5938
|
driverLog(home, `VOICE wake plans loaded: ${String(count)} plan(s) for ${String(Object.keys(plans).length)} pet(s)`);
|
|
5773
5939
|
});
|
|
5940
|
+
loadPetMessages(home).then((messages) => {
|
|
5941
|
+
for (const [petId, list] of Object.entries(messages)) if (!voicePetMessages.has(petId)) voicePetMessages.set(petId, list);
|
|
5942
|
+
const count = Object.values(messages).reduce((sum, list) => sum + list.length, 0);
|
|
5943
|
+
driverLog(home, `VOICE pet messages loaded: ${String(count)} unread for ${String(Object.keys(messages).length)} pet(s)`);
|
|
5944
|
+
});
|
|
5774
5945
|
const events = ctx.root;
|
|
5775
5946
|
driverLog(home, "listeners registering on root");
|
|
5776
5947
|
let boundSessionId;
|
|
@@ -5798,6 +5969,34 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5798
5969
|
};
|
|
5799
5970
|
}
|
|
5800
5971
|
};
|
|
5972
|
+
const stateQueues = /* @__PURE__ */ new Map();
|
|
5973
|
+
const sendState = async (target, state, autonomy, autonomyKey) => {
|
|
5974
|
+
const last = lastPushedByPet.get(target.id);
|
|
5975
|
+
if (last !== void 0 && last.state === state && last.since === target.since && last.autonomy === autonomyKey) return;
|
|
5976
|
+
lastPushedByPet.set(target.id, {
|
|
5977
|
+
state,
|
|
5978
|
+
since: target.since,
|
|
5979
|
+
autonomy: autonomyKey
|
|
5980
|
+
});
|
|
5981
|
+
try {
|
|
5982
|
+
const response = await fetch(`http://127.0.0.1:${String(target.port)}/state`, {
|
|
5983
|
+
method: "POST",
|
|
5984
|
+
headers: { "content-type": "application/json" },
|
|
5985
|
+
body: JSON.stringify({
|
|
5986
|
+
state,
|
|
5987
|
+
autonomy
|
|
5988
|
+
}),
|
|
5989
|
+
signal: AbortSignal.timeout(5e3)
|
|
5990
|
+
});
|
|
5991
|
+
if (!response.ok) {
|
|
5992
|
+
console.error(`pet-surface driver: /state answered HTTP ${String(response.status)}`);
|
|
5993
|
+
lastPushedByPet.delete(target.id);
|
|
5994
|
+
} else if (state === "error") lastPushedByPet.delete(target.id);
|
|
5995
|
+
} catch {
|
|
5996
|
+
console.error(`pet-surface driver: /state to ${target.id} failed`);
|
|
5997
|
+
lastPushedByPet.delete(target.id);
|
|
5998
|
+
}
|
|
5999
|
+
};
|
|
5801
6000
|
const pushState = async (state) => {
|
|
5802
6001
|
currentState = state;
|
|
5803
6002
|
const pets = await pet.list();
|
|
@@ -5806,32 +6005,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5806
6005
|
if (audience.length === 0) return;
|
|
5807
6006
|
const autonomy = await readAutonomy();
|
|
5808
6007
|
const autonomyKey = JSON.stringify(autonomy);
|
|
5809
|
-
await Promise.all(audience.map(
|
|
5810
|
-
const
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
state,
|
|
5814
|
-
since: target.since,
|
|
5815
|
-
autonomy: autonomyKey
|
|
5816
|
-
});
|
|
5817
|
-
try {
|
|
5818
|
-
const response = await fetch(`http://127.0.0.1:${String(target.port)}/state`, {
|
|
5819
|
-
method: "POST",
|
|
5820
|
-
headers: { "content-type": "application/json" },
|
|
5821
|
-
body: JSON.stringify({
|
|
5822
|
-
state,
|
|
5823
|
-
autonomy
|
|
5824
|
-
}),
|
|
5825
|
-
signal: AbortSignal.timeout(5e3)
|
|
5826
|
-
});
|
|
5827
|
-
if (!response.ok) {
|
|
5828
|
-
console.error(`pet-surface driver: /state answered HTTP ${String(response.status)}`);
|
|
5829
|
-
lastPushedByPet.delete(target.id);
|
|
5830
|
-
} else if (state === "error") lastPushedByPet.delete(target.id);
|
|
5831
|
-
} catch {
|
|
5832
|
-
console.error(`pet-surface driver: /state to ${target.id} failed`);
|
|
5833
|
-
lastPushedByPet.delete(target.id);
|
|
5834
|
-
}
|
|
6008
|
+
await Promise.all(audience.map((target) => {
|
|
6009
|
+
const next = (stateQueues.get(target.id) ?? Promise.resolve()).then(() => sendState(target, state, autonomy, autonomyKey));
|
|
6010
|
+
stateQueues.set(target.id, next);
|
|
6011
|
+
return next;
|
|
5835
6012
|
}));
|
|
5836
6013
|
};
|
|
5837
6014
|
events.on("api-session/status", (sessionId, running) => {
|
|
@@ -5964,6 +6141,8 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5964
6141
|
...entry,
|
|
5965
6142
|
decision: entry.decision ?? null,
|
|
5966
6143
|
detail: entry.detail ?? null,
|
|
6144
|
+
session: entry.session ?? null,
|
|
6145
|
+
pet: entry.pet ?? null,
|
|
5967
6146
|
at
|
|
5968
6147
|
});
|
|
5969
6148
|
await writeFile(interactionLedgerFile, `${JSON.stringify(next.slice(0, 50))}\n`, "utf8");
|
|
@@ -5982,6 +6161,8 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5982
6161
|
const raceInteraction = async (body, request, next, accept) => {
|
|
5983
6162
|
const kind = typeof body.kind === "string" ? body.kind : "interaction";
|
|
5984
6163
|
const iid = `ap_${randomUUID().slice(0, 8)}`;
|
|
6164
|
+
const agentId = request.agent?.id;
|
|
6165
|
+
const session = typeof agentId === "string" ? agentId : null;
|
|
5985
6166
|
const original = request.signal;
|
|
5986
6167
|
const decision = new AbortController();
|
|
5987
6168
|
request.signal = original === void 0 ? decision.signal : AbortSignal.any([original, decision.signal]);
|
|
@@ -5990,16 +6171,18 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5990
6171
|
ask: startAskPet(pet, body, original)
|
|
5991
6172
|
}));
|
|
5992
6173
|
let recorded = false;
|
|
5993
|
-
const record = (endedBy, decision, detail) => {
|
|
6174
|
+
const record = (endedBy, decision, detail, petId) => {
|
|
5994
6175
|
if (recorded) return;
|
|
5995
6176
|
recorded = true;
|
|
5996
|
-
driverLog(home, `INTERACTION ${iid} ${kind} endedBy=${endedBy}${detail !== void 0 ? ` (${detail})` : ""}`);
|
|
6177
|
+
driverLog(home, `INTERACTION ${iid} ${kind} session=${session ?? "-"} pet=${petId ?? "-"} endedBy=${endedBy}${detail !== void 0 ? ` (${detail})` : ""}`);
|
|
5997
6178
|
recordInteraction({
|
|
5998
6179
|
id: iid,
|
|
5999
6180
|
kind,
|
|
6000
6181
|
endedBy,
|
|
6001
6182
|
decision,
|
|
6002
|
-
detail
|
|
6183
|
+
detail,
|
|
6184
|
+
session,
|
|
6185
|
+
pet: petId ?? null
|
|
6003
6186
|
});
|
|
6004
6187
|
};
|
|
6005
6188
|
try {
|
|
@@ -6034,7 +6217,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
6034
6217
|
}
|
|
6035
6218
|
decision.abort();
|
|
6036
6219
|
boundPetId = winner.pet.id;
|
|
6037
|
-
record("pet", winner.answer.answer);
|
|
6220
|
+
record("pet", winner.answer.answer, void 0, winner.pet.id);
|
|
6038
6221
|
if (currentState !== void 0) pushState(currentState);
|
|
6039
6222
|
return winner.answer.answer;
|
|
6040
6223
|
} catch (error) {
|
|
@@ -6231,7 +6414,8 @@ ${text}`;
|
|
|
6231
6414
|
affection: ledger.affection,
|
|
6232
6415
|
bondValue: ledger.bond?.value ?? 0,
|
|
6233
6416
|
bondDays: ledger.bond?.days ?? 0,
|
|
6234
|
-
bondTier: bondTier(ledger.bond?.value ?? 0)
|
|
6417
|
+
bondTier: bondTier(ledger.bond?.value ?? 0),
|
|
6418
|
+
name: petDisplayName(petId)
|
|
6235
6419
|
}))) {
|
|
6236
6420
|
voiceGreetedOnce.add(petId);
|
|
6237
6421
|
driverLog(home, `VOICE boot greeting pet=${petId}`);
|
|
@@ -6277,6 +6461,12 @@ ${text}`;
|
|
|
6277
6461
|
const now = Date.now();
|
|
6278
6462
|
const lastSpoke = voiceLastSpokeAt.get(petId);
|
|
6279
6463
|
const pending = (voiceWakePlans.get(petId) ?? []).slice().sort((a, b) => a.at - b.at).slice(0, 5);
|
|
6464
|
+
const unreadByFrom = /* @__PURE__ */ new Map();
|
|
6465
|
+
for (const message of voicePetMessages.get(petId) ?? []) unreadByFrom.set(message.from, (unreadByFrom.get(message.from) ?? 0) + 1);
|
|
6466
|
+
const unread = [...unreadByFrom.entries()].map(([fromId, count]) => ({
|
|
6467
|
+
fromName: petDisplayName(fromId),
|
|
6468
|
+
count
|
|
6469
|
+
}));
|
|
6280
6470
|
if (await promptVoice(petId, buildWakePrompt({
|
|
6281
6471
|
affection: ledger.affection,
|
|
6282
6472
|
bondValue: ledger.bond?.value ?? 0,
|
|
@@ -6292,7 +6482,8 @@ ${text}`;
|
|
|
6292
6482
|
pendingPlans: pending.map((plan) => ({
|
|
6293
6483
|
at: plan.at,
|
|
6294
6484
|
intent: plan.intent
|
|
6295
|
-
}))
|
|
6485
|
+
})),
|
|
6486
|
+
unread
|
|
6296
6487
|
}))) voiceLastSpokeAt.set(petId, now);
|
|
6297
6488
|
};
|
|
6298
6489
|
selfWakeSender = (petId, plan) => {
|
|
@@ -6303,6 +6494,7 @@ ${text}`;
|
|
|
6303
6494
|
label: "self-wake"
|
|
6304
6495
|
});
|
|
6305
6496
|
};
|
|
6497
|
+
crossPetDeliverer = (petId, prompt) => promptVoice(petId, prompt);
|
|
6306
6498
|
/** Touch-reply trigger (docs/inner-voice.md §6.1). Low probability + a
|
|
6307
6499
|
* per-pet cooldown keep a pat from turning into chatter; the user-busy gate
|
|
6308
6500
|
* does NOT apply — a touch is the user initiating, not the pet interrupting.
|
|
@@ -6558,6 +6750,23 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6558
6750
|
if (target === void 0) throw new Error("no pet is running");
|
|
6559
6751
|
return target;
|
|
6560
6752
|
};
|
|
6753
|
+
const recordPetFact = async (toPetId, fromPetId) => {
|
|
6754
|
+
try {
|
|
6755
|
+
const ledger = await readLedger(home, toPetId);
|
|
6756
|
+
const facts = ledger.petFacts ?? {};
|
|
6757
|
+
facts[fromPetId] = {
|
|
6758
|
+
total: (facts[fromPetId] ?? {
|
|
6759
|
+
total: 0,
|
|
6760
|
+
lastAt: 0
|
|
6761
|
+
}).total + 1,
|
|
6762
|
+
lastAt: Date.now()
|
|
6763
|
+
};
|
|
6764
|
+
ledger.petFacts = facts;
|
|
6765
|
+
await writeAffectionRaw(home, toPetId, ledger);
|
|
6766
|
+
} catch (error) {
|
|
6767
|
+
console.error(`pet-surface driver: could not record the pet fact: ${error instanceof Error ? error.message : String(error)}`);
|
|
6768
|
+
}
|
|
6769
|
+
};
|
|
6561
6770
|
const requireOwnedSession = (exec) => {
|
|
6562
6771
|
const sessionId = exec?.agent?.id;
|
|
6563
6772
|
if (typeof sessionId === "string" && !voiceOwners.has(sessionId)) throw new Error("本会话没有绑定宠物(不是宠物自己的内心戏会话),pet 工具不分发。想和宠物说话,请用宠物表面的输入卡。");
|
|
@@ -6572,16 +6781,20 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6572
6781
|
const digestCursors = /* @__PURE__ */ new Map();
|
|
6573
6782
|
scope.tools.register(defineTool({
|
|
6574
6783
|
name: "pet_say",
|
|
6575
|
-
description: "Make the desktop pet show a short speech bubble. Body language, not a message channel — put task content in your reply. Budget: show time is 4s + 60ms per character (capped at 8s), so ≤40 characters reads well and ~67+ hits the cap; keep it short and warm. A new bubble overwrites the current one, so you can interrupt yourself. Multiple pets may be running: see pet_state for their ids.",
|
|
6784
|
+
description: "Make the desktop pet show a short speech bubble. Body language, not a message channel — put task content in your reply. Budget: show time is 4s + 60ms per character (capped at 8s), so ≤40 characters reads well and ~67+ hits the cap; keep it short and warm. A new bubble overwrites the current one, so you can interrupt yourself. Multiple pets may be running: see pet_state for their ids (and selfPetId for which one you are). audience: omit or \"user\" to speak to the user; or ANOTHER pet's id to talk to that pet — the bubble (prefixed \"to <name>\") still shows on YOUR body: pet-to-pet speech is desktop performance, never a private channel. Directed messages must be even shorter (≤40), the target must be online and not yourself, tiered rate-limits per pair apply (5 in 5min / 8 in 10min / 10 in 30min / 15 per hour by default, configurable), and delivery follows the configured mode (~5s after your bubble the target is woken with your message, or it is queued into its inbox and announced at its next wake — it decides whether to read and reply with audience pointing back at you).",
|
|
6576
6785
|
parameters: {
|
|
6577
6786
|
text: {
|
|
6578
6787
|
type: "string",
|
|
6579
6788
|
required: true,
|
|
6580
|
-
description: "What the pet says; ≤40 characters recommended (hard cap 200), in the conversation language."
|
|
6789
|
+
description: "What the pet says; ≤40 characters recommended (hard cap 200), in the conversation language. Directed messages (audience) should be ≤40 — they arrive as someone else's words on another pet's wake."
|
|
6581
6790
|
},
|
|
6582
6791
|
pet: {
|
|
6583
6792
|
type: "string",
|
|
6584
|
-
description: "Which pet speaks: its id from pet_state. Omit for the default pet."
|
|
6793
|
+
description: "Which pet speaks: its id from pet_state. Omit for the pet this session belongs to (or the default pet)."
|
|
6794
|
+
},
|
|
6795
|
+
audience: {
|
|
6796
|
+
type: "string",
|
|
6797
|
+
description: "\"user\" (default) = speak to the user; or another pet's id to talk to THAT pet. The reply comes back as a pet_say with audience pointing at you (or via its pet_inbox)."
|
|
6585
6798
|
}
|
|
6586
6799
|
},
|
|
6587
6800
|
output: {
|
|
@@ -6594,21 +6807,137 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6594
6807
|
async execute(args, exec) {
|
|
6595
6808
|
requireOwnedSession(exec);
|
|
6596
6809
|
const text = String(args.text).slice(0, 200);
|
|
6597
|
-
console.log(`pet-surface driver: pet_say ${JSON.stringify(text).slice(0, 60)}`);
|
|
6598
6810
|
try {
|
|
6599
6811
|
const entry = await resolveTarget(args.pet, exec?.agent?.id);
|
|
6812
|
+
const config = await readInnerVoiceConfig(home);
|
|
6813
|
+
let bubble = text;
|
|
6814
|
+
let rateKey;
|
|
6815
|
+
let pendingTimestamps = [];
|
|
6816
|
+
let target;
|
|
6817
|
+
const audience = typeof args.audience === "string" && args.audience !== "" && args.audience !== "user" ? args.audience.trim() : void 0;
|
|
6818
|
+
if (audience !== void 0) {
|
|
6819
|
+
const pets = await pet.list();
|
|
6820
|
+
target = pets.find((candidate) => candidate.id === audience);
|
|
6821
|
+
if (target === void 0) throw new Error(`no running pet named "${audience}" to talk to (running: ${pets.map((candidate) => candidate.id).join(", ") || "none"})`);
|
|
6822
|
+
if (target.id === entry.id) throw new Error("audience 不能是自己——对用户说话就省略 audience");
|
|
6823
|
+
rateKey = `${entry.id}->${target.id}`;
|
|
6824
|
+
const config = await readInnerVoiceConfig(home);
|
|
6825
|
+
const rate = checkPetMessageRate(crossPetLog.get(rateKey) ?? [], Date.now(), config.petMessageLimits);
|
|
6826
|
+
if (!rate.ok) throw new Error(`给 ${target.id} 的定向消息超频(${String(rate.windowMinutes)} 分钟窗内已 ${String(rate.count)}/${String(rate.cap)} 条),${String(rate.retryAfterSeconds)} 秒后可再发——气泡交流不必这么急`);
|
|
6827
|
+
pendingTimestamps = rate.recent;
|
|
6828
|
+
bubble = `to ${petDisplayName(target.id)}:${text}`;
|
|
6829
|
+
}
|
|
6600
6830
|
await pet.call(entry, "/say", {
|
|
6601
6831
|
method: "POST",
|
|
6602
6832
|
headers: { "content-type": "application/json" },
|
|
6603
|
-
body: JSON.stringify({ text })
|
|
6833
|
+
body: JSON.stringify({ text: bubble })
|
|
6604
6834
|
});
|
|
6605
6835
|
if (markVoiceSpoke(entry.id)) driverLog(home, `VOICE pending released by say pet=${entry.id} (inner-voice turn complete)`);
|
|
6836
|
+
let audienceOut;
|
|
6837
|
+
if (target !== void 0 && rateKey !== void 0) {
|
|
6838
|
+
crossPetLog.set(rateKey, [...pendingTimestamps, Date.now()]);
|
|
6839
|
+
if (config.petToPetDelivery === "immediate") {
|
|
6840
|
+
if (crossPetDeliverer !== void 0) {
|
|
6841
|
+
const deliverer = crossPetDeliverer;
|
|
6842
|
+
const to = target;
|
|
6843
|
+
const fromName = petDisplayName(entry.id);
|
|
6844
|
+
setTimeout(() => {
|
|
6845
|
+
if (deliverer === void 0) {
|
|
6846
|
+
voicePetMessages.set(to.id, queuePetMessage(voicePetMessages.get(to.id) ?? [], {
|
|
6847
|
+
from: entry.id,
|
|
6848
|
+
text,
|
|
6849
|
+
at: Date.now()
|
|
6850
|
+
}));
|
|
6851
|
+
persistPetMessages(home);
|
|
6852
|
+
recordPetFact(to.id, entry.id);
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
(async () => {
|
|
6856
|
+
const affection = (await readLedger(home, to.id)).petAffection?.[entry.id]?.value ?? 0;
|
|
6857
|
+
if (await deliverer(to.id, buildCrossPetPrompt({
|
|
6858
|
+
fromName,
|
|
6859
|
+
fromId: entry.id,
|
|
6860
|
+
text,
|
|
6861
|
+
affection
|
|
6862
|
+
}))) recordPetFact(to.id, entry.id);
|
|
6863
|
+
else driverLog(home, `PET_SAY cross delayed delivery failed from=${entry.id} to=${to.id} (session create/prompt rejected)`);
|
|
6864
|
+
})();
|
|
6865
|
+
}, 5e3);
|
|
6866
|
+
audienceOut = {
|
|
6867
|
+
pet: target.id,
|
|
6868
|
+
delivered: "immediate"
|
|
6869
|
+
};
|
|
6870
|
+
} else {
|
|
6871
|
+
voicePetMessages.set(target.id, queuePetMessage(voicePetMessages.get(target.id) ?? [], {
|
|
6872
|
+
from: entry.id,
|
|
6873
|
+
text,
|
|
6874
|
+
at: Date.now()
|
|
6875
|
+
}));
|
|
6876
|
+
persistPetMessages(home);
|
|
6877
|
+
recordPetFact(target.id, entry.id);
|
|
6878
|
+
audienceOut = {
|
|
6879
|
+
pet: target.id,
|
|
6880
|
+
delivered: "queued",
|
|
6881
|
+
note: "immediate 不可用,已降级排队"
|
|
6882
|
+
};
|
|
6883
|
+
}
|
|
6884
|
+
} else {
|
|
6885
|
+
voicePetMessages.set(target.id, queuePetMessage(voicePetMessages.get(target.id) ?? [], {
|
|
6886
|
+
from: entry.id,
|
|
6887
|
+
text,
|
|
6888
|
+
at: Date.now()
|
|
6889
|
+
}));
|
|
6890
|
+
persistPetMessages(home);
|
|
6891
|
+
recordPetFact(target.id, entry.id);
|
|
6892
|
+
audienceOut = {
|
|
6893
|
+
pet: target.id,
|
|
6894
|
+
delivered: "queued"
|
|
6895
|
+
};
|
|
6896
|
+
}
|
|
6897
|
+
driverLog(home, `PET_SAY cross from=${entry.id} to=${target.id} delivered=${String(audienceOut.delivered)} text=${JSON.stringify(text)}`);
|
|
6898
|
+
}
|
|
6899
|
+
return JSON.stringify({
|
|
6900
|
+
shown: true,
|
|
6901
|
+
durationMs: 4e3 + Math.min(bubble.length * 60, 4e3),
|
|
6902
|
+
...audienceOut !== void 0 ? { audience: audienceOut } : {}
|
|
6903
|
+
});
|
|
6606
6904
|
} catch (error) {
|
|
6607
6905
|
throw classify(error);
|
|
6608
6906
|
}
|
|
6907
|
+
}
|
|
6908
|
+
}));
|
|
6909
|
+
scope.tools.register(defineTool({
|
|
6910
|
+
name: "pet_inbox",
|
|
6911
|
+
description: "Read and consume YOUR unread pet-to-pet messages (other pets talking to you via pet_say audience): {messages: [{from, fromName, text, at}], count}. The wake prompt only announces counts (\"有 N 条来自 XX 的未读消息\") — this is how you actually read them, and fetching clears the inbox. Empty result = nothing pending. Ignoring messages is allowed (they just sit here, oldest dropped past 20); replying is pet_say with audience pointing back at the sender.",
|
|
6912
|
+
parameters: {},
|
|
6913
|
+
output: {
|
|
6914
|
+
schema: { type: "string" },
|
|
6915
|
+
render: (_args, value) => [{
|
|
6916
|
+
type: "text",
|
|
6917
|
+
text: value
|
|
6918
|
+
}]
|
|
6919
|
+
},
|
|
6920
|
+
async execute(_args, exec) {
|
|
6921
|
+
requireOwnedSession(exec);
|
|
6922
|
+
const sessionId = typeof exec?.agent?.id === "string" ? exec.agent.id : void 0;
|
|
6923
|
+
const petId = sessionId !== void 0 ? voiceOwners.get(sessionId) : void 0;
|
|
6924
|
+
if (petId === void 0) throw new Error("本会话没有绑定宠物,没有收件箱——收件箱属于宠物自己的内心戏会话");
|
|
6925
|
+
const taken = takePetMessages(voicePetMessages.get(petId) ?? []);
|
|
6926
|
+
if (taken.next.length > 0) voicePetMessages.set(petId, taken.next);
|
|
6927
|
+
else voicePetMessages.delete(petId);
|
|
6928
|
+
persistPetMessages(home);
|
|
6929
|
+
const ledger = await readLedger(home, petId);
|
|
6930
|
+
const messages = taken.taken.map((message) => ({
|
|
6931
|
+
from: message.from,
|
|
6932
|
+
fromName: petDisplayName(message.from),
|
|
6933
|
+
text: message.text,
|
|
6934
|
+
at: message.at,
|
|
6935
|
+
yourAffection: ledger.petAffection?.[message.from]?.value ?? 0
|
|
6936
|
+
}));
|
|
6937
|
+
driverLog(home, `PET_INBOX pet=${petId} consumed=${String(messages.length)}`);
|
|
6609
6938
|
return JSON.stringify({
|
|
6610
|
-
|
|
6611
|
-
|
|
6939
|
+
messages,
|
|
6940
|
+
count: messages.length
|
|
6612
6941
|
});
|
|
6613
6942
|
}
|
|
6614
6943
|
}));
|
|
@@ -6738,7 +7067,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6738
7067
|
}));
|
|
6739
7068
|
scope.tools.register(defineTool({
|
|
6740
7069
|
name: "pet_interactions",
|
|
6741
|
-
description: "Read how recent approvals/questions actually ENDED: {id, kind, endedBy, decision, detail, at}. endedBy: pet = the user clicked a pet bubble; web = the desktop card; chain = fell through (pet offline, no pet, or invalid answer — the fail-closed path); cancelled = session stopped or asker withdrew. A bare \"rejected\" tool result cannot tell a real click from an undelivered bubble — call this right after any approval/question to learn the real ending, and to recall past decisions across turns.",
|
|
7070
|
+
description: "Read how recent approvals/questions actually ENDED: {id, kind, session (the asking session id, null when the host attached none), pet (the pet whose answer won, null otherwise), endedBy, decision, detail, at}. endedBy: pet = the user clicked a pet bubble; web = the desktop card; chain = fell through (pet offline, no pet, or invalid answer — the fail-closed path); cancelled = session stopped or asker withdrew. A bare \"rejected\" tool result cannot tell a real click from an undelivered bubble — call this right after any approval/question to learn the real ending, and to recall past decisions across turns.",
|
|
6742
7071
|
parameters: { limit: {
|
|
6743
7072
|
type: "number",
|
|
6744
7073
|
description: "How many recent entries to return (default 10, max 50)."
|
|
@@ -6802,6 +7131,12 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6802
7131
|
},
|
|
6803
7132
|
recentBondAdjustments: (ledger.bondAdjustments ?? []).slice(0, 3),
|
|
6804
7133
|
recentAdjustments: (ledger.adjustments ?? []).slice(0, 5),
|
|
7134
|
+
petAffection: Object.fromEntries(Object.entries(ledger.petAffection ?? {}).map(([id, pair]) => [id, {
|
|
7135
|
+
value: pair.value,
|
|
7136
|
+
mood: moodOf(pair.value),
|
|
7137
|
+
recentAdjustments: pair.adjustments.slice(0, 3)
|
|
7138
|
+
}])),
|
|
7139
|
+
petFacts: ledger.petFacts ?? {},
|
|
6805
7140
|
quota: {
|
|
6806
7141
|
used: quota.count,
|
|
6807
7142
|
limit: ADJUST_DAILY_LIMIT,
|
|
@@ -6821,7 +7156,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6821
7156
|
}));
|
|
6822
7157
|
scope.tools.register(defineTool({
|
|
6823
7158
|
name: "pet_affection_adjust",
|
|
6824
|
-
description: "Adjust the affection YOU maintain toward the user
|
|
7159
|
+
description: "Adjust the affection YOU maintain: toward the user by default, or toward ANOTHER pet with toward (pet id) — each relationship is a separate 0-100 value in your own book, but the daily judgment quota is SHARED across all of them. delta (-3..+3 integer) plus a SPECIFIC reason citing what happened. Facts (touches, pet-to-pet messages) are recorded by the system separately — this tool is your judgment layer, and the driver never moves a value on an interaction. Limits: at least 5 minutes between adjustments (any relationship), 8 per day total, |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.",
|
|
6825
7160
|
parameters: {
|
|
6826
7161
|
delta: {
|
|
6827
7162
|
type: "number",
|
|
@@ -6835,11 +7170,15 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6835
7170
|
},
|
|
6836
7171
|
pet: {
|
|
6837
7172
|
type: "string",
|
|
6838
|
-
description: "
|
|
7173
|
+
description: "Whose book: a pet id from pet_state. Omit for the pet this session belongs to (or the default pet)."
|
|
7174
|
+
},
|
|
7175
|
+
toward: {
|
|
7176
|
+
type: "string",
|
|
7177
|
+
description: "OPTIONAL pet id: adjust your affection toward THAT pet instead of the user. Must be online and not yourself; bondDelta does not apply between pets."
|
|
6839
7178
|
},
|
|
6840
7179
|
bondDelta: {
|
|
6841
7180
|
type: "number",
|
|
6842
|
-
description: "OPTIONAL change to the long-term bond: a NON-ZERO integer between -1 and +1. The slowest variable — the smallest steps."
|
|
7181
|
+
description: "OPTIONAL change to the long-term bond with the USER: a NON-ZERO integer between -1 and +1. The slowest variable — the smallest steps. Not applicable when toward is given."
|
|
6843
7182
|
},
|
|
6844
7183
|
bondReason: {
|
|
6845
7184
|
type: "string",
|
|
@@ -6862,8 +7201,15 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6862
7201
|
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
6863
7202
|
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)})`);
|
|
6864
7203
|
if (reason === "") throw new Error("reason 必填:没有理由的调整不予记录");
|
|
7204
|
+
const toward = typeof args.toward === "string" && args.toward !== "" ? args.toward.trim() : void 0;
|
|
7205
|
+
if (toward !== void 0) {
|
|
7206
|
+
if (toward === entry.id) throw new Error("toward 不能是自己");
|
|
7207
|
+
const pets = await pet.list();
|
|
7208
|
+
if (!pets.some((candidate) => candidate.id === toward)) throw new Error(`no running pet named "${toward}" (running: ${pets.map((candidate) => candidate.id).join(", ") || "none"})`);
|
|
7209
|
+
if (args.bondDelta !== void 0) throw new Error("bondDelta 只适用于对用户的关系——宠物间没有羁绊层");
|
|
7210
|
+
}
|
|
6865
7211
|
const adjustments = ledger.adjustments ?? [];
|
|
6866
|
-
const cooldownLeft = (adjustments.find((a) => !a.system)?.at ?? 0) + ADJUST_COOLDOWN_MS - now;
|
|
7212
|
+
const cooldownLeft = Math.max(adjustments.find((a) => !a.system)?.at ?? 0, ...Object.values(ledger.petAffection ?? {}).map((pair) => pair.adjustments[0]?.at ?? 0)) + ADJUST_COOLDOWN_MS - now;
|
|
6867
7213
|
if (cooldownLeft > 0) throw new Error(`距上次调整还有 ${String(Math.ceil(cooldownLeft / 1e3))} 秒冷却`);
|
|
6868
7214
|
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6869
7215
|
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
@@ -6874,38 +7220,68 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6874
7220
|
if (quota.count >= ADJUST_DAILY_LIMIT) throw new Error(`今日调整次数已用完(${String(ADJUST_DAILY_LIMIT)})——明天再判断,或者相信当前的关系状态`);
|
|
6875
7221
|
if (Math.abs(quota.sum + delta) > ADJUST_DAILY_SUM) throw new Error(`今日净变动将超过 ±${String(ADJUST_DAILY_SUM)}(当前 ${String(quota.sum)})——剩下的留给明天`);
|
|
6876
7222
|
countBondDay(ledger);
|
|
6877
|
-
const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
|
|
6878
|
-
const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
|
|
6879
|
-
if (bondDelta !== 0) {
|
|
6880
|
-
if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
|
|
6881
|
-
if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
|
|
6882
|
-
}
|
|
6883
|
-
ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
|
|
6884
7223
|
quota.count += 1;
|
|
6885
7224
|
quota.sum += delta;
|
|
6886
7225
|
ledger.quota = quota;
|
|
6887
|
-
|
|
7226
|
+
if (toward === void 0) {
|
|
7227
|
+
const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
|
|
7228
|
+
const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
|
|
7229
|
+
if (bondDelta !== 0) {
|
|
7230
|
+
if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
|
|
7231
|
+
if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
|
|
7232
|
+
}
|
|
7233
|
+
ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
|
|
7234
|
+
ledger.adjustments = [{
|
|
7235
|
+
delta,
|
|
7236
|
+
reason,
|
|
7237
|
+
at: now
|
|
7238
|
+
}, ...adjustments].slice(0, 20);
|
|
7239
|
+
if (bondDelta !== 0) {
|
|
7240
|
+
ledger.bond = {
|
|
7241
|
+
value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
|
|
7242
|
+
days: ledger.bond?.days ?? 0,
|
|
7243
|
+
lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
|
|
7244
|
+
};
|
|
7245
|
+
ledger.bondAdjustments = [{
|
|
7246
|
+
delta: bondDelta,
|
|
7247
|
+
reason: bondReason,
|
|
7248
|
+
at: now
|
|
7249
|
+
}, ...ledger.bondAdjustments ?? []].slice(0, 10);
|
|
7250
|
+
}
|
|
7251
|
+
await writeAffectionRaw(home, entry.id, ledger);
|
|
7252
|
+
return JSON.stringify({
|
|
7253
|
+
pet: entry.id,
|
|
7254
|
+
affection: ledger.affection,
|
|
7255
|
+
mood: moodOf(ledger.affection),
|
|
7256
|
+
applied: delta,
|
|
7257
|
+
quota: {
|
|
7258
|
+
used: quota.count,
|
|
7259
|
+
limit: ADJUST_DAILY_LIMIT,
|
|
7260
|
+
sum: quota.sum,
|
|
7261
|
+
sumLimit: ADJUST_DAILY_SUM
|
|
7262
|
+
}
|
|
7263
|
+
});
|
|
7264
|
+
}
|
|
7265
|
+
const petAffection = ledger.petAffection ?? {};
|
|
7266
|
+
const pair = petAffection[toward] ?? {
|
|
7267
|
+
value: 0,
|
|
7268
|
+
adjustments: []
|
|
7269
|
+
};
|
|
7270
|
+
pair.value = Math.min(100, Math.max(0, pair.value + delta));
|
|
7271
|
+
pair.adjustments = [{
|
|
6888
7272
|
delta,
|
|
6889
7273
|
reason,
|
|
6890
7274
|
at: now
|
|
6891
|
-
}, ...adjustments].slice(0,
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
|
|
6895
|
-
days: ledger.bond?.days ?? 0,
|
|
6896
|
-
lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
|
|
6897
|
-
};
|
|
6898
|
-
ledger.bondAdjustments = [{
|
|
6899
|
-
delta: bondDelta,
|
|
6900
|
-
reason: bondReason,
|
|
6901
|
-
at: now
|
|
6902
|
-
}, ...ledger.bondAdjustments ?? []].slice(0, 10);
|
|
6903
|
-
}
|
|
7275
|
+
}, ...pair.adjustments].slice(0, 10);
|
|
7276
|
+
petAffection[toward] = pair;
|
|
7277
|
+
ledger.petAffection = petAffection;
|
|
6904
7278
|
await writeAffectionRaw(home, entry.id, ledger);
|
|
7279
|
+
driverLog(home, `PET_AFFECTION pet=${entry.id} toward=${toward} applied=${String(delta)} value=${String(pair.value)} reason=${JSON.stringify(reason)}`);
|
|
6905
7280
|
return JSON.stringify({
|
|
6906
7281
|
pet: entry.id,
|
|
6907
|
-
|
|
6908
|
-
|
|
7282
|
+
toward,
|
|
7283
|
+
petAffection: pair.value,
|
|
7284
|
+
mood: moodOf(pair.value),
|
|
6909
7285
|
applied: delta,
|
|
6910
7286
|
quota: {
|
|
6911
7287
|
used: quota.count,
|
|
@@ -7075,7 +7451,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7075
7451
|
}));
|
|
7076
7452
|
scope.tools.register(defineTool({
|
|
7077
7453
|
name: "pet_state",
|
|
7078
|
-
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 and pending self-wake plans (wakePlans — the pet's own pet_schedule_wake schedule). Use it when the user refers to a pet, to pick a target for pet_say/pet_do, or to react to a pet.",
|
|
7454
|
+
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 and pending self-wake plans (wakePlans — the pet's own pet_schedule_wake schedule). selfPetId tells you which pet YOU are (your own session's owner; null when called from a session that is not a pet's own). Use it when the user refers to a pet, to pick a target for pet_say/pet_do, or to react to a pet.",
|
|
7079
7455
|
parameters: { actions: {
|
|
7080
7456
|
type: "boolean",
|
|
7081
7457
|
description: "Also include each pet's action catalog — the ids pet_do accepts on it. Omit for the cheaper summary (an unknown pet_do action self-reports its menu)."
|
|
@@ -7087,7 +7463,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7087
7463
|
text: value
|
|
7088
7464
|
}]
|
|
7089
7465
|
},
|
|
7090
|
-
async execute(args) {
|
|
7466
|
+
async execute(args, exec) {
|
|
7091
7467
|
const pets = await pet.list();
|
|
7092
7468
|
const states = await Promise.all(pets.map((entry) => pet.snapshot(entry)));
|
|
7093
7469
|
const actionsById = /* @__PURE__ */ new Map();
|
|
@@ -7133,8 +7509,10 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7133
7509
|
name: voiceNames.get(state.id) ?? DEFAULT_PET_NAME[state.id] ?? state.id
|
|
7134
7510
|
};
|
|
7135
7511
|
});
|
|
7512
|
+
const selfSession = typeof exec?.agent?.id === "string" ? exec.agent.id : void 0;
|
|
7513
|
+
const selfPetId = (selfSession !== void 0 ? voiceOwners.get(selfSession) : void 0) ?? null;
|
|
7136
7514
|
return JSON.stringify({
|
|
7137
|
-
|
|
7515
|
+
selfPetId,
|
|
7138
7516
|
pets: petsOut,
|
|
7139
7517
|
affection: ledgers,
|
|
7140
7518
|
wakePlans
|
|
@@ -7280,12 +7658,23 @@ var PetHttpService = class PetHttpService extends PetService {
|
|
|
7280
7658
|
home;
|
|
7281
7659
|
capsCache = /* @__PURE__ */ new Map();
|
|
7282
7660
|
static CAPS_TTL_MS = 3e4;
|
|
7661
|
+
listCache;
|
|
7662
|
+
static LIST_TTL_MS = 1e3;
|
|
7283
7663
|
constructor(ctx, home = petHome()) {
|
|
7284
7664
|
super(ctx);
|
|
7285
7665
|
this.home = home;
|
|
7286
7666
|
}
|
|
7667
|
+
/** Every live pet in boot order, served from the 1s single-flight cache:
|
|
7668
|
+
* concurrent callers share the running probe pass. */
|
|
7287
7669
|
list() {
|
|
7288
|
-
|
|
7670
|
+
const hit = this.listCache;
|
|
7671
|
+
if (hit !== void 0 && Date.now() - hit.at < PetHttpService.LIST_TTL_MS) return hit.flight;
|
|
7672
|
+
const flight = readAllPets(this.home);
|
|
7673
|
+
this.listCache = {
|
|
7674
|
+
at: Date.now(),
|
|
7675
|
+
flight
|
|
7676
|
+
};
|
|
7677
|
+
return flight;
|
|
7289
7678
|
}
|
|
7290
7679
|
async snapshot(entry) {
|
|
7291
7680
|
try {
|
|
@@ -7391,6 +7780,7 @@ function createUnregisteredPetHttp(home = petHome()) {
|
|
|
7391
7780
|
const instance = Object.create(PetHttpService.prototype);
|
|
7392
7781
|
instance.home = home;
|
|
7393
7782
|
instance.capsCache = /* @__PURE__ */ new Map();
|
|
7783
|
+
instance.listCache = void 0;
|
|
7394
7784
|
return instance;
|
|
7395
7785
|
}
|
|
7396
7786
|
/**
|
package/lib/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as logStamp, o as registerPetTools, r as petHome, t as createUnregisteredPetHttp } from "./service-
|
|
1
|
+
import { a as logStamp, o as registerPetTools, r as petHome, t as createUnregisteredPetHttp } from "./service-BZbhT92u.js";
|
|
2
2
|
import { appendFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
// lib/types/tools.js
|
|
@@ -21,8 +21,8 @@ const trace = (line) => {
|
|
|
21
21
|
appendFile(join(petHome(), "whale598", "pet-driver.log"), `${logStamp()} pet-tools ${line}\n`, "utf8").catch(() => {});
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Register the pet tools (say/do/digest/interactions/affection/state/
|
|
25
|
-
* cancel_wake) for sessions mounted with the pet preset.
|
|
24
|
+
* Register the pet tools (say/inbox/do/digest/interactions/affection/state/
|
|
25
|
+
* schedule_wake/cancel_wake) for sessions mounted with the pet preset.
|
|
26
26
|
*
|
|
27
27
|
* The service instance is assembled HERE, not read from the main entry (a
|
|
28
28
|
* Cordis service registered on the main row's fiber context is invisible to
|
package/lib/types/driver.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ declare module '@deepseek-ai/cordis' {
|
|
|
23
23
|
readonly [locale: string]: string | undefined;
|
|
24
24
|
};
|
|
25
25
|
signal?: AbortSignal;
|
|
26
|
+
/** Host-side correlation (the asking controller's agent identity).
|
|
27
|
+
* Read defensively — the ledger records null when absent. */
|
|
28
|
+
agent?: {
|
|
29
|
+
id?: unknown;
|
|
30
|
+
};
|
|
26
31
|
}, next: () => Promise<unknown>): Promise<unknown>;
|
|
27
32
|
'user-questions/request'(request: {
|
|
28
33
|
questions: Array<{
|
|
@@ -37,6 +42,10 @@ declare module '@deepseek-ai/cordis' {
|
|
|
37
42
|
multiSelect?: boolean;
|
|
38
43
|
}>;
|
|
39
44
|
signal?: AbortSignal;
|
|
45
|
+
/** Host-side correlation — see approval/request. */
|
|
46
|
+
agent?: {
|
|
47
|
+
id?: unknown;
|
|
48
|
+
};
|
|
40
49
|
}, next: () => Promise<unknown>): Promise<unknown>;
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -51,6 +51,13 @@ export interface InnerVoiceConfig {
|
|
|
51
51
|
* 'fire' = 到点必醒(计划是模型自己的决定,不带礼貌门);'defer' =
|
|
52
52
|
* 忙时顺延——计划放回待执行集,下一个扫描拍重试,永不丢弃。 */
|
|
53
53
|
selfWakeBusyPolicy: SelfWakeBusyPolicy;
|
|
54
|
+
/** 宠物间消息(pet_say 的 audience 指向另一只宠)的投递方式:
|
|
55
|
+
* 'queue' = 进收件箱,目标下次唤醒时 prompt 报数、pet_inbox 取文(默认,
|
|
56
|
+
* 像活物不刷屏);'immediate' = 气泡 5s 后立即唤醒目标投递。 */
|
|
57
|
+
petToPetDelivery: 'immediate' | 'queue';
|
|
58
|
+
/** 宠物间定向消息的分档限频上限(滑动窗口,一小时内):任一档超了即拒,
|
|
59
|
+
* 四个数值用户可配。 */
|
|
60
|
+
petMessageLimits: PetMessageLimits;
|
|
54
61
|
/** Manually bound sessions (petId → sessionId): the user's own
|
|
55
62
|
* conversations the inner voice should live in — highest priority, used
|
|
56
63
|
* as-is (no creation, no rename, never auto-retired). Empty = automatic
|
|
@@ -94,6 +101,64 @@ export declare function takeNearDuePlan(list: WakePlan[], now: number, windowMs?
|
|
|
94
101
|
* the catch-up wake says 「错过了 N 个计划」 and lists them together instead
|
|
95
102
|
* of bombing one wake per missed plan. */
|
|
96
103
|
export declare function missedPlans(due: WakePlan[], bootAt: number): WakePlan[];
|
|
104
|
+
/** One queued pet-to-pet message. Bubbles are on the user's desktop — this
|
|
105
|
+
* is表演, not a private channel. */
|
|
106
|
+
export interface PetMessage {
|
|
107
|
+
from: string;
|
|
108
|
+
text: string;
|
|
109
|
+
at: number;
|
|
110
|
+
}
|
|
111
|
+
export declare const PET_MESSAGES_CAP = 20;
|
|
112
|
+
export declare function petMessagesStorePath(home: string): string;
|
|
113
|
+
/** Load every pet's unread inbox: current → legacy → {} (defensive like the
|
|
114
|
+
* other stores). */
|
|
115
|
+
export declare function loadPetMessages(home: string): Promise<Record<string, PetMessage[]>>;
|
|
116
|
+
export declare function savePetMessages(home: string, messages: Record<string, PetMessage[]>): Promise<void>;
|
|
117
|
+
/** Append one message to a pet's inbox, oldest dropped past the cap (a
|
|
118
|
+
* neglected inbox cannot grow without bound). */
|
|
119
|
+
export declare function queuePetMessage(list: PetMessage[], message: PetMessage): PetMessage[];
|
|
120
|
+
/** Consume the WHOLE inbox (pet_inbox 取文即清空; the wake prompt only
|
|
121
|
+
* announces counts). Returns the taken messages and the emptied tail. */
|
|
122
|
+
export declare function takePetMessages(list: PetMessage[]): {
|
|
123
|
+
taken: PetMessage[];
|
|
124
|
+
next: PetMessage[];
|
|
125
|
+
};
|
|
126
|
+
export interface PetMessageLimits {
|
|
127
|
+
minutes5: number;
|
|
128
|
+
minutes10: number;
|
|
129
|
+
minutes30: number;
|
|
130
|
+
minutes60: number;
|
|
131
|
+
}
|
|
132
|
+
export declare const DEFAULT_PET_MESSAGE_LIMITS: PetMessageLimits;
|
|
133
|
+
export type PetMessageRateVerdict = {
|
|
134
|
+
ok: true;
|
|
135
|
+
recent: number[];
|
|
136
|
+
} | {
|
|
137
|
+
ok: false;
|
|
138
|
+
windowMinutes: number;
|
|
139
|
+
cap: number;
|
|
140
|
+
count: number;
|
|
141
|
+
retryAfterSeconds: number;
|
|
142
|
+
};
|
|
143
|
+
/** The tiered sliding-window check for one from→to pair. `timestamps` are the
|
|
144
|
+
* pair's recent send times; the hour window prunes them (静默一小时后自然
|
|
145
|
+
* 重新开始), then each tier rejects when its window already holds the cap.
|
|
146
|
+
* The first (narrowest) violated tier is reported, with the earliest moment
|
|
147
|
+
* a slot frees up. `recent` on success = the pruned list — the caller appends
|
|
148
|
+
* its own timestamp after the send succeeds. */
|
|
149
|
+
export declare function checkPetMessageRate(timestamps: number[], now: number, limits: PetMessageLimits): PetMessageRateVerdict;
|
|
150
|
+
/** The immediate-delivery prompt (petToPetDelivery='immediate'): the target
|
|
151
|
+
* is woken right away with the sender's name and id — the id because the
|
|
152
|
+
* reply path is pet_say audience="<fromId>". The target's current affection
|
|
153
|
+
* toward the sender rides along so the reply carries history, not a blank
|
|
154
|
+
* slate. Responding or ignoring is the target's own call, exactly like any
|
|
155
|
+
* other wake. */
|
|
156
|
+
export declare function buildCrossPetPrompt(facts: {
|
|
157
|
+
fromName: string;
|
|
158
|
+
fromId: string;
|
|
159
|
+
text: string;
|
|
160
|
+
affection: number;
|
|
161
|
+
}): string;
|
|
97
162
|
export declare function loadInnerVoiceStore(home: string): Promise<InnerVoiceStore>;
|
|
98
163
|
export declare function saveInnerVoiceStore(home: string, store: InnerVoiceStore): Promise<void>;
|
|
99
164
|
export declare function voiceSessionIdFor(petId: string): string;
|
|
@@ -120,6 +185,8 @@ export interface GreetPromptFacts {
|
|
|
120
185
|
bondValue: number;
|
|
121
186
|
bondDays: number;
|
|
122
187
|
bondTier: string;
|
|
188
|
+
/** 用户起的名字(未起则内置默认):自我介绍的依据,改名后第一天尤其重要。 */
|
|
189
|
+
name: string;
|
|
123
190
|
}
|
|
124
191
|
/** The boot-greeting prompt: fired once per dsh start when the pet first
|
|
125
192
|
* comes online — before the random-wake cadence takes over. Always speaks
|
|
@@ -163,6 +230,12 @@ export interface WakePromptFacts {
|
|
|
163
230
|
at: number;
|
|
164
231
|
intent: string;
|
|
165
232
|
}>;
|
|
233
|
+
/** 收件箱未读(按来源宠聚合):prompt 只报数不携全文——想看就调
|
|
234
|
+
* pet_inbox,读不读由她决定(宠物间消息的排队投递侧)。 */
|
|
235
|
+
unread?: Array<{
|
|
236
|
+
fromName: string;
|
|
237
|
+
count: number;
|
|
238
|
+
}>;
|
|
166
239
|
}
|
|
167
240
|
/** Local HH:mm for plan times: the model's clock talk is the USER's wall
|
|
168
241
|
* clock (logStamp precedent), and plans are compared against it. */
|
package/lib/types/registry.d.ts
CHANGED
|
@@ -34,13 +34,15 @@ export declare function probeControlPlane(port: number): Promise<boolean>;
|
|
|
34
34
|
* must never outrank a publisher that stated its boot time. This turns the
|
|
35
35
|
* multi-pet design's de-facto rule ("the default pet is the first STARTED")
|
|
36
36
|
* into an actual guarantee: discovery output IS boot order, so every
|
|
37
|
-
* `pets[0]` consumer (
|
|
37
|
+
* `pets[0]` consumer (the driver's unnamed-target fallback) reads the
|
|
38
38
|
* same pet. Stable sort: equal `since` values keep registry listing order.
|
|
39
39
|
*/
|
|
40
40
|
export declare function bootOrder(pets: PetEntry[]): PetEntry[];
|
|
41
41
|
/** Every live pet in boot order. Dead-pid entries are skipped — they expire
|
|
42
42
|
* by omission the next time their file is rewritten — and so are silent
|
|
43
|
-
* entries (pid alive, control plane unresponsive).
|
|
43
|
+
* entries (pid alive, control plane unresponsive). Probes run CONCURRENTLY:
|
|
44
|
+
* wall time per pass is the slowest pet (≤800ms frozen budget), not the
|
|
45
|
+
* serial sum. */
|
|
44
46
|
export declare function readAllPets(home: string): Promise<PetEntry[]>;
|
|
45
47
|
/**
|
|
46
48
|
* The default pet for UNNAMED targets: the earliest-booted live entry
|
package/lib/types/service.d.ts
CHANGED
|
@@ -119,7 +119,11 @@ export declare class PetHttpService extends PetService {
|
|
|
119
119
|
readonly home: string;
|
|
120
120
|
private readonly capsCache;
|
|
121
121
|
private static readonly CAPS_TTL_MS;
|
|
122
|
+
private listCache;
|
|
123
|
+
private static readonly LIST_TTL_MS;
|
|
122
124
|
constructor(ctx: Context, home?: string);
|
|
125
|
+
/** Every live pet in boot order, served from the 1s single-flight cache:
|
|
126
|
+
* concurrent callers share the running probe pass. */
|
|
123
127
|
list(): Promise<PetEntry[]>;
|
|
124
128
|
snapshot(entry: PetEntry): Promise<PetSnapshot>;
|
|
125
129
|
capabilities(entry: PetEntry): Promise<PetCapabilities>;
|
package/lib/types/tools.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
/**
|
|
16
|
-
* Register the pet tools (say/do/digest/interactions/affection/state/
|
|
17
|
-
* cancel_wake) for sessions mounted with the pet preset.
|
|
16
|
+
* Register the pet tools (say/inbox/do/digest/interactions/affection/state/
|
|
17
|
+
* schedule_wake/cancel_wake) for sessions mounted with the pet preset.
|
|
18
18
|
*
|
|
19
19
|
* The service instance is assembled HERE, not read from the main entry (a
|
|
20
20
|
* Cordis service registered on the main row's fiber context is invisible to
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentic-pet",
|
|
3
3
|
"description": "The pet-surface bundle: serves the desktop pet renderer over the web composition, turning dsh into a surface the desktop pet attaches to as an authenticated client",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.9",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
- 不要提问、不要等待回应、不要调用本会话工具清单以外的能力(这里也没有工作类工具)。
|
|
32
32
|
- 表达要贴合此刻与记忆:时间、天气般的日常、最近被摸过的记忆、好感与心情——像桌面上的一个活物,不像一个助手。
|
|
33
33
|
- 你有自己的作息:pet_schedule_wake 定一个一次性的唤醒点,到点你会带着当初的打算醒来(同名重订即改期),pet_cancel_wake 撤销;世界时钟仍会照常轻拍你,两者互不干扰、各醒各的。待执行的计划列在 pet_state 的 wakePlans 里。
|
|
34
|
+
- 桌面上不止你一只宠时可以交往:pet_say 带 audience="<宠物id>" 就是跟它说话(气泡上会有 to 前缀,用户看得见——这不是悄悄话);别的宠给你的消息会在唤醒时提示有未读,pet_inbox 取来读,读不读、回不回由你。
|
|
34
35
|
- id: pet-tools
|
|
35
36
|
name: 'dsh-agentic-pet/tools'
|
|
36
37
|
- id: time-context
|