@linxin666/dsh-pet 0.1.20 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +18 -11
- package/README.zh.md +18 -11
- package/assets/whale/pet.json +9 -0
- package/lib/client.js +394 -58
- package/lib/client.js.map +1 -1
- package/lib/index.js +1007 -43
- package/lib/invariant.js +1 -1
- package/lib/{state-Diiq8vjk.js → state-DrMX22GL.js} +86 -28
- package/lib/types/affinity.d.ts +4 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +32 -6
- package/lib/types/chatter.d.ts +111 -0
- package/lib/types/chatter.d.ts.map +1 -0
- package/lib/types/chatter.js +756 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +1 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +133 -16
- package/lib/types/client/PluginSettingsCard.d.ts +28 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +147 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +1 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/sequences.d.ts +10 -0
- package/lib/types/client/sequences.d.ts.map +1 -0
- package/lib/types/client/sequences.js +20 -0
- package/lib/types/event-projection.d.ts +14 -1
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +38 -17
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +15 -7
- package/lib/types/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +3 -1
- package/lib/types/registry.d.ts +7 -1
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +47 -3
- package/lib/types/remarks.d.ts +2 -0
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/remarks.js +35 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +14 -0
- package/lib/types/service.d.ts +6 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +15 -2
- package/lib/types/state.d.ts +7 -4
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +20 -20
- package/package.json +9 -9
- package/src/affinity.test.ts +4 -2
- package/src/affinity.ts +37 -6
- package/src/chatter.test.ts +154 -0
- package/src/chatter.ts +787 -0
- package/src/client/PetDockEntry.test.tsx +93 -0
- package/src/client/PetDockEntry.tsx +1 -0
- package/src/client/PetSprite.test.tsx +253 -3
- package/src/client/PetSprite.tsx +192 -37
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/index.test.tsx +95 -0
- package/src/client/index.ts +1 -0
- package/src/client/locales.ts +4 -0
- package/src/client/pet-css.test.ts +54 -0
- package/src/client/pet.module.css +215 -57
- package/src/client/sequences.test.ts +33 -0
- package/src/client/sequences.ts +33 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/event-projection.ts +49 -16
- package/src/ledger.test.ts +42 -1
- package/src/ledger.ts +15 -7
- package/src/loopback.ts +63 -0
- package/src/persist.test.ts +18 -1
- package/src/persist.ts +3 -1
- package/src/registry.test.ts +79 -4
- package/src/registry.ts +58 -6
- package/src/remarks.ts +36 -0
- package/src/routes.ts +11 -0
- package/src/service.ts +27 -2
- package/src/state.test.ts +13 -0
- package/src/state.ts +24 -18
package/lib/index.js
CHANGED
|
@@ -1,25 +1,856 @@
|
|
|
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-
|
|
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-DrMX22GL.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";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { basename, 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
|
+
"loading 99%……就差最后一步",
|
|
465
|
+
"嗯……让我捋捋",
|
|
466
|
+
"灵感来了,先记小本本上",
|
|
467
|
+
"脑子在冒烟,但还能撑",
|
|
468
|
+
"这个报错,我好像在哪见过",
|
|
469
|
+
"专注模式 ON,请勿打扰",
|
|
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
|
+
"思考.gif 加载中",
|
|
505
|
+
"我本地能跑啊……哦,我就是干活的",
|
|
506
|
+
"有点饿,小鱼干存货还够吗",
|
|
507
|
+
"刚想通,一被打断又忘了,气",
|
|
508
|
+
"缓冲中,请稍候",
|
|
509
|
+
"这网速,比我思考还慢",
|
|
510
|
+
"脑子在后台跑批",
|
|
511
|
+
"内存不足,但热情够",
|
|
512
|
+
"404:思路未找到,重试中",
|
|
513
|
+
"这需求有点玄学,但能写",
|
|
514
|
+
"诶,这 bug 还会闪现?",
|
|
515
|
+
"刚说简单,结果打脸了",
|
|
516
|
+
"自信满满,结果翻车",
|
|
517
|
+
"这活不难,就是有点复杂",
|
|
518
|
+
"我装的,其实心里没底",
|
|
519
|
+
"别看我稳,我也慌",
|
|
520
|
+
"假装很懂的样子,其实在查文档",
|
|
521
|
+
"窗外鸟叫了两声,我听到了",
|
|
522
|
+
"打了个哈欠,没人看见",
|
|
523
|
+
"今天的状态:七分精神三分困",
|
|
524
|
+
"刚想偷懒,又想起来你还在等",
|
|
525
|
+
"数了数今天的产出,还行",
|
|
526
|
+
"有点想伸懒腰",
|
|
527
|
+
"饿意来袭,忍住",
|
|
528
|
+
"灵感像猫一样,不追它自己来",
|
|
529
|
+
"坐太久,尾巴麻了",
|
|
530
|
+
"快了快了",
|
|
531
|
+
"马上马上",
|
|
532
|
+
"等下,我记得在哪见过",
|
|
533
|
+
"呃,忘了,重新想",
|
|
534
|
+
"诶,这个思路可以",
|
|
535
|
+
"嗯?有意思",
|
|
536
|
+
"行,就这么办",
|
|
537
|
+
"好嘞",
|
|
538
|
+
"收到收到",
|
|
539
|
+
"冲了冲了",
|
|
540
|
+
"稳",
|
|
541
|
+
"妥",
|
|
542
|
+
"得嘞",
|
|
543
|
+
"你忙你的,我盯着呢",
|
|
544
|
+
"放心,有我呢",
|
|
545
|
+
"咱俩配合,无往不利",
|
|
546
|
+
"你专注的样子,我默默记下了"
|
|
547
|
+
];
|
|
548
|
+
/** Keyword-triggered whisper rules, most specific moods first. */
|
|
549
|
+
const WHISPER_RULES = [
|
|
550
|
+
{
|
|
551
|
+
keywords: [
|
|
552
|
+
"测试通过",
|
|
553
|
+
"测试全过",
|
|
554
|
+
"全部通过",
|
|
555
|
+
"all tests pass",
|
|
556
|
+
"tests passed",
|
|
557
|
+
"test passed",
|
|
558
|
+
"全绿"
|
|
559
|
+
],
|
|
560
|
+
pool: [
|
|
561
|
+
"全绿!亮瞎我眼了",
|
|
562
|
+
"测试全过,击掌~",
|
|
563
|
+
"稳了稳了,这波稳得很",
|
|
564
|
+
"绿灯一排排,看着就舒坦",
|
|
565
|
+
"能跑!没报错!",
|
|
566
|
+
"全绿,收工摸鱼去",
|
|
567
|
+
"测试过了,尾巴翘上天",
|
|
568
|
+
"漂亮,一次过",
|
|
569
|
+
"测试过了,奖励自己一口小鱼干",
|
|
570
|
+
"绿得发光,稳",
|
|
571
|
+
"又双叒叕全绿",
|
|
572
|
+
"这波测试,赢得干脆"
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
keywords: [
|
|
577
|
+
"错误",
|
|
578
|
+
"失败",
|
|
579
|
+
"报错",
|
|
580
|
+
"异常",
|
|
581
|
+
"崩溃",
|
|
582
|
+
"bug",
|
|
583
|
+
"error",
|
|
584
|
+
"failed",
|
|
585
|
+
"exception",
|
|
586
|
+
"traceback",
|
|
587
|
+
"找不到",
|
|
588
|
+
"不对了"
|
|
589
|
+
],
|
|
590
|
+
pool: [
|
|
591
|
+
"哎呀,踩到小石子了",
|
|
592
|
+
"翻车了……没事,扶起来继续",
|
|
593
|
+
"错误是进步的脚印,我懂",
|
|
594
|
+
"这报错我盯上它了",
|
|
595
|
+
"我本地能跑啊?哦,我一直在跑",
|
|
596
|
+
"别慌,深呼吸,先看报错",
|
|
597
|
+
"bug 你站住,我看见你了",
|
|
598
|
+
"绷不住了……好,继续修",
|
|
599
|
+
"报错这东西,见一个修一个",
|
|
600
|
+
"又是它,老熟人了",
|
|
601
|
+
"问题不大,就是有点问题",
|
|
602
|
+
"先别慌,我看看到底咋回事",
|
|
603
|
+
"这错报得,比我还委屈",
|
|
604
|
+
"修好它,今天才不算白干"
|
|
605
|
+
]
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
keywords: [
|
|
609
|
+
"等等",
|
|
610
|
+
"不对",
|
|
611
|
+
"重新想",
|
|
612
|
+
"再想想",
|
|
613
|
+
"换个思路",
|
|
614
|
+
"我搞错了",
|
|
615
|
+
"纠正",
|
|
616
|
+
"其实应该"
|
|
617
|
+
],
|
|
618
|
+
pool: [
|
|
619
|
+
"嗯?让我再想想……",
|
|
620
|
+
"推翻重来,也是种勇气",
|
|
621
|
+
"发现岔路,及时掉头",
|
|
622
|
+
"不对不对,重来重来",
|
|
623
|
+
"自我纠错的瞬间,最帅了",
|
|
624
|
+
"呃,刚说错了,收回",
|
|
625
|
+
"哎,绕远了,拉回来",
|
|
626
|
+
"回头一看,原来这么简单",
|
|
627
|
+
"纠正完,思路清爽多了",
|
|
628
|
+
"转弯不丢人,卡死才丢人"
|
|
629
|
+
]
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
keywords: [
|
|
633
|
+
"首先",
|
|
634
|
+
"接下来",
|
|
635
|
+
"第一步",
|
|
636
|
+
"第二步",
|
|
637
|
+
"计划",
|
|
638
|
+
"步骤",
|
|
639
|
+
"todo",
|
|
640
|
+
"任务拆解",
|
|
641
|
+
"分工"
|
|
642
|
+
],
|
|
643
|
+
pool: [
|
|
644
|
+
"排排坐,分果果",
|
|
645
|
+
"计划通,执行开始",
|
|
646
|
+
"一步一步来,不慌",
|
|
647
|
+
"大任务切成小块块,好下口",
|
|
648
|
+
"清单列好了,逐个击破",
|
|
649
|
+
"谋定而后动,这节奏我熟",
|
|
650
|
+
"先干这个,再干那个",
|
|
651
|
+
"头绪理清了,开整",
|
|
652
|
+
"步骤在手,心里不慌",
|
|
653
|
+
"安排得明明白白"
|
|
654
|
+
]
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
keywords: [
|
|
658
|
+
"终于",
|
|
659
|
+
"搞定",
|
|
660
|
+
"完成了",
|
|
661
|
+
"解决了",
|
|
662
|
+
"成功了",
|
|
663
|
+
"修复了",
|
|
664
|
+
"done",
|
|
665
|
+
"fixed",
|
|
666
|
+
"solved",
|
|
667
|
+
"完成啦"
|
|
668
|
+
],
|
|
669
|
+
pool: [
|
|
670
|
+
"太好了,又翻过一页",
|
|
671
|
+
"搞定,收工~",
|
|
672
|
+
"攻下一城,击掌!",
|
|
673
|
+
"难题被拿下了,转个圈",
|
|
674
|
+
"努力没白费,开心",
|
|
675
|
+
"齐活,漂亮",
|
|
676
|
+
"收工收工,今天圆满",
|
|
677
|
+
"完成!心里踏实了",
|
|
678
|
+
"这波,稳得一批",
|
|
679
|
+
"任务清零,舒服",
|
|
680
|
+
"搞定,可以伸个懒腰了",
|
|
681
|
+
"又完成一件,成就感+1"
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
keywords: [
|
|
686
|
+
"谢谢",
|
|
687
|
+
"感谢",
|
|
688
|
+
"thank"
|
|
689
|
+
],
|
|
690
|
+
pool: [
|
|
691
|
+
"不客气呀,顺手的事",
|
|
692
|
+
"被感谢了,心里甜甜的",
|
|
693
|
+
"能帮上忙就好~",
|
|
694
|
+
"你的谢意,我收进口袋啦",
|
|
695
|
+
"这话我爱听",
|
|
696
|
+
"客气啥,应该的",
|
|
697
|
+
"收下这份心意,干劲+1",
|
|
698
|
+
"你谢我,我谢你,扯平啦"
|
|
699
|
+
]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
keywords: [
|
|
703
|
+
"复杂",
|
|
704
|
+
"棘手",
|
|
705
|
+
"困难",
|
|
706
|
+
"难点",
|
|
707
|
+
"坑",
|
|
708
|
+
"头疼",
|
|
709
|
+
"tricky",
|
|
710
|
+
"complex"
|
|
711
|
+
],
|
|
712
|
+
pool: [
|
|
713
|
+
"难不倒我们俩的",
|
|
714
|
+
"越难啃的骨头越香",
|
|
715
|
+
"硬骨头?我最喜欢了",
|
|
716
|
+
"复杂问题拆开看,小事",
|
|
717
|
+
"这坑我们一起填",
|
|
718
|
+
"有点东西,但不多",
|
|
719
|
+
"硬骨头,慢慢啃",
|
|
720
|
+
"问题越难,赢的时候越爽",
|
|
721
|
+
"绕是绕不过去的,正面刚",
|
|
722
|
+
"再难的题,拆开都是小问号"
|
|
723
|
+
]
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
keywords: [
|
|
727
|
+
"检查",
|
|
728
|
+
"审查",
|
|
729
|
+
"确认一下",
|
|
730
|
+
"核对",
|
|
731
|
+
"review",
|
|
732
|
+
"仔细看看",
|
|
733
|
+
"验证"
|
|
734
|
+
],
|
|
735
|
+
pool: [
|
|
736
|
+
"火眼金睛,启动",
|
|
737
|
+
"让我仔细瞧瞧",
|
|
738
|
+
"细节魔鬼,一个都不放过",
|
|
739
|
+
"认真检查的样子最迷人",
|
|
740
|
+
"多核一遍,稳上加稳",
|
|
741
|
+
"再看一眼,不亏",
|
|
742
|
+
"确认键,点了才安心",
|
|
743
|
+
"细节控上线",
|
|
744
|
+
"查完这遍,稳了"
|
|
745
|
+
]
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
keywords: [
|
|
749
|
+
"搜索",
|
|
750
|
+
"查一下",
|
|
751
|
+
"资料",
|
|
752
|
+
"文档",
|
|
753
|
+
"搜一搜",
|
|
754
|
+
"找找",
|
|
755
|
+
"查询"
|
|
756
|
+
],
|
|
757
|
+
pool: [
|
|
758
|
+
"去知识的海洋里捞一捞",
|
|
759
|
+
"翻翻找找,线索快出来",
|
|
760
|
+
"检索小雷达启动",
|
|
761
|
+
"答案就藏在某个角落",
|
|
762
|
+
"线索有点散,拼一下",
|
|
763
|
+
"找东西,我最在行",
|
|
764
|
+
"答案在网线那头等我",
|
|
765
|
+
"翻箱倒柜中,稍等"
|
|
766
|
+
]
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
keywords: [
|
|
770
|
+
"写代码",
|
|
771
|
+
"实现",
|
|
772
|
+
"编码",
|
|
773
|
+
"函数",
|
|
774
|
+
"接口",
|
|
775
|
+
"重构"
|
|
776
|
+
],
|
|
777
|
+
pool: [
|
|
778
|
+
"指尖跳舞,代码开花",
|
|
779
|
+
"把逻辑织成网",
|
|
780
|
+
"一行一行,垒起小城堡",
|
|
781
|
+
"这代码写得,我自己都佩服",
|
|
782
|
+
"码着码着,天就亮了",
|
|
783
|
+
"代码跑通了,比中奖还开心",
|
|
784
|
+
"这行代码,写得有点帅",
|
|
785
|
+
"写完再润润,讲究"
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
];
|
|
789
|
+
/**
|
|
790
|
+
* The murmur engine (碎碎念): watches the model's own output and lets the pet
|
|
791
|
+
* whisper its inner voice. Two ways to earn a whisper:
|
|
792
|
+
* - a keyword rule matches the fresh chunk text (themed whisper);
|
|
793
|
+
* - enough output volume flowed by without one (ambient whisper).
|
|
794
|
+
* A cooldown keeps whispers occasional; all picks are round-robin so tests
|
|
795
|
+
* reproduce exact lines.
|
|
796
|
+
*/
|
|
797
|
+
var WhisperEngine = class {
|
|
798
|
+
cooldownMs;
|
|
799
|
+
charBudget;
|
|
800
|
+
counters = /* @__PURE__ */ new Map();
|
|
801
|
+
genericCursor = 0;
|
|
802
|
+
lastWhisperAt = Number.NEGATIVE_INFINITY;
|
|
803
|
+
charsSinceWhisper = 0;
|
|
804
|
+
constructor(cooldownMs = WHISPER_COOLDOWN_MS, charBudget = 420) {
|
|
805
|
+
this.cooldownMs = cooldownMs;
|
|
806
|
+
this.charBudget = charBudget;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Feed one model-output chunk (reasoning or text). Returns the whisper to
|
|
810
|
+
* show, or undefined when the moment stays quiet.
|
|
811
|
+
*/
|
|
812
|
+
feed(text, nowMs) {
|
|
813
|
+
if (text.length === 0) return void 0;
|
|
814
|
+
if (!(nowMs - this.lastWhisperAt >= this.cooldownMs)) {
|
|
815
|
+
this.charsSinceWhisper += text.length;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const haystack = text.toLowerCase();
|
|
819
|
+
for (let ruleIndex = 0; ruleIndex < WHISPER_RULES.length; ruleIndex += 1) {
|
|
820
|
+
const rule = WHISPER_RULES[ruleIndex];
|
|
821
|
+
if (!rule.keywords.some((keyword) => haystack.includes(keyword))) continue;
|
|
822
|
+
const index = (this.counters.get(ruleIndex) ?? 0) % rule.pool.length;
|
|
823
|
+
this.counters.set(ruleIndex, index + 1);
|
|
824
|
+
return this.speak(rule.pool[index], nowMs);
|
|
825
|
+
}
|
|
826
|
+
this.charsSinceWhisper += text.length;
|
|
827
|
+
if (this.charsSinceWhisper < this.charBudget) return void 0;
|
|
828
|
+
const line = WHISPER_GENERIC_POOL[this.genericCursor % WHISPER_GENERIC_POOL.length];
|
|
829
|
+
this.genericCursor += 1;
|
|
830
|
+
return this.speak(line, nowMs);
|
|
831
|
+
}
|
|
832
|
+
speak(line, nowMs) {
|
|
833
|
+
this.lastWhisperAt = nowMs;
|
|
834
|
+
this.charsSinceWhisper = 0;
|
|
835
|
+
return line;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
//#endregion
|
|
10
839
|
//#region src/event-projection.ts
|
|
11
840
|
/** Fresh projection runtime for a newly seen session. */
|
|
12
841
|
function emptyProjectionRuntime() {
|
|
13
842
|
return {
|
|
14
843
|
activeTools: /* @__PURE__ */ new Set(),
|
|
15
844
|
officialEventsSeen: false,
|
|
16
|
-
stepHadFailure: false
|
|
845
|
+
stepHadFailure: false,
|
|
846
|
+
voice: new StatusVoice(),
|
|
847
|
+
whispers: new WhisperEngine()
|
|
17
848
|
};
|
|
18
849
|
}
|
|
19
850
|
/** Keep tool names readable inside the compact status bubble. */
|
|
20
851
|
function displayToolName(name) {
|
|
21
852
|
const compact = name.replace(/\s+/g, " ").trim() || "工具";
|
|
22
|
-
return compact.length <= 24 ? compact :
|
|
853
|
+
return compact.length <= 24 ? compact : compact.slice(0, 21) + "...";
|
|
23
854
|
}
|
|
24
855
|
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
25
856
|
function isActivityPhase(phase) {
|
|
@@ -36,44 +867,57 @@ function isActivityPhase(phase) {
|
|
|
36
867
|
/**
|
|
37
868
|
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
38
869
|
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
870
|
+
* @param nowMs - injected wall clock for copy rotation and whisper pacing.
|
|
39
871
|
*/
|
|
40
|
-
function projectOfficialEvent(event, runtime) {
|
|
872
|
+
function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
41
873
|
switch (event.type) {
|
|
42
874
|
case "turn/start":
|
|
43
875
|
runtime.activeTools.clear();
|
|
44
876
|
runtime.stepHadFailure = false;
|
|
45
877
|
return { input: {
|
|
46
878
|
phase: "waiting",
|
|
47
|
-
line: "
|
|
879
|
+
line: runtime.voice.scene("prepare", nowMs)
|
|
48
880
|
} };
|
|
49
881
|
case "step/start":
|
|
50
882
|
runtime.activeTools.clear();
|
|
51
883
|
runtime.stepHadFailure = false;
|
|
52
884
|
return { input: {
|
|
53
885
|
phase: "waiting",
|
|
54
|
-
line: "
|
|
886
|
+
line: runtime.voice.scene("waiting", nowMs)
|
|
55
887
|
} };
|
|
56
888
|
case "assistant/chunk": {
|
|
57
889
|
const { chunk } = event.data;
|
|
58
|
-
if (chunk.type === "reasoning-delta" && chunk.text.length > 0)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
890
|
+
if (chunk.type === "reasoning-delta" && chunk.text.length > 0) {
|
|
891
|
+
const whisper = runtime.whispers.feed(chunk.text, nowMs);
|
|
892
|
+
return {
|
|
893
|
+
input: {
|
|
894
|
+
phase: "thinking",
|
|
895
|
+
line: runtime.voice.scene("thinking", nowMs)
|
|
896
|
+
},
|
|
897
|
+
...whisper === void 0 ? {} : { whisper }
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (chunk.type === "text-delta" && chunk.text.length > 0) {
|
|
901
|
+
const whisper = runtime.whispers.feed(chunk.text, nowMs);
|
|
902
|
+
return {
|
|
903
|
+
input: {
|
|
904
|
+
phase: "review",
|
|
905
|
+
line: runtime.voice.scene("review", nowMs)
|
|
906
|
+
},
|
|
907
|
+
...whisper === void 0 ? {} : { whisper }
|
|
908
|
+
};
|
|
909
|
+
}
|
|
66
910
|
return;
|
|
67
911
|
}
|
|
68
912
|
case "assistant/message": return { input: {
|
|
69
913
|
phase: "review",
|
|
70
|
-
line: "
|
|
914
|
+
line: runtime.voice.scene("review", nowMs)
|
|
71
915
|
} };
|
|
72
916
|
case "tool/call":
|
|
73
917
|
runtime.activeTools.add(String(event.data.callId));
|
|
74
918
|
return { input: {
|
|
75
919
|
phase: "tool",
|
|
76
|
-
line:
|
|
920
|
+
line: runtime.voice.tool(event.data.name, displayToolName(event.data.name), toolArgHint(event.data.name, event.data.arguments), nowMs)
|
|
77
921
|
} };
|
|
78
922
|
case "tool/result": {
|
|
79
923
|
const block = event.data.message.content[0];
|
|
@@ -81,14 +925,14 @@ function projectOfficialEvent(event, runtime) {
|
|
|
81
925
|
runtime.stepHadFailure ||= event.data.error !== void 0 || block.isError === true;
|
|
82
926
|
if (runtime.activeTools.size > 0) return { input: {
|
|
83
927
|
phase: "tool",
|
|
84
|
-
line:
|
|
928
|
+
line: runtime.voice.toolRemaining(runtime.activeTools.size, nowMs)
|
|
85
929
|
} };
|
|
86
930
|
return runtime.stepHadFailure ? { input: {
|
|
87
931
|
phase: "failed",
|
|
88
|
-
line: "
|
|
932
|
+
line: runtime.voice.scene("toolFailed", nowMs)
|
|
89
933
|
} } : { input: {
|
|
90
934
|
phase: "thinking",
|
|
91
|
-
line: "
|
|
935
|
+
line: runtime.voice.scene("toolResult", nowMs)
|
|
92
936
|
} };
|
|
93
937
|
}
|
|
94
938
|
case "turn/end":
|
|
@@ -97,25 +941,25 @@ function projectOfficialEvent(event, runtime) {
|
|
|
97
941
|
case "completed": return {
|
|
98
942
|
input: {
|
|
99
943
|
phase: "done",
|
|
100
|
-
line: "
|
|
944
|
+
line: runtime.voice.scene("done", nowMs)
|
|
101
945
|
},
|
|
102
946
|
completedTurn: event.data.turn
|
|
103
947
|
};
|
|
104
948
|
case "error": return { input: {
|
|
105
949
|
phase: "failed",
|
|
106
|
-
line: "
|
|
950
|
+
line: runtime.voice.scene("failed", nowMs)
|
|
107
951
|
} };
|
|
108
952
|
case "max-tokens": return { input: {
|
|
109
953
|
phase: "failed",
|
|
110
|
-
line: "
|
|
954
|
+
line: runtime.voice.scene("maxTokens", nowMs)
|
|
111
955
|
} };
|
|
112
956
|
case "interrupted": return { input: {
|
|
113
957
|
phase: "failed",
|
|
114
|
-
line: "
|
|
958
|
+
line: runtime.voice.scene("interrupted", nowMs)
|
|
115
959
|
} };
|
|
116
960
|
case "blocked": return { input: {
|
|
117
961
|
phase: "waiting",
|
|
118
|
-
line: "
|
|
962
|
+
line: runtime.voice.scene("blocked", nowMs)
|
|
119
963
|
} };
|
|
120
964
|
case "aborted": return { input: { phase: "idle" } };
|
|
121
965
|
default: return { input: { phase: "idle" } };
|
|
@@ -344,12 +1188,20 @@ var PetLedger = class {
|
|
|
344
1188
|
*/
|
|
345
1189
|
interact(kind, nowMs) {
|
|
346
1190
|
if (kind === "feed") this.settleTreats(nowMs);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1191
|
+
const before = this.current.affinity;
|
|
1192
|
+
const outcome = applyInteraction(before, kind, nowMs, this.affinityConfig);
|
|
1193
|
+
if (kind === "feed" && !outcome.accepted) {
|
|
1194
|
+
this.current = {
|
|
1195
|
+
...this.current,
|
|
1196
|
+
affinity: outcome.affinity
|
|
1197
|
+
};
|
|
1198
|
+
this.dirty = true;
|
|
1199
|
+
return {
|
|
1200
|
+
reaction: this.picker.pickAt("feedCooldown", before.feedRejects),
|
|
1201
|
+
delta: 0,
|
|
1202
|
+
affinity: this.affinityView(nowMs)
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
353
1205
|
if (kind === "feed") {
|
|
354
1206
|
const consume = consumeTreat(this.current.treats);
|
|
355
1207
|
if (!consume.ok) return {
|
|
@@ -363,15 +1215,14 @@ var PetLedger = class {
|
|
|
363
1215
|
};
|
|
364
1216
|
this.dirty = true;
|
|
365
1217
|
}
|
|
366
|
-
|
|
367
|
-
this.current
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
1218
|
+
this.current = {
|
|
1219
|
+
...this.current,
|
|
1220
|
+
affinity: outcome.affinity
|
|
1221
|
+
};
|
|
1222
|
+
this.dirty = true;
|
|
1223
|
+
const count = kind === "pet" ? outcome.accepted ? before.pets : before.petRejects : before.feeds;
|
|
373
1224
|
return {
|
|
374
|
-
reaction: this.picker.
|
|
1225
|
+
reaction: this.picker.pickAt(outcome.accepted ? kind : "petCooldown", count),
|
|
375
1226
|
delta: outcome.delta,
|
|
376
1227
|
affinity: this.affinityView(nowMs)
|
|
377
1228
|
};
|
|
@@ -424,7 +1275,7 @@ const defaultDisplayConfig = {
|
|
|
424
1275
|
visible: true,
|
|
425
1276
|
size: 160,
|
|
426
1277
|
right: 24,
|
|
427
|
-
bottom:
|
|
1278
|
+
bottom: 120
|
|
428
1279
|
};
|
|
429
1280
|
const DISPLAY_INSET_MAX = 1e4;
|
|
430
1281
|
/** Pet id the legacy single-pet installs resolve to on migration. */
|
|
@@ -483,6 +1334,8 @@ function loadPetPersist(dir = petHomeDir()) {
|
|
|
483
1334
|
lastFeedAt: clamp(finiteNum(rawAffinity.lastFeedAt, 0), Number.MAX_SAFE_INTEGER),
|
|
484
1335
|
pets: clamp(finiteNum(rawAffinity.pets, 0), Number.MAX_SAFE_INTEGER),
|
|
485
1336
|
feeds: clamp(finiteNum(rawAffinity.feeds, 0), Number.MAX_SAFE_INTEGER),
|
|
1337
|
+
petRejects: clamp(finiteNum(rawAffinity.petRejects, 0), Number.MAX_SAFE_INTEGER),
|
|
1338
|
+
feedRejects: clamp(finiteNum(rawAffinity.feedRejects, 0), Number.MAX_SAFE_INTEGER),
|
|
486
1339
|
turns: clamp(finiteNum(rawAffinity.turns, 0), Number.MAX_SAFE_INTEGER)
|
|
487
1340
|
};
|
|
488
1341
|
const rawTreats = parsed.treats ?? {};
|
|
@@ -706,6 +1559,42 @@ const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
706
1559
|
/** Safe path-segment charset for atlas files. */
|
|
707
1560
|
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
708
1561
|
const PET_NAME_MAX_LENGTH$1 = 80;
|
|
1562
|
+
const PET_PHASES = [
|
|
1563
|
+
"idle",
|
|
1564
|
+
"waiting",
|
|
1565
|
+
"thinking",
|
|
1566
|
+
"tool",
|
|
1567
|
+
"review",
|
|
1568
|
+
"done",
|
|
1569
|
+
"failed"
|
|
1570
|
+
];
|
|
1571
|
+
/** Validate optional scene sequences without rejecting an otherwise usable pet. */
|
|
1572
|
+
function normalizeSequences(raw, id, warn) {
|
|
1573
|
+
if (raw === void 0) return void 0;
|
|
1574
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1575
|
+
warn("manifest " + id + ": sequences must be an object keyed by activity phase");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const sequences = {};
|
|
1579
|
+
for (const [phase, value] of Object.entries(raw)) {
|
|
1580
|
+
if (!PET_PHASES.includes(phase)) {
|
|
1581
|
+
warn("manifest " + id + ": unknown sequence phase " + JSON.stringify(phase));
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (!Array.isArray(value) || value.length < 5) {
|
|
1585
|
+
warn("manifest " + id + ": sequence " + phase + " must contain at least 5 animations");
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const unknownIndex = value.findIndex((animation) => typeof animation !== "string" || !PET_ROW_ORDER.includes(animation));
|
|
1589
|
+
if (unknownIndex !== -1) {
|
|
1590
|
+
const unknown = value[unknownIndex];
|
|
1591
|
+
warn("manifest " + id + ": sequence " + phase + " contains unknown animation " + JSON.stringify(unknown));
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
sequences[phase] = value;
|
|
1595
|
+
}
|
|
1596
|
+
return Object.keys(sequences).length === 0 ? void 0 : sequences;
|
|
1597
|
+
}
|
|
709
1598
|
/**
|
|
710
1599
|
* Normalize one parsed manifest into a renderable pet entry, or undefined
|
|
711
1600
|
* (with a warning recorded) when the manifest violates the contract.
|
|
@@ -739,10 +1628,12 @@ function resolvePetManifest(raw, dir, options = {}) {
|
|
|
739
1628
|
height: finiteInt(rawCell.height, DEFAULT_PET_CELL.height, 2048)
|
|
740
1629
|
};
|
|
741
1630
|
const columns = finiteInt(source.columns, 8, 32);
|
|
1631
|
+
const atlasRowCount = source.spriteVersionNumber === 2 ? 11 : 9;
|
|
742
1632
|
const rows = DEFAULT_FRAME_COUNTS.map((fallback, index) => {
|
|
743
1633
|
return finiteInt(Array.isArray(source.frames) ? source.frames[index] : void 0, fallback, columns);
|
|
744
1634
|
});
|
|
745
1635
|
const remarks = normalizePetRemarks(source.remarks, (message) => warn("manifest " + id + ": " + message));
|
|
1636
|
+
const sequences = normalizeSequences(source.sequences, id, warn);
|
|
746
1637
|
const trackOverrides = typeof source.tracks === "object" && source.tracks !== null ? source.tracks : {};
|
|
747
1638
|
const tracks = {};
|
|
748
1639
|
for (const [row, animation] of PET_ROW_ORDER.entries()) {
|
|
@@ -769,7 +1660,9 @@ function resolvePetManifest(raw, dir, options = {}) {
|
|
|
769
1660
|
cell,
|
|
770
1661
|
columns,
|
|
771
1662
|
rows,
|
|
1663
|
+
atlasRows: atlasRowCount,
|
|
772
1664
|
tracks,
|
|
1665
|
+
...sequences === void 0 ? {} : { sequences },
|
|
773
1666
|
atlasUrl: assetUrl(assetPrefix, id, spritesheet),
|
|
774
1667
|
manifestUrl: assetUrl(assetPrefix, id, "pet.json"),
|
|
775
1668
|
dir,
|
|
@@ -838,7 +1731,12 @@ function loadPetRegistry(options) {
|
|
|
838
1731
|
byId.set(entry.id, entry);
|
|
839
1732
|
}
|
|
840
1733
|
for (const manifest of options.extra ?? []) {
|
|
841
|
-
const
|
|
1734
|
+
const raw = manifest.spritesheetPath;
|
|
1735
|
+
const dir = raw === void 0 || isAbsolute(raw) ? join(packageRoot, "assets", "extra") : dirname(resolve(packageRoot, raw));
|
|
1736
|
+
const entry = resolvePetManifest(raw === void 0 || isAbsolute(raw) ? manifest : {
|
|
1737
|
+
...manifest,
|
|
1738
|
+
spritesheetPath: basename(raw)
|
|
1739
|
+
}, dir, {
|
|
842
1740
|
assetPrefix,
|
|
843
1741
|
warnings
|
|
844
1742
|
});
|
|
@@ -863,7 +1761,9 @@ function petEntryView(entry) {
|
|
|
863
1761
|
cell: entry.cell,
|
|
864
1762
|
columns: entry.columns,
|
|
865
1763
|
rows: entry.rows,
|
|
1764
|
+
atlasRows: entry.atlasRows,
|
|
866
1765
|
tracks: entry.tracks,
|
|
1766
|
+
...entry.sequences === void 0 ? {} : { sequences: entry.sequences },
|
|
867
1767
|
atlasUrl: entry.atlasUrl,
|
|
868
1768
|
manifestUrl: entry.manifestUrl
|
|
869
1769
|
};
|
|
@@ -999,7 +1899,7 @@ var PetService = class extends Service {
|
|
|
999
1899
|
const transition = projectOfficialEvent(event, runtime);
|
|
1000
1900
|
if (transition === void 0) return;
|
|
1001
1901
|
runtime.officialEventsSeen = true;
|
|
1002
|
-
this.applyActivity(session, transition.input);
|
|
1902
|
+
this.applyActivity(session, transition.input, transition.whisper);
|
|
1003
1903
|
if (transition.completedTurn !== void 0) this.rewardTurn(String(session.id), transition.completedTurn);
|
|
1004
1904
|
}), this.ctx.on("session/disposed", (session) => {
|
|
1005
1905
|
this.ledger.forgetSession(String(session.id));
|
|
@@ -1036,9 +1936,13 @@ var PetService = class extends Service {
|
|
|
1036
1936
|
* the session becomes the host-global display session (most recent
|
|
1037
1937
|
* meaningful event wins the sprite animation).
|
|
1038
1938
|
*/
|
|
1039
|
-
applyActivity(session, input) {
|
|
1939
|
+
applyActivity(session, input, whisper) {
|
|
1040
1940
|
const activity = this.activityOf(session);
|
|
1041
1941
|
activity.lastInput = input;
|
|
1942
|
+
if (whisper !== void 0) activity.whisper = {
|
|
1943
|
+
text: whisper,
|
|
1944
|
+
at: Date.now()
|
|
1945
|
+
};
|
|
1042
1946
|
activity.machine.onActivityStatus(input);
|
|
1043
1947
|
activity.machine.onSessionActive();
|
|
1044
1948
|
this.sessionActivity.delete(session);
|
|
@@ -1164,12 +2068,15 @@ var PetService = class extends Service {
|
|
|
1164
2068
|
phase: perSession.phase
|
|
1165
2069
|
});
|
|
1166
2070
|
}
|
|
2071
|
+
const whisper = (this.displaySession === void 0 ? void 0 : this.sessionActivity.get(this.displaySession))?.whisper;
|
|
2072
|
+
const freshWhisper = whisper !== void 0 && Date.now() - whisper.at < 8e3 ? whisper.text : void 0;
|
|
1167
2073
|
return {
|
|
1168
2074
|
animation: snapshot.animation,
|
|
1169
2075
|
...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
|
|
1170
2076
|
phase: snapshot.phase,
|
|
1171
2077
|
sessionActive: snapshot.sessionActive,
|
|
1172
2078
|
sessions,
|
|
2079
|
+
...freshWhisper === void 0 ? {} : { whisper: freshWhisper },
|
|
1173
2080
|
affinity: this.ledger.affinityView(Date.now()),
|
|
1174
2081
|
display: { ...this.ledger.snapshot.display },
|
|
1175
2082
|
pet: {
|
|
@@ -1191,6 +2098,51 @@ var PetService = class extends Service {
|
|
|
1191
2098
|
}
|
|
1192
2099
|
};
|
|
1193
2100
|
//#endregion
|
|
2101
|
+
//#region src/loopback.ts
|
|
2102
|
+
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
2103
|
+
function isIPv4Loopback(v4) {
|
|
2104
|
+
const parts = v4.split(".");
|
|
2105
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
2106
|
+
}
|
|
2107
|
+
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
2108
|
+
function isLoopbackAddress(address) {
|
|
2109
|
+
if (address === void 0) return false;
|
|
2110
|
+
const normalized = address.toLowerCase();
|
|
2111
|
+
if (normalized === "::1") return true;
|
|
2112
|
+
if (normalized.startsWith("::ffff:")) return isIPv4Loopback(normalized.slice(7));
|
|
2113
|
+
return isIPv4Loopback(normalized);
|
|
2114
|
+
}
|
|
2115
|
+
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
2116
|
+
function isLoopbackHostname(hostname) {
|
|
2117
|
+
if (hostname === "localhost" || hostname === "[::1]") return true;
|
|
2118
|
+
return isIPv4Loopback(hostname);
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Request-level trust fence: a loopback socket address AND a loopback Host
|
|
2122
|
+
* header, plus browser same-origin markers. The socket address is
|
|
2123
|
+
* authoritative; X-Forwarded-For is never trusted.
|
|
2124
|
+
*/
|
|
2125
|
+
function isLoopbackRequest(request) {
|
|
2126
|
+
if (!isLoopbackAddress(request.socket.remoteAddress)) return false;
|
|
2127
|
+
const host = request.headers.host;
|
|
2128
|
+
if (typeof host !== "string") return false;
|
|
2129
|
+
let hostUrl;
|
|
2130
|
+
try {
|
|
2131
|
+
hostUrl = new URL("http://" + host);
|
|
2132
|
+
} catch {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
if (!isLoopbackHostname(hostUrl.hostname)) return false;
|
|
2136
|
+
if (request.headers["sec-fetch-site"] === "cross-site") return false;
|
|
2137
|
+
const origin = request.headers.origin;
|
|
2138
|
+
if (origin === void 0) return true;
|
|
2139
|
+
try {
|
|
2140
|
+
return new URL(origin).host === hostUrl.host;
|
|
2141
|
+
} catch {
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
//#endregion
|
|
1194
2146
|
//#region src/routes.ts
|
|
1195
2147
|
/**
|
|
1196
2148
|
* Pet HTTP routes — the browser half talks to the host through plain
|
|
@@ -1265,12 +2217,22 @@ function readJsonBody(req) {
|
|
|
1265
2217
|
req.on("error", reject);
|
|
1266
2218
|
});
|
|
1267
2219
|
}
|
|
2220
|
+
/** Shared route fence: the browser UI is a loopback client; LAN hosts stay out. */
|
|
2221
|
+
function guard(req, res) {
|
|
2222
|
+
if (isLoopbackRequest(req)) return true;
|
|
2223
|
+
json(res, 403, {
|
|
2224
|
+
ok: false,
|
|
2225
|
+
error: "forbidden: loopback-only"
|
|
2226
|
+
});
|
|
2227
|
+
return false;
|
|
2228
|
+
}
|
|
1268
2229
|
/** Wrap one async service call as a GET JSON route. */
|
|
1269
2230
|
function getRoute(path, run) {
|
|
1270
2231
|
return {
|
|
1271
2232
|
kind: "exact",
|
|
1272
2233
|
path,
|
|
1273
2234
|
handler: (req, res) => {
|
|
2235
|
+
if (!guard(req, res)) return;
|
|
1274
2236
|
if (!requireMethod(req, res, "GET")) return;
|
|
1275
2237
|
run().then((value) => json(res, 200, value), (error) => {
|
|
1276
2238
|
json(res, 500, {
|
|
@@ -1287,6 +2249,7 @@ function postRoute(path, run) {
|
|
|
1287
2249
|
kind: "exact",
|
|
1288
2250
|
path,
|
|
1289
2251
|
handler: (req, res) => {
|
|
2252
|
+
if (!guard(req, res)) return Promise.resolve();
|
|
1290
2253
|
if (!requireMethod(req, res, "POST")) return Promise.resolve();
|
|
1291
2254
|
return readJsonBody(req).then((body) => {
|
|
1292
2255
|
return run(typeof body === "object" && body !== null ? body : {}).then((value) => json(res, 200, value), (error) => {
|
|
@@ -1322,6 +2285,7 @@ function dirAliases(registry) {
|
|
|
1322
2285
|
function assetHandler(registry) {
|
|
1323
2286
|
const aliases = dirAliases(registry);
|
|
1324
2287
|
return (req, res) => {
|
|
2288
|
+
if (!guard(req, res)) return;
|
|
1325
2289
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1326
2290
|
res.writeHead(405);
|
|
1327
2291
|
res.end();
|