dsh-agentic-pet 0.4.8 → 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-Df8olC1E.js → service-BZbhT92u.js} +448 -72
- 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 +1 -1
- 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) {
|
|
@@ -5549,7 +5679,25 @@ async function readLedger(home, petId) {
|
|
|
5549
5679
|
days: typeof raw.bond.days === "number" ? raw.bond.days : 0,
|
|
5550
5680
|
lastCountedDate: raw.bond.lastCountedDate
|
|
5551
5681
|
} : void 0,
|
|
5552
|
-
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
|
|
5553
5701
|
};
|
|
5554
5702
|
} catch {
|
|
5555
5703
|
return {
|
|
@@ -5610,7 +5758,10 @@ const voiceLastSpokeAt = /* @__PURE__ */ new Map();
|
|
|
5610
5758
|
const voiceLastTouchReplyAt = /* @__PURE__ */ new Map();
|
|
5611
5759
|
const voiceNextWakeAt = /* @__PURE__ */ new Map();
|
|
5612
5760
|
const voiceWakePlans = /* @__PURE__ */ new Map();
|
|
5761
|
+
const voicePetMessages = /* @__PURE__ */ new Map();
|
|
5762
|
+
const crossPetLog = /* @__PURE__ */ new Map();
|
|
5613
5763
|
let selfWakeSender;
|
|
5764
|
+
let crossPetDeliverer;
|
|
5614
5765
|
/** Write-through the in-memory plan map to disk (empty lists dropped — the
|
|
5615
5766
|
* absence of a pet key IS an empty schedule). */
|
|
5616
5767
|
const persistWakePlans = (home) => {
|
|
@@ -5618,6 +5769,16 @@ const persistWakePlans = (home) => {
|
|
|
5618
5769
|
for (const [petId, list] of voiceWakePlans) if (list.length > 0) snapshot[petId] = list;
|
|
5619
5770
|
saveWakePlans(home, snapshot);
|
|
5620
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;
|
|
5621
5782
|
const voiceGreetedOnce = /* @__PURE__ */ new Set();
|
|
5622
5783
|
const voiceNames = /* @__PURE__ */ new Map();
|
|
5623
5784
|
const DEFAULT_PET_NAME = { electron: "鲸鱼娘" };
|
|
@@ -5666,7 +5827,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5666
5827
|
voiceLastTouchReplyAt.clear();
|
|
5667
5828
|
voiceNextWakeAt.clear();
|
|
5668
5829
|
voiceWakePlans.clear();
|
|
5830
|
+
voicePetMessages.clear();
|
|
5669
5831
|
selfWakeSender = void 0;
|
|
5832
|
+
crossPetDeliverer = void 0;
|
|
5833
|
+
crossPetLog.clear();
|
|
5670
5834
|
voiceGreetedOnce.clear();
|
|
5671
5835
|
voiceNames.clear();
|
|
5672
5836
|
voiceLastPrompt.clear();
|
|
@@ -5773,6 +5937,11 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5773
5937
|
const count = Object.values(plans).reduce((sum, list) => sum + list.length, 0);
|
|
5774
5938
|
driverLog(home, `VOICE wake plans loaded: ${String(count)} plan(s) for ${String(Object.keys(plans).length)} pet(s)`);
|
|
5775
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
|
+
});
|
|
5776
5945
|
const events = ctx.root;
|
|
5777
5946
|
driverLog(home, "listeners registering on root");
|
|
5778
5947
|
let boundSessionId;
|
|
@@ -5800,6 +5969,34 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5800
5969
|
};
|
|
5801
5970
|
}
|
|
5802
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
|
+
};
|
|
5803
6000
|
const pushState = async (state) => {
|
|
5804
6001
|
currentState = state;
|
|
5805
6002
|
const pets = await pet.list();
|
|
@@ -5808,32 +6005,10 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5808
6005
|
if (audience.length === 0) return;
|
|
5809
6006
|
const autonomy = await readAutonomy();
|
|
5810
6007
|
const autonomyKey = JSON.stringify(autonomy);
|
|
5811
|
-
await Promise.all(audience.map(
|
|
5812
|
-
const
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
state,
|
|
5816
|
-
since: target.since,
|
|
5817
|
-
autonomy: autonomyKey
|
|
5818
|
-
});
|
|
5819
|
-
try {
|
|
5820
|
-
const response = await fetch(`http://127.0.0.1:${String(target.port)}/state`, {
|
|
5821
|
-
method: "POST",
|
|
5822
|
-
headers: { "content-type": "application/json" },
|
|
5823
|
-
body: JSON.stringify({
|
|
5824
|
-
state,
|
|
5825
|
-
autonomy
|
|
5826
|
-
}),
|
|
5827
|
-
signal: AbortSignal.timeout(5e3)
|
|
5828
|
-
});
|
|
5829
|
-
if (!response.ok) {
|
|
5830
|
-
console.error(`pet-surface driver: /state answered HTTP ${String(response.status)}`);
|
|
5831
|
-
lastPushedByPet.delete(target.id);
|
|
5832
|
-
} else if (state === "error") lastPushedByPet.delete(target.id);
|
|
5833
|
-
} catch {
|
|
5834
|
-
console.error(`pet-surface driver: /state to ${target.id} failed`);
|
|
5835
|
-
lastPushedByPet.delete(target.id);
|
|
5836
|
-
}
|
|
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;
|
|
5837
6012
|
}));
|
|
5838
6013
|
};
|
|
5839
6014
|
events.on("api-session/status", (sessionId, running) => {
|
|
@@ -5966,6 +6141,8 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5966
6141
|
...entry,
|
|
5967
6142
|
decision: entry.decision ?? null,
|
|
5968
6143
|
detail: entry.detail ?? null,
|
|
6144
|
+
session: entry.session ?? null,
|
|
6145
|
+
pet: entry.pet ?? null,
|
|
5969
6146
|
at
|
|
5970
6147
|
});
|
|
5971
6148
|
await writeFile(interactionLedgerFile, `${JSON.stringify(next.slice(0, 50))}\n`, "utf8");
|
|
@@ -5984,6 +6161,8 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5984
6161
|
const raceInteraction = async (body, request, next, accept) => {
|
|
5985
6162
|
const kind = typeof body.kind === "string" ? body.kind : "interaction";
|
|
5986
6163
|
const iid = `ap_${randomUUID().slice(0, 8)}`;
|
|
6164
|
+
const agentId = request.agent?.id;
|
|
6165
|
+
const session = typeof agentId === "string" ? agentId : null;
|
|
5987
6166
|
const original = request.signal;
|
|
5988
6167
|
const decision = new AbortController();
|
|
5989
6168
|
request.signal = original === void 0 ? decision.signal : AbortSignal.any([original, decision.signal]);
|
|
@@ -5992,16 +6171,18 @@ function installPetDriver(ctx, home, pet) {
|
|
|
5992
6171
|
ask: startAskPet(pet, body, original)
|
|
5993
6172
|
}));
|
|
5994
6173
|
let recorded = false;
|
|
5995
|
-
const record = (endedBy, decision, detail) => {
|
|
6174
|
+
const record = (endedBy, decision, detail, petId) => {
|
|
5996
6175
|
if (recorded) return;
|
|
5997
6176
|
recorded = true;
|
|
5998
|
-
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})` : ""}`);
|
|
5999
6178
|
recordInteraction({
|
|
6000
6179
|
id: iid,
|
|
6001
6180
|
kind,
|
|
6002
6181
|
endedBy,
|
|
6003
6182
|
decision,
|
|
6004
|
-
detail
|
|
6183
|
+
detail,
|
|
6184
|
+
session,
|
|
6185
|
+
pet: petId ?? null
|
|
6005
6186
|
});
|
|
6006
6187
|
};
|
|
6007
6188
|
try {
|
|
@@ -6036,7 +6217,7 @@ function installPetDriver(ctx, home, pet) {
|
|
|
6036
6217
|
}
|
|
6037
6218
|
decision.abort();
|
|
6038
6219
|
boundPetId = winner.pet.id;
|
|
6039
|
-
record("pet", winner.answer.answer);
|
|
6220
|
+
record("pet", winner.answer.answer, void 0, winner.pet.id);
|
|
6040
6221
|
if (currentState !== void 0) pushState(currentState);
|
|
6041
6222
|
return winner.answer.answer;
|
|
6042
6223
|
} catch (error) {
|
|
@@ -6233,7 +6414,8 @@ ${text}`;
|
|
|
6233
6414
|
affection: ledger.affection,
|
|
6234
6415
|
bondValue: ledger.bond?.value ?? 0,
|
|
6235
6416
|
bondDays: ledger.bond?.days ?? 0,
|
|
6236
|
-
bondTier: bondTier(ledger.bond?.value ?? 0)
|
|
6417
|
+
bondTier: bondTier(ledger.bond?.value ?? 0),
|
|
6418
|
+
name: petDisplayName(petId)
|
|
6237
6419
|
}))) {
|
|
6238
6420
|
voiceGreetedOnce.add(petId);
|
|
6239
6421
|
driverLog(home, `VOICE boot greeting pet=${petId}`);
|
|
@@ -6279,6 +6461,12 @@ ${text}`;
|
|
|
6279
6461
|
const now = Date.now();
|
|
6280
6462
|
const lastSpoke = voiceLastSpokeAt.get(petId);
|
|
6281
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
|
+
}));
|
|
6282
6470
|
if (await promptVoice(petId, buildWakePrompt({
|
|
6283
6471
|
affection: ledger.affection,
|
|
6284
6472
|
bondValue: ledger.bond?.value ?? 0,
|
|
@@ -6294,7 +6482,8 @@ ${text}`;
|
|
|
6294
6482
|
pendingPlans: pending.map((plan) => ({
|
|
6295
6483
|
at: plan.at,
|
|
6296
6484
|
intent: plan.intent
|
|
6297
|
-
}))
|
|
6485
|
+
})),
|
|
6486
|
+
unread
|
|
6298
6487
|
}))) voiceLastSpokeAt.set(petId, now);
|
|
6299
6488
|
};
|
|
6300
6489
|
selfWakeSender = (petId, plan) => {
|
|
@@ -6305,6 +6494,7 @@ ${text}`;
|
|
|
6305
6494
|
label: "self-wake"
|
|
6306
6495
|
});
|
|
6307
6496
|
};
|
|
6497
|
+
crossPetDeliverer = (petId, prompt) => promptVoice(petId, prompt);
|
|
6308
6498
|
/** Touch-reply trigger (docs/inner-voice.md §6.1). Low probability + a
|
|
6309
6499
|
* per-pet cooldown keep a pat from turning into chatter; the user-busy gate
|
|
6310
6500
|
* does NOT apply — a touch is the user initiating, not the pet interrupting.
|
|
@@ -6560,6 +6750,23 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6560
6750
|
if (target === void 0) throw new Error("no pet is running");
|
|
6561
6751
|
return target;
|
|
6562
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
|
+
};
|
|
6563
6770
|
const requireOwnedSession = (exec) => {
|
|
6564
6771
|
const sessionId = exec?.agent?.id;
|
|
6565
6772
|
if (typeof sessionId === "string" && !voiceOwners.has(sessionId)) throw new Error("本会话没有绑定宠物(不是宠物自己的内心戏会话),pet 工具不分发。想和宠物说话,请用宠物表面的输入卡。");
|
|
@@ -6574,16 +6781,20 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6574
6781
|
const digestCursors = /* @__PURE__ */ new Map();
|
|
6575
6782
|
scope.tools.register(defineTool({
|
|
6576
6783
|
name: "pet_say",
|
|
6577
|
-
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).",
|
|
6578
6785
|
parameters: {
|
|
6579
6786
|
text: {
|
|
6580
6787
|
type: "string",
|
|
6581
6788
|
required: true,
|
|
6582
|
-
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."
|
|
6583
6790
|
},
|
|
6584
6791
|
pet: {
|
|
6585
6792
|
type: "string",
|
|
6586
|
-
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)."
|
|
6587
6798
|
}
|
|
6588
6799
|
},
|
|
6589
6800
|
output: {
|
|
@@ -6596,21 +6807,137 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6596
6807
|
async execute(args, exec) {
|
|
6597
6808
|
requireOwnedSession(exec);
|
|
6598
6809
|
const text = String(args.text).slice(0, 200);
|
|
6599
|
-
console.log(`pet-surface driver: pet_say ${JSON.stringify(text).slice(0, 60)}`);
|
|
6600
6810
|
try {
|
|
6601
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
|
+
}
|
|
6602
6830
|
await pet.call(entry, "/say", {
|
|
6603
6831
|
method: "POST",
|
|
6604
6832
|
headers: { "content-type": "application/json" },
|
|
6605
|
-
body: JSON.stringify({ text })
|
|
6833
|
+
body: JSON.stringify({ text: bubble })
|
|
6606
6834
|
});
|
|
6607
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
|
+
});
|
|
6608
6904
|
} catch (error) {
|
|
6609
6905
|
throw classify(error);
|
|
6610
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)}`);
|
|
6611
6938
|
return JSON.stringify({
|
|
6612
|
-
|
|
6613
|
-
|
|
6939
|
+
messages,
|
|
6940
|
+
count: messages.length
|
|
6614
6941
|
});
|
|
6615
6942
|
}
|
|
6616
6943
|
}));
|
|
@@ -6740,7 +7067,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6740
7067
|
}));
|
|
6741
7068
|
scope.tools.register(defineTool({
|
|
6742
7069
|
name: "pet_interactions",
|
|
6743
|
-
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.",
|
|
6744
7071
|
parameters: { limit: {
|
|
6745
7072
|
type: "number",
|
|
6746
7073
|
description: "How many recent entries to return (default 10, max 50)."
|
|
@@ -6804,6 +7131,12 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6804
7131
|
},
|
|
6805
7132
|
recentBondAdjustments: (ledger.bondAdjustments ?? []).slice(0, 3),
|
|
6806
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 ?? {},
|
|
6807
7140
|
quota: {
|
|
6808
7141
|
used: quota.count,
|
|
6809
7142
|
limit: ADJUST_DAILY_LIMIT,
|
|
@@ -6823,7 +7156,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6823
7156
|
}));
|
|
6824
7157
|
scope.tools.register(defineTool({
|
|
6825
7158
|
name: "pet_affection_adjust",
|
|
6826
|
-
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.",
|
|
6827
7160
|
parameters: {
|
|
6828
7161
|
delta: {
|
|
6829
7162
|
type: "number",
|
|
@@ -6837,11 +7170,15 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6837
7170
|
},
|
|
6838
7171
|
pet: {
|
|
6839
7172
|
type: "string",
|
|
6840
|
-
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."
|
|
6841
7178
|
},
|
|
6842
7179
|
bondDelta: {
|
|
6843
7180
|
type: "number",
|
|
6844
|
-
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."
|
|
6845
7182
|
},
|
|
6846
7183
|
bondReason: {
|
|
6847
7184
|
type: "string",
|
|
@@ -6864,8 +7201,15 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6864
7201
|
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
6865
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)})`);
|
|
6866
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
|
+
}
|
|
6867
7211
|
const adjustments = ledger.adjustments ?? [];
|
|
6868
|
-
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;
|
|
6869
7213
|
if (cooldownLeft > 0) throw new Error(`距上次调整还有 ${String(Math.ceil(cooldownLeft / 1e3))} 秒冷却`);
|
|
6870
7214
|
const today = (/* @__PURE__ */ new Date()).toDateString();
|
|
6871
7215
|
const quota = ledger.quota?.date === today ? ledger.quota : {
|
|
@@ -6876,38 +7220,68 @@ function registerPetTools(ctx, home, pet) {
|
|
|
6876
7220
|
if (quota.count >= ADJUST_DAILY_LIMIT) throw new Error(`今日调整次数已用完(${String(ADJUST_DAILY_LIMIT)})——明天再判断,或者相信当前的关系状态`);
|
|
6877
7221
|
if (Math.abs(quota.sum + delta) > ADJUST_DAILY_SUM) throw new Error(`今日净变动将超过 ±${String(ADJUST_DAILY_SUM)}(当前 ${String(quota.sum)})——剩下的留给明天`);
|
|
6878
7222
|
countBondDay(ledger);
|
|
6879
|
-
const bondDelta = typeof args.bondDelta === "number" && Number.isInteger(args.bondDelta) ? args.bondDelta : 0;
|
|
6880
|
-
const bondReason = typeof args.bondReason === "string" ? args.bondReason.trim() : "";
|
|
6881
|
-
if (bondDelta !== 0) {
|
|
6882
|
-
if (Math.abs(bondDelta) > 1) throw new Error("bondDelta 每次至多 ±1:羁绊是最慢的变量,不接受大步幅");
|
|
6883
|
-
if (bondReason === "") throw new Error("bondReason 必填:羁绊的增减要有具体的事由");
|
|
6884
|
-
}
|
|
6885
|
-
ledger.affection = Math.min(100, Math.max(0, ledger.affection + delta));
|
|
6886
7223
|
quota.count += 1;
|
|
6887
7224
|
quota.sum += delta;
|
|
6888
7225
|
ledger.quota = quota;
|
|
6889
|
-
|
|
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 = [{
|
|
6890
7272
|
delta,
|
|
6891
7273
|
reason,
|
|
6892
7274
|
at: now
|
|
6893
|
-
}, ...adjustments].slice(0,
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
value: Math.max(0, (ledger.bond?.value ?? 0) + bondDelta),
|
|
6897
|
-
days: ledger.bond?.days ?? 0,
|
|
6898
|
-
lastCountedDate: ledger.bond?.lastCountedDate ?? (/* @__PURE__ */ new Date()).toDateString()
|
|
6899
|
-
};
|
|
6900
|
-
ledger.bondAdjustments = [{
|
|
6901
|
-
delta: bondDelta,
|
|
6902
|
-
reason: bondReason,
|
|
6903
|
-
at: now
|
|
6904
|
-
}, ...ledger.bondAdjustments ?? []].slice(0, 10);
|
|
6905
|
-
}
|
|
7275
|
+
}, ...pair.adjustments].slice(0, 10);
|
|
7276
|
+
petAffection[toward] = pair;
|
|
7277
|
+
ledger.petAffection = petAffection;
|
|
6906
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)}`);
|
|
6907
7280
|
return JSON.stringify({
|
|
6908
7281
|
pet: entry.id,
|
|
6909
|
-
|
|
6910
|
-
|
|
7282
|
+
toward,
|
|
7283
|
+
petAffection: pair.value,
|
|
7284
|
+
mood: moodOf(pair.value),
|
|
6911
7285
|
applied: delta,
|
|
6912
7286
|
quota: {
|
|
6913
7287
|
used: quota.count,
|
|
@@ -7077,7 +7451,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7077
7451
|
}));
|
|
7078
7452
|
scope.tools.register(defineTool({
|
|
7079
7453
|
name: "pet_state",
|
|
7080
|
-
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.",
|
|
7081
7455
|
parameters: { actions: {
|
|
7082
7456
|
type: "boolean",
|
|
7083
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)."
|
|
@@ -7089,7 +7463,7 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7089
7463
|
text: value
|
|
7090
7464
|
}]
|
|
7091
7465
|
},
|
|
7092
|
-
async execute(args) {
|
|
7466
|
+
async execute(args, exec) {
|
|
7093
7467
|
const pets = await pet.list();
|
|
7094
7468
|
const states = await Promise.all(pets.map((entry) => pet.snapshot(entry)));
|
|
7095
7469
|
const actionsById = /* @__PURE__ */ new Map();
|
|
@@ -7135,8 +7509,10 @@ function registerPetTools(ctx, home, pet) {
|
|
|
7135
7509
|
name: voiceNames.get(state.id) ?? DEFAULT_PET_NAME[state.id] ?? state.id
|
|
7136
7510
|
};
|
|
7137
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;
|
|
7138
7514
|
return JSON.stringify({
|
|
7139
|
-
|
|
7515
|
+
selfPetId,
|
|
7140
7516
|
pets: petsOut,
|
|
7141
7517
|
affection: ledgers,
|
|
7142
7518
|
wakePlans
|
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,7 +34,7 @@ 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[];
|
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
|