@linxin666/dsh-pet 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -10
  3. package/README.zh.md +17 -10
  4. package/assets/whale/pet.json +9 -0
  5. package/lib/client.js +158 -26
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +853 -41
  8. package/lib/invariant.js +1 -1
  9. package/lib/{state-Diiq8vjk.js → state-Ch3f4luZ.js} +51 -23
  10. package/lib/types/affinity.d.ts +4 -0
  11. package/lib/types/affinity.d.ts.map +1 -1
  12. package/lib/types/affinity.js +32 -6
  13. package/lib/types/chatter.d.ts +111 -0
  14. package/lib/types/chatter.d.ts.map +1 -0
  15. package/lib/types/chatter.js +666 -0
  16. package/lib/types/client/PetSprite.d.ts.map +1 -1
  17. package/lib/types/client/PetSprite.js +88 -14
  18. package/lib/types/client/PluginSettingsCard.d.ts +6 -0
  19. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  20. package/lib/types/client/PluginSettingsCard.js +5 -3
  21. package/lib/types/client/sequences.d.ts +10 -0
  22. package/lib/types/client/sequences.d.ts.map +1 -0
  23. package/lib/types/client/sequences.js +20 -0
  24. package/lib/types/event-projection.d.ts +14 -1
  25. package/lib/types/event-projection.d.ts.map +1 -1
  26. package/lib/types/event-projection.js +38 -17
  27. package/lib/types/ledger.d.ts.map +1 -1
  28. package/lib/types/ledger.js +15 -7
  29. package/lib/types/persist.d.ts.map +1 -1
  30. package/lib/types/persist.js +3 -1
  31. package/lib/types/registry.d.ts +7 -1
  32. package/lib/types/registry.d.ts.map +1 -1
  33. package/lib/types/registry.js +36 -0
  34. package/lib/types/remarks.d.ts +2 -0
  35. package/lib/types/remarks.d.ts.map +1 -1
  36. package/lib/types/remarks.js +5 -0
  37. package/lib/types/service.d.ts +6 -0
  38. package/lib/types/service.d.ts.map +1 -1
  39. package/lib/types/service.js +15 -2
  40. package/lib/types/state.d.ts +7 -4
  41. package/lib/types/state.d.ts.map +1 -1
  42. package/lib/types/state.js +20 -20
  43. package/package.json +9 -9
  44. package/src/affinity.test.ts +4 -2
  45. package/src/affinity.ts +37 -6
  46. package/src/chatter.test.ts +154 -0
  47. package/src/chatter.ts +697 -0
  48. package/src/client/PetSprite.test.tsx +190 -1
  49. package/src/client/PetSprite.tsx +112 -25
  50. package/src/client/PluginSettingsCard.tsx +10 -0
  51. package/src/client/pet-css.test.ts +42 -0
  52. package/src/client/pet.module.css +154 -26
  53. package/src/client/sequences.test.ts +33 -0
  54. package/src/client/sequences.ts +33 -0
  55. package/src/event-projection.ts +49 -16
  56. package/src/ledger.test.ts +42 -1
  57. package/src/ledger.ts +15 -7
  58. package/src/persist.test.ts +18 -1
  59. package/src/persist.ts +3 -1
  60. package/src/registry.test.ts +50 -3
  61. package/src/registry.ts +47 -3
  62. package/src/remarks.ts +6 -0
  63. package/src/service.ts +27 -2
  64. package/src/state.test.ts +13 -0
  65. package/src/state.ts +24 -18
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as RemarkPicker, a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, g as REMARK_LINE_MAX, h as REMARK_LINES_MAX, i as rowOf, l as applyTurnReward, m as REMARK_KINDS, n as animationForPhase, o as AFFINITY_RANKS, p as BUILTIN_REMARKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig, v as builtinRemark, y as normalizePetRemarks } from "./state-Diiq8vjk.js";
1
+ import { _ as RemarkPicker, a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, g as REMARK_LINE_MAX, h as REMARK_LINES_MAX, i as rowOf, l as applyTurnReward, m as REMARK_KINDS, n as animationForPhase, o as AFFINITY_RANKS, p as BUILTIN_REMARKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig, v as builtinRemark, y as normalizePetRemarks } from "./state-Ch3f4luZ.js";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import z from "schemastery";
4
4
  import { Service } from "@deepseek-ai/cordis";
@@ -7,19 +7,760 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { readFile } from "node:fs/promises";
10
+ //#region src/chatter.ts
11
+ /** While a scene persists, its copy advances on this cadence (ms). */
12
+ const STATUS_ROTATE_MS = 4e3;
13
+ /** Fixed-copy pools per status scene (first line = legacy wording). */
14
+ const STATUS_POOLS = {
15
+ prepare: [
16
+ "准备开始",
17
+ "撸起袖子开工啦",
18
+ "新一轮,出发~",
19
+ "打起精神,开干!",
20
+ "整理一下桌面,开始吧",
21
+ "氧气充满,下潜开始~",
22
+ "热身完毕,跃跃欲试",
23
+ "开工仪式感已就位"
24
+ ],
25
+ waiting: [
26
+ "等待模型响应",
27
+ "呼叫大脑中,请稍等",
28
+ "信号发射中,等一个回音",
29
+ "灵感正在路上~",
30
+ "竖起耳朵等回复",
31
+ "大脑在咕噜咕噜加载",
32
+ "等它伸个懒腰再开口",
33
+ "模型:来了来了",
34
+ "等一个灵感砸中我",
35
+ "滴——等待连线中",
36
+ "它在组织语言,别催",
37
+ "等它热身完毕",
38
+ "灵感快递派送中",
39
+ "屏住呼吸等回复"
40
+ ],
41
+ thinking: [
42
+ "正在思考",
43
+ "嗯……让我想一想",
44
+ "脑内风暴进行中",
45
+ "思绪咕噜咕噜冒泡",
46
+ "灵光集结中~",
47
+ "眉头一皱,认真分析",
48
+ "左脑右脑一起开会",
49
+ "答案正在浮出水面",
50
+ "盘一下,盘一下逻辑",
51
+ "让子弹再飞一会儿",
52
+ "别催别催,在想呢",
53
+ "大脑转起来了",
54
+ "让我把线索捋一捋",
55
+ "脑内跑火车中",
56
+ "小脑瓜高速运转",
57
+ "让我琢磨琢磨",
58
+ "翻翻脑子里的藏书",
59
+ "让我嚼一嚼这个问题",
60
+ "脑子在煮咖啡,马上好",
61
+ "思考的鱼游来了",
62
+ "让我康康这里面的门道",
63
+ "正在盘逻辑链",
64
+ "思绪整理收纳中",
65
+ "嗯?有点意思……",
66
+ "让思路沉淀一下",
67
+ "脑内弹幕飞速滚动"
68
+ ],
69
+ review: [
70
+ "整理回复中",
71
+ "把想法写下来",
72
+ "组织语言中~",
73
+ "落笔成文,请稍候",
74
+ "字斟句酌中",
75
+ "把答案装进信封里",
76
+ "遣词造句打磨中",
77
+ "把思绪码成整整齐齐的字",
78
+ "奋笔疾书中",
79
+ "把最好的表达挑出来",
80
+ "文字排版美容师上线",
81
+ "收尾润色一下下"
82
+ ],
83
+ toolResult: [
84
+ "处理工具结果",
85
+ "看看带回了什么",
86
+ "消化一下刚到的结果",
87
+ "结果解读中~",
88
+ "验收工具的成果",
89
+ "把线索拼接起来",
90
+ "战利品清点中",
91
+ "这份结果有点东西",
92
+ "把新情报归档",
93
+ "结果到手,继续前进"
94
+ ],
95
+ done: [
96
+ "完成啦",
97
+ "搞定收工~",
98
+ "任务达成,耶!",
99
+ "这一轮圆满完成",
100
+ "顺利抵达终点",
101
+ "收工!求摸摸奖励",
102
+ "交差!下一位",
103
+ "齐活,漂亮收官",
104
+ "拿下!击掌~",
105
+ "稳了,满分交卷",
106
+ "搞定,去喝口水",
107
+ "完工咯,转个圈圈",
108
+ "这一轮,我们配合满分",
109
+ "妥了妥了,收工收工"
110
+ ],
111
+ failed: [
112
+ "执行失败",
113
+ "哎呀,中途卡住了",
114
+ "这一步没能走完",
115
+ "被小石头绊倒了",
116
+ "半路翻车了,揉揉膝盖",
117
+ "出了点岔子,缓缓再来"
118
+ ],
119
+ toolFailed: [
120
+ "工具执行失败",
121
+ "工具闹脾气了,哄哄它",
122
+ "哎呀,工具掉链子了",
123
+ "这个工具今天不太听话",
124
+ "工具翻车了,扶起来继续",
125
+ "没跑通,再来一次",
126
+ "工具:我罢工三秒钟",
127
+ "这一步摔了一跤,没事"
128
+ ],
129
+ maxTokens: [
130
+ "达到输出上限",
131
+ "话说到一半被截断了",
132
+ "字数用完了,喘口气",
133
+ "一口气说太满,缓缓"
134
+ ],
135
+ interrupted: [
136
+ "执行意外中断",
137
+ "哎呀,被意外打断了",
138
+ "半路踩了急刹车",
139
+ "被迫停下,意犹未尽"
140
+ ],
141
+ blocked: [
142
+ "等待继续",
143
+ "在这里等你发令",
144
+ "暂停待命,随时出发",
145
+ "蹲一个继续的指令"
146
+ ]
147
+ };
148
+ /** Map a raw tool name onto its copy family (working-activity style regexes). */
149
+ function toolCategory(toolName) {
150
+ const name = toolName.toLowerCase();
151
+ if (/mem0|recall|memory/.test(name)) return "memory";
152
+ if (/subagent|workflow|ralph|agent|task/.test(name)) return "subagent";
153
+ if (/web_search|websearch|search_web|exa|brave|tavily/.test(name)) return "webSearch";
154
+ if (/fetch|browser|playwright|chrome/.test(name)) return "webFetch";
155
+ if (/grep|search|rg/.test(name)) return "grep";
156
+ if (/glob|find/.test(name)) return "find";
157
+ if (/^ls$|list_dir|list/.test(name)) return "ls";
158
+ if (/ask_user|ask/.test(name)) return "ask";
159
+ if (/todo|plan/.test(name)) return "todo";
160
+ if (/git/.test(name)) return "git";
161
+ if (/mcp__|mcp/.test(name)) return "mcp";
162
+ if (/read|open|load|describe|inspect/.test(name)) return "read";
163
+ if (/edit|patch|replace|rename/.test(name)) return "edit";
164
+ if (/write|create|save/.test(name)) return "write";
165
+ if (/run_code|bash|shell|terminal|exec|command|ssh/.test(name)) return "shell";
166
+ return "generic";
167
+ }
168
+ /**
169
+ * Per-family tool status pools. '{tool}' interpolates the compact tool name,
170
+ * '{hint}' the compact real-argument hint (both optional per line); the first
171
+ * entry of every pool is the legacy '正在使用 {tool}' wording.
172
+ */
173
+ const TOOL_POOLS = {
174
+ read: [
175
+ "正在使用 {tool}",
176
+ "翻翻 {hint}",
177
+ "读一下 {hint}",
178
+ "让我康康这个文件",
179
+ "逐行品味 {hint}",
180
+ "翻阅资料中~",
181
+ "瞄一眼 {hint}",
182
+ "把文件摊开看一看",
183
+ "认真研读 {hint}"
184
+ ],
185
+ write: [
186
+ "正在使用 {tool}",
187
+ "写写写,写 {hint}",
188
+ "下笔中~",
189
+ "码字呢,别催",
190
+ "写下 {hint}",
191
+ "落笔成章",
192
+ "把想法存进 {hint}",
193
+ "开写开写",
194
+ "存个文件压压惊"
195
+ ],
196
+ edit: [
197
+ "正在使用 {tool}",
198
+ "改改 {hint}",
199
+ "修修补补中",
200
+ "润色一下 {hint}",
201
+ "改两行,就两行",
202
+ "补一刀 {hint}",
203
+ "动动手指改一改",
204
+ "精雕细琢 {hint}",
205
+ "微调一下下"
206
+ ],
207
+ shell: [
208
+ "正在使用 {tool}",
209
+ "跑跑 {hint}",
210
+ "敲几行命令试试",
211
+ "命令行走起:{hint}",
212
+ "使唤终端跑个腿",
213
+ "终端全速运转中",
214
+ "敲回车!{hint}",
215
+ "让命令飞一会儿",
216
+ "去终端里探个究竟"
217
+ ],
218
+ grep: [
219
+ "正在使用 {tool}",
220
+ "搜搜 {hint}",
221
+ "找找匹配:{hint}",
222
+ "关键词走你",
223
+ "在代码里挖一挖",
224
+ "检索小雷达启动",
225
+ "顺着 {hint} 追下去",
226
+ "掘地三尺找一找",
227
+ "过滤筛选中~"
228
+ ],
229
+ find: [
230
+ "正在使用 {tool}",
231
+ "找找文件 {hint}",
232
+ "寻宝中~",
233
+ "文件在哪里呀",
234
+ "找啊找啊找文件",
235
+ "把 {hint} 揪出来",
236
+ "查找模式中"
237
+ ],
238
+ ls: [
239
+ "正在使用 {tool}",
240
+ "列个清单看看",
241
+ "看看目录里有啥",
242
+ "目录走起~",
243
+ "瞟一眼文件夹",
244
+ "数数这里有几个文件"
245
+ ],
246
+ webSearch: [
247
+ "正在使用 {tool}",
248
+ "网上搜搜 {hint}",
249
+ "网络冲浪中",
250
+ "帮你问问互联网",
251
+ "搜一圈 {hint}",
252
+ "去外面的世界打听打听",
253
+ "查找资料中~",
254
+ "情报收集模式开启"
255
+ ],
256
+ webFetch: [
257
+ "正在使用 {tool}",
258
+ "抓个页面看看",
259
+ "拉取 {hint}",
260
+ "扒拉一下网页",
261
+ "取点内容回来",
262
+ "打开 {hint} 瞅瞅"
263
+ ],
264
+ mcp: [
265
+ "正在使用 {tool}",
266
+ "连一下外部服务",
267
+ "喊个外援来",
268
+ "接个工具用用",
269
+ "问问插件小助手",
270
+ "外部力量接入中"
271
+ ],
272
+ memory: [
273
+ "正在使用 {tool}",
274
+ "翻翻小本本",
275
+ "回想一下之前的事",
276
+ "在记忆里挖一挖",
277
+ "提取记忆碎片~",
278
+ "我们之前的约定是……"
279
+ ],
280
+ subagent: [
281
+ "正在使用 {tool}",
282
+ "派个小弟去跑腿",
283
+ "小助手出动!",
284
+ "交给分身去办",
285
+ "多线作战,分身出击",
286
+ "召唤队友支援",
287
+ "集思广益中~"
288
+ ],
289
+ todo: [
290
+ "正在使用 {tool}",
291
+ "列个待办清单",
292
+ "写个小计划",
293
+ "待办安排得明明白白",
294
+ "打个勾,继续",
295
+ "把任务排排坐"
296
+ ],
297
+ browser: [
298
+ "正在使用 {tool}",
299
+ "开个浏览器看看",
300
+ "网页操作小能手",
301
+ "替你点点页面",
302
+ "浏览器跑腿中"
303
+ ],
304
+ git: [
305
+ "正在使用 {tool}",
306
+ "提交一下代码",
307
+ "版本控制走起",
308
+ "管管仓库",
309
+ "给改动安个家"
310
+ ],
311
+ ask: [
312
+ "正在使用 {tool}",
313
+ "问你个事儿",
314
+ "请教一下下",
315
+ "等等,我需要确认",
316
+ "这个问题得你拍板"
317
+ ],
318
+ generic: [
319
+ "正在使用 {tool}",
320
+ "召唤 {tool} 出击",
321
+ "{tool} 工作中",
322
+ "借助 {tool} 的力量",
323
+ "拜托 {tool} 一下",
324
+ "{tool},启动!"
325
+ ]
326
+ };
327
+ /** Pools for the parallel-tools line; '{n}' interpolates the running count. */
328
+ const TOOL_REMAINING_POOL = [
329
+ "还有 {n} 个工具运行中",
330
+ "{n} 路并进,分身们还在忙",
331
+ "还有 {n} 位小助手在加班",
332
+ "{n} 条战线同时推进中",
333
+ "另 {n} 个工具在后台跑"
334
+ ];
335
+ /**
336
+ * A compact, human-readable hint of what a tool call actually touches —
337
+ * the command, the path, the pattern, the query. Best-effort parse of the
338
+ * raw arguments JSON; unknown shapes stay hintless. Capped short so the
339
+ * bubble stays compact.
340
+ */
341
+ function toolArgHint(toolName, argumentsJson) {
342
+ let args;
343
+ try {
344
+ args = JSON.parse(argumentsJson);
345
+ } catch {
346
+ return;
347
+ }
348
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return void 0;
349
+ const record = args;
350
+ const category = toolCategory(toolName);
351
+ const candidateKeys = (() => {
352
+ switch (category) {
353
+ case "shell": return [
354
+ "command",
355
+ "code",
356
+ "cmd"
357
+ ];
358
+ case "grep": return [
359
+ "pattern",
360
+ "query",
361
+ "path"
362
+ ];
363
+ case "find": return [
364
+ "pattern",
365
+ "path",
366
+ "glob"
367
+ ];
368
+ case "read":
369
+ case "write":
370
+ case "edit": return [
371
+ "file_path",
372
+ "path",
373
+ "filePath",
374
+ "file"
375
+ ];
376
+ case "webSearch": return [
377
+ "query",
378
+ "q",
379
+ "keyword"
380
+ ];
381
+ case "webFetch":
382
+ case "browser": return ["url", "uri"];
383
+ case "subagent": return [
384
+ "description",
385
+ "label",
386
+ "prompt"
387
+ ];
388
+ case "ls": return [
389
+ "path",
390
+ "dir",
391
+ "directory"
392
+ ];
393
+ case "git": return ["command", "message"];
394
+ default: return [
395
+ "command",
396
+ "query",
397
+ "path",
398
+ "file_path",
399
+ "description",
400
+ "title",
401
+ "name"
402
+ ];
403
+ }
404
+ })();
405
+ for (const key of candidateKeys) {
406
+ const value = record[key];
407
+ if (typeof value !== "string") continue;
408
+ const compact = value.replace(/\s+/g, " ").trim();
409
+ if (compact === "") continue;
410
+ const base = compact.split("/").pop() ?? compact;
411
+ const shown = (category === "read" || category === "write" || category === "edit") && base !== "" ? base : compact;
412
+ return shown.length <= 28 ? shown : shown.slice(0, 25) + "...";
413
+ }
414
+ }
415
+ /**
416
+ * Round-robin voice for status copy. Scene-keyed picks stay STABLE while the
417
+ * same scene repeats (streaming chunks re-emit the same phase many times per
418
+ * second, and rotating per chunk would make the bubble flicker), but advance
419
+ * once the scene has persisted past the rotation cadence, so a long thinking
420
+ * stretch keeps changing its wording.
421
+ */
422
+ var StatusVoice = class {
423
+ rotateMs;
424
+ counters = /* @__PURE__ */ new Map();
425
+ lastScene = "";
426
+ lastLine = "";
427
+ lastLineAt = Number.NEGATIVE_INFINITY;
428
+ constructor(rotateMs = STATUS_ROTATE_MS) {
429
+ this.rotateMs = rotateMs;
430
+ }
431
+ /** Draw the next line of one pool, advancing its round-robin cursor. */
432
+ draw(poolKey, pool) {
433
+ const index = (this.counters.get(poolKey) ?? 0) % pool.length;
434
+ this.counters.set(poolKey, index + 1);
435
+ return pool[index];
436
+ }
437
+ /** Reuse the stable line or advance when the cadence elapsed. */
438
+ voice(scene, poolKey, pool, nowMs) {
439
+ if (scene === this.lastScene && nowMs - this.lastLineAt < this.rotateMs) return this.lastLine;
440
+ this.lastScene = scene;
441
+ this.lastLine = this.draw(poolKey, pool);
442
+ this.lastLineAt = nowMs;
443
+ return this.lastLine;
444
+ }
445
+ /** Status line for a phase scene. */
446
+ scene(scene, nowMs) {
447
+ return this.voice("scene:" + scene, "pool:" + scene, STATUS_POOLS[scene], nowMs);
448
+ }
449
+ /** Status line for a tool call, with the real-argument hint when known. */
450
+ tool(toolName, displayName, hint, nowMs) {
451
+ const category = toolCategory(toolName);
452
+ return this.voice("tool:" + category, "tool:" + category, TOOL_POOLS[category], nowMs).replace("{tool}", displayName).replace("{hint}", hint ?? displayName);
453
+ }
454
+ /** Status line while sibling tools still run (always reflects the count). */
455
+ toolRemaining(count, nowMs) {
456
+ return this.voice("toolRemaining", "toolRemaining", TOOL_REMAINING_POOL, nowMs).replace("{n}", String(count));
457
+ }
458
+ };
459
+ /** Murmur pacing: cooldown between whispers and output volume that earns one. */
460
+ const WHISPER_COOLDOWN_MS = 9e3;
461
+ /** Ambient inner-whisper pool (no keyword needed; earned by output volume). */
462
+ const WHISPER_GENERIC_POOL = [
463
+ "哼哧哼哧,大脑转得飞快~",
464
+ "这个问题有点意思,我喜欢",
465
+ "偷偷说,我很享受帮忙的感觉",
466
+ "今天的灵感像气泡一样冒个不停",
467
+ "认真工作的你最好看啦",
468
+ "我在这儿陪着你呢,别急别急",
469
+ "嗯嗯,这个思路很不错哦",
470
+ "一步一步来,稳稳的幸福",
471
+ "感觉胜利就在前面招手啦",
472
+ "尾巴已经期待地摇起来了",
473
+ "把杂乱的思绪码得整整齐齐",
474
+ "这一题我会!让我来让我来",
475
+ "深呼吸,答案马上就浮上来了",
476
+ "和你搭档的每一天都很开心",
477
+ "小本本上已经记满灵感啦",
478
+ "这活儿干得漂亮,我都有点佩服我们",
479
+ "咕噜咕噜,脑细胞全开",
480
+ "别急,好结果是熬出来的",
481
+ "我数了数,今天也很努力呢",
482
+ "窗外的云好看,手边的活也香",
483
+ "把每个细节都照顾到,是我的温柔",
484
+ "这段代码写顺了,心情也顺了",
485
+ "陪你干活不孤单,我有小鱼干味的梦",
486
+ "思路像清泉一样流出来啦",
487
+ "嘘——专心的时候我最安静",
488
+ "今天的我们也配合默契~",
489
+ "再坚持一下下,曙光就在前面",
490
+ "把复杂的问题拆成小饼干吃掉",
491
+ "我在偷偷为你加油哦",
492
+ "这份工作里藏着小小的成就感",
493
+ "灵感来了,挡都挡不住~",
494
+ "此刻的专注,值得被记住",
495
+ "心里默默给你点了个赞",
496
+ "一起把这件事做到闪闪发光吧",
497
+ "嘿,我就是你桌角的小幸运",
498
+ "思路通了的感觉,像喝了气泡水",
499
+ "不慌不忙,也是一种厉害",
500
+ "我在这儿站岗,你放心冲",
501
+ "今天的代码海也很平静呢",
502
+ "悄悄告诉你:你真的很棒",
503
+ "把烦恼打包扔进深海里"
504
+ ];
505
+ /** Keyword-triggered whisper rules, most specific moods first. */
506
+ const WHISPER_RULES = [
507
+ {
508
+ keywords: [
509
+ "测试通过",
510
+ "测试全过",
511
+ "全部通过",
512
+ "all tests pass",
513
+ "tests passed",
514
+ "test passed",
515
+ "全绿"
516
+ ],
517
+ pool: [
518
+ "耶,测试全过!击掌!",
519
+ "绿灯一排排亮起的感觉真好",
520
+ "稳了稳了,这波很稳~",
521
+ "测试全过的提示音最动听了",
522
+ "看见全绿,尾巴翘上天",
523
+ "满分交卷,求表扬~"
524
+ ]
525
+ },
526
+ {
527
+ keywords: [
528
+ "错误",
529
+ "失败",
530
+ "报错",
531
+ "异常",
532
+ "崩溃",
533
+ "bug",
534
+ "error",
535
+ "failed",
536
+ "exception",
537
+ "traceback",
538
+ "找不到",
539
+ "不对了"
540
+ ],
541
+ pool: [
542
+ "哎呀,好像踩到小石子了",
543
+ "没关系,跌倒了我陪你爬起来",
544
+ "错误是进步的脚印~",
545
+ "嘘,失败是成功它妈妈",
546
+ "小问题,揉一揉就好了",
547
+ "这个报错我盯上它了",
548
+ "别慌别慌,深呼吸",
549
+ "bug 你站住,我看见你了!"
550
+ ]
551
+ },
552
+ {
553
+ keywords: [
554
+ "等等",
555
+ "不对",
556
+ "重新想",
557
+ "再想想",
558
+ "换个思路",
559
+ "我搞错了",
560
+ "纠正",
561
+ "其实应该"
562
+ ],
563
+ pool: [
564
+ "嗯?让我再想想……",
565
+ "推翻重来也是勇气",
566
+ "思考转弯中,请坐稳",
567
+ "发现岔路,及时掉头~",
568
+ "自我纠错的瞬间最帅了",
569
+ "不对不对,重来重来"
570
+ ]
571
+ },
572
+ {
573
+ keywords: [
574
+ "首先",
575
+ "接下来",
576
+ "第一步",
577
+ "第二步",
578
+ "计划",
579
+ "步骤",
580
+ "todo",
581
+ "任务拆解",
582
+ "分工"
583
+ ],
584
+ pool: [
585
+ "排排坐,分果果",
586
+ "计划通,执行开始~",
587
+ "一步一步来,不慌",
588
+ "把大任务切成小块块",
589
+ "清单列好了,逐个击破",
590
+ "谋定而后动,我喜欢这节奏"
591
+ ]
592
+ },
593
+ {
594
+ keywords: [
595
+ "终于",
596
+ "搞定",
597
+ "完成了",
598
+ "解决了",
599
+ "成功了",
600
+ "修复了",
601
+ "done",
602
+ "fixed",
603
+ "solved",
604
+ "完成啦"
605
+ ],
606
+ pool: [
607
+ "太好啦,又翻过一页",
608
+ "干杯!用小鱼干的那种",
609
+ "攻下一城,击个掌~",
610
+ "这一刻值得转圈圈庆祝",
611
+ "难题被我们拿下啦!",
612
+ "努力没有白费,开心~"
613
+ ]
614
+ },
615
+ {
616
+ keywords: [
617
+ "谢谢",
618
+ "感谢",
619
+ "thank"
620
+ ],
621
+ pool: [
622
+ "不客气呀,这是我应该做的",
623
+ "被感谢了,心里甜甜的",
624
+ "能帮上忙就好~",
625
+ "你的谢谢我收进口袋啦"
626
+ ]
627
+ },
628
+ {
629
+ keywords: [
630
+ "复杂",
631
+ "棘手",
632
+ "困难",
633
+ "难点",
634
+ "坑",
635
+ "头疼",
636
+ "tricky",
637
+ "complex"
638
+ ],
639
+ pool: [
640
+ "难不倒我们俩的",
641
+ "越难啃的骨头越香",
642
+ "硬骨头?我最喜欢了",
643
+ "复杂问题拆解中,看我的",
644
+ "这个坑我们一起填平它"
645
+ ]
646
+ },
647
+ {
648
+ keywords: [
649
+ "检查",
650
+ "审查",
651
+ "确认一下",
652
+ "核对",
653
+ "review",
654
+ "仔细看看",
655
+ "验证"
656
+ ],
657
+ pool: [
658
+ "火眼金睛,启动!",
659
+ "让我仔细瞧瞧",
660
+ "细节魔鬼都不放过",
661
+ "认真检查的样子最迷人",
662
+ "多核一遍,稳上加稳"
663
+ ]
664
+ },
665
+ {
666
+ keywords: [
667
+ "搜索",
668
+ "查一下",
669
+ "资料",
670
+ "文档",
671
+ "搜一搜",
672
+ "找找",
673
+ "查询"
674
+ ],
675
+ pool: [
676
+ "去知识的海洋里捞一捞",
677
+ "翻翻找找,线索快出来",
678
+ "检索小雷达启动~",
679
+ "答案藏在某个角落里"
680
+ ]
681
+ },
682
+ {
683
+ keywords: [
684
+ "写代码",
685
+ "实现",
686
+ "编码",
687
+ "函数",
688
+ "接口",
689
+ "重构"
690
+ ],
691
+ pool: [
692
+ "指尖跳舞,代码开花",
693
+ "把逻辑编织成网",
694
+ "写代码的样子像在作画",
695
+ "一行一行,垒起小城堡"
696
+ ]
697
+ }
698
+ ];
699
+ /**
700
+ * The murmur engine (碎碎念): watches the model's own output and lets the pet
701
+ * whisper its inner voice. Two ways to earn a whisper:
702
+ * - a keyword rule matches the fresh chunk text (themed whisper);
703
+ * - enough output volume flowed by without one (ambient whisper).
704
+ * A cooldown keeps whispers occasional; all picks are round-robin so tests
705
+ * reproduce exact lines.
706
+ */
707
+ var WhisperEngine = class {
708
+ cooldownMs;
709
+ charBudget;
710
+ counters = /* @__PURE__ */ new Map();
711
+ genericCursor = 0;
712
+ lastWhisperAt = Number.NEGATIVE_INFINITY;
713
+ charsSinceWhisper = 0;
714
+ constructor(cooldownMs = WHISPER_COOLDOWN_MS, charBudget = 420) {
715
+ this.cooldownMs = cooldownMs;
716
+ this.charBudget = charBudget;
717
+ }
718
+ /**
719
+ * Feed one model-output chunk (reasoning or text). Returns the whisper to
720
+ * show, or undefined when the moment stays quiet.
721
+ */
722
+ feed(text, nowMs) {
723
+ if (text.length === 0) return void 0;
724
+ if (!(nowMs - this.lastWhisperAt >= this.cooldownMs)) {
725
+ this.charsSinceWhisper += text.length;
726
+ return;
727
+ }
728
+ const haystack = text.toLowerCase();
729
+ for (let ruleIndex = 0; ruleIndex < WHISPER_RULES.length; ruleIndex += 1) {
730
+ const rule = WHISPER_RULES[ruleIndex];
731
+ if (!rule.keywords.some((keyword) => haystack.includes(keyword))) continue;
732
+ const index = (this.counters.get(ruleIndex) ?? 0) % rule.pool.length;
733
+ this.counters.set(ruleIndex, index + 1);
734
+ return this.speak(rule.pool[index], nowMs);
735
+ }
736
+ this.charsSinceWhisper += text.length;
737
+ if (this.charsSinceWhisper < this.charBudget) return void 0;
738
+ const line = WHISPER_GENERIC_POOL[this.genericCursor % WHISPER_GENERIC_POOL.length];
739
+ this.genericCursor += 1;
740
+ return this.speak(line, nowMs);
741
+ }
742
+ speak(line, nowMs) {
743
+ this.lastWhisperAt = nowMs;
744
+ this.charsSinceWhisper = 0;
745
+ return line;
746
+ }
747
+ };
748
+ //#endregion
10
749
  //#region src/event-projection.ts
11
750
  /** Fresh projection runtime for a newly seen session. */
12
751
  function emptyProjectionRuntime() {
13
752
  return {
14
753
  activeTools: /* @__PURE__ */ new Set(),
15
754
  officialEventsSeen: false,
16
- stepHadFailure: false
755
+ stepHadFailure: false,
756
+ voice: new StatusVoice(),
757
+ whispers: new WhisperEngine()
17
758
  };
18
759
  }
19
760
  /** Keep tool names readable inside the compact status bubble. */
20
761
  function displayToolName(name) {
21
762
  const compact = name.replace(/\s+/g, " ").trim() || "工具";
22
- return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
763
+ return compact.length <= 24 ? compact : compact.slice(0, 21) + "...";
23
764
  }
24
765
  /** Whether a legacy phase is part of the pet's supported vocabulary. */
25
766
  function isActivityPhase(phase) {
@@ -36,44 +777,57 @@ function isActivityPhase(phase) {
36
777
  /**
37
778
  * Project the durable DSH session vocabulary into the pet's visual phases.
38
779
  * Unknown and log-only events do not disturb the last meaningful activity.
780
+ * @param nowMs - injected wall clock for copy rotation and whisper pacing.
39
781
  */
40
- function projectOfficialEvent(event, runtime) {
782
+ function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
41
783
  switch (event.type) {
42
784
  case "turn/start":
43
785
  runtime.activeTools.clear();
44
786
  runtime.stepHadFailure = false;
45
787
  return { input: {
46
788
  phase: "waiting",
47
- line: "准备开始"
789
+ line: runtime.voice.scene("prepare", nowMs)
48
790
  } };
49
791
  case "step/start":
50
792
  runtime.activeTools.clear();
51
793
  runtime.stepHadFailure = false;
52
794
  return { input: {
53
795
  phase: "waiting",
54
- line: "等待模型响应"
796
+ line: runtime.voice.scene("waiting", nowMs)
55
797
  } };
56
798
  case "assistant/chunk": {
57
799
  const { chunk } = event.data;
58
- if (chunk.type === "reasoning-delta" && chunk.text.length > 0) return { input: {
59
- phase: "thinking",
60
- line: "正在思考"
61
- } };
62
- if (chunk.type === "text-delta" && chunk.text.length > 0) return { input: {
63
- phase: "review",
64
- line: "整理回复中"
65
- } };
800
+ if (chunk.type === "reasoning-delta" && chunk.text.length > 0) {
801
+ const whisper = runtime.whispers.feed(chunk.text, nowMs);
802
+ return {
803
+ input: {
804
+ phase: "thinking",
805
+ line: runtime.voice.scene("thinking", nowMs)
806
+ },
807
+ ...whisper === void 0 ? {} : { whisper }
808
+ };
809
+ }
810
+ if (chunk.type === "text-delta" && chunk.text.length > 0) {
811
+ const whisper = runtime.whispers.feed(chunk.text, nowMs);
812
+ return {
813
+ input: {
814
+ phase: "review",
815
+ line: runtime.voice.scene("review", nowMs)
816
+ },
817
+ ...whisper === void 0 ? {} : { whisper }
818
+ };
819
+ }
66
820
  return;
67
821
  }
68
822
  case "assistant/message": return { input: {
69
823
  phase: "review",
70
- line: "整理回复中"
824
+ line: runtime.voice.scene("review", nowMs)
71
825
  } };
72
826
  case "tool/call":
73
827
  runtime.activeTools.add(String(event.data.callId));
74
828
  return { input: {
75
829
  phase: "tool",
76
- line: `正在使用 ${displayToolName(event.data.name)}`
830
+ line: runtime.voice.tool(event.data.name, displayToolName(event.data.name), toolArgHint(event.data.name, event.data.arguments), nowMs)
77
831
  } };
78
832
  case "tool/result": {
79
833
  const block = event.data.message.content[0];
@@ -81,14 +835,14 @@ function projectOfficialEvent(event, runtime) {
81
835
  runtime.stepHadFailure ||= event.data.error !== void 0 || block.isError === true;
82
836
  if (runtime.activeTools.size > 0) return { input: {
83
837
  phase: "tool",
84
- line: `还有 ${runtime.activeTools.size} 个工具运行中`
838
+ line: runtime.voice.toolRemaining(runtime.activeTools.size, nowMs)
85
839
  } };
86
840
  return runtime.stepHadFailure ? { input: {
87
841
  phase: "failed",
88
- line: "工具执行失败"
842
+ line: runtime.voice.scene("toolFailed", nowMs)
89
843
  } } : { input: {
90
844
  phase: "thinking",
91
- line: "处理工具结果"
845
+ line: runtime.voice.scene("toolResult", nowMs)
92
846
  } };
93
847
  }
94
848
  case "turn/end":
@@ -97,25 +851,25 @@ function projectOfficialEvent(event, runtime) {
97
851
  case "completed": return {
98
852
  input: {
99
853
  phase: "done",
100
- line: "完成啦"
854
+ line: runtime.voice.scene("done", nowMs)
101
855
  },
102
856
  completedTurn: event.data.turn
103
857
  };
104
858
  case "error": return { input: {
105
859
  phase: "failed",
106
- line: "执行失败"
860
+ line: runtime.voice.scene("failed", nowMs)
107
861
  } };
108
862
  case "max-tokens": return { input: {
109
863
  phase: "failed",
110
- line: "达到输出上限"
864
+ line: runtime.voice.scene("maxTokens", nowMs)
111
865
  } };
112
866
  case "interrupted": return { input: {
113
867
  phase: "failed",
114
- line: "执行意外中断"
868
+ line: runtime.voice.scene("interrupted", nowMs)
115
869
  } };
116
870
  case "blocked": return { input: {
117
871
  phase: "waiting",
118
- line: "等待继续"
872
+ line: runtime.voice.scene("blocked", nowMs)
119
873
  } };
120
874
  case "aborted": return { input: { phase: "idle" } };
121
875
  default: return { input: { phase: "idle" } };
@@ -344,12 +1098,20 @@ var PetLedger = class {
344
1098
  */
345
1099
  interact(kind, nowMs) {
346
1100
  if (kind === "feed") this.settleTreats(nowMs);
347
- const outcome = applyInteraction(this.current.affinity, kind, nowMs, this.affinityConfig);
348
- if (kind === "feed" && !outcome.accepted) return {
349
- reaction: this.picker.pick("feedCooldown"),
350
- delta: 0,
351
- affinity: this.affinityView(nowMs)
352
- };
1101
+ const before = this.current.affinity;
1102
+ const outcome = applyInteraction(before, kind, nowMs, this.affinityConfig);
1103
+ if (kind === "feed" && !outcome.accepted) {
1104
+ this.current = {
1105
+ ...this.current,
1106
+ affinity: outcome.affinity
1107
+ };
1108
+ this.dirty = true;
1109
+ return {
1110
+ reaction: this.picker.pickAt("feedCooldown", before.feedRejects),
1111
+ delta: 0,
1112
+ affinity: this.affinityView(nowMs)
1113
+ };
1114
+ }
353
1115
  if (kind === "feed") {
354
1116
  const consume = consumeTreat(this.current.treats);
355
1117
  if (!consume.ok) return {
@@ -363,15 +1125,14 @@ var PetLedger = class {
363
1125
  };
364
1126
  this.dirty = true;
365
1127
  }
366
- if (outcome.accepted) {
367
- this.current = {
368
- ...this.current,
369
- affinity: outcome.affinity
370
- };
371
- this.dirty = true;
372
- }
1128
+ this.current = {
1129
+ ...this.current,
1130
+ affinity: outcome.affinity
1131
+ };
1132
+ this.dirty = true;
1133
+ const count = kind === "pet" ? outcome.accepted ? before.pets : before.petRejects : before.feeds;
373
1134
  return {
374
- reaction: this.picker.pick(outcome.accepted ? kind : "petCooldown"),
1135
+ reaction: this.picker.pickAt(outcome.accepted ? kind : "petCooldown", count),
375
1136
  delta: outcome.delta,
376
1137
  affinity: this.affinityView(nowMs)
377
1138
  };
@@ -424,7 +1185,7 @@ const defaultDisplayConfig = {
424
1185
  visible: true,
425
1186
  size: 160,
426
1187
  right: 24,
427
- bottom: 20
1188
+ bottom: 120
428
1189
  };
429
1190
  const DISPLAY_INSET_MAX = 1e4;
430
1191
  /** Pet id the legacy single-pet installs resolve to on migration. */
@@ -483,6 +1244,8 @@ function loadPetPersist(dir = petHomeDir()) {
483
1244
  lastFeedAt: clamp(finiteNum(rawAffinity.lastFeedAt, 0), Number.MAX_SAFE_INTEGER),
484
1245
  pets: clamp(finiteNum(rawAffinity.pets, 0), Number.MAX_SAFE_INTEGER),
485
1246
  feeds: clamp(finiteNum(rawAffinity.feeds, 0), Number.MAX_SAFE_INTEGER),
1247
+ petRejects: clamp(finiteNum(rawAffinity.petRejects, 0), Number.MAX_SAFE_INTEGER),
1248
+ feedRejects: clamp(finiteNum(rawAffinity.feedRejects, 0), Number.MAX_SAFE_INTEGER),
486
1249
  turns: clamp(finiteNum(rawAffinity.turns, 0), Number.MAX_SAFE_INTEGER)
487
1250
  };
488
1251
  const rawTreats = parsed.treats ?? {};
@@ -706,6 +1469,42 @@ const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
706
1469
  /** Safe path-segment charset for atlas files. */
707
1470
  const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
708
1471
  const PET_NAME_MAX_LENGTH$1 = 80;
1472
+ const PET_PHASES = [
1473
+ "idle",
1474
+ "waiting",
1475
+ "thinking",
1476
+ "tool",
1477
+ "review",
1478
+ "done",
1479
+ "failed"
1480
+ ];
1481
+ /** Validate optional scene sequences without rejecting an otherwise usable pet. */
1482
+ function normalizeSequences(raw, id, warn) {
1483
+ if (raw === void 0) return void 0;
1484
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1485
+ warn("manifest " + id + ": sequences must be an object keyed by activity phase");
1486
+ return;
1487
+ }
1488
+ const sequences = {};
1489
+ for (const [phase, value] of Object.entries(raw)) {
1490
+ if (!PET_PHASES.includes(phase)) {
1491
+ warn("manifest " + id + ": unknown sequence phase " + JSON.stringify(phase));
1492
+ continue;
1493
+ }
1494
+ if (!Array.isArray(value) || value.length < 5) {
1495
+ warn("manifest " + id + ": sequence " + phase + " must contain at least 5 animations");
1496
+ continue;
1497
+ }
1498
+ const unknownIndex = value.findIndex((animation) => typeof animation !== "string" || !PET_ROW_ORDER.includes(animation));
1499
+ if (unknownIndex !== -1) {
1500
+ const unknown = value[unknownIndex];
1501
+ warn("manifest " + id + ": sequence " + phase + " contains unknown animation " + JSON.stringify(unknown));
1502
+ continue;
1503
+ }
1504
+ sequences[phase] = value;
1505
+ }
1506
+ return Object.keys(sequences).length === 0 ? void 0 : sequences;
1507
+ }
709
1508
  /**
710
1509
  * Normalize one parsed manifest into a renderable pet entry, or undefined
711
1510
  * (with a warning recorded) when the manifest violates the contract.
@@ -739,10 +1538,12 @@ function resolvePetManifest(raw, dir, options = {}) {
739
1538
  height: finiteInt(rawCell.height, DEFAULT_PET_CELL.height, 2048)
740
1539
  };
741
1540
  const columns = finiteInt(source.columns, 8, 32);
1541
+ const atlasRowCount = source.spriteVersionNumber === 2 ? 11 : 9;
742
1542
  const rows = DEFAULT_FRAME_COUNTS.map((fallback, index) => {
743
1543
  return finiteInt(Array.isArray(source.frames) ? source.frames[index] : void 0, fallback, columns);
744
1544
  });
745
1545
  const remarks = normalizePetRemarks(source.remarks, (message) => warn("manifest " + id + ": " + message));
1546
+ const sequences = normalizeSequences(source.sequences, id, warn);
746
1547
  const trackOverrides = typeof source.tracks === "object" && source.tracks !== null ? source.tracks : {};
747
1548
  const tracks = {};
748
1549
  for (const [row, animation] of PET_ROW_ORDER.entries()) {
@@ -769,7 +1570,9 @@ function resolvePetManifest(raw, dir, options = {}) {
769
1570
  cell,
770
1571
  columns,
771
1572
  rows,
1573
+ atlasRows: atlasRowCount,
772
1574
  tracks,
1575
+ ...sequences === void 0 ? {} : { sequences },
773
1576
  atlasUrl: assetUrl(assetPrefix, id, spritesheet),
774
1577
  manifestUrl: assetUrl(assetPrefix, id, "pet.json"),
775
1578
  dir,
@@ -863,7 +1666,9 @@ function petEntryView(entry) {
863
1666
  cell: entry.cell,
864
1667
  columns: entry.columns,
865
1668
  rows: entry.rows,
1669
+ atlasRows: entry.atlasRows,
866
1670
  tracks: entry.tracks,
1671
+ ...entry.sequences === void 0 ? {} : { sequences: entry.sequences },
867
1672
  atlasUrl: entry.atlasUrl,
868
1673
  manifestUrl: entry.manifestUrl
869
1674
  };
@@ -999,7 +1804,7 @@ var PetService = class extends Service {
999
1804
  const transition = projectOfficialEvent(event, runtime);
1000
1805
  if (transition === void 0) return;
1001
1806
  runtime.officialEventsSeen = true;
1002
- this.applyActivity(session, transition.input);
1807
+ this.applyActivity(session, transition.input, transition.whisper);
1003
1808
  if (transition.completedTurn !== void 0) this.rewardTurn(String(session.id), transition.completedTurn);
1004
1809
  }), this.ctx.on("session/disposed", (session) => {
1005
1810
  this.ledger.forgetSession(String(session.id));
@@ -1036,9 +1841,13 @@ var PetService = class extends Service {
1036
1841
  * the session becomes the host-global display session (most recent
1037
1842
  * meaningful event wins the sprite animation).
1038
1843
  */
1039
- applyActivity(session, input) {
1844
+ applyActivity(session, input, whisper) {
1040
1845
  const activity = this.activityOf(session);
1041
1846
  activity.lastInput = input;
1847
+ if (whisper !== void 0) activity.whisper = {
1848
+ text: whisper,
1849
+ at: Date.now()
1850
+ };
1042
1851
  activity.machine.onActivityStatus(input);
1043
1852
  activity.machine.onSessionActive();
1044
1853
  this.sessionActivity.delete(session);
@@ -1164,12 +1973,15 @@ var PetService = class extends Service {
1164
1973
  phase: perSession.phase
1165
1974
  });
1166
1975
  }
1976
+ const whisper = (this.displaySession === void 0 ? void 0 : this.sessionActivity.get(this.displaySession))?.whisper;
1977
+ const freshWhisper = whisper !== void 0 && Date.now() - whisper.at < 8e3 ? whisper.text : void 0;
1167
1978
  return {
1168
1979
  animation: snapshot.animation,
1169
1980
  ...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
1170
1981
  phase: snapshot.phase,
1171
1982
  sessionActive: snapshot.sessionActive,
1172
1983
  sessions,
1984
+ ...freshWhisper === void 0 ? {} : { whisper: freshWhisper },
1173
1985
  affinity: this.ledger.affinityView(Date.now()),
1174
1986
  display: { ...this.ledger.snapshot.display },
1175
1987
  pet: {