magic-agent 1.0.0 → 1.0.2
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-CONTRACT.md +298 -0
- package/README.md +35 -0
- package/client-ui/assets/OFL-Caveat.txt +93 -0
- package/client-ui/assets/caveat.woff2 +0 -0
- package/client-ui/index.html +246 -0
- package/client-ui/pc/app.css +5110 -0
- package/client-ui/pc/assets/damiao-mascot.png +0 -0
- package/client-ui/pc/assets/icon.png +0 -0
- package/client-ui/pc/index.html +154 -0
- package/client-ui/pc/js/00-base.js +52 -0
- package/client-ui/pc/js/03-chat-message.js +880 -0
- package/client-ui/pc/js/04-task-list.js +430 -0
- package/client-ui/pc/js/05-task-list-render.js +812 -0
- package/client-ui/pc/js/06-ask-answer.js +204 -0
- package/client-ui/pc/js/07-chat-stream-render.js +493 -0
- package/client-ui/pc/js/08-ask-event-map.js +171 -0
- package/client-ui/pc/js/09-stream-apply.js +367 -0
- package/client-ui/pc/js/10-markdown.js +430 -0
- package/client-ui/pc/js/18-askbox.js +262 -0
- package/client-ui/pc/js/19-attachments.js +679 -0
- package/client-ui/pc/js/29-msg-variants.js +462 -0
- package/client-ui/pc/js/90-glue.js +212 -0
- package/client-ui/pc/js/91-workbench-stubs.js +134 -0
- package/client-ui/pc/js/92-composer.js +406 -0
- package/client-ui/pc/js/93-rail.js +362 -0
- package/client-ui/pc/vendor/highlight.min.js +1262 -0
- package/client-ui/pc/vendor/marked.min.js +6 -0
- package/client-ui/pc/vendor/purify.min.js +3 -0
- package/client-ui/shared/conversation.css +256 -0
- package/client-ui/shared/conversation.js +1225 -0
- package/client-ui/shared/transport-http.js +123 -0
- package/client-ui-mobile/assets/OFL-Caveat.txt +93 -0
- package/client-ui-mobile/assets/caveat.woff2 +0 -0
- package/client-ui-mobile/index.html +173 -0
- package/npm/bin/magic-agent.js +43 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* ---------- 流式事件 → 画面(**唯一一份映射**)----------
|
|
2
|
+
2026-09-17 用户定稿:「派发任务也要走流式,就走对话框提交 1 个方式」「要在任务中看到流式效果」。
|
|
3
|
+
所以对话框提交(bindHomeAsk)与需求派发(qaReqStreamApply)共用这一个开关:
|
|
4
|
+
thinking → 思考区、text → 逐字正文、tool_use/tool_result → 工具卡、result → 权威全文、
|
|
5
|
+
error/end → 收尾行。调用方只管提供 ctx(手柄 + 工具登记表 + 收尾动作)。
|
|
6
|
+
|
|
7
|
+
2026-09-17 追加(委派/子代理显示,对齐 Claude Code 原生 stream 形态):
|
|
8
|
+
· 归一化(qaEventNormalize):assistant/user 的 message.content 块、system 生命周期
|
|
9
|
+
事件、result 别名(result/duration_ms)统一翻译成扁平事件词表;parent_tool_use_id
|
|
10
|
+
随事件带成 `parent` —— 引擎已扁平的事件原样通过,渲染层不写第二份判断;
|
|
11
|
+
· ① tool_use.name 为 Agent/Task(主会话)→ 委派卡(.qa-agent),登记进 ctx.agents;
|
|
12
|
+
· ③ 带 parent 的事件渲染进父委派卡的嵌套区(子工具卡/子正文/子注脚);
|
|
13
|
+
· ② system 生命周期(task_started/progress/notification)驱动父卡状态胶囊;
|
|
14
|
+
父卡缺席 → 降级为 .qa-note 轻量行,不再静默丢弃;
|
|
15
|
+
· ④ tool_result 配不到卡片 → 「未配对结果」单行卡兜底,不再静默丢弃。 */
|
|
16
|
+
/* 2026-09-18 追加(⑤ 提问卡):tool_use.name = AskUserQuestion → 落成**可点选项**的提问卡
|
|
17
|
+
(.qa-ask),不再把 questions/options 的 JSON 摊在普通工具卡的「输入」里;
|
|
18
|
+
用户点选项 = 把答案作为追问发回这一问(见 qaAskPick / qaAskDeliver)。 */
|
|
19
|
+
|
|
20
|
+
/** content 块(string | [{type:'text',text},…] | {text})→ 纯文本 */
|
|
21
|
+
function qaBlockText(c){
|
|
22
|
+
if(c == null) return "";
|
|
23
|
+
if(typeof c === "string") return c;
|
|
24
|
+
if(Array.isArray(c)) return c.map(x=>(x && typeof x === "object") ? (x.text || "") : String(x)).join("");
|
|
25
|
+
if(typeof c === "object") return c.text != null ? String(c.text) : JSON.stringify(c);
|
|
26
|
+
return String(c);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ---------- tool_use 载荷 → 工具调用单元(按 description 分割统计)----------
|
|
30
|
+
2026-09-18 用户:「当工具调用返回的信息中有 description 需要以文本显示出来。调用了多少个
|
|
31
|
+
工具按照出现了 description 进行分割统计」+「分割成显示多次工具调用 中间插入一些 description」。
|
|
32
|
+
一条 tool_use 事件里可能装着不止一次调用 —— 参数是数组、是几份 JSON 拼在一起、或者干脆不是
|
|
33
|
+
JSON(命令行文本里逐条带 description)。原来整份载荷摊在参数摘要里 → 看着是**一次**调用、
|
|
34
|
+
行里写着一大坨 JSON,统计行也只加 1。
|
|
35
|
+
这里把它切成若干「调用单元」:一个单元画一张卡、卡上摘要显示自己的 description 文本,
|
|
36
|
+
统计行按单元数计数 —— 于是「调用了多少个工具」= 出现了几处 description。
|
|
37
|
+
单元形状 { name, desc, args }:desc 可能为空(没有 description 的载荷),此时回落原行为。 */
|
|
38
|
+
function qaJsonSeq(text){
|
|
39
|
+
// 扫出字符串里并列的多份顶层 JSON 值(对象 / 数组)→ [{ obj, text }];扫不动就回落
|
|
40
|
+
const out = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
while(i < text.length){
|
|
43
|
+
while(i < text.length && /[\s,]/.test(text[i])) i++;
|
|
44
|
+
if(text[i] !== "{" && text[i] !== "[") break;
|
|
45
|
+
let depth = 0, j = i, inStr = false, esc = false;
|
|
46
|
+
for(; j < text.length; j++){
|
|
47
|
+
const c = text[j];
|
|
48
|
+
if(inStr){
|
|
49
|
+
if(esc) esc = false;
|
|
50
|
+
else if(c === "\\") esc = true;
|
|
51
|
+
else if(c === '"') inStr = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if(c === '"'){ inStr = true; continue; }
|
|
55
|
+
if(c === "{" || c === "[") depth++;
|
|
56
|
+
else if((c === "}" || c === "]") && --depth === 0){ j++; break; }
|
|
57
|
+
}
|
|
58
|
+
const slice = text.slice(i, j);
|
|
59
|
+
try{ out.push({ obj: JSON.parse(slice), text: slice }); }catch(e){ break; }
|
|
60
|
+
i = j;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function qaDescTexts(text){
|
|
65
|
+
// 兜底:载荷不是 JSON 时,按 `"description": "…"` 出现处取描述文本(出现几次 = 几次调用)
|
|
66
|
+
const out = [];
|
|
67
|
+
const re = /"description"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
68
|
+
let m;
|
|
69
|
+
while((m = re.exec(String(text || "")))) out.push(m[1].replace(/\\(["\\/nrt])/g, "$1"));
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function qaToolUnits(name, argsText){
|
|
73
|
+
const nm = String(name || "tool");
|
|
74
|
+
const raw = String(argsText == null ? "" : argsText);
|
|
75
|
+
const mk = (desc, args, obj)=> ({ name: nm, args: String(args == null ? "" : args),
|
|
76
|
+
desc: String(desc || (obj && (obj.description || obj.subagent_type || obj.command)) || "") });
|
|
77
|
+
const trim = raw.trim();
|
|
78
|
+
if(!trim) return [mk("", raw)];
|
|
79
|
+
let root;
|
|
80
|
+
try{ root = JSON.parse(trim); }catch(e){ root = undefined; }
|
|
81
|
+
if(Array.isArray(root)) return root.map(o=>mk("", JSON.stringify(o, null, 2), o)); // 参数是数组 → 逐个
|
|
82
|
+
if(root && typeof root === "object") return [mk("", trim, root)]; // 单份对象 → 一次调用
|
|
83
|
+
const seq = qaJsonSeq(trim); // JSON 拼接 → 逐个
|
|
84
|
+
if(seq.length > 1) return seq.map(s=>mk("", s.text, s.obj));
|
|
85
|
+
const ds = qaDescTexts(raw); // 非 JSON → 按 description 切
|
|
86
|
+
if(ds.length > 1) return ds.map(d=>mk(d, ""));
|
|
87
|
+
return [mk("", raw)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 原生 stream 形态 → 渲染层扁平事件(返回数组;扁平事件原样单元素通过)。
|
|
91
|
+
* claude 原生:assistant/user 的 content 在 message.content[] 里(tool_use 的参数在
|
|
92
|
+
* input 对象、tool_result 的正文在 content 块里)—— 翻译成 {id,name,text} / {id,text,ok}。 */
|
|
93
|
+
function qaEventNormalize(ev){
|
|
94
|
+
if(!ev || typeof ev !== "object") return [];
|
|
95
|
+
const t = ev.type;
|
|
96
|
+
const parent = ev.parent_tool_use_id || null;
|
|
97
|
+
if(t === "assistant" || t === "user"){
|
|
98
|
+
const blocks = ev.message && Array.isArray(ev.message.content) ? ev.message.content : null;
|
|
99
|
+
if(!blocks) return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
for(const b of blocks){
|
|
102
|
+
if(!b || typeof b !== "object") continue;
|
|
103
|
+
if(b.type === "thinking"){
|
|
104
|
+
const txt = b.thinking || b.text || "";
|
|
105
|
+
if(txt) out.push({ type:"thinking", text:txt, parent });
|
|
106
|
+
}else if(b.type === "text"){
|
|
107
|
+
// user 消息里的 text 块(任务 prompt 等)不上屏:委派卡「任务词」里已有
|
|
108
|
+
if(t === "assistant" && b.text) out.push({ type:"text", text:b.text, parent });
|
|
109
|
+
}else if(b.type === "tool_use"){
|
|
110
|
+
out.push({ type:"tool_use", id:b.id, name:b.name,
|
|
111
|
+
text: b.input != null ? JSON.stringify(b.input, null, 2) : (b.text || ""),
|
|
112
|
+
inputObj: b.input || null, parent });
|
|
113
|
+
}else if(b.type === "tool_result"){
|
|
114
|
+
out.push({ type:"tool_result", id:b.tool_use_id, text:qaBlockText(b.content),
|
|
115
|
+
ok:!b.is_error, parent });
|
|
116
|
+
}
|
|
117
|
+
// 其余块(image / document 等)不进对话流
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
if(t === "system"){
|
|
122
|
+
// ② 生命周期事件原样放行(字段保留),渲染层按 subtype 分发
|
|
123
|
+
return [{ type:"system", subtype:ev.subtype || "", task_id:ev.task_id,
|
|
124
|
+
tool_use_id:ev.tool_use_id, status:ev.status, usage:ev.usage, parent }];
|
|
125
|
+
}
|
|
126
|
+
if(t === "result"){
|
|
127
|
+
// claude 原生 result:正文叫 result、耗时叫 duration_ms —— 补别名对齐现有词表
|
|
128
|
+
const e2 = Object.assign({}, ev);
|
|
129
|
+
if(e2.text == null && e2.result != null) e2.text = String(e2.result);
|
|
130
|
+
if(e2.latency_ms == null && e2.duration_ms != null) e2.latency_ms = e2.duration_ms;
|
|
131
|
+
return [e2];
|
|
132
|
+
}
|
|
133
|
+
return [ev]; // thinking/text/tool_use/tool_result/error/end/start 等扁平事件
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ---------- 提问(AskUserQuestion)解析 ----------
|
|
137
|
+
2026-09-18 用户:「返回了 askuserquestion,需要 ui 在对话框中增加选择显示 而不是显示这个 json」。
|
|
138
|
+
引擎把这一问塞在 tool_use 的参数里,形状**不稳定**(实测三种都出现过):
|
|
139
|
+
① {questions:"[{…}]"} —— questions 是**再编码一次的 JSON 字符串**(真机截图就是这个形状)
|
|
140
|
+
② {questions:[{…}]} —— 正常数组
|
|
141
|
+
③ {question:"…",options:[…]} —— 没有 questions 包裹的单问
|
|
142
|
+
解析失败一律返回 null → 调用方回落普通工具卡(信息不丢,只是不好看)。 */
|
|
143
|
+
function qaIsAskTool(name){
|
|
144
|
+
return /ask[_\s-]?user[_\s-]?question/i.test(String(name || ""));
|
|
145
|
+
}
|
|
146
|
+
function qaAskParse(argsText){
|
|
147
|
+
let o = argsText;
|
|
148
|
+
if(typeof o === "string"){
|
|
149
|
+
const s = o.trim(); if(!s) return null;
|
|
150
|
+
try{ o = JSON.parse(s); }catch(e){ return null; }
|
|
151
|
+
}
|
|
152
|
+
if(!o || typeof o !== "object") return null;
|
|
153
|
+
let qs = o.questions;
|
|
154
|
+
if(typeof qs === "string"){ try{ qs = JSON.parse(qs); }catch(e){ return null; } }
|
|
155
|
+
if(qs && !Array.isArray(qs) && typeof qs === "object") qs = [qs];
|
|
156
|
+
if(!Array.isArray(qs)) qs = (o.question || o.options) ? [o] : []; // ③ 无包裹的单问
|
|
157
|
+
const out = qs.map(q=>{
|
|
158
|
+
if(!q || typeof q !== "object") return null;
|
|
159
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
160
|
+
return {
|
|
161
|
+
header: String(q.header || ""),
|
|
162
|
+
question: String(q.question || ""),
|
|
163
|
+
multiSelect: !!q.multiSelect,
|
|
164
|
+
options: opts.filter(x=>x && typeof x === "object")
|
|
165
|
+
.map(x=>({ label: String(x.label || ""), description: String(x.description || "") }))
|
|
166
|
+
.filter(x=>x.label),
|
|
167
|
+
};
|
|
168
|
+
}).filter(q=>q && (q.question || q.options.length));
|
|
169
|
+
return out.length ? out : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/* ---------- 统一提问事件(magic-agent 2026-09-18 起 `{"type":"ask", …}`)----------
|
|
2
|
+
与 qaAskParse 同一个出口结构(askCard 只认那一种),但**不用猜形状** —— magic-agent 已经把
|
|
3
|
+
两种 wire 形态(tool_use 的三种编码 / SDK 层 control_request)归一化过,且额外带回两样东西:
|
|
4
|
+
· `session_id` —— 追加通道的寻址 id(答案要靠它 `--append` 回同一条会话);
|
|
5
|
+
· `kind` —— question(模型提问)/ permission(工具待授权)。
|
|
6
|
+
两种事件都要认:老版 CLI 只发 tool_use(走 qaAskParse),新版两条都发(tool_use 先到、
|
|
7
|
+
ask 后到 → 由 qaStreamApplyOne 去重,不会画两张卡)。 */
|
|
8
|
+
function qaAskFromEvent(ask){
|
|
9
|
+
if(!ask || typeof ask !== "object") return null;
|
|
10
|
+
if(ask.kind && ask.kind !== "question") return null; // 待授权(permission)暂不走提问卡
|
|
11
|
+
const qs = Array.isArray(ask.questions) ? ask.questions : [];
|
|
12
|
+
const out = qs.map(q=>({
|
|
13
|
+
header: String(q.header || ""),
|
|
14
|
+
question: String(q.text || q.question || ""), // 统一格式用 text;老形状用 question
|
|
15
|
+
multiSelect: !!(q.multi_select || q.multiSelect),
|
|
16
|
+
options: (Array.isArray(q.options) ? q.options : [])
|
|
17
|
+
.map(o=>({ label: String(o.label || ""), description: String(o.description || "") }))
|
|
18
|
+
.filter(o=>o.label),
|
|
19
|
+
})).filter(q=>q.question || q.options.length);
|
|
20
|
+
return out.length ? out : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function qaStreamApply(ev, ctx){
|
|
24
|
+
for(const e of qaEventNormalize(ev)) qaStreamApplyOne(e, ctx);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* 「这一轮里主进程**自动换过模型**」的 rid 集合(2026-09-21)。
|
|
28
|
+
为什么需要记:换完模型主进程会把跑通的那个**升级成链首**(promoteModel),
|
|
29
|
+
但那只在 close 前做 —— 所以 `start` 那一刻链首还是旧的。到 `end` 时再重取一次链,
|
|
30
|
+
「自动」档显示的名字才会跟上新链首(否则首页那枚下拉还写着那个已经跑不通的模型)。
|
|
31
|
+
用 Set 按 rid 记(不是全局布尔):同一刻可能有多条会话在跑,布尔会被互相踩掉。 */
|
|
32
|
+
const qaModelSwitched = new Set();
|
|
33
|
+
|
|
34
|
+
function qaStreamApplyOne(ev, ctx){
|
|
35
|
+
const h = ctx && ctx.h; if(!h || !ev) return;
|
|
36
|
+
const agents = ctx.agents || (ctx.agents = new Map()); // tool_use_id → 委派卡(①③②共用)
|
|
37
|
+
// parent 双拼写兼容:归一化事件带 `parent`;引擎自己扁平化但保留原字段的事件带 parent_tool_use_id
|
|
38
|
+
const evParent = ev.parent != null ? String(ev.parent)
|
|
39
|
+
: (ev.parent_tool_use_id != null ? String(ev.parent_tool_use_id) : null);
|
|
40
|
+
const agOf = (id)=> id != null ? agents.get(id) : null;
|
|
41
|
+
const finishTools = (ok)=>{ // 流结束/失败时兜底:还在转圈的工具卡一律收账
|
|
42
|
+
ctx.tools.forEach(tc=>{ if(tc.state === "running") tc.done("", ok); });
|
|
43
|
+
ctx.tools.clear();
|
|
44
|
+
agents.forEach(a=>{ // 漏登记的委派卡也落定胶囊(正常路径 done 已处理)
|
|
45
|
+
if(a.state === "running" && typeof a.notify === "function") a.notify("已结束", ok);
|
|
46
|
+
});
|
|
47
|
+
if(typeof h.endTools === "function") h.endTools(); // 收工具调用统计行
|
|
48
|
+
};
|
|
49
|
+
switch(ev.type){
|
|
50
|
+
/* 一轮开始:主进程把**这一轮真正会用的模型**带回来了。三件事都要做(2026-09-21):
|
|
51
|
+
① 「生成中」胶囊写成实际模型 —— 配置里的模型链会自动降级(前一个没额度就换下一个),
|
|
52
|
+
换完还写着原来那个就是「界面说 A、实际跑 B」;
|
|
53
|
+
② 把这个实际模型**记回这条任务**(`qaTaskSetModel`)→ 任务对话那枚下拉的文案与
|
|
54
|
+
选中项当场变成实际在跑的那个(用户要的「自动切换模型的时候 对话框下拉的选中
|
|
55
|
+
模型显示也要变」),下次追问也不会先试那个死掉的;
|
|
56
|
+
③ 若用户**手选过**模型(`qaModelPick` 非空)而它正是被换掉的那个 → 一并改掉
|
|
57
|
+
(别再拿一个刚被证明跑不通的模型当「新建任务默认」);
|
|
58
|
+
`ev.attempt > 0` = 这是换过模型之后的那一轮(主进程只在降级重试时才带它)。
|
|
59
|
+
取不到出口(旧手柄 / 没有 taskId)就只做能做的,不抛。 */
|
|
60
|
+
case "start": {
|
|
61
|
+
const eng = String(ev.engine || qaEngine || "");
|
|
62
|
+
const mdl = String(ev.model || "");
|
|
63
|
+
const retry = Number(ev.attempt || 0) > 0;
|
|
64
|
+
if(typeof h.setRunLabel === "function"){
|
|
65
|
+
h.setRunLabel(`${eng}${mdl ? " · " + qaModelName(mdl, eng) : ""} · 生成中`);
|
|
66
|
+
}
|
|
67
|
+
/* 头像右边的「引擎 · 模型」小标同一时机跟成实际值(与胶囊同一条口径:
|
|
68
|
+
界面说 A、实际跑 B 不行)。机器侧 `.qa-trace`(审查轮 / 数字分身的活动行)
|
|
69
|
+
是 qaTraceToLeft 另建的节点,句柄管不到它 —— 走 `__traceWrap` 直接改文字。 */
|
|
70
|
+
if(typeof h.setWho === "function") h.setWho(eng, mdl);
|
|
71
|
+
const tw = (h.el && h.el.__traceWrap) ? h.el.__traceWrap.querySelector(".qa-who") : null;
|
|
72
|
+
if(tw) tw.textContent = qaWhoLabel(eng, mdl);
|
|
73
|
+
if(mdl && retry) qaModelSwitched.add(ev.rid); // 记一笔:这轮换过模型(end 时重取链,见 Set 注释)
|
|
74
|
+
if(mdl && ctx && ctx.taskId && typeof qaTaskSetModel === "function") qaTaskSetModel(ctx.taskId, mdl);
|
|
75
|
+
if(mdl && retry && qaModelPick && qaModelPick !== mdl && typeof qaModelSet === "function"){
|
|
76
|
+
qaModelSet(mdl, "home2", null); // 手选的那个跑不通 → 换成实际跑通的(首页下拉跟着变)
|
|
77
|
+
}
|
|
78
|
+
/* ⚠️ 降级**落到「引擎默认」**(不带 -m)时也要如实回写(2026-09-21):
|
|
79
|
+
主进程的候选链在链尾多一格「引擎默认」(见 desktop/chat-model.cjs::allowEngineDefault)——
|
|
80
|
+
那一格跑通时 `model` 就是空串。空串是**合法态**,但上面几条 `mdl &&` 守卫会整体跳过,
|
|
81
|
+
于是任务卡与下拉会继续写着那个已经跑不通的模型(实测:openclaw 指定模型 → 401 认证失败
|
|
82
|
+
→ 已改用引擎默认答完,界面却还显示那个模型)。这里把它清成「自动」= 与实际相符。 */
|
|
83
|
+
if(retry && !mdl){
|
|
84
|
+
qaModelSwitched.add(ev.rid); // 同样要记一笔:end 时重取链,下拉跟上新状态
|
|
85
|
+
if(ctx && ctx.taskId && typeof qaTaskSetModel === "function") qaTaskSetModel(ctx.taskId, "");
|
|
86
|
+
if(qaModelPick && typeof qaModelSet === "function") qaModelSet("", "home2", null);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "thinking":
|
|
91
|
+
// ③ 子代理思考 → 父卡嵌套区注脚;主会话思考 → 思考区
|
|
92
|
+
if(evParent && agOf(evParent)) agOf(evParent).subNote(ev.text || "");
|
|
93
|
+
else {
|
|
94
|
+
h.appendThinking(ev.text || "");
|
|
95
|
+
/* 过程步骤(2026-09-23):主会话思考也记进台账 —— 收尾时随消息一起落库,
|
|
96
|
+
历史重画时按 kind 分流成折叠块(见 27-conv-log.js)。只记**主会话**:
|
|
97
|
+
与 run.msgText 同一口径,子代理内部的过程属于委派卡,不摊进对话流。 */
|
|
98
|
+
if(ctx && typeof ctx.onStep === "function") ctx.onStep({ type: "thinking", text: String(ev.text || "") });
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "text":
|
|
102
|
+
// ③ 子代理正文 → 父卡嵌套区;主会话正文 → 逐字正文
|
|
103
|
+
if(evParent && agOf(evParent)) agOf(evParent).subText(ev.text || "");
|
|
104
|
+
else {
|
|
105
|
+
h.appendText(ev.text || "");
|
|
106
|
+
/* 消息台账(2026-09-22):主会话正文逐段累加进 run.msgText —— 收尾时它就是要落进
|
|
107
|
+
消息文件的那条「引擎说了什么」(持久层只存消息,不存 HTML,见 27-conv-log.js)。
|
|
108
|
+
⚠️ 只累计**主会话**正文:子代理(委派卡)里的话不属于这条对话的正文。 */
|
|
109
|
+
if(ctx && typeof ctx.onText === "function") ctx.onText(ev.text || "");
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
/* ⑤ 统一提问事件(magic-agent 2026-09-18 起):卡面直接由它渲染 —— 形状稳定,不必再猜
|
|
113
|
+
tool_use 参数里的三种形态;它还把 session_id 带回来了(追加通道的寻址 id,见 qaAskSend)。 */
|
|
114
|
+
case "ask": {
|
|
115
|
+
const sid = ev.session_id ? String(ev.session_id) : "";
|
|
116
|
+
/* 顺手把会话 id 绑到会话上:它来自 init 行,比 result 行早得多 —— 这一轮就算中途被打断,
|
|
117
|
+
会话锚点也不会丢(README「会话 id 闭环」那条要求)。 */
|
|
118
|
+
if(sid && typeof ctx.onSession === "function") ctx.onSession(sid);
|
|
119
|
+
const tid = (ev.ask && ev.ask.tool_use_id) ? String(ev.ask.tool_use_id) : "";
|
|
120
|
+
/* 同一问会走两条路各报一次(tool_use 事件先到、这条后到)→ 已经画过卡的只**补寻址信息**,
|
|
121
|
+
不再画第二张。补的是卡上的 dataset.sid/rid:快照恢复后照样答得回去(见 askCard)。 */
|
|
122
|
+
const prev = tid ? ctx.tools.get(tid) : null;
|
|
123
|
+
if(prev && typeof prev.bindAsk === "function"){
|
|
124
|
+
prev.bindAsk({ sid, rid: ctx.rid });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const qs = qaAskFromEvent(ev.ask);
|
|
128
|
+
if(qs && typeof h.askCard === "function"){
|
|
129
|
+
const tc = h.askCard((ev.ask && ev.ask.tool_name) || "AskUserQuestion", qs,
|
|
130
|
+
{ title: ev.text || "", sid, rid: ctx.rid });
|
|
131
|
+
if(tid) ctx.tools.set(tid, tc);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "tool_call":
|
|
136
|
+
case "tool_use": {
|
|
137
|
+
// magic-agent 实测事件名 tool_use(旧约定 tool_call):载荷 { id, name, text(参数JSON) }
|
|
138
|
+
/* ⚠️ 这里**不再**预先 h.beginTools()(2026-09-18):带 description 的调用走独立卡、不进组,
|
|
139
|
+
组头改由第一张**无 description** 的卡到达时点亮(见 toolCall)—— 预先点亮会让
|
|
140
|
+
「这一轮工具全带 description」时留下一个「已调用工具 · 0个」的空组。 */
|
|
141
|
+
const tid = ev.tid != null ? ev.tid : ev.id;
|
|
142
|
+
const argsText = ev.text || ev.args || "";
|
|
143
|
+
const ag = agOf(evParent);
|
|
144
|
+
/* 提问卡判定:qaAskParse 只跑一次(形状不稳、解析不便宜,下面还要用结果) */
|
|
145
|
+
const askQs = (typeof h.askCard === "function" && qaIsAskTool(ev.name)) ? qaAskParse(argsText) : null;
|
|
146
|
+
if(/^agent$|^task$/i.test(String(ev.name || "")) && !ag){
|
|
147
|
+
// ① 主会话委派:专属卡(任务词 + 状态胶囊 + 嵌套区),按 id 登记
|
|
148
|
+
let o = ev.inputObj || null;
|
|
149
|
+
if(!o && argsText){ try { o = JSON.parse(argsText); } catch(e2){ /* 非 JSON 参数只显示原文 */ } }
|
|
150
|
+
const desc = (o && (o.description || o.subagent_type)) || ev.preview || "";
|
|
151
|
+
const prompt = (o && (o.prompt || o.task || o.promptText)) || "";
|
|
152
|
+
const card = typeof h.agentCall === "function"
|
|
153
|
+
? h.agentCall(ev.name || "Agent", String(desc || ""), argsText, String(prompt))
|
|
154
|
+
: h.toolCall(ev.name || "Agent", ev.preview || "", argsText); // 旧手柄无 agentCall → 退普通卡
|
|
155
|
+
if(tid != null){ ctx.tools.set(tid, card); agents.set(String(tid), card); }
|
|
156
|
+
}else if(ag && typeof ag.subTool === "function"){
|
|
157
|
+
// ③ 子代理内部工具(含子代理再委派)→ 父卡嵌套区,结果仍按 id 配对
|
|
158
|
+
// 参数里有 description 就把它当摘要显示(人话优先于参数 JSON);嵌套区不按它拆卡
|
|
159
|
+
const u = qaToolUnits(ev.name, argsText)[0];
|
|
160
|
+
const tc = ag.subTool(u.name, u.desc || ev.preview || "", u.args, !!u.desc);
|
|
161
|
+
if(tid != null) ctx.tools.set(tid, tc);
|
|
162
|
+
}else if(typeof h.askCard === "function" && qaIsAskTool(ev.name) && askQs){
|
|
163
|
+
/* ⑤ 提问(AskUserQuestion)→ 可点选的提问卡,而不是把 questions JSON 摊在工具卡里
|
|
164
|
+
(2026-09-18 用户:「需要 ui 在对话框中增加选择显示 而不是显示这个 json」)。
|
|
165
|
+
⚠️ 解析失败(qaAskParse → null)时不走这里 → 回落下面的普通工具卡:宁可不好看,
|
|
166
|
+
也不能把引擎问的话丢掉。
|
|
167
|
+
⚠️ 新版 magic-agent 会为同一问**再发一条统一 ask 事件**(带 session_id)→ 那条到达时
|
|
168
|
+
只给这张卡补 sid/rid,不再画第二张(见上面 case "ask" 的去重)。 */
|
|
169
|
+
const tc = h.askCard(ev.name || "AskUserQuestion", askQs, { title: argsText });
|
|
170
|
+
if(tid != null) ctx.tools.set(tid, tc);
|
|
171
|
+
}else{
|
|
172
|
+
/* 普通工具:按 qaToolUnits 切分 —— 一条事件里装了几次调用就画几张卡(中间就是各自的
|
|
173
|
+
description 文本)。切出来的单元按**有没有 description** 分流(见 toolCall):
|
|
174
|
+
带 description 的独立显示在正文流里,不带的归进折叠组 —— 组计数因此只数组内卡。
|
|
175
|
+
⚠️ 结果回执(tool_result)只带一个 id,配不齐多张卡 → 只把 id 挂给第一张,
|
|
176
|
+
其余由流收尾(finishTools)兜底落定,不会留下常驻转圈。 */
|
|
177
|
+
/* 参数对象:人话标题与「文件变更」推导都要用(ev.inputObj 优先,其次自己解一次 JSON)。
|
|
178
|
+
解不动 = 非 JSON 参数(命令行文本等)→ 保持 null,回落原来的参数摘要在行里。 */
|
|
179
|
+
let argsObj = ev.inputObj || null;
|
|
180
|
+
if(!argsObj && argsText){ try{ argsObj = JSON.parse(String(argsText).trim()); }catch(e2){} }
|
|
181
|
+
const units = qaToolUnits(ev.name, argsText);
|
|
182
|
+
units.forEach((u, i)=>{
|
|
183
|
+
/* 人话标题(2026-09-28,对齐 anywhere 的 timelineToolTitle):行里不再顶
|
|
184
|
+
「内部工具名 + 参数 JSON」,改写「运行了 <命令>」「编辑 <文件>」「搜索了 <词>」
|
|
185
|
+
这类句子;原始名与参数降级进行 title 与明细「输入」段,排障照旧拿得到。
|
|
186
|
+
多单元(一条事件装多次调用)时每个单元按**自己的**参数生成标题 ——
|
|
187
|
+
否则三张卡会写着同一句话。 */
|
|
188
|
+
let uo = argsObj;
|
|
189
|
+
if(units.length > 1 && u.args){ try{ uo = JSON.parse(String(u.args).trim()); }catch(e3){ uo = null; } }
|
|
190
|
+
/* 形态:本地判定与服务端归一化层给的 `tool_kind` 合并(见 qaToolKindMerge)——
|
|
191
|
+
两个界面由此共用一份词表,本地名字表没见过的引擎也不至于掉进通用卡。 */
|
|
192
|
+
const uKind = qaToolKindMerge(u.name, uo, ev.tool_kind);
|
|
193
|
+
const tc = h.toolCall(u.name, u.desc || ev.preview || "", u.args, !!u.desc,
|
|
194
|
+
{ kind: uKind, title: qaToolHumanTitle(u.name, uo, u.args || argsText) });
|
|
195
|
+
/* 文件变更(2026-09-28):magic-agent 不发 file_change 事件,但**改动就在参数里**
|
|
196
|
+
(Edit 的 old/new_string、MultiEdit 的 edits[]、Write 的 content)——
|
|
197
|
+
从参数推导一条「文件变更行 + Diff」贴进明细,用户不必再读参数 JSON 才知道改了哪几行。
|
|
198
|
+
⚠️ 推导的 diff 没有上下文行与行号(见 qaDerivedDiffRows),所以不画 hunk、不编行号。 */
|
|
199
|
+
const fc = uKind === "file_change" ? qaFileChangeOf(u.name, uo) : null;
|
|
200
|
+
if(fc && tc && typeof tc.addSection === "function") tc.addSection(qaFileChangeNode(fc));
|
|
201
|
+
if(tid != null && i === 0) ctx.tools.set(tid, tc);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/* 过程步骤(2026-09-23):无论哪个分支,工具调用本身都记进台账(收尾时落库)。
|
|
205
|
+
同一问的重复事件在上面 bindAsk 处已提前 break,不会记两遍;tid 随条目带上,
|
|
206
|
+
tool_result 到达时按它配对补结果(见 bindHomeAsk 的 onStep)。 */
|
|
207
|
+
if(ctx && typeof ctx.onStep === "function")
|
|
208
|
+
ctx.onStep({ type: "tool", name: String(ev.name || "tool"), args: String(argsText || ""), tid: tid != null ? String(tid) : "" });
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "tool_result": {
|
|
212
|
+
// 期望载荷: { tid|id, text(结果串,可缺省), ok(布尔,缺省 true) }
|
|
213
|
+
const tid = ev.tid != null ? ev.tid : ev.id;
|
|
214
|
+
const tc = tid != null ? ctx.tools.get(tid) : null;
|
|
215
|
+
if(tc) tc.done(ev.text || "", ev.ok !== false);
|
|
216
|
+
else if(evParent && agOf(evParent) && typeof agOf(evParent).subNote === "function")
|
|
217
|
+
agOf(evParent).subNote("结果回执:" + String(ev.text || "").slice(0, 400)); // ③ 无卡可填 → 父卡注脚
|
|
218
|
+
else if(typeof h.marker === "function"){
|
|
219
|
+
/* ④ 未配对结果 → **单行标记行** + 可展开明细(2026-09-28)。
|
|
220
|
+
原来是「未配对结果」工具卡:一张带「输入 / 输出」两段的卡,而输入永远是空的 ——
|
|
221
|
+
形态比内容还重。对齐 anywhere:配不到卡片的结果落成一条 marker,
|
|
222
|
+
正文留在可展开的明细里,且状态用徽章说清(不再只有一个「叉」)。
|
|
223
|
+
⚠️ 仍然**不丢内容**:这条路径存在的意义就是「配不上对也不静默丢弃」。 */
|
|
224
|
+
const fail = ev.ok === false;
|
|
225
|
+
h.marker({
|
|
226
|
+
icon: QA_V_ICONS.terminal,
|
|
227
|
+
title: "未配对结果" + (tid != null && tid !== "" ? " · " + String(tid).slice(0, 14) : ""),
|
|
228
|
+
status: fail ? "failed" : "completed",
|
|
229
|
+
destructive: fail,
|
|
230
|
+
detail: ev.text ? String(ev.text) : null,
|
|
231
|
+
detailLabel: "output",
|
|
232
|
+
hint: "这条结果配不到对应的工具卡(工具事件缺 id,或结果晚于收尾到达)",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/* 过程步骤(2026-09-23):结果也进台账 —— 配不上对的由 onStep 内部兜成独立条目 */
|
|
236
|
+
if(ctx && typeof ctx.onStep === "function")
|
|
237
|
+
ctx.onStep({ type: "tool_result", tid: tid != null ? String(tid) : "", text: String(ev.text || ""), ok: ev.ok !== false });
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "system": {
|
|
241
|
+
/* ② 生命周期:task_started / task_progress / task_notification → 驱动父委派卡胶囊。
|
|
242
|
+
父卡缺席、以及**其余 subtype** → 单行标记行(2026-09-28)。
|
|
243
|
+
原来是 h.note 打一行 10px 灰字:事件里的 task_id / usage / tool_use_id / status
|
|
244
|
+
这些排障要的字段**全丢**,只留下「系统 · xxx」三个字。
|
|
245
|
+
现在统一成 anywhere 的 Marker 形态:图标 + 人话标题 + 状态徽章 + 可展开的原始载荷。 */
|
|
246
|
+
const st = String(ev.subtype || "");
|
|
247
|
+
const ag = ev.tool_use_id != null ? agents.get(String(ev.tool_use_id)) : null;
|
|
248
|
+
const sysDetail = (label)=>({
|
|
249
|
+
kind: "system", subtype: st, label: label,
|
|
250
|
+
task_id: ev.task_id, tool_use_id: ev.tool_use_id, status: ev.status, usage: ev.usage,
|
|
251
|
+
});
|
|
252
|
+
const sysMarker = (title, opts)=> (typeof h.marker === "function")
|
|
253
|
+
? h.marker(Object.assign({ icon: QA_V_ICONS.clock, title: title, detail: sysDetail(title) }, opts || {}))
|
|
254
|
+
: (typeof h.note === "function" ? h.note(title) : null);
|
|
255
|
+
/* ④ 上下文压缩边界 → **分隔条**(anywhere 的 `compact` kind 就是这一格,2026-09-28)。
|
|
256
|
+
为什么走分隔条而不是标记行:压缩是**横切整条会话**的分界,不是「某件事发生了」,
|
|
257
|
+
左右细线夹一行字正好说清「上下文在这儿被截断重排过」—— 后续回答忽然「忘了前面」时,
|
|
258
|
+
用户得有这条凭据。
|
|
259
|
+
⚠️ 触发面:magic-agent 自己**不转发** system 事件(它只读 result / system:init 的 subtype),
|
|
260
|
+
所以这条路实际只会在**被测引擎 / 原生流**上到达(claude 自动压缩时发的 system 事件)。
|
|
261
|
+
⚠️ 子类型名用 `/compact/i` 宽松匹配:本仓库没有固化它的 fixture,**不硬编**一个具体
|
|
262
|
+
字符串(编错了的表现是静默不触发,比不写还糟)。
|
|
263
|
+
⚠️ 「进行中 / 已完成」按**事件自己的字段**判(subtype 里的字样 + status),不猜。 */
|
|
264
|
+
if(/compact/i.test(st)){
|
|
265
|
+
const active = /start|begin|progress/i.test(st)
|
|
266
|
+
|| ev.status === "running" || ev.status === "pending";
|
|
267
|
+
if(typeof h.sep === "function"){
|
|
268
|
+
h.sep(active ? "会话上下文压缩中" : "会话上下文已压缩", {
|
|
269
|
+
title: "上下文压缩边界(system/" + st + ")—— 之后的内容可能不再包含更早的上下文",
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if(st === "task_started"){
|
|
275
|
+
if(ag && typeof ag.start === "function") ag.start();
|
|
276
|
+
else sysMarker("子代理已启动" + (ev.task_id ? " · " + String(ev.task_id).slice(0, 8) : ""), { status: "running" });
|
|
277
|
+
}else if(st === "task_progress"){
|
|
278
|
+
const n = ev.usage && ev.usage.tool_uses;
|
|
279
|
+
const label = "子代理" + (n != null ? ` · ${n} 次工具调用` : " · 进行中");
|
|
280
|
+
if(ag && typeof ag.progress === "function") ag.progress(label);
|
|
281
|
+
else sysMarker(label, { status: "running" });
|
|
282
|
+
}else if(st === "task_notification"){
|
|
283
|
+
const u = ev.usage || {};
|
|
284
|
+
const dur = u.duration_ms != null ? (u.duration_ms / 1000).toFixed(1) + "s" : "";
|
|
285
|
+
const label = (ev.status === "completed" ? "完成" : String(ev.status || "已结束"))
|
|
286
|
+
+ (dur ? ` · ${dur}` : "") + (u.tool_uses != null ? ` · ${u.tool_uses} 步` : "");
|
|
287
|
+
if(ag && typeof ag.notify === "function") ag.notify(label, ev.status === "completed");
|
|
288
|
+
else sysMarker("子代理" + label, { status: String(ev.status || "completed") });
|
|
289
|
+
}else{
|
|
290
|
+
sysMarker("系统 · " + (st || ev.status || "事件"), { status: ev.status || "" });
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "result":
|
|
295
|
+
// result 行是权威全文:以它覆盖正文(流式增量可能丢字符),并按 markdown 完整解析
|
|
296
|
+
if(ev.text) h.setResult(ev.text);
|
|
297
|
+
if(ev.latency_ms != null) ctx.setMs(ev.latency_ms);
|
|
298
|
+
// 会话 id 写回任务(追问续接同一会话);sessionId / session_id 双拼写兼容
|
|
299
|
+
if(ev.sessionId || ev.session_id) ctx.onSession(ev.sessionId || ev.session_id);
|
|
300
|
+
break;
|
|
301
|
+
case "error":
|
|
302
|
+
/* ⚠️ 失败那轮**也可能带着会话 id**(2026-09-23):主进程把「引擎一个字都没说」的
|
|
303
|
+
turn_end 改判成失败时(见 main.js 的空回答兜底),会话 id 是随 error 一起回来的 ——
|
|
304
|
+
绑上它,下一轮追问仍接同一条会话(不然会退化成 `-c` 续接最近一次会话)。
|
|
305
|
+
与 result / turn_end 两条一样,sessionId / session_id 双拼写兼容。 */
|
|
306
|
+
if(ev.sessionId || ev.session_id) ctx.onSession(ev.sessionId || ev.session_id);
|
|
307
|
+
h.endThinking(); finishTools(false);
|
|
308
|
+
qaRunEnd(h);
|
|
309
|
+
ctx.onError(ev);
|
|
310
|
+
break;
|
|
311
|
+
/* 过程提示(**不是**终态事件):主进程用它说「这一轮正在自动换模型重试」这类事
|
|
312
|
+
(2026-09-21 起对话侧的模型来自配置界面的降级链,前一个没额度就自动换下一个)。
|
|
313
|
+
落成一行轻量注脚(h.note,与 system 事件同一处渲染),**不收尾**——
|
|
314
|
+
重试那一轮仍带同一个 rid 继续推事件,画面照常往下走。 */
|
|
315
|
+
case "notice":
|
|
316
|
+
/* 过程提示:落成**标记行** + 可展开的原始载荷(2026-09-28)。
|
|
317
|
+
「这一轮正在自动换模型重试」这类事原来只有一行文字,重试次数 / 原因 / 目标模型
|
|
318
|
+
这些字段看不到 —— 而它们正是用户会追问的(「为什么答得慢了」)。 */
|
|
319
|
+
if(typeof h.marker === "function")
|
|
320
|
+
h.marker({ icon: QA_V_ICONS.clock, title: String(ev.text || ev.notice || "提示"),
|
|
321
|
+
detail: Object.assign({}, ev, { kind: "notice" }), status: ev.status || "" });
|
|
322
|
+
else if(typeof h.note === "function") h.note(String(ev.text || ev.notice || ""));
|
|
323
|
+
break;
|
|
324
|
+
case "end":
|
|
325
|
+
h.endThinking(); finishTools(true);
|
|
326
|
+
qaRunEnd(h);
|
|
327
|
+
/* 这一轮若**自动换过模型** → 此刻重取一次链(2026-09-21):
|
|
328
|
+
主进程在 close 前把跑通的那个升级成了链首,所以只有到这时「自动」档显示的
|
|
329
|
+
名字才是新的。不重取的话,首页那枚下拉会一直写着那个已经跑不通的模型。
|
|
330
|
+
⚠️ 放在 onDone 之前:onDone 会落快照 / 改任务状态,重取链是纯读操作,先做完更稳。 */
|
|
331
|
+
if(qaModelSwitched.has(ev.rid)){
|
|
332
|
+
qaModelSwitched.delete(ev.rid);
|
|
333
|
+
if(typeof qaModelChainLoad === "function") qaModelChainLoad(qaEngine, true);
|
|
334
|
+
}
|
|
335
|
+
if(ev.sessionId) ctx.onSession(ev.sessionId); // result 行缺失时的兜底会话 id
|
|
336
|
+
ctx.onDone(ev);
|
|
337
|
+
break;
|
|
338
|
+
case "turn_end":
|
|
339
|
+
/* 一轮结束(magic-agent 2026-09-18 起;claude/codebuddy 的常驻会话每轮收尾发一次)。
|
|
340
|
+
⚠️ **这一轮到此就完成了**,别等 "end":keep-alive 默认开之后,magic-agent 在最后一轮
|
|
341
|
+
结束后还会留着会话等追加(默认空闲 5m 才收工),"end"(进程 close)要等到那时候才来。
|
|
342
|
+
⚠️ 顺序上 turn_end 在**最终 result 信封之前**(那个信封要等会话结束才输出),所以
|
|
343
|
+
权威正文与会话 id 都得从这条事件上取,不能等 result:
|
|
344
|
+
· ev.text = 该轮权威正文(引擎 result 行的 result)→ setResult 覆盖增量
|
|
345
|
+
· ev.session_id = 该轮会话 id → 绑到任务上(下一轮追问续接同一会话)
|
|
346
|
+
其余收尾动作与 "end" 完全一致(收思考区/工具卡/运行胶囊 + 快照 + 状态落库)。 */
|
|
347
|
+
if(ev.text) h.setResult(ev.text);
|
|
348
|
+
if(ev.session_id) ctx.onSession(ev.session_id);
|
|
349
|
+
h.endThinking(); finishTools(true);
|
|
350
|
+
qaRunEnd(h);
|
|
351
|
+
ctx.onDone({ ok: true, code: 0, text: ev.text || "" });
|
|
352
|
+
break;
|
|
353
|
+
default:
|
|
354
|
+
/* 形态不认识 ≠ 内容丢掉(2026-09-28,对齐 anywhere 的 UnknownTimelineItem)。
|
|
355
|
+
原来是静默 break:引擎上了新事件类型,画面上「什么都没发生」,
|
|
356
|
+
排障只能去翻日志 —— 而事件本身就在手里。
|
|
357
|
+
现在落成一条可展开的标记行,原始载荷完整留在明细里。
|
|
358
|
+
⚠️ 心跳/保活这类**刻意的噪音**除外:每一跳都画一条会让画布全是垃圾。 */
|
|
359
|
+
if(ev.type && !/^(ping|pong|heartbeat|keepalive|open|log)$/i.test(String(ev.type))
|
|
360
|
+
&& typeof h.marker === "function"){
|
|
361
|
+
h.marker({ icon: QA_V_ICONS.clock, title: "未识别事件 · " + String(ev.type),
|
|
362
|
+
detail: ev, hint: "这次的事件类型本版渲染器还没有对应形态,原始载荷见明细" });
|
|
363
|
+
}
|
|
364
|
+
break; // 心跳 / 日志等刻意噪音:忽略
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|