@evandu/super-pi 0.1.137 → 0.1.139
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/agents/super-pi-knowledge/index.ts +11 -13
- package/agents/super-pi-memory/index.ts +11 -5
- package/bin/super-pi.js +7 -11
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -6
- package/dist/client/assets/{CodeMirrorEditor-CkA_5D5r.js → CodeMirrorEditor-H7SHGhdS.js} +2 -2
- package/dist/client/assets/{dist-DcdzfM-n.js → dist-2XXLO-IZ.js} +1 -1
- package/dist/client/assets/{dist-ufRHdQ8G.js → dist-BEM6tttY.js} +1 -1
- package/dist/client/assets/{dist-9Fm8XKu4.js → dist-BfUeRJKr.js} +1 -1
- package/dist/client/assets/{dist-CrxmpY93.js → dist-BokzcFLE.js} +1 -1
- package/dist/client/assets/{dist-BfEmujEC.js → dist-BoucVzKA.js} +1 -1
- package/dist/client/assets/{dist-B02Erd0t.js → dist-BxTGjU84.js} +1 -1
- package/dist/client/assets/{dist-DGhdkud4.js → dist-C-MUXK_t.js} +1 -1
- package/dist/client/assets/{dist-k6bOz9RS.js → dist-C8NllalC.js} +1 -1
- package/dist/client/assets/{dist-C2tSd-AY.js → dist-CSdzvQnf.js} +1 -1
- package/dist/client/assets/{dist-BySjislr.js → dist-CUNctZ5v.js} +1 -1
- package/dist/client/assets/{dist-B3Mfuv1D.js → dist-CZ9Krsno.js} +1 -1
- package/dist/client/assets/{dist-DbovsYeH.js → dist-C_ftUQZN.js} +1 -1
- package/dist/client/assets/{dist-j9X7j381.js → dist-CuMnZOGE.js} +1 -1
- package/dist/client/assets/{dist-D-LtG92p.js → dist-CukPKrEn.js} +1 -1
- package/dist/client/assets/{dist-b9CfAgzG.js → dist-D3Ta0y4e.js} +1 -1
- package/dist/client/assets/{dist-BsqzrZ5R.js → dist-DJ8SwQl_.js} +1 -1
- package/dist/client/assets/{dist-BqGjijI6.js → dist-DLKVFZhO.js} +1 -1
- package/dist/client/assets/{dist-MC2XuZgq.js → dist-Jw-GQ-re.js} +1 -1
- package/dist/client/assets/{dist-D_umYm0o.js → dist-NlopeJ5R.js} +1 -1
- package/dist/client/assets/{dist-CsNytPug.js → dist-POoQ4j29.js} +1 -1
- package/dist/client/assets/{dist-DdVs8Y0X.js → dist-cP2vnTP_.js} +1 -1
- package/dist/client/assets/{dist-DL4Q05ts.js → dist-zUJqdbfo.js} +1 -1
- package/dist/client/assets/{index-DVmE8H4Y.js → index-B32VZVYx.js} +13 -13
- package/dist/client/assets/index-Bdym8BPo.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.d.ts +13 -4
- package/dist/server/application/conversation-service.js +112 -61
- package/dist/server/application/run-service.d.ts +13 -0
- package/dist/server/application/run-service.js +255 -69
- package/dist/server/application/scheduled-task-service.js +8 -1
- package/dist/server/application/workspace-service.js +16 -1
- package/dist/server/feishu-cli.js +15 -4
- package/dist/server/file-index.d.ts +30 -0
- package/dist/server/file-index.js +49 -4
- package/dist/server/files.d.ts +21 -0
- package/dist/server/files.js +38 -0
- package/dist/server/http-server.js +1455 -1412
- package/dist/server/index.js +3 -4
- package/dist/server/logging.js +56 -3
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +1 -0
- package/dist/server/pi-session.d.ts +118 -0
- package/dist/server/pi-session.js +132 -10
- package/dist/server/runtime.d.ts +47 -0
- package/dist/server/runtime.js +183 -84
- package/dist/server/store.d.ts +44 -1
- package/dist/server/store.js +114 -12
- package/dist/server/subagent-runs.js +18 -7
- package/dist/server/subagents.js +7 -0
- package/dist/server/tool-runner.js +13 -4
- package/dist/server/types.d.ts +27 -0
- package/dist/server/ws-server.d.ts +34 -5
- package/dist/server/ws-server.js +170 -46
- package/package.json +15 -1
- package/scripts/diagnose.mjs +161 -58
- package/dist/client/assets/index-FxTrUapG.css +0 -1
package/scripts/diagnose.mjs
CHANGED
|
@@ -1,31 +1,154 @@
|
|
|
1
|
-
// `super_pi diagnose
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// many seconds, here are three of them" — and answering it from the developer's machine was wrong
|
|
5
|
-
// twice, because the developer machine is fast and a wall clock does not transfer. So the measurement
|
|
6
|
-
// has to run where the problem is, which means it has to ship with the product.
|
|
7
|
-
//
|
|
8
|
-
// WHAT IT PRINTS, AND WHY THAT SHAPE. Three spans, because they belong to DIFFERENT OWNERS and
|
|
9
|
-
// conflating them is how two wrong root causes got written down:
|
|
10
|
-
//
|
|
11
|
-
// 1. picker data -> Super Pi (the catalog read)
|
|
12
|
-
// 2. send -> waiting -> Super Pi (session build + first prompt)
|
|
13
|
-
// 3. waiting -> text -> the MODEL (not ours, and it grows with the conversation)
|
|
14
|
-
//
|
|
15
|
-
// And under each, the STRUCTURE — how many Pi processes were started, how many sessions were built,
|
|
16
|
-
// how many were reused. Structure is the part that survives the difference between a fast machine and a
|
|
17
|
-
// slow one; seconds are not. If the process count is zero and a span is still long, the time is not in
|
|
18
|
-
// Super Pi, and this says so rather than leaving it to be guessed at.
|
|
1
|
+
// `super_pi diagnose`: local runtime probe, or --url read-only network probe from THIS machine.
|
|
2
|
+
// Each report names what was observed. A loopback span cannot exonerate a remote transport, a
|
|
3
|
+
// process count cannot assign latency, and stored JSON is not compressed wire traffic.
|
|
19
4
|
import { spawn } from "node:child_process";
|
|
20
5
|
import { mkdtempSync, rmSync, existsSync, statSync, readFileSync } from "node:fs";
|
|
21
6
|
import { homedir, tmpdir } from "node:os";
|
|
22
7
|
import { join } from "node:path";
|
|
23
8
|
import { pathToFileURL } from "node:url";
|
|
24
9
|
import { createRequire } from "node:module";
|
|
10
|
+
import { request as httpRequest } from "node:http";
|
|
11
|
+
import { request as httpsRequest } from "node:https";
|
|
12
|
+
import { hostname } from "node:os";
|
|
13
|
+
import { gunzipSync, inflateSync, brotliDecompressSync } from "node:zlib";
|
|
25
14
|
|
|
26
|
-
/**
|
|
15
|
+
/** Compatibility requirement, not evidence of the cause of any measured delay. */
|
|
27
16
|
const PI_NODE_REQUIREMENT = "22.19.0";
|
|
28
17
|
|
|
18
|
+
export function parseDiagnoseArgs(args) {
|
|
19
|
+
const result = {};
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (/^\d+$/.test(arg) && result.turns === undefined) { result.turns = Number(arg); continue; }
|
|
23
|
+
const [name, ...inline] = arg.split("=");
|
|
24
|
+
const key = { "--url": "url", "--workspace": "workspaceId", "--conversation": "conversationId" }[name];
|
|
25
|
+
if (!key) throw new Error(`Unknown diagnose option: ${name}`);
|
|
26
|
+
const value = inline.length ? inline.join("=") : args[++i];
|
|
27
|
+
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${name}`);
|
|
28
|
+
result[key] = value;
|
|
29
|
+
}
|
|
30
|
+
if (!result.url && (result.workspaceId || result.conversationId)) throw new Error("--workspace/--conversation require --url");
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Raw encoded HTTP body bytes: fetch() transparently decompresses and cannot measure this. */
|
|
35
|
+
function measureHttp(url, headers, timeoutMs) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const start = performance.now();
|
|
38
|
+
const req = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { headers, agent: false }, response => {
|
|
39
|
+
const headersMs = performance.now() - start;
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let encodedBodyBytes = 0;
|
|
42
|
+
response.on("data", chunk => {
|
|
43
|
+
encodedBodyBytes += chunk.length;
|
|
44
|
+
if (encodedBodyBytes > 64 * 1024 * 1024) req.destroy(new Error("HTTP sample exceeds 64 MiB"));
|
|
45
|
+
else chunks.push(chunk);
|
|
46
|
+
});
|
|
47
|
+
response.on("error", reject);
|
|
48
|
+
response.on("end", () => {
|
|
49
|
+
try {
|
|
50
|
+
const completeMs = performance.now() - start;
|
|
51
|
+
const encoded = Buffer.concat(chunks);
|
|
52
|
+
const encoding = response.headers["content-encoding"] ?? "identity";
|
|
53
|
+
const opts = { maxOutputLength: 64 * 1024 * 1024 };
|
|
54
|
+
const body = encoding === "gzip" ? gunzipSync(encoded, opts) : encoding === "deflate" ? inflateSync(encoded, opts)
|
|
55
|
+
: encoding === "br" ? brotliDecompressSync(encoded, opts) : encoded;
|
|
56
|
+
if (response.statusCode !== 200) throw new Error(`HTTP ${response.statusCode}; redirects are not followed`);
|
|
57
|
+
resolve({ headersMs, bodyMs: completeMs - headersMs, completeMs, encodedBodyBytes, decodedBodyBytes: body.length, encoding, body: JSON.parse(body.toString()) });
|
|
58
|
+
} catch (error) { reject(error); }
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
const timer = setTimeout(() => req.destroy(new Error("HTTP probe deadline exceeded")), timeoutMs);
|
|
62
|
+
req.on("close", () => clearTimeout(timer));
|
|
63
|
+
req.on("error", reject);
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read-only probe from THIS computer to an existing server. Never sends application commands. */
|
|
69
|
+
export async function diagnoseRemote({ url, workspaceId, conversationId, token = process.env.SUPER_PI_ACCESS_TOKEN, timeoutMs = 15000 } = {}) {
|
|
70
|
+
const target = new URL(url);
|
|
71
|
+
if (!["http:", "https:"].includes(target.protocol) || target.username || target.password || target.search || target.hash || target.pathname !== "/") {
|
|
72
|
+
throw new Error("--url must be an http(s) origin without credentials, path, query or fragment");
|
|
73
|
+
}
|
|
74
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(target.hostname);
|
|
75
|
+
const result = { mode: "remote", origin: `${hostname()} (${process.platform})`, target: target.origin, loopback };
|
|
76
|
+
const headers = { "accept-encoding": "gzip", ...(token ? { authorization: `Bearer ${token}` } : {}) };
|
|
77
|
+
try {
|
|
78
|
+
const { body: health, ...measurement } = await measureHttp(new URL("/api/health", target), headers, timeoutMs);
|
|
79
|
+
if (health.service !== "super-pi") throw new Error("Target is not a Super Pi health endpoint");
|
|
80
|
+
result.health = measurement;
|
|
81
|
+
const { WebSocket } = await import("ws");
|
|
82
|
+
const endpoint = new URL("/ws", target);
|
|
83
|
+
endpoint.protocol = target.protocol === "https:" ? "wss:" : "ws:";
|
|
84
|
+
endpoint.searchParams.set("state", "focused");
|
|
85
|
+
if (conversationId) endpoint.searchParams.set("conversationId", conversationId);
|
|
86
|
+
result.websocket = await new Promise((resolve, reject) => {
|
|
87
|
+
const started = performance.now();
|
|
88
|
+
const socket = new WebSocket(endpoint, { headers, handshakeTimeout: timeoutMs, maxPayload: 64 * 1024 * 1024 });
|
|
89
|
+
const sample = { decodedMessageBytes: 0, messages: 0, pingRttMs: [] };
|
|
90
|
+
let first = false, pingStart;
|
|
91
|
+
const timer = setTimeout(() => fail(new Error("WebSocket probe deadline exceeded")), timeoutMs);
|
|
92
|
+
function fail(error) { clearTimeout(timer); socket.terminate(); reject(error); }
|
|
93
|
+
socket.on("error", fail);
|
|
94
|
+
socket.once("close", () => { if (sample.pingRttMs.length !== 3) fail(new Error("WebSocket closed before probe completed")); });
|
|
95
|
+
socket.on("open", () => { sample.upgradeMs = performance.now() - started; sample.compression = socket.extensions || "none"; });
|
|
96
|
+
socket.on("message", raw => {
|
|
97
|
+
sample.messages++;
|
|
98
|
+
sample.decodedMessageBytes += raw.length;
|
|
99
|
+
try {
|
|
100
|
+
const event = JSON.parse(raw.toString());
|
|
101
|
+
if (!first && event.type === "state") {
|
|
102
|
+
first = true;
|
|
103
|
+
sample.firstStateMs = performance.now() - started;
|
|
104
|
+
sample.stateJsonBytes = raw.length;
|
|
105
|
+
pingStart = performance.now(); socket.ping("diagnose-0");
|
|
106
|
+
}
|
|
107
|
+
} catch (error) { fail(error); }
|
|
108
|
+
});
|
|
109
|
+
socket.on("pong", raw => {
|
|
110
|
+
if (raw.toString() !== `diagnose-${sample.pingRttMs.length}`) return;
|
|
111
|
+
sample.pingRttMs.push(performance.now() - pingStart);
|
|
112
|
+
if (sample.pingRttMs.length < 3) { pingStart = performance.now(); socket.ping(`diagnose-${sample.pingRttMs.length}`); return; }
|
|
113
|
+
// Includes HTTP upgrade, WS headers, catalogues and pongs, before closing. This is NOT an
|
|
114
|
+
// individual state frame size, nor Ethernet/TCP/TLS overhead. Label the counter honestly.
|
|
115
|
+
sample.socketInputBytes = socket._socket.bytesRead;
|
|
116
|
+
sample.sampleMs = performance.now() - started;
|
|
117
|
+
clearTimeout(timer); socket.terminate(); resolve(sample);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
if (workspaceId) {
|
|
121
|
+
const indexUrl = new URL("/api/files/index", target);
|
|
122
|
+
indexUrl.searchParams.set("workspaceId", workspaceId);
|
|
123
|
+
const { body: _index, ...index } = await measureHttp(indexUrl, headers, timeoutMs);
|
|
124
|
+
result.fileIndex = index;
|
|
125
|
+
}
|
|
126
|
+
} catch (error) { result.error = error instanceof Error ? error.message : String(error); }
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderRemoteReport(result) {
|
|
131
|
+
const lines = ["Super Pi 远程链路诊断(只读)", ` 探针位置:${result.origin}`, ` 目标:${result.target}`,
|
|
132
|
+
` 路径:${result.loopback ? "回环地址,未覆盖其他电脑的访问路径" : "此电脑到目标;请在发生问题的客户端电脑执行"}`];
|
|
133
|
+
const http = (name, sample) => {
|
|
134
|
+
if (!sample) return;
|
|
135
|
+
lines.push(` ${name}: 收到响应头 ${sample.headersMs.toFixed(1)}ms,响应体传输 ${sample.bodyMs.toFixed(1)}ms,总计 ${sample.completeMs.toFixed(1)}ms`);
|
|
136
|
+
lines.push(` 编码后响应体 ${sample.encodedBodyBytes} B,解码后 ${sample.decodedBodyBytes} B,编码 ${sample.encoding}`);
|
|
137
|
+
};
|
|
138
|
+
http("health", result.health);
|
|
139
|
+
if (result.websocket) {
|
|
140
|
+
const ws = result.websocket;
|
|
141
|
+
lines.push(` WS: upgrade ${ws.upgradeMs.toFixed(1)}ms,首个 focused state ${ws.firstStateMs.toFixed(1)}ms,state JSON ${ws.stateJsonBytes} B`);
|
|
142
|
+
lines.push(` socket 输入 ${ws.socketInputBytes} B,解码后消息合计 ${ws.decodedMessageBytes} B,压缩 ${ws.compression}`);
|
|
143
|
+
lines.push(` Ping RTT: ${ws.pingRttMs.map(n => n.toFixed(1) + "ms").join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
http("文件索引(显式指定 workspace)", result.fileIndex);
|
|
146
|
+
if (result.error) lines.push(` 失败:${result.error}`);
|
|
147
|
+
lines.push(" 范围:响应头等待包含网络与服务端处理,不能当作纯服务端耗时。socket 输入含 upgrade/WS 帧,未计 IP/TCP/TLS 开销。");
|
|
148
|
+
lines.push(" 未测:浏览器点击/解析/绘制、创建/停止/撤回、模型调用;未创建会话或发送业务命令。小样本不能推算链路带宽。");
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
29
152
|
/**
|
|
30
153
|
* Compares two dotted versions.
|
|
31
154
|
*
|
|
@@ -43,22 +166,9 @@ function atLeast(actual, required) {
|
|
|
43
166
|
}
|
|
44
167
|
|
|
45
168
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* data directory, so its state is empty and its writes are instant - which is exactly why it could
|
|
50
|
-
* not see the thing it was looking for. A real server holds every conversation a person has ever had,
|
|
51
|
-
* and a message used to cost a full write of all of it before the browser was told the turn had
|
|
52
|
-
* started. The size of that state is invisible from a fresh install and decisive on a real one, so it
|
|
53
|
-
* is reported from the REAL data directory, read-only, and the write is measured on a COPY.
|
|
54
|
-
*
|
|
55
|
-
* THE NUMBER THAT MATTERS IS THE STATE, NOT THE FILE. An earlier version of this report called the
|
|
56
|
-
* SQLite FILE size "the state", and that is a different number: the file holds the base snapshot AND
|
|
57
|
-
* the incremental overlay tables, and the overlay is never pruned, so the file grows with every write
|
|
58
|
-
* while the state the BROWSER RECEIVES does not grow the same way. The number that costs time is the
|
|
59
|
-
* one that is cloned (`store.snapshot()`), serialised (`JSON.stringify`) and parsed (`JSON.parse` in
|
|
60
|
-
* the browser) on every state broadcast - so that is what is measured here, by rebuilding the state
|
|
61
|
-
* out of the database and measuring its JSON.
|
|
169
|
+
* Read-only storage context. A fresh throwaway server cannot reveal real history size. Rebuild
|
|
170
|
+
* logical state via the repository layout, but never label that JSON as actual browser traffic:
|
|
171
|
+
* focused projection, per-message compression and transport overhead produce different sizes.
|
|
62
172
|
*/
|
|
63
173
|
function measureRealState(dataDir) {
|
|
64
174
|
const result = { path: "", bytes: 0, fileBytes: 0, conversations: 0, messages: 0, fullWriteMs: undefined };
|
|
@@ -72,20 +182,18 @@ function measureRealState(dataDir) {
|
|
|
72
182
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
73
183
|
result.conversations = parsed.conversations?.length ?? 0;
|
|
74
184
|
result.messages = (parsed.conversations ?? []).reduce((sum, c) => sum + (c.messages?.length ?? 0), 0);
|
|
75
|
-
//
|
|
76
|
-
result.bytes = Buffer.byteLength(
|
|
185
|
+
// Logical storage state, not a focused projection or compressed network measurement.
|
|
186
|
+
result.bytes = Buffer.byteLength(JSON.stringify(parsed), "utf8");
|
|
77
187
|
} else {
|
|
78
188
|
// THE SQLITE STATE, REBUILT AND MEASURED. The repository's own loader is the authority on what
|
|
79
189
|
// the state IS, so the report does not guess at it: the database is opened READ-ONLY, the
|
|
80
190
|
// overlays are folded onto the base snapshot the way `load()` folds them, and the result is
|
|
81
|
-
// measured as JSON
|
|
191
|
+
// measured as JSON. The browser's scoped projection and compression are separate measurements.
|
|
82
192
|
const measured = measureSqliteState(path);
|
|
83
193
|
if (measured.bytes > 0) {
|
|
84
194
|
result.bytes = measured.bytes;
|
|
85
195
|
result.conversations = measured.conversations;
|
|
86
196
|
result.messages = measured.messages;
|
|
87
|
-
} else {
|
|
88
|
-
result.bytes = result.fileBytes;
|
|
89
197
|
}
|
|
90
198
|
}
|
|
91
199
|
} catch { /* unreadable is not a fact worth reporting */ }
|
|
@@ -400,10 +508,13 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
|
|
|
400
508
|
|
|
401
509
|
/** Renders a diagnosis as the report a person reads. */
|
|
402
510
|
export function renderReport(result) {
|
|
511
|
+
if (result.mode === "remote") return renderRemoteReport(result);
|
|
403
512
|
const lines = [];
|
|
404
513
|
const ms = (value) => (typeof value === "number" ? `${String(value).padStart(6)}ms` : " ?");
|
|
405
514
|
|
|
406
515
|
lines.push(`Super Pi 延迟诊断 — ${new Date().toISOString()}`);
|
|
516
|
+
lines.push(" 测量范围:本机临时 server + 回环连接;未测用户电脑到云服务器的网络,也未测浏览器点击/绘制。");
|
|
517
|
+
lines.push(" 远程访问请在客户端电脑运行:super_pi diagnose --url http://SERVER:3080");
|
|
407
518
|
lines.push("");
|
|
408
519
|
lines.push(` Node ${result.node} (Pi 要求 >= ${result.required}:${result.nodeOk ? "满足" : "过低"})`);
|
|
409
520
|
lines.push(` server 就绪耗时 ${ms(result.bootMs)}`);
|
|
@@ -426,7 +537,7 @@ export function renderReport(result) {
|
|
|
426
537
|
}
|
|
427
538
|
for (const turn of result.turns ?? []) {
|
|
428
539
|
lines.push(` 第${turn.turn}轮 发送 -> 等待中 ${ms(turn.toWaitingMs)} Super Pi (建会话 + 首个 prompt)`);
|
|
429
|
-
lines.push(` 第${turn.turn}轮 等待中 -> 出字 ${ms(turn.modelMs)}
|
|
540
|
+
lines.push(` 第${turn.turn}轮 等待中 -> 出字 ${ms(turn.modelMs)} 首字等待 (包含 runtime、供应商及传输,未单独归因)`);
|
|
430
541
|
}
|
|
431
542
|
if (result.interrupt) {
|
|
432
543
|
if (Array.isArray(result.interruptTrace) && result.interruptTrace.length) {
|
|
@@ -438,7 +549,7 @@ export function renderReport(result) {
|
|
|
438
549
|
lines.push(" 打断(点「打断并回答」之后):");
|
|
439
550
|
lines.push(` 点击 -> 新一轮报等待中 ${ms(result.interrupt.toWaitingMs)} Super Pi (等旧轮退完 + 等槽位)`);
|
|
440
551
|
lines.push(` 点击 -> 出字 ${ms(result.interrupt.toTextMs)} 整条链`);
|
|
441
|
-
lines.push(`
|
|
552
|
+
lines.push(` 其中首字等待 ${ms(result.interrupt.modelMs)} runtime/供应商/传输,未单独归因`);
|
|
442
553
|
lines.push(` 打断后启动的 Pi 进程数: ${result.interrupt.spawned}`);
|
|
443
554
|
} else {
|
|
444
555
|
lines.push("");
|
|
@@ -459,8 +570,7 @@ export function renderReport(result) {
|
|
|
459
570
|
if (result.systemPromptBytes) {
|
|
460
571
|
lines.push("");
|
|
461
572
|
lines.push(` 发给模型的系统提示:${(result.systemPromptBytes / 1024).toFixed(1)}KB`);
|
|
462
|
-
lines.push("
|
|
463
|
-
lines.push(" 所以在慢链路上,它直接就是首字之前的那几秒。");
|
|
573
|
+
lines.push(" 这是逻辑提示体积;供应商协议、压缩、缓存和网络吞吐未在此测量,不能据此换算首字延迟。");
|
|
464
574
|
}
|
|
465
575
|
|
|
466
576
|
const real = result.realState;
|
|
@@ -468,33 +578,26 @@ export function renderReport(result) {
|
|
|
468
578
|
lines.push("");
|
|
469
579
|
lines.push(" 你真实的 state(上面那些数字是在一个全新的空目录上量的,看不到它):");
|
|
470
580
|
lines.push(` ${real.path}`);
|
|
471
|
-
const megabytes = (real.bytes / 1024 / 1024).toFixed(2);
|
|
581
|
+
const megabytes = real.bytes ? (real.bytes / 1024 / 1024).toFixed(2) : "未测";
|
|
472
582
|
const shape = real.conversations ? `,${real.conversations} 个会话,${real.messages} 条消息` : "";
|
|
473
|
-
lines.push(`
|
|
583
|
+
lines.push(` 存储状态重建后的 JSON:${real.bytes ? megabytes + "MB" : megabytes}${shape}(不是浏览器传输量)`);
|
|
474
584
|
if (real.fileBytes && real.fileBytes !== real.bytes) {
|
|
475
585
|
lines.push(` 数据库文件本身:${(real.fileBytes / 1024 / 1024).toFixed(2)}MB`);
|
|
476
|
-
lines.push("
|
|
477
|
-
lines.push(" 真正花时间的是上面那个数字——每次广播都要克隆它、序列化它、");
|
|
478
|
-
lines.push(" 再在浏览器里解析一遍。)");
|
|
586
|
+
lines.push(" (数据库含基础快照和增量表;磁盘体积、重建 JSON、focused JSON 与压缩传输量必须分开。)");
|
|
479
587
|
}
|
|
480
588
|
if (real.bytes > 1024 * 1024) {
|
|
481
|
-
lines.push("
|
|
482
|
-
lines.push(" 在慢机器上每一次广播都是秒级,而一轮里会广播好几次。");
|
|
589
|
+
lines.push(" 全量状态很大时,应检查克隆/持久化成本;现代 focused 客户端只收摘要与选中详情,不能据全库大小断言远程耗时。");
|
|
483
590
|
}
|
|
484
591
|
}
|
|
485
592
|
|
|
486
593
|
|
|
487
594
|
lines.push("");
|
|
488
595
|
if (!result.nodeOk) {
|
|
489
|
-
lines.push(` Node 版本低于 Pi 的要求(>= ${result.required}
|
|
490
|
-
lines.push(" 大部分时间:模块加载和 jiti 转译都会更慢。请先升级 Node 再跑一次——否则这份报告");
|
|
491
|
-
lines.push(" 描述的是一台 Pi 本来就不该在上面运行的机器。");
|
|
596
|
+
lines.push(` Node 版本低于 Pi 的要求(>= ${result.required})。请升级 Node 以满足兼容要求;版本不匹配本身不能证明延迟根因。`);
|
|
492
597
|
lines.push("");
|
|
493
598
|
}
|
|
494
599
|
lines.push(" 怎么看这份报告:");
|
|
495
|
-
lines.push(" -
|
|
496
|
-
lines.push("
|
|
497
|
-
lines.push(" - 第 1、2 段是 Super Pi 自己的。如果这两段是秒级,这份报告就是该发出来的东西。");
|
|
498
|
-
lines.push(" - 第 3 段是模型,会随对话变长。Super Pi 改不了它。");
|
|
600
|
+
lines.push(" - 进程数与会话构建/复用数说明执行结构,零进程不代表零开销或已经复用。");
|
|
601
|
+
lines.push(" - 各阶段是本机探针观察值;未结合跨机器传输和浏览器 trace 时,不能排除 Super Pi 或网络。");
|
|
499
602
|
return lines.join("\n");
|
|
500
603
|
}
|