@migor/cli 0.10.7 → 0.10.8
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/CHANGELOG.md +39 -1
- package/dist/inspector/full-view.js +96 -16
- package/dist/inspector/index.js +6 -5
- package/dist/inspector/trace-view.bundle.js +113 -27
- package/dist/inspector/trace-view.css +12 -0
- package/dist/inspector/view.js +12 -8
- package/dist/templates/package.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@
|
|
|
5
5
|
(0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
|
|
6
6
|
决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
|
|
7
7
|
|
|
8
|
+
## [0.10.8] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
|
|
12
|
+
调试面板展开态的**第二档**:字符串里套着的 JSON 也铺开。无破坏性变更、无迁移动作。
|
|
13
|
+
两包同步 0.10.7 → 0.10.8;`@migor/agentia` 此次仍只有版本常量变化(`src/` 未动)。
|
|
14
|
+
|
|
15
|
+
- **上一档解决的是「容器长什么样」,但真实 payload 里最常见的那一层不是容器**:工具把结构
|
|
16
|
+
`JSON.stringify` 后塞回文本字段(`tool.output.content`、MCP 的 text block、SSE 的 `data:`、
|
|
17
|
+
`content[0].text`)。这些值的类型是字符串 ⇒ 走纯文本老路 ⇒ 展开后是一串 `{\"a\":1`
|
|
18
|
+
的反斜杠汤,比 0.10.7 之前那个「一坨」更劝退。
|
|
19
|
+
- **现在这类字符串也按容器铺开**,但**如实标出它原本是字符串**:行内打 `str→json` 标记,
|
|
20
|
+
根级头部写 `string · 232 字符 → object · 3 项`。只写解析后的规模,读者会以为工具真返回了对象。
|
|
21
|
+
- **判据是首尾字符,不是「`JSON.parse` 成功」**:`"123"` / `"null"` / `"true"` 都能 parse,
|
|
22
|
+
把它们显示成数字 / 空值 / 布尔就是**改写工具返回的事实**。trim 后首尾须是 `{`…`}` 或
|
|
23
|
+
`[`…`]`,不吻合连 parse 都不试(普通文本占绝大多数,负路径不付反序列化成本);超 100k 字符
|
|
24
|
+
不解析。
|
|
25
|
+
- **只解析一层**:进了「解析出来的子树」就不再往下解析。一层套一层是真实现象,逐层展开会把
|
|
26
|
+
10KB 放大成十万个节点。
|
|
27
|
+
- **表格里的格子不解析**:同构表已经放弃逐层展开,格子里放不下嵌套块。
|
|
28
|
+
- 字符串根上点「复制」拿到的是**原始字符串**(能直接喂 `jq`),不是重排后的 JSON。
|
|
29
|
+
|
|
30
|
+
### 新增:字符串里的 JSON 也铺开(2026-11-12 ref-trace-json-string)
|
|
31
|
+
|
|
32
|
+
- **判据同源**:新导出 `hasStructuredBody(v)` 与块内共用同一份 `jsonInString`;`view.js` 用它
|
|
33
|
+
取代原先的 `typeof v === 'object'` ⇒「挂不挂块」与「块里画什么」不会分叉。
|
|
34
|
+
`createFullView` 与 `hasStructuredBody` 都进公共导出(bundle 导出面必须与 `src/index.js`
|
|
35
|
+
逐字一致,有 `test/bundle.test.js` 守着)。
|
|
36
|
+
- **宿主侧无需改动**:本次没有新增静态件 —— `full-view.js` / `view.js` / `index.js` 都已在
|
|
37
|
+
CLI inspector 的 STATIC 白名单里,`dist/inspector` 随构建更新即可。
|
|
38
|
+
- **验证**:trace-view 测试 75 → 90(+15);反向验证 6 条各自咬住(摘标记 ⇒ 7 条红;
|
|
39
|
+
去「只解析一层」⇒ 嵌套用例红;接线退回「只认容器」⇒ 字符串出参挂块用例红;去 100k 上限 ⇒
|
|
40
|
+
超长用例红;去首尾先筛 ⇒ parse 计数与谓词两条红;头部只写解析后类型 ⇒ 头部用例红)。
|
|
41
|
+
另记一条**等价变异**的教训:第一版里「parse 结果再判类型」那段代码永远走不到(首尾判据
|
|
42
|
+
已经挡住标量字符串),任何用例都咬不住,已删除 —— 留着只会让下一个人以为那里有守卫。
|
|
43
|
+
决策见 `docs/spec.md` §10 2026-11-12 ref-trace-json-string,守卫登记见 `docs/guards.md` §1。
|
|
44
|
+
|
|
8
45
|
## [0.10.7] - 2026-11-12
|
|
9
46
|
|
|
10
47
|
### 变更
|
|
@@ -4472,7 +4509,8 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
|
|
|
4472
4509
|
首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
|
|
4473
4510
|
框架本体单包;CLI 独立成包(workspaces)。
|
|
4474
4511
|
|
|
4475
|
-
[Unreleased]: https://github.com/migoric/agentia/compare/v0.10.
|
|
4512
|
+
[Unreleased]: https://github.com/migoric/agentia/compare/v0.10.8...HEAD
|
|
4513
|
+
[0.10.8]: https://github.com/migoric/agentia/releases/tag/v0.10.8
|
|
4476
4514
|
[0.10.7]: https://github.com/migoric/agentia/releases/tag/v0.10.7
|
|
4477
4515
|
[0.10.6]: https://github.com/migoric/agentia/releases/tag/v0.10.6
|
|
4478
4516
|
[0.10.5]: https://github.com/migoric/agentia/releases/tag/v0.10.5
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* 吃掉 9 行。同构且各列都是标量时改成表格,表头一行、数据一行。
|
|
17
17
|
* 3. **路径拿不到**:想引用 `results[0].name` 只能用手数层级。每个键悬停出一个 `⧉`,
|
|
18
18
|
* 点一下把路径写进剪贴板(表头写的是 `results[*].name`,因为它是一整列)。
|
|
19
|
+
* 4. **字符串里套着的 JSON 读不了**(2026-11-12):MCP 的 text block、SSE 的 `data:`、把结果
|
|
20
|
+
* 序列化后又塞回 `content` 的工具 —— 这些值的类型是**字符串**,此前只显示成一串
|
|
21
|
+
* `{\"a\":1` 的反斜杠汤,比无缩进的 JSON 更难读。现在首尾是 `{`…`}` / `[`…`]` 且解析成功的
|
|
22
|
+
* 字符串按容器铺开,并在行里打 `str→json` 标记 —— 不标出来,读者会以为工具真的返回了对象。
|
|
19
23
|
*
|
|
20
24
|
* ## 边界(都是「不这么做就会坏」的地方,不是口味)
|
|
21
25
|
*
|
|
@@ -35,6 +39,11 @@
|
|
|
35
39
|
* 会原样复现(这正是 `view.js` 的 `expanded` 集合存在的原因)。
|
|
36
40
|
* - **表格化只在"真的同构"时发生**:元素必须全是普通对象、键集完全一致、列数 1..6、
|
|
37
41
|
* 每列都是标量。任一不满足就回落树形 —— 表格里没法逐层展开,判错一次就是**丢数据**。
|
|
42
|
+
* - **字符串里的 JSON 只解析一层,且必须标出来**:判据是**首尾字符**(`{`…`}` / `[`…`]`),
|
|
43
|
+
* 不是「`JSON.parse` 成功」—— `"123"` / `"null"` / `"true"` 都能 parse 成功,显示成
|
|
44
|
+
* 数字/空值/布尔就是**改写工具返回的事实**。解析结果内部的字符串**不再二次解析**:
|
|
45
|
+
* 一层套一层是业务里的真实现象(SSE data 里再套 JSON),逐层展开会让 10KB 变成十万个节点。
|
|
46
|
+
* 表格单元格也不解析 —— 同构表已经放弃了逐层展开,单元格里放不下嵌套块。
|
|
38
47
|
*
|
|
39
48
|
* 零依赖、纯 DOM 构造(只用 createElement / appendChild / textContent / className /
|
|
40
49
|
* dataset / innerHTML=''),与 view.js 共用同一套 DOM stub 跑测试。
|
|
@@ -50,6 +59,8 @@ const MAX_DEPTH = 6;
|
|
|
50
59
|
const OPEN_DEPTH = 2;
|
|
51
60
|
/** 表格最多几列:再多横向就没法看了,回落树形 */
|
|
52
61
|
const MAX_COLS = 6;
|
|
62
|
+
/** 多长的字符串才值得尝试从里面解析 JSON。再长的话,parse 与随后的渲染成本都不值得 */
|
|
63
|
+
const MAX_JSON_STR = 100_000;
|
|
53
64
|
|
|
54
65
|
function fvEl(tag, cls, text) {
|
|
55
66
|
const node = document.createElement(tag);
|
|
@@ -73,6 +84,18 @@ function isStructured(v) {
|
|
|
73
84
|
return v !== null && typeof v === 'object';
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
/**
|
|
88
|
+
* 有没有「结构化正文」可铺 —— 容器,或者**装着 JSON 的字符串**。
|
|
89
|
+
*
|
|
90
|
+
* 后者是业务里的常态:`tool.output` 的 `content` 常常就是 `JSON.stringify(...)` 的结果
|
|
91
|
+
* (工具把结构序列化后塞回文本字段,MCP 的 text block 更是如此)。此前这种值是字符串 ⇒
|
|
92
|
+
* 走纯文本老路 ⇒ 展开后是一串 `{\"a\":1` 的反斜杠汤。判据与 `jsonInString` 完全同一套,
|
|
93
|
+
* 所以「挂不挂块」与「块里画什么」不会跑偏。
|
|
94
|
+
*/
|
|
95
|
+
export function hasStructuredBody(v) {
|
|
96
|
+
return isStructured(v) || jsonInString(v) !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/** 普通对象(排除数组与 null)—— 表格化与"容器"判定都只要这一种 */
|
|
77
100
|
function isPlainObject(v) {
|
|
78
101
|
return isStructured(v) && !Array.isArray(v);
|
|
@@ -114,6 +137,39 @@ function oneLine(v) {
|
|
|
114
137
|
return `<${kind} · ${sizeOf(v)} 项> ${clipped}`;
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 「字符串里套着 JSON」:返回解析出来的容器,其余(普通文本、标量字符串、坏 JSON)一律 `null`。
|
|
142
|
+
*
|
|
143
|
+
* 判据是**首尾字符**而不是「parse 成功」,这一条是这段代码的全部要害:`"123"`、`"null"`、
|
|
144
|
+
* `"true"` 都能被 `JSON.parse` 吃下去,但把它们显示成数字/空值/布尔,等于把工具返回的
|
|
145
|
+
* 字符串改写成另一种类型 —— 调试面板的第一职责是如实。所以先看 trim 后的首尾一个字符,
|
|
146
|
+
* 不吻合就连 parse 都不试(普通文本占绝大多数,省掉无谓的反序列化)。
|
|
147
|
+
* 截断过的 JSON(尾部被切掉)在这里自然失败,回落成原样字符串。
|
|
148
|
+
*/
|
|
149
|
+
function jsonInString(v) {
|
|
150
|
+
if (typeof v !== 'string' || v.length < 2 || v.length > MAX_JSON_STR) return null;
|
|
151
|
+
const t = v.trim();
|
|
152
|
+
if (t.length < 2) return null;
|
|
153
|
+
const head = t[0];
|
|
154
|
+
const tail = t[t.length - 1];
|
|
155
|
+
if (!((head === '{' && tail === '}') || (head === '[' && tail === ']'))) return null;
|
|
156
|
+
try {
|
|
157
|
+
// 首尾判据已经保证了这里解析出来的必然是对象或数组(`{`…`}` / `[`…`]`),
|
|
158
|
+
// 所以不再重复判类型 —— 那会是一条永远走不到的防御分支(变异测试实测盖不住)。
|
|
159
|
+
return JSON.parse(t);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 「这个字符串里装着 JSON」的标记。**必须显眼**:不标出来,读者会以为工具真的返回了对象。
|
|
166
|
+
* title 顺带交代两件容易误会的事 —— 原文还在(`.tr-io` 与块内原值),路径需先 parse 这一层。 */
|
|
167
|
+
function jsonBadge() {
|
|
168
|
+
const b = fvEl('span', 'tr-full-json', 'str→json');
|
|
169
|
+
b.title = '这个值是字符串,内容是 JSON(已按对象展示;内部路径需先 JSON.parse 这一层)';
|
|
170
|
+
return b;
|
|
171
|
+
}
|
|
172
|
+
|
|
117
173
|
/** 子键的 JSON 路径:对象用 `.key`,数组用 `[i]`。根是空串,所以 `[0]` 不会变成 `.[0]` */
|
|
118
174
|
function pathOf(parent, key, isArr) {
|
|
119
175
|
return isArr ? `${parent}[${key}]` : parent ? `${parent}.${key}` : key;
|
|
@@ -246,8 +302,9 @@ function startRow(depth, caret) {
|
|
|
246
302
|
* @param {any} v 值
|
|
247
303
|
* @param {number} depth 当前缩进层级(根为 0)
|
|
248
304
|
* @param {string} path 当前值的 JSON 路径(根为空串)
|
|
305
|
+
* @param {boolean} [allowParse] 还允许不允从字符串里解析 JSON(进了「解析出来的子树」就为 false)
|
|
249
306
|
*/
|
|
250
|
-
function renderInto(ctx, v, depth, path) {
|
|
307
|
+
function renderInto(ctx, v, depth, path, allowParse = true) {
|
|
251
308
|
const box = ctx.body;
|
|
252
309
|
|
|
253
310
|
if (!isStructured(v)) {
|
|
@@ -272,14 +329,19 @@ function renderInto(ctx, v, depth, path) {
|
|
|
272
329
|
const child = isArr ? v[Number(k)] : v[k];
|
|
273
330
|
const keyText = isArr ? `[${k}]` : `${k}:`;
|
|
274
331
|
const childPath = pathOf(path, k, isArr);
|
|
332
|
+
// 字符串里套着的 JSON(`content: "{\"rows\":[...]}"`)⇒ 按容器铺。`allowParse` 只在
|
|
333
|
+
// 「刚从一个字符串里解析出来」的那棵子树上为 false —— 一层套一层会放大成十万个节点。
|
|
334
|
+
const parsed = allowParse ? jsonInString(child) : null;
|
|
335
|
+
const val = parsed ?? child;
|
|
336
|
+
const fromJson = parsed !== null;
|
|
275
337
|
// 「能否折叠」只看三件事:是容器、非空、没到深度上限 —— 与下面几个分支同一套判据。
|
|
276
338
|
// 先算出来是为了让箭头槽位在起行时就有正确字形(叶子行留空格位,键名才会对齐)。
|
|
277
|
-
const foldable = isStructured(
|
|
339
|
+
const foldable = isStructured(val) && sizeOf(val) > 0 && depth < MAX_DEPTH;
|
|
278
340
|
const open = foldable && isOpen(ctx.fold, childPath, depth);
|
|
279
341
|
const row = startRow(depth, foldable ? (open ? '▾' : '▸') : '');
|
|
280
342
|
|
|
281
343
|
// 叶子:同一行里写完(缩进 + 键名 + 路径 + 值)
|
|
282
|
-
if (!isStructured(
|
|
344
|
+
if (!isStructured(val)) {
|
|
283
345
|
const p = primitive(child);
|
|
284
346
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
285
347
|
row.appendChild(pathBtn(childPath));
|
|
@@ -291,28 +353,31 @@ function renderInto(ctx, v, depth, path) {
|
|
|
291
353
|
// 深度用尽 ⇒ 整体压成一行(并标明它是个容器),避免缩进吃掉全部宽度
|
|
292
354
|
if (depth >= MAX_DEPTH) {
|
|
293
355
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
294
|
-
row.appendChild(
|
|
356
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
357
|
+
row.appendChild(fvEl('span', 'tr-full-null', ' ' + oneLine(val)));
|
|
295
358
|
box.appendChild(row);
|
|
296
359
|
continue;
|
|
297
360
|
}
|
|
298
361
|
|
|
299
|
-
// 空容器:留一对括号,别让它看起来像"展开失败"
|
|
300
|
-
if (sizeOf(
|
|
362
|
+
// 空容器:留一对括号,别让它看起来像"展开失败"(`"{}"` / `"[]"` 这类字符串也走这里)
|
|
363
|
+
if (sizeOf(val) === 0) {
|
|
301
364
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
302
|
-
row.appendChild(
|
|
365
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
366
|
+
row.appendChild(fvEl('span', 'tr-full-null', Array.isArray(val) ? ' []' : ' {}'));
|
|
303
367
|
box.appendChild(row);
|
|
304
368
|
continue;
|
|
305
369
|
}
|
|
306
370
|
|
|
307
371
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
308
372
|
row.appendChild(pathBtn(childPath));
|
|
373
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
309
374
|
if (!open) {
|
|
310
|
-
const kind = Array.isArray(
|
|
311
|
-
row.appendChild(fvEl('span', 'tr-full-hint', ` { ${kind} · ${sizeOf(
|
|
312
|
-
} else if (Array.isArray(
|
|
375
|
+
const kind = Array.isArray(val) ? 'array' : 'object';
|
|
376
|
+
row.appendChild(fvEl('span', 'tr-full-hint', ` { ${kind} · ${sizeOf(val)} 项 }`));
|
|
377
|
+
} else if (Array.isArray(val) && tableColsOf(val)) {
|
|
313
378
|
// 成表时行内不铺子项,只在键名后标一句规模 —— 否则 `▾ targets:` 后面直接跟着表格,
|
|
314
379
|
// 看不出这是个数组、也看不出表格是不是全部内容(是不是被截断了)
|
|
315
|
-
row.appendChild(fvEl('span', 'tr-full-hint', ` array · ${
|
|
380
|
+
row.appendChild(fvEl('span', 'tr-full-hint', ` array · ${val.length} 项`));
|
|
316
381
|
}
|
|
317
382
|
row.dataset.fold = childPath;
|
|
318
383
|
row.addEventListener('click', (e) => {
|
|
@@ -323,7 +388,7 @@ function renderInto(ctx, v, depth, path) {
|
|
|
323
388
|
});
|
|
324
389
|
box.appendChild(row);
|
|
325
390
|
|
|
326
|
-
if (open) renderInto(ctx,
|
|
391
|
+
if (open) renderInto(ctx, val, depth + 1, childPath, fromJson ? false : allowParse);
|
|
327
392
|
}
|
|
328
393
|
|
|
329
394
|
if (keys.length > show.length) {
|
|
@@ -344,14 +409,28 @@ function renderInto(ctx, v, depth, path) {
|
|
|
344
409
|
export function createFullView(value, opts = {}) {
|
|
345
410
|
const fold = opts.fold instanceof Map ? opts.fold : new Map();
|
|
346
411
|
|
|
412
|
+
// 根是字符串 ⇒ 只有「里面装着 JSON」才会进来(调用方用 hasStructuredBody 先筛过)。
|
|
413
|
+
// 头部同时写出**两件事**:它原本是字符串(字符数)、解析后是什么规模 —— 只写后者,
|
|
414
|
+
// 读者会以为工具真的返回了对象。
|
|
415
|
+
const parsedRoot = jsonInString(value);
|
|
416
|
+
const root = parsedRoot ?? value;
|
|
417
|
+
const fromJson = parsedRoot !== null;
|
|
418
|
+
|
|
347
419
|
const box = fvEl('div', 'tr-full');
|
|
348
|
-
box.dataset.kind = Array.isArray(
|
|
420
|
+
box.dataset.kind = Array.isArray(root) ? 'array' : 'object';
|
|
349
421
|
|
|
350
422
|
const head = fvEl('div', 'tr-full-head');
|
|
351
|
-
|
|
423
|
+
const label = typeLabelOf(root);
|
|
424
|
+
head.appendChild(
|
|
425
|
+
fvEl('span', 'tr-full-type', fromJson ? `string · ${value.length} 字符 → ${label}` : label),
|
|
426
|
+
);
|
|
427
|
+
if (fromJson) head.appendChild(jsonBadge());
|
|
352
428
|
const copy = fvEl('span', 'tr-full-copy', '复制');
|
|
353
429
|
copy.dataset.copy = '1';
|
|
354
|
-
|
|
430
|
+
// 字符串根复制的是**原文**(拿到手能直接喂 jq);容器根复制的是缩进后的 JSON
|
|
431
|
+
copy.addEventListener('click', () =>
|
|
432
|
+
copyInto(copy, fromJson ? String(value) : safeStringify(root), '已复制'),
|
|
433
|
+
);
|
|
355
434
|
head.appendChild(copy);
|
|
356
435
|
box.appendChild(head);
|
|
357
436
|
|
|
@@ -360,7 +439,8 @@ export function createFullView(value, opts = {}) {
|
|
|
360
439
|
// 只重画正文(不动头部):折叠不需要重建整棵树,滚动位置与头部反馈都留着
|
|
361
440
|
ctx.repaint = () => {
|
|
362
441
|
body.innerHTML = '';
|
|
363
|
-
|
|
442
|
+
// 根若是从字符串解析来的,它的子树不再二次解析(只解析一层,防嵌套放大)
|
|
443
|
+
renderInto(ctx, root, 0, '', !fromJson);
|
|
364
444
|
};
|
|
365
445
|
ctx.repaint();
|
|
366
446
|
|
package/dist/inspector/index.js
CHANGED
|
@@ -12,8 +12,9 @@ export {
|
|
|
12
12
|
} from './view.js';
|
|
13
13
|
export { playTrace } from './fromTrace.js';
|
|
14
14
|
export { summarizeTrace, renderSummary } from './summary.js';
|
|
15
|
-
/* 结构化正文块(2026-11-11):`full`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
15
|
+
/* 结构化正文块(2026-11-11):`full` 是容器时,渲染器会给展开行挂上这个块;
|
|
16
|
+
* 2026-11-12 起「装着 JSON 的字符串」也走这条路(`hasStructuredBody` 是同一个判据,
|
|
17
|
+
* 宿主自己判断要不要挂块时用得上)。单独导出是因为它是**纯函数式的 DOM 构造**
|
|
18
|
+
* (入参 → 元素),宿主可以拿它渲染任意值 —— 而 bundle 的导出面必须与这里逐字一致
|
|
19
|
+
* (`test/bundle.test.js`),所以内部件不进公共面、这两件必须进。 */
|
|
20
|
+
export { createFullView, hasStructuredBody } from './full-view.js';
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* 吃掉 9 行。同构且各列都是标量时改成表格,表头一行、数据一行。
|
|
22
22
|
* 3. **路径拿不到**:想引用 `results[0].name` 只能用手数层级。每个键悬停出一个 `⧉`,
|
|
23
23
|
* 点一下把路径写进剪贴板(表头写的是 `results[*].name`,因为它是一整列)。
|
|
24
|
+
* 4. **字符串里套着的 JSON 读不了**(2026-11-12):MCP 的 text block、SSE 的 `data:`、把结果
|
|
25
|
+
* 序列化后又塞回 `content` 的工具 —— 这些值的类型是**字符串**,此前只显示成一串
|
|
26
|
+
* `{\"a\":1` 的反斜杠汤,比无缩进的 JSON 更难读。现在首尾是 `{`…`}` / `[`…`]` 且解析成功的
|
|
27
|
+
* 字符串按容器铺开,并在行里打 `str→json` 标记 —— 不标出来,读者会以为工具真的返回了对象。
|
|
24
28
|
*
|
|
25
29
|
* ## 边界(都是「不这么做就会坏」的地方,不是口味)
|
|
26
30
|
*
|
|
@@ -40,6 +44,11 @@
|
|
|
40
44
|
* 会原样复现(这正是 `view.js` 的 `expanded` 集合存在的原因)。
|
|
41
45
|
* - **表格化只在"真的同构"时发生**:元素必须全是普通对象、键集完全一致、列数 1..6、
|
|
42
46
|
* 每列都是标量。任一不满足就回落树形 —— 表格里没法逐层展开,判错一次就是**丢数据**。
|
|
47
|
+
* - **字符串里的 JSON 只解析一层,且必须标出来**:判据是**首尾字符**(`{`…`}` / `[`…`]`),
|
|
48
|
+
* 不是「`JSON.parse` 成功」—— `"123"` / `"null"` / `"true"` 都能 parse 成功,显示成
|
|
49
|
+
* 数字/空值/布尔就是**改写工具返回的事实**。解析结果内部的字符串**不再二次解析**:
|
|
50
|
+
* 一层套一层是业务里的真实现象(SSE data 里再套 JSON),逐层展开会让 10KB 变成十万个节点。
|
|
51
|
+
* 表格单元格也不解析 —— 同构表已经放弃了逐层展开,单元格里放不下嵌套块。
|
|
43
52
|
*
|
|
44
53
|
* 零依赖、纯 DOM 构造(只用 createElement / appendChild / textContent / className /
|
|
45
54
|
* dataset / innerHTML=''),与 view.js 共用同一套 DOM stub 跑测试。
|
|
@@ -55,6 +64,8 @@ const MAX_DEPTH = 6;
|
|
|
55
64
|
const OPEN_DEPTH = 2;
|
|
56
65
|
/** 表格最多几列:再多横向就没法看了,回落树形 */
|
|
57
66
|
const MAX_COLS = 6;
|
|
67
|
+
/** 多长的字符串才值得尝试从里面解析 JSON。再长的话,parse 与随后的渲染成本都不值得 */
|
|
68
|
+
const MAX_JSON_STR = 100_000;
|
|
58
69
|
|
|
59
70
|
function fvEl(tag, cls, text) {
|
|
60
71
|
const node = document.createElement(tag);
|
|
@@ -78,6 +89,18 @@ function isStructured(v) {
|
|
|
78
89
|
return v !== null && typeof v === 'object';
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 有没有「结构化正文」可铺 —— 容器,或者**装着 JSON 的字符串**。
|
|
94
|
+
*
|
|
95
|
+
* 后者是业务里的常态:`tool.output` 的 `content` 常常就是 `JSON.stringify(...)` 的结果
|
|
96
|
+
* (工具把结构序列化后塞回文本字段,MCP 的 text block 更是如此)。此前这种值是字符串 ⇒
|
|
97
|
+
* 走纯文本老路 ⇒ 展开后是一串 `{\"a\":1` 的反斜杠汤。判据与 `jsonInString` 完全同一套,
|
|
98
|
+
* 所以「挂不挂块」与「块里画什么」不会跑偏。
|
|
99
|
+
*/
|
|
100
|
+
function hasStructuredBody(v) {
|
|
101
|
+
return isStructured(v) || jsonInString(v) !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
/** 普通对象(排除数组与 null)—— 表格化与"容器"判定都只要这一种 */
|
|
82
105
|
function isPlainObject(v) {
|
|
83
106
|
return isStructured(v) && !Array.isArray(v);
|
|
@@ -119,6 +142,39 @@ function oneLine(v) {
|
|
|
119
142
|
return `<${kind} · ${sizeOf(v)} 项> ${clipped}`;
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* 「字符串里套着 JSON」:返回解析出来的容器,其余(普通文本、标量字符串、坏 JSON)一律 `null`。
|
|
147
|
+
*
|
|
148
|
+
* 判据是**首尾字符**而不是「parse 成功」,这一条是这段代码的全部要害:`"123"`、`"null"`、
|
|
149
|
+
* `"true"` 都能被 `JSON.parse` 吃下去,但把它们显示成数字/空值/布尔,等于把工具返回的
|
|
150
|
+
* 字符串改写成另一种类型 —— 调试面板的第一职责是如实。所以先看 trim 后的首尾一个字符,
|
|
151
|
+
* 不吻合就连 parse 都不试(普通文本占绝大多数,省掉无谓的反序列化)。
|
|
152
|
+
* 截断过的 JSON(尾部被切掉)在这里自然失败,回落成原样字符串。
|
|
153
|
+
*/
|
|
154
|
+
function jsonInString(v) {
|
|
155
|
+
if (typeof v !== 'string' || v.length < 2 || v.length > MAX_JSON_STR) return null;
|
|
156
|
+
const t = v.trim();
|
|
157
|
+
if (t.length < 2) return null;
|
|
158
|
+
const head = t[0];
|
|
159
|
+
const tail = t[t.length - 1];
|
|
160
|
+
if (!((head === '{' && tail === '}') || (head === '[' && tail === ']'))) return null;
|
|
161
|
+
try {
|
|
162
|
+
// 首尾判据已经保证了这里解析出来的必然是对象或数组(`{`…`}` / `[`…`]`),
|
|
163
|
+
// 所以不再重复判类型 —— 那会是一条永远走不到的防御分支(变异测试实测盖不住)。
|
|
164
|
+
return JSON.parse(t);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 「这个字符串里装着 JSON」的标记。**必须显眼**:不标出来,读者会以为工具真的返回了对象。
|
|
171
|
+
* title 顺带交代两件容易误会的事 —— 原文还在(`.tr-io` 与块内原值),路径需先 parse 这一层。 */
|
|
172
|
+
function jsonBadge() {
|
|
173
|
+
const b = fvEl('span', 'tr-full-json', 'str→json');
|
|
174
|
+
b.title = '这个值是字符串,内容是 JSON(已按对象展示;内部路径需先 JSON.parse 这一层)';
|
|
175
|
+
return b;
|
|
176
|
+
}
|
|
177
|
+
|
|
122
178
|
/** 子键的 JSON 路径:对象用 `.key`,数组用 `[i]`。根是空串,所以 `[0]` 不会变成 `.[0]` */
|
|
123
179
|
function pathOf(parent, key, isArr) {
|
|
124
180
|
return isArr ? `${parent}[${key}]` : parent ? `${parent}.${key}` : key;
|
|
@@ -251,8 +307,9 @@ function startRow(depth, caret) {
|
|
|
251
307
|
* @param {any} v 值
|
|
252
308
|
* @param {number} depth 当前缩进层级(根为 0)
|
|
253
309
|
* @param {string} path 当前值的 JSON 路径(根为空串)
|
|
310
|
+
* @param {boolean} [allowParse] 还允许不允从字符串里解析 JSON(进了「解析出来的子树」就为 false)
|
|
254
311
|
*/
|
|
255
|
-
function renderInto(ctx, v, depth, path) {
|
|
312
|
+
function renderInto(ctx, v, depth, path, allowParse = true) {
|
|
256
313
|
const box = ctx.body;
|
|
257
314
|
|
|
258
315
|
if (!isStructured(v)) {
|
|
@@ -277,14 +334,19 @@ function renderInto(ctx, v, depth, path) {
|
|
|
277
334
|
const child = isArr ? v[Number(k)] : v[k];
|
|
278
335
|
const keyText = isArr ? `[${k}]` : `${k}:`;
|
|
279
336
|
const childPath = pathOf(path, k, isArr);
|
|
337
|
+
// 字符串里套着的 JSON(`content: "{\"rows\":[...]}"`)⇒ 按容器铺。`allowParse` 只在
|
|
338
|
+
// 「刚从一个字符串里解析出来」的那棵子树上为 false —— 一层套一层会放大成十万个节点。
|
|
339
|
+
const parsed = allowParse ? jsonInString(child) : null;
|
|
340
|
+
const val = parsed ?? child;
|
|
341
|
+
const fromJson = parsed !== null;
|
|
280
342
|
// 「能否折叠」只看三件事:是容器、非空、没到深度上限 —— 与下面几个分支同一套判据。
|
|
281
343
|
// 先算出来是为了让箭头槽位在起行时就有正确字形(叶子行留空格位,键名才会对齐)。
|
|
282
|
-
const foldable = isStructured(
|
|
344
|
+
const foldable = isStructured(val) && sizeOf(val) > 0 && depth < MAX_DEPTH;
|
|
283
345
|
const open = foldable && isOpen(ctx.fold, childPath, depth);
|
|
284
346
|
const row = startRow(depth, foldable ? (open ? '▾' : '▸') : '');
|
|
285
347
|
|
|
286
348
|
// 叶子:同一行里写完(缩进 + 键名 + 路径 + 值)
|
|
287
|
-
if (!isStructured(
|
|
349
|
+
if (!isStructured(val)) {
|
|
288
350
|
const p = primitive(child);
|
|
289
351
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
290
352
|
row.appendChild(pathBtn(childPath));
|
|
@@ -296,28 +358,31 @@ function renderInto(ctx, v, depth, path) {
|
|
|
296
358
|
// 深度用尽 ⇒ 整体压成一行(并标明它是个容器),避免缩进吃掉全部宽度
|
|
297
359
|
if (depth >= MAX_DEPTH) {
|
|
298
360
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
299
|
-
row.appendChild(
|
|
361
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
362
|
+
row.appendChild(fvEl('span', 'tr-full-null', ' ' + oneLine(val)));
|
|
300
363
|
box.appendChild(row);
|
|
301
364
|
continue;
|
|
302
365
|
}
|
|
303
366
|
|
|
304
|
-
// 空容器:留一对括号,别让它看起来像"展开失败"
|
|
305
|
-
if (sizeOf(
|
|
367
|
+
// 空容器:留一对括号,别让它看起来像"展开失败"(`"{}"` / `"[]"` 这类字符串也走这里)
|
|
368
|
+
if (sizeOf(val) === 0) {
|
|
306
369
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
307
|
-
row.appendChild(
|
|
370
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
371
|
+
row.appendChild(fvEl('span', 'tr-full-null', Array.isArray(val) ? ' []' : ' {}'));
|
|
308
372
|
box.appendChild(row);
|
|
309
373
|
continue;
|
|
310
374
|
}
|
|
311
375
|
|
|
312
376
|
row.appendChild(fvEl('span', 'tr-full-key', keyText));
|
|
313
377
|
row.appendChild(pathBtn(childPath));
|
|
378
|
+
if (fromJson) row.appendChild(jsonBadge());
|
|
314
379
|
if (!open) {
|
|
315
|
-
const kind = Array.isArray(
|
|
316
|
-
row.appendChild(fvEl('span', 'tr-full-hint', ` { ${kind} · ${sizeOf(
|
|
317
|
-
} else if (Array.isArray(
|
|
380
|
+
const kind = Array.isArray(val) ? 'array' : 'object';
|
|
381
|
+
row.appendChild(fvEl('span', 'tr-full-hint', ` { ${kind} · ${sizeOf(val)} 项 }`));
|
|
382
|
+
} else if (Array.isArray(val) && tableColsOf(val)) {
|
|
318
383
|
// 成表时行内不铺子项,只在键名后标一句规模 —— 否则 `▾ targets:` 后面直接跟着表格,
|
|
319
384
|
// 看不出这是个数组、也看不出表格是不是全部内容(是不是被截断了)
|
|
320
|
-
row.appendChild(fvEl('span', 'tr-full-hint', ` array · ${
|
|
385
|
+
row.appendChild(fvEl('span', 'tr-full-hint', ` array · ${val.length} 项`));
|
|
321
386
|
}
|
|
322
387
|
row.dataset.fold = childPath;
|
|
323
388
|
row.addEventListener('click', (e) => {
|
|
@@ -328,7 +393,7 @@ function renderInto(ctx, v, depth, path) {
|
|
|
328
393
|
});
|
|
329
394
|
box.appendChild(row);
|
|
330
395
|
|
|
331
|
-
if (open) renderInto(ctx,
|
|
396
|
+
if (open) renderInto(ctx, val, depth + 1, childPath, fromJson ? false : allowParse);
|
|
332
397
|
}
|
|
333
398
|
|
|
334
399
|
if (keys.length > show.length) {
|
|
@@ -349,14 +414,28 @@ function renderInto(ctx, v, depth, path) {
|
|
|
349
414
|
function createFullView(value, opts = {}) {
|
|
350
415
|
const fold = opts.fold instanceof Map ? opts.fold : new Map();
|
|
351
416
|
|
|
417
|
+
// 根是字符串 ⇒ 只有「里面装着 JSON」才会进来(调用方用 hasStructuredBody 先筛过)。
|
|
418
|
+
// 头部同时写出**两件事**:它原本是字符串(字符数)、解析后是什么规模 —— 只写后者,
|
|
419
|
+
// 读者会以为工具真的返回了对象。
|
|
420
|
+
const parsedRoot = jsonInString(value);
|
|
421
|
+
const root = parsedRoot ?? value;
|
|
422
|
+
const fromJson = parsedRoot !== null;
|
|
423
|
+
|
|
352
424
|
const box = fvEl('div', 'tr-full');
|
|
353
|
-
box.dataset.kind = Array.isArray(
|
|
425
|
+
box.dataset.kind = Array.isArray(root) ? 'array' : 'object';
|
|
354
426
|
|
|
355
427
|
const head = fvEl('div', 'tr-full-head');
|
|
356
|
-
|
|
428
|
+
const label = typeLabelOf(root);
|
|
429
|
+
head.appendChild(
|
|
430
|
+
fvEl('span', 'tr-full-type', fromJson ? `string · ${value.length} 字符 → ${label}` : label),
|
|
431
|
+
);
|
|
432
|
+
if (fromJson) head.appendChild(jsonBadge());
|
|
357
433
|
const copy = fvEl('span', 'tr-full-copy', '复制');
|
|
358
434
|
copy.dataset.copy = '1';
|
|
359
|
-
|
|
435
|
+
// 字符串根复制的是**原文**(拿到手能直接喂 jq);容器根复制的是缩进后的 JSON
|
|
436
|
+
copy.addEventListener('click', () =>
|
|
437
|
+
copyInto(copy, fromJson ? String(value) : safeStringify(root), '已复制'),
|
|
438
|
+
);
|
|
360
439
|
head.appendChild(copy);
|
|
361
440
|
box.appendChild(head);
|
|
362
441
|
|
|
@@ -365,7 +444,8 @@ function createFullView(value, opts = {}) {
|
|
|
365
444
|
// 只重画正文(不动头部):折叠不需要重建整棵树,滚动位置与头部反馈都留着
|
|
366
445
|
ctx.repaint = () => {
|
|
367
446
|
body.innerHTML = '';
|
|
368
|
-
|
|
447
|
+
// 根若是从字符串解析来的,它的子树不再二次解析(只解析一层,防嵌套放大)
|
|
448
|
+
renderInto(ctx, root, 0, '', !fromJson);
|
|
369
449
|
};
|
|
370
450
|
ctx.repaint();
|
|
371
451
|
|
|
@@ -393,10 +473,13 @@ function createFullView(value, opts = {}) {
|
|
|
393
473
|
* 宿主需提供的 CSS 变量:--ice --faint --text(见 trace-view.css)。
|
|
394
474
|
*/
|
|
395
475
|
|
|
396
|
-
/* 展开态的正文有两种形状,由 `full` 的类型决定(2026-11-11
|
|
397
|
-
* -
|
|
398
|
-
* - 对象/数组 → 追加一个 `.tr-full` 结构化块(折叠 / 成表 / 路径复制),`.tr-io` 由 CSS
|
|
399
|
-
*
|
|
476
|
+
/* 展开态的正文有两种形状,由 `full` 的类型决定(2026-11-11,2026-11-12 扩了一档):
|
|
477
|
+
* - 纯文本字符串 → 老路:`.tr-io` 里放原文,靠 `.tr-open` 换行(纯文本正文、官网模拟回放);
|
|
478
|
+
* - 对象/数组 → 追加一个 `.tr-full` 结构化块(折叠 / 成表 / 路径复制),`.tr-io` 由 CSS 隐藏;
|
|
479
|
+
* - **装着 JSON 的字符串**(`content: "{\"rows\":[...]}"`,工具把结构序列化后塞回文本字段)
|
|
480
|
+
* → 同上一档,块头写明「string · N 字符 → object · M 项」并打 `str→json` 标记。
|
|
481
|
+
* 判据与块内完全同源(`hasStructuredBody`),「挂不挂块」与「块里画什么」因此不会跑偏。
|
|
482
|
+
* 前两档并存是刻意的:纯文本那条路有 test/style.test.js 的两个不变量守着(最小宽度、caret 常显),
|
|
400
483
|
* 全换成本件会让那两个守卫失去落点。 */
|
|
401
484
|
|
|
402
485
|
|
|
@@ -523,9 +606,10 @@ function createTraceView(rootEl, opts = {}) {
|
|
|
523
606
|
// 这一格整个不渲染,别把「无工具」显示成假工具名(.tr-name 是 flex:none,.tr-io 自然占满)
|
|
524
607
|
if (ev.tool) row.appendChild(el('span', 'tr-name', ev.tool));
|
|
525
608
|
// 展开态用全文(ev.full),折叠态用摘要(ev.text)—— 折叠态因此与加展开之前逐字一致。
|
|
526
|
-
// `full`
|
|
527
|
-
//
|
|
528
|
-
|
|
609
|
+
// `full` 是对象/数组、或**装着 JSON 的字符串**时(2026-11-11 / 2026-11-12):`.tr-io`
|
|
610
|
+
// 仍放**原文**(结构化块挂在行后、由 CSS 隐藏本格),所以「展开拿到的是原文而不是
|
|
611
|
+
// 62 字符摘要」这条不变量原样成立。
|
|
612
|
+
const structured = hasStructuredBody(ev.full);
|
|
529
613
|
const fullText = structured ? rawArg(ev.full) : ev.full || '';
|
|
530
614
|
const io = el('span', 'tr-io', (open ? fullText : ev.text) || '');
|
|
531
615
|
// 展开后正文已在行里,再挂一个占满整行的原生 tooltip 只会挡视线
|
|
@@ -1089,10 +1173,11 @@ function escapeHtml(s) {
|
|
|
1089
1173
|
|
|
1090
1174
|
|
|
1091
1175
|
|
|
1092
|
-
/* 结构化正文块(2026-11-11):`full`
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1176
|
+
/* 结构化正文块(2026-11-11):`full` 是容器时,渲染器会给展开行挂上这个块;
|
|
1177
|
+
* 2026-11-12 起「装着 JSON 的字符串」也走这条路(`hasStructuredBody` 是同一个判据,
|
|
1178
|
+
* 宿主自己判断要不要挂块时用得上)。单独导出是因为它是**纯函数式的 DOM 构造**
|
|
1179
|
+
* (入参 → 元素),宿主可以拿它渲染任意值 —— 而 bundle 的导出面必须与这里逐字一致
|
|
1180
|
+
* (`test/bundle.test.js`),所以内部件不进公共面、这两件必须进。 */
|
|
1096
1181
|
|
|
1097
1182
|
export {
|
|
1098
1183
|
CAP_ICO,
|
|
@@ -1102,6 +1187,7 @@ export {
|
|
|
1102
1187
|
fmtArg,
|
|
1103
1188
|
fmtMs,
|
|
1104
1189
|
fmtNum,
|
|
1190
|
+
hasStructuredBody,
|
|
1105
1191
|
playTrace,
|
|
1106
1192
|
rawArg,
|
|
1107
1193
|
renderSummary,
|
|
@@ -284,6 +284,18 @@
|
|
|
284
284
|
.tr-full-hint {
|
|
285
285
|
color: var(--faint);
|
|
286
286
|
}
|
|
287
|
+
/* 「这个字符串里装着 JSON」的标记。它不是装饰,是**事实的一部分**:没有它,读者会以为
|
|
288
|
+
工具真的返回了对象。所以常显、不等 hover,也不跟 .tr-full-hint 一样隐形 —— 但保持
|
|
289
|
+
低对比度,避免每行都在喊。 */
|
|
290
|
+
.tr-full-json {
|
|
291
|
+
color: var(--faint);
|
|
292
|
+
font-size: 10px;
|
|
293
|
+
line-height: 1.4;
|
|
294
|
+
border: 1px solid #2c2c32;
|
|
295
|
+
border-radius: 3px;
|
|
296
|
+
padding: 0 4px;
|
|
297
|
+
margin-right: 4px;
|
|
298
|
+
}
|
|
287
299
|
/* `⧉` 复制路径:hover 才露。与 .tr-caret 的区别是**它暴露的是动作、不是信息** ——
|
|
288
300
|
路径的内容(键名)始终写在行里,常显只会给每行多一个字形噪声。
|
|
289
301
|
没有剪贴板实现时照样显示、点了不谎报成功(见 full-view.js 的 copyInto)。 */
|
package/dist/inspector/view.js
CHANGED
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
* 宿主需提供的 CSS 变量:--ice --faint --text(见 trace-view.css)。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
/* 展开态的正文有两种形状,由 `full` 的类型决定(2026-11-11
|
|
21
|
-
* -
|
|
22
|
-
* - 对象/数组 → 追加一个 `.tr-full` 结构化块(折叠 / 成表 / 路径复制),`.tr-io` 由 CSS
|
|
23
|
-
*
|
|
20
|
+
/* 展开态的正文有两种形状,由 `full` 的类型决定(2026-11-11,2026-11-12 扩了一档):
|
|
21
|
+
* - 纯文本字符串 → 老路:`.tr-io` 里放原文,靠 `.tr-open` 换行(纯文本正文、官网模拟回放);
|
|
22
|
+
* - 对象/数组 → 追加一个 `.tr-full` 结构化块(折叠 / 成表 / 路径复制),`.tr-io` 由 CSS 隐藏;
|
|
23
|
+
* - **装着 JSON 的字符串**(`content: "{\"rows\":[...]}"`,工具把结构序列化后塞回文本字段)
|
|
24
|
+
* → 同上一档,块头写明「string · N 字符 → object · M 项」并打 `str→json` 标记。
|
|
25
|
+
* 判据与块内完全同源(`hasStructuredBody`),「挂不挂块」与「块里画什么」因此不会跑偏。
|
|
26
|
+
* 前两档并存是刻意的:纯文本那条路有 test/style.test.js 的两个不变量守着(最小宽度、caret 常显),
|
|
24
27
|
* 全换成本件会让那两个守卫失去落点。 */
|
|
25
|
-
import { createFullView } from './full-view.js';
|
|
28
|
+
import { createFullView, hasStructuredBody } from './full-view.js';
|
|
26
29
|
|
|
27
30
|
export const CAP_ICO = { tool: '⚙', skill: '◆', prompt: '¶', subagent: '⊕' };
|
|
28
31
|
|
|
@@ -147,9 +150,10 @@ export function createTraceView(rootEl, opts = {}) {
|
|
|
147
150
|
// 这一格整个不渲染,别把「无工具」显示成假工具名(.tr-name 是 flex:none,.tr-io 自然占满)
|
|
148
151
|
if (ev.tool) row.appendChild(el('span', 'tr-name', ev.tool));
|
|
149
152
|
// 展开态用全文(ev.full),折叠态用摘要(ev.text)—— 折叠态因此与加展开之前逐字一致。
|
|
150
|
-
// `full`
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
+
// `full` 是对象/数组、或**装着 JSON 的字符串**时(2026-11-11 / 2026-11-12):`.tr-io`
|
|
154
|
+
// 仍放**原文**(结构化块挂在行后、由 CSS 隐藏本格),所以「展开拿到的是原文而不是
|
|
155
|
+
// 62 字符摘要」这条不变量原样成立。
|
|
156
|
+
const structured = hasStructuredBody(ev.full);
|
|
153
157
|
const fullText = structured ? rawArg(ev.full) : ev.full || '';
|
|
154
158
|
const io = el('span', 'tr-io', (open ? fullText : ev.text) || '');
|
|
155
159
|
// 展开后正文已在行里,再挂一个占满整行的原生 tooltip 只会挡视线
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@migor/agentia": "^0.10.
|
|
14
|
+
"@migor/agentia": "^0.10.8"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@migor/cli": "^0.10.
|
|
17
|
+
"@migor/cli": "^0.10.8",
|
|
18
18
|
"tsx": "^4.19.0",
|
|
19
19
|
"typescript": "^7.0.2",
|
|
20
20
|
"@types/node": "^22.0.0"
|
package/package.json
CHANGED