aipeek 0.2.5 → 0.2.6
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/dist/{chunk-6EZKMGRD.cjs → chunk-5ZZYOETF.cjs} +78 -81
- package/dist/{chunk-X3HAXWFJ.js → chunk-XA2LT6I4.js} +72 -75
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -2
- package/dist/plugin.js +1 -1
- package/package.json +2 -1
- package/src/client/client-patch.ts +35 -3
- package/src/client/client.ts +7 -17
- package/src/core/action.ts +54 -1
- package/src/core/compact.ts +4 -30
- package/src/core/detail.ts +10 -26
- package/src/core/util.ts +53 -0
- package/src/server/plugin.ts +21 -32
|
@@ -71,13 +71,54 @@ function check(raw) {
|
|
|
71
71
|
function truncate(s, max) {
|
|
72
72
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
73
73
|
}
|
|
74
|
+
function formatValue(v, seen = /* @__PURE__ */ new Set()) {
|
|
75
|
+
if (v === null || v === void 0)
|
|
76
|
+
return String(v);
|
|
77
|
+
const t = typeof v;
|
|
78
|
+
if (t === "string")
|
|
79
|
+
return v;
|
|
80
|
+
if (t === "number" || t === "boolean" || t === "bigint")
|
|
81
|
+
return String(v);
|
|
82
|
+
if (t === "symbol")
|
|
83
|
+
return v.toString();
|
|
84
|
+
if (t === "function")
|
|
85
|
+
return `[Function: ${v.name || "anonymous"}]`;
|
|
86
|
+
const obj = v;
|
|
87
|
+
if (seen.has(obj))
|
|
88
|
+
return "[Circular]";
|
|
89
|
+
if (v instanceof Error)
|
|
90
|
+
return v.stack || `${v.name}: ${v.message}`;
|
|
91
|
+
seen.add(obj);
|
|
92
|
+
if (v instanceof Map) {
|
|
93
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`);
|
|
94
|
+
return `Map(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
95
|
+
}
|
|
96
|
+
if (v instanceof Set) {
|
|
97
|
+
const items = [...v.values()].slice(0, 15).map((val) => formatValue(val, seen));
|
|
98
|
+
return `Set(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(v)) {
|
|
101
|
+
const items = v.slice(0, 30).map((val) => formatValue(val, seen));
|
|
102
|
+
return `[${items.join(", ")}${v.length > 30 ? ", \u2026" : ""}]`;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(v);
|
|
106
|
+
} catch (e2) {
|
|
107
|
+
const entries = Object.entries(v).slice(0, 15);
|
|
108
|
+
const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`);
|
|
109
|
+
return `{${parts.join(", ")}}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function appStackFrames(stack, max) {
|
|
113
|
+
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, max);
|
|
114
|
+
}
|
|
74
115
|
function compactUrl(url, search) {
|
|
75
116
|
try {
|
|
76
117
|
const u = new URL(url);
|
|
77
118
|
if (search && u.search)
|
|
78
119
|
return `${u.pathname}?${truncate(u.search.slice(1), search)}`;
|
|
79
120
|
return u.pathname;
|
|
80
|
-
} catch (
|
|
121
|
+
} catch (e3) {
|
|
81
122
|
return truncate(url, 80);
|
|
82
123
|
}
|
|
83
124
|
}
|
|
@@ -249,7 +290,7 @@ function isApiUrl(url) {
|
|
|
249
290
|
try {
|
|
250
291
|
const u = new URL(url);
|
|
251
292
|
return u.pathname.startsWith("/api") || u.pathname.includes("/graphql");
|
|
252
|
-
} catch (
|
|
293
|
+
} catch (e4) {
|
|
253
294
|
return false;
|
|
254
295
|
}
|
|
255
296
|
}
|
|
@@ -287,17 +328,12 @@ function compactErrors(errors) {
|
|
|
287
328
|
for (const err of seen.values()) {
|
|
288
329
|
lines.push(err.message);
|
|
289
330
|
if (err.stack) {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
lines.push(` at ${frame}`);
|
|
293
|
-
}
|
|
331
|
+
for (const frame of appStackFrames(err.stack, 5))
|
|
332
|
+
lines.push(` ${frame}`);
|
|
294
333
|
}
|
|
295
334
|
}
|
|
296
335
|
return lines.join("\n");
|
|
297
336
|
}
|
|
298
|
-
function filterStack(stack) {
|
|
299
|
-
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => l.slice(3)).filter((l) => !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, 5);
|
|
300
|
-
}
|
|
301
337
|
function compactState(state) {
|
|
302
338
|
if (!state || !Object.keys(state).length)
|
|
303
339
|
return "";
|
|
@@ -306,7 +342,7 @@ function compactState(state) {
|
|
|
306
342
|
lines.push(`${name}:`);
|
|
307
343
|
if (typeof value === "object" && value !== null) {
|
|
308
344
|
for (const [k, v] of Object.entries(value)) {
|
|
309
|
-
lines.push(` ${k}: ${formatValue(v)}`);
|
|
345
|
+
lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
|
|
310
346
|
}
|
|
311
347
|
} else {
|
|
312
348
|
lines.push(` ${String(value)}`);
|
|
@@ -314,19 +350,6 @@ function compactState(state) {
|
|
|
314
350
|
}
|
|
315
351
|
return lines.join("\n");
|
|
316
352
|
}
|
|
317
|
-
function formatValue(v) {
|
|
318
|
-
if (v === null || v === void 0)
|
|
319
|
-
return String(v);
|
|
320
|
-
if (typeof v === "string")
|
|
321
|
-
return v;
|
|
322
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
323
|
-
return String(v);
|
|
324
|
-
if (typeof v === "object") {
|
|
325
|
-
const s = JSON.stringify(v);
|
|
326
|
-
return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
|
|
327
|
-
}
|
|
328
|
-
return String(v);
|
|
329
|
-
}
|
|
330
353
|
function compact(raw) {
|
|
331
354
|
return {
|
|
332
355
|
url: raw.url,
|
|
@@ -463,12 +486,10 @@ function detailError(errors, index, full) {
|
|
|
463
486
|
}
|
|
464
487
|
const lines = [err.message];
|
|
465
488
|
if (err.stack) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (totalApp > 3)
|
|
471
|
-
lines.push(` ... ${totalApp - 3} more app frames`);
|
|
489
|
+
const all = appStackFrames(err.stack, Infinity);
|
|
490
|
+
lines.push(...all.slice(0, 3));
|
|
491
|
+
if (all.length > 3)
|
|
492
|
+
lines.push(` ... ${all.length - 3} more app frames`);
|
|
472
493
|
}
|
|
473
494
|
if (err.line != null)
|
|
474
495
|
lines.push(`location: ${err.source || ""}:${err.line}:${_nullishCoalesce(err.column, () => ( 0))}`);
|
|
@@ -486,9 +507,9 @@ function detailState(state, name, full) {
|
|
|
486
507
|
const value = state[name];
|
|
487
508
|
if (full) {
|
|
488
509
|
try {
|
|
489
|
-
return JSON.stringify(value, null, 2);
|
|
490
|
-
} catch (
|
|
491
|
-
return
|
|
510
|
+
return _nullishCoalesce(JSON.stringify(value, null, 2), () => ( formatValue(value)));
|
|
511
|
+
} catch (e5) {
|
|
512
|
+
return formatValue(value);
|
|
492
513
|
}
|
|
493
514
|
}
|
|
494
515
|
if (typeof value !== "object" || value === null)
|
|
@@ -506,25 +527,14 @@ function isArraySentinel(v) {
|
|
|
506
527
|
return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
|
|
507
528
|
}
|
|
508
529
|
function formatSummaryValue(v) {
|
|
509
|
-
if (v ===
|
|
510
|
-
return
|
|
511
|
-
|
|
512
|
-
if (isArraySentinel(v))
|
|
513
|
-
return v;
|
|
514
|
-
return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
|
|
515
|
-
}
|
|
516
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
517
|
-
return String(v);
|
|
518
|
-
if (typeof v === "object") {
|
|
519
|
-
const s = JSON.stringify(v);
|
|
520
|
-
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
521
|
-
}
|
|
522
|
-
return String(v);
|
|
530
|
+
if (typeof v === "string" && isArraySentinel(v))
|
|
531
|
+
return v;
|
|
532
|
+
return truncate(formatValue(v), 80);
|
|
523
533
|
}
|
|
524
534
|
function jsonSchema(sample) {
|
|
525
535
|
try {
|
|
526
536
|
return schemaOf(JSON.parse(sample), 0);
|
|
527
|
-
} catch (
|
|
537
|
+
} catch (e6) {
|
|
528
538
|
return null;
|
|
529
539
|
}
|
|
530
540
|
}
|
|
@@ -753,6 +763,10 @@ function readBody(req) {
|
|
|
753
763
|
req.on("end", () => resolve2(s));
|
|
754
764
|
});
|
|
755
765
|
}
|
|
766
|
+
function send(res, status, body) {
|
|
767
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
768
|
+
res.end(body);
|
|
769
|
+
}
|
|
756
770
|
var __dirname = _path.dirname.call(void 0, _url.fileURLToPath.call(void 0, _chunkZ2Y65YOYcjs.importMetaUrl));
|
|
757
771
|
var clientDir = _fs.existsSync.call(void 0, _path.resolve.call(void 0, __dirname, "../client")) ? _path.resolve.call(void 0, __dirname, "../client") : _path.resolve.call(void 0, __dirname, "../src/client");
|
|
758
772
|
var clientPath = _path.resolve.call(void 0, clientDir, "client.ts");
|
|
@@ -853,7 +867,7 @@ ${END_TAG}
|
|
|
853
867
|
const sep = content.endsWith("\n") ? "" : "\n";
|
|
854
868
|
_fs.writeFileSync.call(void 0, path, `${content}${sep}
|
|
855
869
|
${block}`);
|
|
856
|
-
} catch (
|
|
870
|
+
} catch (e7) {
|
|
857
871
|
}
|
|
858
872
|
}
|
|
859
873
|
function aipeekPlugin() {
|
|
@@ -1001,7 +1015,7 @@ function aipeekPlugin() {
|
|
|
1001
1015
|
if (msg)
|
|
1002
1016
|
server.config.logger.warn(msg);
|
|
1003
1017
|
}
|
|
1004
|
-
} catch (
|
|
1018
|
+
} catch (e8) {
|
|
1005
1019
|
}
|
|
1006
1020
|
}, 500);
|
|
1007
1021
|
});
|
|
@@ -1015,13 +1029,11 @@ function aipeekPlugin() {
|
|
|
1015
1029
|
if (!code && req.method === "POST")
|
|
1016
1030
|
code = await readBody(req);
|
|
1017
1031
|
if (!code) {
|
|
1018
|
-
res
|
|
1019
|
-
res.end("eval needs ?code= or a POST body");
|
|
1032
|
+
send(res, 400, "eval needs ?code= or a POST body");
|
|
1020
1033
|
return;
|
|
1021
1034
|
}
|
|
1022
1035
|
const r = await evalInClient(code);
|
|
1023
|
-
res
|
|
1024
|
-
res.end(r.ok ? _nullishCoalesce(r.value, () => ( "undefined")) : `error: ${r.error}`);
|
|
1036
|
+
send(res, r.ok ? 200 : 422, r.ok ? _nullishCoalesce(r.value, () => ( "undefined")) : `error: ${r.error}`);
|
|
1025
1037
|
return;
|
|
1026
1038
|
}
|
|
1027
1039
|
if (parts[0] === "dom") {
|
|
@@ -1029,14 +1041,12 @@ function aipeekPlugin() {
|
|
|
1029
1041
|
url.searchParams.get("scope") || void 0,
|
|
1030
1042
|
url.searchParams.get("sel") || void 0
|
|
1031
1043
|
);
|
|
1032
|
-
res
|
|
1033
|
-
res.end(dom || "(empty)");
|
|
1044
|
+
send(res, 200, dom || "(empty)");
|
|
1034
1045
|
return;
|
|
1035
1046
|
}
|
|
1036
1047
|
if (parts[0] === "screen") {
|
|
1037
1048
|
const screen = await collectScreenFromClient();
|
|
1038
|
-
res
|
|
1039
|
-
res.end(screen || "(empty)");
|
|
1049
|
+
send(res, 200, screen || "(empty)");
|
|
1040
1050
|
return;
|
|
1041
1051
|
}
|
|
1042
1052
|
if (parts[0] === "chain") {
|
|
@@ -1047,8 +1057,7 @@ function aipeekPlugin() {
|
|
|
1047
1057
|
if (!Array.isArray(steps))
|
|
1048
1058
|
throw new Error("body must be a JSON array");
|
|
1049
1059
|
} catch (e) {
|
|
1050
|
-
res
|
|
1051
|
-
res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1060
|
+
send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1052
1061
|
return;
|
|
1053
1062
|
}
|
|
1054
1063
|
lastRaw = null;
|
|
@@ -1074,8 +1083,7 @@ function aipeekPlugin() {
|
|
|
1074
1083
|
break;
|
|
1075
1084
|
}
|
|
1076
1085
|
}
|
|
1077
|
-
res
|
|
1078
|
-
res.end(lastUi ? `${lines.join("\n")}
|
|
1086
|
+
send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
|
|
1079
1087
|
|
|
1080
1088
|
--- ui after ---
|
|
1081
1089
|
${lastUi}` : lines.join("\n"));
|
|
@@ -1093,8 +1101,7 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1093
1101
|
};
|
|
1094
1102
|
const check2 = resolveAction(parts[0], args);
|
|
1095
1103
|
if (!check2.valid) {
|
|
1096
|
-
res
|
|
1097
|
-
res.end(check2.error);
|
|
1104
|
+
send(res, 400, _nullishCoalesce(check2.error, () => ( "invalid action")));
|
|
1098
1105
|
return;
|
|
1099
1106
|
}
|
|
1100
1107
|
const result = await sendAction(parts[0], args);
|
|
@@ -1105,15 +1112,13 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1105
1112
|
const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
|
|
1106
1113
|
const file = _path.resolve.call(void 0, dir, name);
|
|
1107
1114
|
_fs.writeFileSync.call(void 0, file, _buffer.Buffer.from(result.dataUrl.split(",")[1], "base64"));
|
|
1108
|
-
res
|
|
1109
|
-
res.end(`saved: ${file}`);
|
|
1115
|
+
send(res, 200, `saved: ${file}`);
|
|
1110
1116
|
return;
|
|
1111
1117
|
}
|
|
1112
|
-
res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1113
1118
|
const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
|
|
1114
1119
|
|
|
1115
1120
|
clickable: ${result.detail}` : ""}`;
|
|
1116
|
-
res.
|
|
1121
|
+
send(res, result.ok ? 200 : 422, result.ui ? `${head}
|
|
1117
1122
|
|
|
1118
1123
|
--- ui after ---
|
|
1119
1124
|
${result.ui}` : head);
|
|
@@ -1124,8 +1129,7 @@ ${result.ui}` : head);
|
|
|
1124
1129
|
lastRaw = raw2;
|
|
1125
1130
|
const result = check(raw2);
|
|
1126
1131
|
const output = emitCheck(result);
|
|
1127
|
-
res
|
|
1128
|
-
res.end(output);
|
|
1132
|
+
send(res, result.pass ? 200 : 417, output);
|
|
1129
1133
|
return;
|
|
1130
1134
|
}
|
|
1131
1135
|
if (parts.length >= 1) {
|
|
@@ -1133,29 +1137,22 @@ ${result.ui}` : head);
|
|
|
1133
1137
|
lastRaw = await collectFromClient();
|
|
1134
1138
|
const result = detail(lastRaw, parts[0], parts[1], full);
|
|
1135
1139
|
if (result !== null) {
|
|
1136
|
-
res
|
|
1137
|
-
res.end(result);
|
|
1140
|
+
send(res, 200, result);
|
|
1138
1141
|
return;
|
|
1139
1142
|
}
|
|
1140
|
-
res
|
|
1141
|
-
res.end(`not found: ${parts.join("/")}`);
|
|
1143
|
+
send(res, 404, `not found: ${parts.join("/")}`);
|
|
1142
1144
|
return;
|
|
1143
1145
|
}
|
|
1144
1146
|
const raw = await collectFromClient();
|
|
1145
1147
|
lastRaw = raw;
|
|
1146
1148
|
if (full) {
|
|
1147
1149
|
const compacted = compact(raw);
|
|
1148
|
-
|
|
1149
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1150
|
-
res.end(output);
|
|
1150
|
+
send(res, 200, emit(compacted));
|
|
1151
1151
|
} else {
|
|
1152
|
-
|
|
1153
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1154
|
-
res.end(output);
|
|
1152
|
+
send(res, 200, emitSummary(raw));
|
|
1155
1153
|
}
|
|
1156
1154
|
} catch (err) {
|
|
1157
|
-
res
|
|
1158
|
-
res.end(err instanceof Error ? err.message : "unknown error");
|
|
1155
|
+
send(res, 504, err instanceof Error ? err.message : "unknown error");
|
|
1159
1156
|
}
|
|
1160
1157
|
});
|
|
1161
1158
|
}
|
|
@@ -67,6 +67,47 @@ function check(raw) {
|
|
|
67
67
|
function truncate(s, max) {
|
|
68
68
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
69
69
|
}
|
|
70
|
+
function formatValue(v, seen = /* @__PURE__ */ new Set()) {
|
|
71
|
+
if (v === null || v === void 0)
|
|
72
|
+
return String(v);
|
|
73
|
+
const t = typeof v;
|
|
74
|
+
if (t === "string")
|
|
75
|
+
return v;
|
|
76
|
+
if (t === "number" || t === "boolean" || t === "bigint")
|
|
77
|
+
return String(v);
|
|
78
|
+
if (t === "symbol")
|
|
79
|
+
return v.toString();
|
|
80
|
+
if (t === "function")
|
|
81
|
+
return `[Function: ${v.name || "anonymous"}]`;
|
|
82
|
+
const obj = v;
|
|
83
|
+
if (seen.has(obj))
|
|
84
|
+
return "[Circular]";
|
|
85
|
+
if (v instanceof Error)
|
|
86
|
+
return v.stack || `${v.name}: ${v.message}`;
|
|
87
|
+
seen.add(obj);
|
|
88
|
+
if (v instanceof Map) {
|
|
89
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`);
|
|
90
|
+
return `Map(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
91
|
+
}
|
|
92
|
+
if (v instanceof Set) {
|
|
93
|
+
const items = [...v.values()].slice(0, 15).map((val) => formatValue(val, seen));
|
|
94
|
+
return `Set(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(v)) {
|
|
97
|
+
const items = v.slice(0, 30).map((val) => formatValue(val, seen));
|
|
98
|
+
return `[${items.join(", ")}${v.length > 30 ? ", \u2026" : ""}]`;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return JSON.stringify(v);
|
|
102
|
+
} catch {
|
|
103
|
+
const entries = Object.entries(v).slice(0, 15);
|
|
104
|
+
const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`);
|
|
105
|
+
return `{${parts.join(", ")}}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function appStackFrames(stack, max) {
|
|
109
|
+
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, max);
|
|
110
|
+
}
|
|
70
111
|
function compactUrl(url, search) {
|
|
71
112
|
try {
|
|
72
113
|
const u = new URL(url);
|
|
@@ -283,17 +324,12 @@ function compactErrors(errors) {
|
|
|
283
324
|
for (const err of seen.values()) {
|
|
284
325
|
lines.push(err.message);
|
|
285
326
|
if (err.stack) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
lines.push(` at ${frame}`);
|
|
289
|
-
}
|
|
327
|
+
for (const frame of appStackFrames(err.stack, 5))
|
|
328
|
+
lines.push(` ${frame}`);
|
|
290
329
|
}
|
|
291
330
|
}
|
|
292
331
|
return lines.join("\n");
|
|
293
332
|
}
|
|
294
|
-
function filterStack(stack) {
|
|
295
|
-
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => l.slice(3)).filter((l) => !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, 5);
|
|
296
|
-
}
|
|
297
333
|
function compactState(state) {
|
|
298
334
|
if (!state || !Object.keys(state).length)
|
|
299
335
|
return "";
|
|
@@ -302,7 +338,7 @@ function compactState(state) {
|
|
|
302
338
|
lines.push(`${name}:`);
|
|
303
339
|
if (typeof value === "object" && value !== null) {
|
|
304
340
|
for (const [k, v] of Object.entries(value)) {
|
|
305
|
-
lines.push(` ${k}: ${formatValue(v)}`);
|
|
341
|
+
lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
|
|
306
342
|
}
|
|
307
343
|
} else {
|
|
308
344
|
lines.push(` ${String(value)}`);
|
|
@@ -310,19 +346,6 @@ function compactState(state) {
|
|
|
310
346
|
}
|
|
311
347
|
return lines.join("\n");
|
|
312
348
|
}
|
|
313
|
-
function formatValue(v) {
|
|
314
|
-
if (v === null || v === void 0)
|
|
315
|
-
return String(v);
|
|
316
|
-
if (typeof v === "string")
|
|
317
|
-
return v;
|
|
318
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
319
|
-
return String(v);
|
|
320
|
-
if (typeof v === "object") {
|
|
321
|
-
const s = JSON.stringify(v);
|
|
322
|
-
return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
|
|
323
|
-
}
|
|
324
|
-
return String(v);
|
|
325
|
-
}
|
|
326
349
|
function compact(raw) {
|
|
327
350
|
return {
|
|
328
351
|
url: raw.url,
|
|
@@ -459,12 +482,10 @@ function detailError(errors, index, full) {
|
|
|
459
482
|
}
|
|
460
483
|
const lines = [err.message];
|
|
461
484
|
if (err.stack) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (totalApp > 3)
|
|
467
|
-
lines.push(` ... ${totalApp - 3} more app frames`);
|
|
485
|
+
const all = appStackFrames(err.stack, Infinity);
|
|
486
|
+
lines.push(...all.slice(0, 3));
|
|
487
|
+
if (all.length > 3)
|
|
488
|
+
lines.push(` ... ${all.length - 3} more app frames`);
|
|
468
489
|
}
|
|
469
490
|
if (err.line != null)
|
|
470
491
|
lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
|
|
@@ -482,9 +503,9 @@ function detailState(state, name, full) {
|
|
|
482
503
|
const value = state[name];
|
|
483
504
|
if (full) {
|
|
484
505
|
try {
|
|
485
|
-
return JSON.stringify(value, null, 2);
|
|
506
|
+
return JSON.stringify(value, null, 2) ?? formatValue(value);
|
|
486
507
|
} catch {
|
|
487
|
-
return
|
|
508
|
+
return formatValue(value);
|
|
488
509
|
}
|
|
489
510
|
}
|
|
490
511
|
if (typeof value !== "object" || value === null)
|
|
@@ -502,20 +523,9 @@ function isArraySentinel(v) {
|
|
|
502
523
|
return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
|
|
503
524
|
}
|
|
504
525
|
function formatSummaryValue(v) {
|
|
505
|
-
if (v ===
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
if (isArraySentinel(v))
|
|
509
|
-
return v;
|
|
510
|
-
return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
|
|
511
|
-
}
|
|
512
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
513
|
-
return String(v);
|
|
514
|
-
if (typeof v === "object") {
|
|
515
|
-
const s = JSON.stringify(v);
|
|
516
|
-
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
517
|
-
}
|
|
518
|
-
return String(v);
|
|
526
|
+
if (typeof v === "string" && isArraySentinel(v))
|
|
527
|
+
return v;
|
|
528
|
+
return truncate(formatValue(v), 80);
|
|
519
529
|
}
|
|
520
530
|
function jsonSchema(sample) {
|
|
521
531
|
try {
|
|
@@ -749,6 +759,10 @@ function readBody(req) {
|
|
|
749
759
|
req.on("end", () => resolve2(s));
|
|
750
760
|
});
|
|
751
761
|
}
|
|
762
|
+
function send(res, status, body) {
|
|
763
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
764
|
+
res.end(body);
|
|
765
|
+
}
|
|
752
766
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
753
767
|
var clientDir = existsSync(resolve(__dirname2, "../client")) ? resolve(__dirname2, "../client") : resolve(__dirname2, "../src/client");
|
|
754
768
|
var clientPath = resolve(clientDir, "client.ts");
|
|
@@ -1011,13 +1025,11 @@ function aipeekPlugin() {
|
|
|
1011
1025
|
if (!code && req.method === "POST")
|
|
1012
1026
|
code = await readBody(req);
|
|
1013
1027
|
if (!code) {
|
|
1014
|
-
res
|
|
1015
|
-
res.end("eval needs ?code= or a POST body");
|
|
1028
|
+
send(res, 400, "eval needs ?code= or a POST body");
|
|
1016
1029
|
return;
|
|
1017
1030
|
}
|
|
1018
1031
|
const r = await evalInClient(code);
|
|
1019
|
-
res
|
|
1020
|
-
res.end(r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
1032
|
+
send(res, r.ok ? 200 : 422, r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
1021
1033
|
return;
|
|
1022
1034
|
}
|
|
1023
1035
|
if (parts[0] === "dom") {
|
|
@@ -1025,14 +1037,12 @@ function aipeekPlugin() {
|
|
|
1025
1037
|
url.searchParams.get("scope") || void 0,
|
|
1026
1038
|
url.searchParams.get("sel") || void 0
|
|
1027
1039
|
);
|
|
1028
|
-
res
|
|
1029
|
-
res.end(dom || "(empty)");
|
|
1040
|
+
send(res, 200, dom || "(empty)");
|
|
1030
1041
|
return;
|
|
1031
1042
|
}
|
|
1032
1043
|
if (parts[0] === "screen") {
|
|
1033
1044
|
const screen = await collectScreenFromClient();
|
|
1034
|
-
res
|
|
1035
|
-
res.end(screen || "(empty)");
|
|
1045
|
+
send(res, 200, screen || "(empty)");
|
|
1036
1046
|
return;
|
|
1037
1047
|
}
|
|
1038
1048
|
if (parts[0] === "chain") {
|
|
@@ -1043,8 +1053,7 @@ function aipeekPlugin() {
|
|
|
1043
1053
|
if (!Array.isArray(steps))
|
|
1044
1054
|
throw new Error("body must be a JSON array");
|
|
1045
1055
|
} catch (e) {
|
|
1046
|
-
res
|
|
1047
|
-
res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1056
|
+
send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1048
1057
|
return;
|
|
1049
1058
|
}
|
|
1050
1059
|
lastRaw = null;
|
|
@@ -1070,8 +1079,7 @@ function aipeekPlugin() {
|
|
|
1070
1079
|
break;
|
|
1071
1080
|
}
|
|
1072
1081
|
}
|
|
1073
|
-
res
|
|
1074
|
-
res.end(lastUi ? `${lines.join("\n")}
|
|
1082
|
+
send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
|
|
1075
1083
|
|
|
1076
1084
|
--- ui after ---
|
|
1077
1085
|
${lastUi}` : lines.join("\n"));
|
|
@@ -1089,8 +1097,7 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1089
1097
|
};
|
|
1090
1098
|
const check2 = resolveAction(parts[0], args);
|
|
1091
1099
|
if (!check2.valid) {
|
|
1092
|
-
res
|
|
1093
|
-
res.end(check2.error);
|
|
1100
|
+
send(res, 400, check2.error ?? "invalid action");
|
|
1094
1101
|
return;
|
|
1095
1102
|
}
|
|
1096
1103
|
const result = await sendAction(parts[0], args);
|
|
@@ -1101,15 +1108,13 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1101
1108
|
const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
|
|
1102
1109
|
const file = resolve(dir, name);
|
|
1103
1110
|
writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
|
|
1104
|
-
res
|
|
1105
|
-
res.end(`saved: ${file}`);
|
|
1111
|
+
send(res, 200, `saved: ${file}`);
|
|
1106
1112
|
return;
|
|
1107
1113
|
}
|
|
1108
|
-
res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1109
1114
|
const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
|
|
1110
1115
|
|
|
1111
1116
|
clickable: ${result.detail}` : ""}`;
|
|
1112
|
-
res.
|
|
1117
|
+
send(res, result.ok ? 200 : 422, result.ui ? `${head}
|
|
1113
1118
|
|
|
1114
1119
|
--- ui after ---
|
|
1115
1120
|
${result.ui}` : head);
|
|
@@ -1120,8 +1125,7 @@ ${result.ui}` : head);
|
|
|
1120
1125
|
lastRaw = raw2;
|
|
1121
1126
|
const result = check(raw2);
|
|
1122
1127
|
const output = emitCheck(result);
|
|
1123
|
-
res
|
|
1124
|
-
res.end(output);
|
|
1128
|
+
send(res, result.pass ? 200 : 417, output);
|
|
1125
1129
|
return;
|
|
1126
1130
|
}
|
|
1127
1131
|
if (parts.length >= 1) {
|
|
@@ -1129,29 +1133,22 @@ ${result.ui}` : head);
|
|
|
1129
1133
|
lastRaw = await collectFromClient();
|
|
1130
1134
|
const result = detail(lastRaw, parts[0], parts[1], full);
|
|
1131
1135
|
if (result !== null) {
|
|
1132
|
-
res
|
|
1133
|
-
res.end(result);
|
|
1136
|
+
send(res, 200, result);
|
|
1134
1137
|
return;
|
|
1135
1138
|
}
|
|
1136
|
-
res
|
|
1137
|
-
res.end(`not found: ${parts.join("/")}`);
|
|
1139
|
+
send(res, 404, `not found: ${parts.join("/")}`);
|
|
1138
1140
|
return;
|
|
1139
1141
|
}
|
|
1140
1142
|
const raw = await collectFromClient();
|
|
1141
1143
|
lastRaw = raw;
|
|
1142
1144
|
if (full) {
|
|
1143
1145
|
const compacted = compact(raw);
|
|
1144
|
-
|
|
1145
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1146
|
-
res.end(output);
|
|
1146
|
+
send(res, 200, emit(compacted));
|
|
1147
1147
|
} else {
|
|
1148
|
-
|
|
1149
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1150
|
-
res.end(output);
|
|
1148
|
+
send(res, 200, emitSummary(raw));
|
|
1151
1149
|
}
|
|
1152
1150
|
} catch (err) {
|
|
1153
|
-
res
|
|
1154
|
-
res.end(err instanceof Error ? err.message : "unknown error");
|
|
1151
|
+
send(res, 504, err instanceof Error ? err.message : "unknown error");
|
|
1155
1152
|
}
|
|
1156
1153
|
});
|
|
1157
1154
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
|
|
9
9
|
require('./chunk-Z2Y65YOY.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
@@ -14,4 +14,4 @@ require('./chunk-Z2Y65YOY.cjs');
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
exports.aipeekPlugin =
|
|
17
|
+
exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.check = _chunk5ZZYOETFcjs.check; exports.diffState = _chunk5ZZYOETFcjs.diffState; exports.emitCheck = _chunk5ZZYOETFcjs.emitCheck; exports.emitDiff = _chunk5ZZYOETFcjs.emitDiff; exports.emitSummary = _chunk5ZZYOETFcjs.emitSummary;
|
package/dist/index.js
CHANGED
package/dist/plugin.cjs
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
|
|
7
7
|
require('./chunk-Z2Y65YOY.cjs');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
exports.END_TAG =
|
|
13
|
+
exports.END_TAG = _chunk5ZZYOETFcjs.END_TAG; exports.START_TAG = _chunk5ZZYOETFcjs.START_TAG; exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.injectClaudeMd = _chunk5ZZYOETFcjs.injectClaudeMd;
|
package/dist/plugin.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aipeek",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Gives AI a peek into your running browser app — UI tree, console, network, errors, state",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"jsdom": "^29.1.1",
|
|
45
|
+
"teact": "file:../teact",
|
|
45
46
|
"tsup": "^8.4.0",
|
|
46
47
|
"typescript": "~5.7.2",
|
|
47
48
|
"vitest": "^3.1.2"
|
|
@@ -39,7 +39,7 @@ for (const level of ['log', 'info', 'warn', 'error', 'debug'] as const) {
|
|
|
39
39
|
console[level] = (...args: unknown[]) => {
|
|
40
40
|
pushBounded(consoleLogs, {
|
|
41
41
|
level,
|
|
42
|
-
text: args.map(a => typeof a === 'string' ? a :
|
|
42
|
+
text: args.map(a => typeof a === 'string' ? a : formatValue(a)).join(' '),
|
|
43
43
|
timestamp: Date.now(),
|
|
44
44
|
}, MAX_CONSOLE)
|
|
45
45
|
orig.apply(console, args)
|
|
@@ -302,11 +302,43 @@ function sampleOf(v: unknown, d: number): unknown {
|
|
|
302
302
|
return null
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
// client-patch 是自包含内联脚本(esbuild transformSync,无模块解析)——不能 import core/util。
|
|
306
|
+
// 这是 core/util.ts formatValue 的镜像;两处契约由各自单测守,client 侧靠运行时验证。
|
|
307
|
+
function formatValue(v: unknown, seen: Set<object> = new Set()): string {
|
|
308
|
+
if (v === null || v === undefined)
|
|
309
|
+
return String(v)
|
|
310
|
+
const t = typeof v
|
|
311
|
+
if (t === 'string')
|
|
312
|
+
return v as string
|
|
313
|
+
if (t === 'number' || t === 'boolean' || t === 'bigint')
|
|
314
|
+
return String(v)
|
|
315
|
+
if (t === 'symbol')
|
|
316
|
+
return (v as symbol).toString()
|
|
317
|
+
if (t === 'function')
|
|
318
|
+
return `[Function: ${(v as { name?: string }).name || 'anonymous'}]`
|
|
319
|
+
const obj = v as object
|
|
320
|
+
if (seen.has(obj))
|
|
321
|
+
return '[Circular]'
|
|
322
|
+
if (v instanceof Error)
|
|
323
|
+
return v.stack || `${v.name}: ${v.message}`
|
|
324
|
+
seen.add(obj)
|
|
325
|
+
if (v instanceof Map) {
|
|
326
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`)
|
|
327
|
+
return `Map(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
328
|
+
}
|
|
329
|
+
if (v instanceof Set) {
|
|
330
|
+
const items = [...v.values()].slice(0, 15).map(val => formatValue(val, seen))
|
|
331
|
+
return `Set(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(v)) {
|
|
334
|
+
const items = v.slice(0, 30).map(val => formatValue(val, seen))
|
|
335
|
+
return `[${items.join(', ')}${v.length > 30 ? ', …' : ''}]`
|
|
336
|
+
}
|
|
306
337
|
try {
|
|
307
338
|
return JSON.stringify(v)
|
|
308
339
|
}
|
|
309
340
|
catch {
|
|
310
|
-
|
|
341
|
+
const entries = Object.entries(v as Record<string, unknown>).slice(0, 15)
|
|
342
|
+
return `{${entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`).join(', ')}}`
|
|
311
343
|
}
|
|
312
344
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ActionArgs, ActionType } from '../core/action'
|
|
7
7
|
import type { ErrorEntry, LogEntry, NetworkRequest } from '../core/types'
|
|
8
|
-
import { INTERACTIVE, performAction } from '../core/action'
|
|
8
|
+
import { INTERACTIVE, performAction, runEval, withDialogGuard } from '../core/action'
|
|
9
9
|
|
|
10
10
|
declare global {
|
|
11
11
|
interface Window { __AIPEEK_STORES__?: Record<string, unknown> }
|
|
@@ -643,7 +643,10 @@ if (import.meta.hot) {
|
|
|
643
643
|
import.meta.hot.on('aipeek:action', async (msg: { id: number, type: ActionType, args: ActionArgs, requireVisible?: boolean }) => {
|
|
644
644
|
if (skip(msg))
|
|
645
645
|
return
|
|
646
|
-
|
|
646
|
+
// Guard against native alert/confirm/prompt freezing the probe (see withDialogGuard).
|
|
647
|
+
const { result, dialogs } = await withDialogGuard(() => performAction(msg.type, msg.args))
|
|
648
|
+
if (dialogs.length)
|
|
649
|
+
result.detail = `${result.detail ?? ''} [auto-dismissed ${dialogs.join('; ')}]`.trim()
|
|
647
650
|
// For mutating actions, settle the DOM then ship both the full UI tree and
|
|
648
651
|
// the compact screen projection — the caller skips a round-trip to /ui, and
|
|
649
652
|
// /chain uses the per-step screen so an interaction's every transition shows.
|
|
@@ -654,24 +657,11 @@ if (import.meta.hot) {
|
|
|
654
657
|
import.meta.hot!.send('aipeek:result', { id: msg.id, ...result })
|
|
655
658
|
})
|
|
656
659
|
|
|
657
|
-
// eval: run server-supplied code in the page
|
|
658
|
-
// code can `await` and use `return`; non-string results are JSON-stringified.
|
|
660
|
+
// eval: run server-supplied code in the page with auto-return (see runEval).
|
|
659
661
|
import.meta.hot.on('aipeek:eval', async (msg: { id: number, code: string, requireVisible?: boolean }) => {
|
|
660
662
|
if (skip(msg))
|
|
661
663
|
return
|
|
662
|
-
|
|
663
|
-
let value: string | undefined
|
|
664
|
-
let error: string | undefined
|
|
665
|
-
try {
|
|
666
|
-
// eslint-disable-next-line no-new-func
|
|
667
|
-
const fn = new Function(`return (async () => { ${msg.code} })()`)
|
|
668
|
-
const result = await fn()
|
|
669
|
-
value = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
670
|
-
}
|
|
671
|
-
catch (e) {
|
|
672
|
-
ok = false
|
|
673
|
-
error = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e)
|
|
674
|
-
}
|
|
664
|
+
const { ok, value, error } = await runEval(msg.code)
|
|
675
665
|
import.meta.hot!.send('aipeek:eval-result', { id: msg.id, ok, value, error })
|
|
676
666
|
})
|
|
677
667
|
|
package/src/core/action.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function resolveAction(type: string, args: ActionArgs): { valid: boolean,
|
|
|
57
57
|
|
|
58
58
|
// --- Browser-side execution (client.ts only) ---
|
|
59
59
|
|
|
60
|
-
export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable]'
|
|
60
|
+
export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable], [aria-label]'
|
|
61
61
|
|
|
62
62
|
function visibleText(el: Element): string {
|
|
63
63
|
const aria = el.getAttribute('aria-label')
|
|
@@ -114,6 +114,59 @@ export async function performAction(type: ActionType, args: ActionArgs): Promise
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export interface EvalResult { ok: boolean, value?: string, error?: string }
|
|
118
|
+
|
|
119
|
+
// Run server-supplied JS in the page with auto-return (Chrome-console / Node-REPL
|
|
120
|
+
// ergonomics): try the whole code as a single expression first so `qsa(...).length`
|
|
121
|
+
// or `1+1` yield a value without an explicit `return`. If that fails to *compile*
|
|
122
|
+
// (multi-statement, contains `return`, etc.) fall back to a plain statement block.
|
|
123
|
+
// A compile error only swaps the wrapper; a runtime throw surfaces as the error.
|
|
124
|
+
// undefined results are dropped (no value); objects are JSON-stringified.
|
|
125
|
+
export async function runEval(code: string): Promise<EvalResult> {
|
|
126
|
+
try {
|
|
127
|
+
let fn: () => Promise<unknown>
|
|
128
|
+
try {
|
|
129
|
+
// eslint-disable-next-line no-new-func
|
|
130
|
+
fn = new Function(`return (async () => (${code}))()`) as () => Promise<unknown>
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// eslint-disable-next-line no-new-func
|
|
134
|
+
fn = new Function(`return (async () => { ${code} })()`) as () => Promise<unknown>
|
|
135
|
+
}
|
|
136
|
+
const result = await fn()
|
|
137
|
+
const value = result === undefined ? undefined : typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
138
|
+
return { ok: true, value }
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return { ok: false, error: e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Native alert/confirm/prompt are *synchronous* and freeze the whole JS thread until
|
|
146
|
+
// a human dismisses them — which deadlocks the probe (it runs on that same thread, so
|
|
147
|
+
// the HMR channel can never answer and every curl times out). A click that hits a
|
|
148
|
+
// `copy-to-clipboard` fallback or a `confirm("delete?")` would hang aipeek forever.
|
|
149
|
+
// So we stub them for the duration of `fn`: auto-answer (confirm→true, prompt→default,
|
|
150
|
+
// alert→noop) and return what was suppressed so the caller can report it. Always
|
|
151
|
+
// restored in finally — the page's own dialogs work again after the action settles.
|
|
152
|
+
export async function withDialogGuard<T>(fn: () => Promise<T>): Promise<{ result: T, dialogs: string[] }> {
|
|
153
|
+
const realAlert = window.alert
|
|
154
|
+
const realConfirm = window.confirm
|
|
155
|
+
const realPrompt = window.prompt
|
|
156
|
+
const dialogs: string[] = []
|
|
157
|
+
window.alert = (m?: unknown) => { dialogs.push(`alert: ${String(m ?? '')}`.slice(0, 80)) }
|
|
158
|
+
window.confirm = (m?: unknown) => { dialogs.push(`confirm→true: ${String(m ?? '')}`.slice(0, 80)); return true }
|
|
159
|
+
window.prompt = (m?: unknown, d?: string) => { dialogs.push(`prompt→default: ${String(m ?? '')}`.slice(0, 80)); return d ?? '' }
|
|
160
|
+
try {
|
|
161
|
+
return { result: await fn(), dialogs }
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
window.alert = realAlert
|
|
165
|
+
window.confirm = realConfirm
|
|
166
|
+
window.prompt = realPrompt
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
// Click like a human, not like el.click(). A real click is a *position* the browser
|
|
118
171
|
// hit-tests, then a full event sequence at that point — hover, pointerdown/mousedown,
|
|
119
172
|
// browser-decided focus, pointerup/mouseup, click. Two things matter that el.click()
|
package/src/core/compact.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompactState, ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
|
|
2
|
-
import { compactUrl, truncate } from './util'
|
|
2
|
+
import { appStackFrames, compactUrl, formatValue, truncate } from './util'
|
|
3
3
|
|
|
4
4
|
const SLOW_THRESHOLD = 1000
|
|
5
5
|
|
|
@@ -262,26 +262,14 @@ export function compactErrors(errors: ErrorEntry[]): string {
|
|
|
262
262
|
for (const err of seen.values()) {
|
|
263
263
|
lines.push(err.message)
|
|
264
264
|
if (err.stack) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
lines.push(` at ${frame}`)
|
|
268
|
-
}
|
|
265
|
+
for (const frame of appStackFrames(err.stack, 5))
|
|
266
|
+
lines.push(` ${frame}`)
|
|
269
267
|
}
|
|
270
268
|
}
|
|
271
269
|
|
|
272
270
|
return lines.join('\n')
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
function filterStack(stack: string): string[] {
|
|
276
|
-
return stack
|
|
277
|
-
.split('\n')
|
|
278
|
-
.map(l => l.trim())
|
|
279
|
-
.filter(l => l.startsWith('at '))
|
|
280
|
-
.map(l => l.slice(3))
|
|
281
|
-
.filter(l => !l.includes('node_modules') && !l.includes('<anonymous>'))
|
|
282
|
-
.slice(0, 5)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
273
|
// --- State ---
|
|
286
274
|
|
|
287
275
|
export function compactState(state: Record<string, unknown>): string {
|
|
@@ -293,7 +281,7 @@ export function compactState(state: Record<string, unknown>): string {
|
|
|
293
281
|
lines.push(`${name}:`)
|
|
294
282
|
if (typeof value === 'object' && value !== null) {
|
|
295
283
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
296
|
-
lines.push(` ${k}: ${formatValue(v)}`)
|
|
284
|
+
lines.push(` ${k}: ${truncate(formatValue(v), 120)}`)
|
|
297
285
|
}
|
|
298
286
|
}
|
|
299
287
|
else {
|
|
@@ -303,20 +291,6 @@ export function compactState(state: Record<string, unknown>): string {
|
|
|
303
291
|
return lines.join('\n')
|
|
304
292
|
}
|
|
305
293
|
|
|
306
|
-
function formatValue(v: unknown): string {
|
|
307
|
-
if (v === null || v === undefined)
|
|
308
|
-
return String(v)
|
|
309
|
-
if (typeof v === 'string')
|
|
310
|
-
return v
|
|
311
|
-
if (typeof v === 'number' || typeof v === 'boolean')
|
|
312
|
-
return String(v)
|
|
313
|
-
if (typeof v === 'object') {
|
|
314
|
-
const s = JSON.stringify(v)
|
|
315
|
-
return s.length > 120 ? `${s.slice(0, 120)}…` : s
|
|
316
|
-
}
|
|
317
|
-
return String(v)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
294
|
// --- Main ---
|
|
321
295
|
|
|
322
296
|
export function compact(raw: RawState): CompactState {
|
package/src/core/detail.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
|
|
2
2
|
import { compactUI } from './compact'
|
|
3
|
-
import { truncate } from './util'
|
|
3
|
+
import { appStackFrames, formatValue, truncate } from './util'
|
|
4
4
|
|
|
5
5
|
export function detail(raw: RawState, section: string, index: string | undefined, full: boolean): string | null {
|
|
6
6
|
switch (section) {
|
|
@@ -130,15 +130,10 @@ function detailError(errors: ErrorEntry[], index: string | undefined, full: bool
|
|
|
130
130
|
|
|
131
131
|
const lines = [err.message]
|
|
132
132
|
if (err.stack) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
if (appFrames.length)
|
|
138
|
-
lines.push(...appFrames)
|
|
139
|
-
const totalApp = err.stack.split('\n').filter(l => l.trim().startsWith('at ') && !l.includes('node_modules')).length
|
|
140
|
-
if (totalApp > 3)
|
|
141
|
-
lines.push(` ... ${totalApp - 3} more app frames`)
|
|
133
|
+
const all = appStackFrames(err.stack, Infinity)
|
|
134
|
+
lines.push(...all.slice(0, 3))
|
|
135
|
+
if (all.length > 3)
|
|
136
|
+
lines.push(` ... ${all.length - 3} more app frames`)
|
|
142
137
|
}
|
|
143
138
|
if (err.line != null)
|
|
144
139
|
lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
|
|
@@ -159,10 +154,10 @@ function detailState(state: Record<string, unknown>, name: string | undefined, f
|
|
|
159
154
|
const value = state[name]
|
|
160
155
|
if (full) {
|
|
161
156
|
try {
|
|
162
|
-
return JSON.stringify(value, null, 2)
|
|
157
|
+
return JSON.stringify(value, null, 2) ?? formatValue(value)
|
|
163
158
|
}
|
|
164
159
|
catch {
|
|
165
|
-
return
|
|
160
|
+
return formatValue(value) // 循环引用 / Error / Map → 不再 {}
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
163
|
if (typeof value !== 'object' || value === null)
|
|
@@ -182,20 +177,9 @@ function isArraySentinel(v: string): boolean {
|
|
|
182
177
|
}
|
|
183
178
|
|
|
184
179
|
function formatSummaryValue(v: unknown): string {
|
|
185
|
-
if (v ===
|
|
186
|
-
return
|
|
187
|
-
|
|
188
|
-
if (isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
|
|
189
|
-
return v
|
|
190
|
-
return v.length > 80 ? `${v.slice(0, 80)}…` : v
|
|
191
|
-
}
|
|
192
|
-
if (typeof v === 'number' || typeof v === 'boolean')
|
|
193
|
-
return String(v)
|
|
194
|
-
if (typeof v === 'object') {
|
|
195
|
-
const s = JSON.stringify(v)
|
|
196
|
-
return s.length > 80 ? `${s.slice(0, 80)}…` : s
|
|
197
|
-
}
|
|
198
|
-
return String(v)
|
|
180
|
+
if (typeof v === 'string' && isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
|
|
181
|
+
return v
|
|
182
|
+
return truncate(formatValue(v), 80)
|
|
199
183
|
}
|
|
200
184
|
|
|
201
185
|
// --- JSON schema fallback ---
|
package/src/core/util.ts
CHANGED
|
@@ -2,6 +2,59 @@ export function truncate(s: string, max: number): string {
|
|
|
2
2
|
return s.length > max ? `${s.slice(0, max)}…` : s
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
// unknown → 人类可读字符串。穷举 JS 类型,让 JSON.stringify 的「漏网类型 → {}」失败模式不可能出现。
|
|
6
|
+
// 返回未截断字符串——截断由调用方按各自 max 处理。
|
|
7
|
+
export function formatValue(v: unknown, seen: Set<object> = new Set()): string {
|
|
8
|
+
if (v === null || v === undefined)
|
|
9
|
+
return String(v)
|
|
10
|
+
const t = typeof v
|
|
11
|
+
if (t === 'string')
|
|
12
|
+
return v as string
|
|
13
|
+
if (t === 'number' || t === 'boolean' || t === 'bigint')
|
|
14
|
+
return String(v)
|
|
15
|
+
if (t === 'symbol')
|
|
16
|
+
return (v as symbol).toString()
|
|
17
|
+
if (t === 'function')
|
|
18
|
+
return `[Function: ${(v as { name?: string }).name || 'anonymous'}]`
|
|
19
|
+
// 此后 v 是 object
|
|
20
|
+
const obj = v as object
|
|
21
|
+
if (seen.has(obj))
|
|
22
|
+
return '[Circular]'
|
|
23
|
+
if (v instanceof Error)
|
|
24
|
+
return v.stack || `${v.name}: ${v.message}`
|
|
25
|
+
seen.add(obj)
|
|
26
|
+
if (v instanceof Map) {
|
|
27
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`)
|
|
28
|
+
return `Map(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
29
|
+
}
|
|
30
|
+
if (v instanceof Set) {
|
|
31
|
+
const items = [...v.values()].slice(0, 15).map(val => formatValue(val, seen))
|
|
32
|
+
return `Set(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(v)) {
|
|
35
|
+
const items = v.slice(0, 30).map(val => formatValue(val, seen))
|
|
36
|
+
return `[${items.join(', ')}${v.length > 30 ? ', …' : ''}]`
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(v)
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 循环引用 / getter 抛错 → 手动遍历可枚举键,循环处标 [Circular]
|
|
43
|
+
const entries = Object.entries(v as Record<string, unknown>).slice(0, 15)
|
|
44
|
+
const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`)
|
|
45
|
+
return `{${parts.join(', ')}}`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// stack → 应用栈帧(去 node_modules / <anonymous>),保留 `at ` 前缀。max 与溢出提示由调用方决定。
|
|
50
|
+
export function appStackFrames(stack: string, max: number): string[] {
|
|
51
|
+
return stack
|
|
52
|
+
.split('\n')
|
|
53
|
+
.map(l => l.trim())
|
|
54
|
+
.filter(l => l.startsWith('at ') && !l.includes('node_modules') && !l.includes('<anonymous>'))
|
|
55
|
+
.slice(0, max)
|
|
56
|
+
}
|
|
57
|
+
|
|
5
58
|
// pathname only; pass `search` to append a truncated query string
|
|
6
59
|
export function compactUrl(url: string, search?: number): string {
|
|
7
60
|
try {
|
package/src/server/plugin.ts
CHANGED
|
@@ -21,6 +21,12 @@ function readBody(req: { on: (e: string, cb: (c: unknown) => void) => void }): P
|
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// 所有端点都回文本响应——writeHead(Content-Type) + end 二连写了 15 遍(且有 2 处漏 charset)。收敛成一处。
|
|
25
|
+
function send(res: { writeHead: (s: number, h: Record<string, string>) => void, end: (b: string) => void }, status: number, body: string) {
|
|
26
|
+
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
27
|
+
res.end(body)
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
25
31
|
// client/core sources are read at runtime (client.ts served via Vite transform,
|
|
26
32
|
// client-patch.ts compiled by esbuild). Two layouts: source consumption
|
|
@@ -331,13 +337,11 @@ export function aipeekPlugin(): Plugin {
|
|
|
331
337
|
if (!code && req.method === 'POST')
|
|
332
338
|
code = await readBody(req)
|
|
333
339
|
if (!code) {
|
|
334
|
-
res
|
|
335
|
-
res.end('eval needs ?code= or a POST body')
|
|
340
|
+
send(res, 400, 'eval needs ?code= or a POST body')
|
|
336
341
|
return
|
|
337
342
|
}
|
|
338
343
|
const r = await evalInClient(code)
|
|
339
|
-
res
|
|
340
|
-
res.end(r.ok ? (r.value ?? 'undefined') : `error: ${r.error}`)
|
|
344
|
+
send(res, r.ok ? 200 : 422, r.ok ? (r.value ?? 'undefined') : `error: ${r.error}`)
|
|
341
345
|
return
|
|
342
346
|
}
|
|
343
347
|
|
|
@@ -347,16 +351,14 @@ export function aipeekPlugin(): Plugin {
|
|
|
347
351
|
url.searchParams.get('scope') || undefined,
|
|
348
352
|
url.searchParams.get('sel') || undefined,
|
|
349
353
|
)
|
|
350
|
-
res
|
|
351
|
-
res.end(dom || '(empty)')
|
|
354
|
+
send(res, 200, dom || '(empty)')
|
|
352
355
|
return
|
|
353
356
|
}
|
|
354
357
|
|
|
355
358
|
// /__aipeek/screen — state-machine projection {view, modal, focus, knobs}
|
|
356
359
|
if (parts[0] === 'screen') {
|
|
357
360
|
const screen = await collectScreenFromClient()
|
|
358
|
-
res
|
|
359
|
-
res.end(screen || '(empty)')
|
|
361
|
+
send(res, 200, screen || '(empty)')
|
|
360
362
|
return
|
|
361
363
|
}
|
|
362
364
|
|
|
@@ -372,8 +374,7 @@ export function aipeekPlugin(): Plugin {
|
|
|
372
374
|
throw new Error('body must be a JSON array')
|
|
373
375
|
}
|
|
374
376
|
catch (e) {
|
|
375
|
-
res
|
|
376
|
-
res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`)
|
|
377
|
+
send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`)
|
|
377
378
|
return
|
|
378
379
|
}
|
|
379
380
|
lastRaw = null
|
|
@@ -402,8 +403,7 @@ export function aipeekPlugin(): Plugin {
|
|
|
402
403
|
break
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
|
-
res
|
|
406
|
-
res.end(lastUi ? `${lines.join('\n')}\n\n--- ui after ---\n${lastUi}` : lines.join('\n'))
|
|
406
|
+
send(res, allOk ? 200 : 422, lastUi ? `${lines.join('\n')}\n\n--- ui after ---\n${lastUi}` : lines.join('\n'))
|
|
407
407
|
return
|
|
408
408
|
}
|
|
409
409
|
|
|
@@ -420,8 +420,7 @@ export function aipeekPlugin(): Plugin {
|
|
|
420
420
|
}
|
|
421
421
|
const check = resolveAction(parts[0], args)
|
|
422
422
|
if (!check.valid) {
|
|
423
|
-
res
|
|
424
|
-
res.end(check.error)
|
|
423
|
+
send(res, 400, check.error ?? 'invalid action')
|
|
425
424
|
return
|
|
426
425
|
}
|
|
427
426
|
const result = await sendAction(parts[0], args)
|
|
@@ -432,13 +431,11 @@ export function aipeekPlugin(): Plugin {
|
|
|
432
431
|
const name = q.get('out') || `shot-${result.dataUrl.length}.png`
|
|
433
432
|
const file = resolve(dir, name)
|
|
434
433
|
writeFileSync(file, Buffer.from(result.dataUrl.split(',')[1], 'base64'))
|
|
435
|
-
res
|
|
436
|
-
res.end(`saved: ${file}`)
|
|
434
|
+
send(res, 200, `saved: ${file}`)
|
|
437
435
|
return
|
|
438
436
|
}
|
|
439
|
-
res.writeHead(result.ok ? 200 : 422, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
440
437
|
const head = result.ok ? (result.detail || 'ok') : `${result.error}${result.detail ? `\n\nclickable: ${result.detail}` : ''}`
|
|
441
|
-
res.
|
|
438
|
+
send(res, result.ok ? 200 : 422, result.ui ? `${head}\n\n--- ui after ---\n${result.ui}` : head)
|
|
442
439
|
return
|
|
443
440
|
}
|
|
444
441
|
|
|
@@ -448,8 +445,7 @@ export function aipeekPlugin(): Plugin {
|
|
|
448
445
|
lastRaw = raw
|
|
449
446
|
const result = check(raw)
|
|
450
447
|
const output = emitCheck(result)
|
|
451
|
-
res
|
|
452
|
-
res.end(output)
|
|
448
|
+
send(res, result.pass ? 200 : 417, output)
|
|
453
449
|
return
|
|
454
450
|
}
|
|
455
451
|
|
|
@@ -459,12 +455,10 @@ export function aipeekPlugin(): Plugin {
|
|
|
459
455
|
lastRaw = await collectFromClient()
|
|
460
456
|
const result = detail(lastRaw, parts[0], parts[1], full)
|
|
461
457
|
if (result !== null) {
|
|
462
|
-
res
|
|
463
|
-
res.end(result)
|
|
458
|
+
send(res, 200, result)
|
|
464
459
|
return
|
|
465
460
|
}
|
|
466
|
-
res
|
|
467
|
-
res.end(`not found: ${parts.join('/')}`)
|
|
461
|
+
send(res, 404, `not found: ${parts.join('/')}`)
|
|
468
462
|
return
|
|
469
463
|
}
|
|
470
464
|
|
|
@@ -473,19 +467,14 @@ export function aipeekPlugin(): Plugin {
|
|
|
473
467
|
lastRaw = raw
|
|
474
468
|
if (full) {
|
|
475
469
|
const compacted = compact(raw)
|
|
476
|
-
|
|
477
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
478
|
-
res.end(output)
|
|
470
|
+
send(res, 200, emit(compacted))
|
|
479
471
|
}
|
|
480
472
|
else {
|
|
481
|
-
|
|
482
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
483
|
-
res.end(output)
|
|
473
|
+
send(res, 200, emitSummary(raw))
|
|
484
474
|
}
|
|
485
475
|
}
|
|
486
476
|
catch (err) {
|
|
487
|
-
res
|
|
488
|
-
res.end(err instanceof Error ? err.message : 'unknown error')
|
|
477
|
+
send(res, 504, err instanceof Error ? err.message : 'unknown error')
|
|
489
478
|
}
|
|
490
479
|
})
|
|
491
480
|
},
|