mslxdff 0.1.160 → 0.1.161
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/bin/mslxdff.js +4 -4
- package/docs/cli_help_mini.md +135 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/autostart.js +3 -0
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/empty-turn.js +119 -0
- package/src/chat-pipeline/index.js +126 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +53 -19
- package/src/cli/commands/daemon.js +4 -4
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -0
- package/src/cli/commands/provider/index.js +126 -124
- package/src/cli/commands/provider/models.js +142 -139
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/commands/system.js +2 -2
- package/src/cli/format.js +6 -0
- package/src/cli/policy.js +2 -2
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +101 -96
- package/src/logs.js +14 -1
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +5 -2
- package/src/models.js +225 -225
- package/src/providers/AGENTS.md +46 -0
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/globalqwenwork/account-store.js +141 -0
- package/src/providers/globalqwenwork/constants.js +73 -0
- package/src/providers/globalqwenwork/cosy.js +123 -0
- package/src/providers/globalqwenwork/crypto.js +220 -0
- package/src/providers/globalqwenwork/http.js +22 -0
- package/src/providers/globalqwenwork/index.js +331 -0
- package/src/providers/globalqwenwork/payload.js +145 -0
- package/src/providers/globalqwenwork/rsa.js +56 -0
- package/src/providers/globalqwenwork/sse.js +270 -0
- package/src/providers/globalqwenwork/stream.js +132 -0
- package/src/providers/globalqwenwork/upstream.js +122 -0
- package/src/providers/globalqwenwork.js +1 -0
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +183 -183
- package/src/providers/qoder/index.js +230 -230
- package/src/providers/qoder/sse.js +103 -103
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +142 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/registry.js +74 -66
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/sse.js +19 -2
- package/src/reasoning.js +32 -32
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/exhausted-handler.js +23 -8
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/local-handler.js +3 -1
- package/src/routes/chat/relay-pipeline.js +276 -264
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +255 -255
- package/src/routes/helpers.js +20 -4
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-hold.js +138 -0
- package/src/routes/stream-scan.js +192 -0
- package/src/routes/stream.js +386 -393
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/lifecycle-forensics.js +116 -0
- package/src/runtime/lifecycle-log.js +23 -0
- package/src/runtime/provider-gate.js +34 -33
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/talk-log.js +226 -0
- package/src/timeline.js +5 -2
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
package/src/talk-log.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// 环形对话日志(talk log):按「供应商-模型-talk.log」分文件,只保留最近 1 小时的完整对话。
|
|
2
|
+
// 记三件事:我问了什么 / 模型怎么想(reasoning)/ 模型怎么答(content),供人工调研模型的回答方式。
|
|
3
|
+
// 与 model-trace 相反:那边刻意不落正文(「加日志」≠「倒数据」),这里专门落正文 —— 所以必须脱敏 + 封顶。
|
|
4
|
+
// 默认开;MSLXDFF_TALK_LOG=0 关闭,窗口 MSLXDFF_TALK_LOG_WINDOW_MIN(分,默认 60),单文件 MSLXDFF_TALK_LOG_MAX_KB(默认 5MB)。
|
|
5
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { logDir } from "./logs.js";
|
|
8
|
+
import { fmtShanghaiYMDHMS } from "./time.js";
|
|
9
|
+
|
|
10
|
+
const SUBDIR = "talk";
|
|
11
|
+
const DEFAULT_WINDOW_MIN = 60;
|
|
12
|
+
const DEFAULT_MAX_KB = 5 * 1024; // 5MB per talk-log file (ring buffer)
|
|
13
|
+
const MARK = "#talk-entry";
|
|
14
|
+
// 正常路径只 append;整文件重写(环形淘汰)只在「最老条目出窗」或「超字节上限」时发生。
|
|
15
|
+
// oldestCache 记住每个文件当前最老的 ts,避免每次写入都回读全文件。
|
|
16
|
+
const oldestCache = new Map();
|
|
17
|
+
|
|
18
|
+
export function talkLogEnabled() { return String(process.env.MSLXDFF_TALK_LOG ?? "1") !== "0"; }
|
|
19
|
+
function windowMs() { const n = Number(process.env.MSLXDFF_TALK_LOG_WINDOW_MIN); return Number.isFinite(n) && n > 0 ? n * 60000 : DEFAULT_WINDOW_MIN * 60000; }
|
|
20
|
+
function maxBytes() { const n = Number(process.env.MSLXDFF_TALK_LOG_MAX_KB); return Number.isFinite(n) && n > 0 ? n * 1024 : DEFAULT_MAX_KB * 1024; }
|
|
21
|
+
/** 仅测试用:进程重启等价物(环形窗口的内存基线清空)。 */
|
|
22
|
+
export function resetTalkLogCache() { oldestCache.clear(); }
|
|
23
|
+
|
|
24
|
+
/** 供应商-模型-talk.log:qoder/qfmodel → qoder-qfmodel-talk.log;裸 id(免费池)直接用一个名字段。 */
|
|
25
|
+
export function talkLogName(model) {
|
|
26
|
+
const s = String(model || "unknown").trim().toLowerCase();
|
|
27
|
+
const segs = s.includes("/") ? s.split("/").filter((x) => x && x !== "unknown") : [s];
|
|
28
|
+
const safe = segs.join("-").replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 180);
|
|
29
|
+
return `${safe || "unknown"}-talk.log`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function talkDir() { return join(logDir(), SUBDIR); }
|
|
33
|
+
export function talkLogFile(model) { return join(talkDir(), talkLogName(model)); }
|
|
34
|
+
|
|
35
|
+
// 正文里的凭据一律先抹再落盘(保留换行,不像 model-trace 那样压成一行 —— 这里要读的是回答本身)。
|
|
36
|
+
const MASKS = [
|
|
37
|
+
[/([?&](?:token|refresh[_-]?token|access[_-]?token|api[_-]?key|apikey|key|secret|password|cookie)=)[^\s&]+/gi, "$1[已脱敏]"],
|
|
38
|
+
[/(authorization|bearer|proxy-authorization|cookie|x-api-key)\s*[:=]\s*[^\s,;]+/gi, "$1=[已脱敏]"],
|
|
39
|
+
[/("(?:apiKey|api_key|accessToken|refreshToken|secret|password|client_secret)"\s*:\s*)"[^"]*"/gi, '$1"[已脱敏]"'],
|
|
40
|
+
[/\beyJ[\w-]{6,}\.[\w-]{6,}\.[\w-]{6,}\b/g, "[已脱敏-JWT]"],
|
|
41
|
+
[/\b(?:sk|pk|ghp|gho|ghu|ghs|ghr|xox[baprs])-[\w-]{10,}\b/g, "[已脱敏-key]"],
|
|
42
|
+
// 裸 k=v(提问里常出现 token=…、password=…):只掐足够长的值,避免把「token 数量」这类正常技术讨论也糊掉
|
|
43
|
+
[/\b(token|accessToken|refreshToken|api[_-]?key|apiKey|secret|password|cookie|client_secret|authorization)\s*[:=]\s*[\w./+-]{12,}/gi, "$1=[已脱敏]"],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** 脱敏 + 按字节封顶截断(中文 3 字节/字,逐字累加才不会超预算)。 */
|
|
47
|
+
export function maskText(v, cap = 100000) {
|
|
48
|
+
let s = String(v ?? "");
|
|
49
|
+
for (const [re, repl] of MASKS) s = s.replace(re, repl);
|
|
50
|
+
if (Buffer.byteLength(s, "utf8") <= cap) return s;
|
|
51
|
+
const tail = Buffer.from("\n…[已截断]", "utf8");
|
|
52
|
+
const budget = Math.max(0, cap - tail.length);
|
|
53
|
+
let bytes = 0, i = 0;
|
|
54
|
+
for (const ch of s) {
|
|
55
|
+
const n = Buffer.byteLength(ch, "utf8");
|
|
56
|
+
if (bytes + n > budget) break;
|
|
57
|
+
bytes += n;
|
|
58
|
+
i += ch.length;
|
|
59
|
+
}
|
|
60
|
+
return s.slice(0, i) + "\n…[已截断]";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function textOf(content) {
|
|
64
|
+
if (typeof content === "string") return content;
|
|
65
|
+
if (Array.isArray(content)) return content.map((p) => p?.text || (p?.image_url ? `[图片 ${String(p.image_url.url).slice(0, 60)}]` : "")).filter(Boolean).join("\n");
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 只取「我发给大模型的问题」= 最后一条 user 消息(agent 回路每轮都带全量历史,全落会把环形窗口占满)。 */
|
|
70
|
+
export function lastUserQuestion(body) {
|
|
71
|
+
const msgs = Array.isArray(body?.messages) ? body.messages : null;
|
|
72
|
+
if (msgs) { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i]?.role === "user") return textOf(msgs[i].content); }
|
|
73
|
+
const inp = body?.input; // Responses 端点形状兜底
|
|
74
|
+
if (typeof inp === "string") return inp;
|
|
75
|
+
if (Array.isArray(inp)) { for (let i = inp.length - 1; i >= 0; i--) if (inp[i]?.role === "user") return textOf(inp[i].content); }
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const CUT_IN = "----8<----", CUT_OUT = "---->8----";
|
|
80
|
+
|
|
81
|
+
function pushBlock(out, label, text, cap) {
|
|
82
|
+
const m = maskText(text, cap);
|
|
83
|
+
if (!m.trim()) return;
|
|
84
|
+
out.push(`[${label} · ${m.length} 字]`, CUT_IN, m, CUT_OUT, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 一条对话 = 一个可读块:头部一行元信息(ts= 是环形淘汰的唯一依据),正文三段。 */
|
|
88
|
+
export function formatTalkEntry({ reqId, model, via, hops, status, stream, elapsedMs, question, thinking, answer, tools, usage, upstream, account, pick, ts } = {}) {
|
|
89
|
+
const now = Number.isFinite(ts) && ts > 0 ? ts : Date.now();
|
|
90
|
+
const head = [
|
|
91
|
+
`${MARK} ts=${now}`, `time=${fmtShanghaiYMDHMS(new Date(now))}`, `req=${reqId || "-"}`, `model=${model || "-"}`,
|
|
92
|
+
via ? `via=${via}` : "", hops != null ? `hops=${hops}` : "", `status=${status ?? "-"}`,
|
|
93
|
+
stream ? "stream=1" : "stream=0", Number.isFinite(elapsedMs) ? `elapsed=${Math.round(elapsedMs)}ms` : "",
|
|
94
|
+
usage ? `usage(prompt=${usage.prompt_tokens ?? "-"},completion=${usage.completion_tokens ?? "-"})` : "",
|
|
95
|
+
upstream ? `upstream=${upstream}` : "", account ? `account=${account}` : "", pick ? `pick=${pick}` : "",
|
|
96
|
+
].filter(Boolean).join(" ");
|
|
97
|
+
const out = [head, ""];
|
|
98
|
+
pushBlock(out, "我问", question, 20000);
|
|
99
|
+
pushBlock(out, "思考", thinking, 100000);
|
|
100
|
+
pushBlock(out, "回答", answer, 200000);
|
|
101
|
+
const t = tools == null || tools === "" ? "" : String(tools);
|
|
102
|
+
pushBlock(out, "工具调用", t, 20000);
|
|
103
|
+
out.push("[END]", "");
|
|
104
|
+
return out.join("\n") + "\n";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 只读文件头 8KB 取最老一条的 ts(进程重启后重建内存基线,不必回读全文件)。 */
|
|
108
|
+
function firstTs(file) {
|
|
109
|
+
let fd = null;
|
|
110
|
+
try {
|
|
111
|
+
fd = openSync(file, "r");
|
|
112
|
+
const buf = Buffer.alloc(8192);
|
|
113
|
+
const n = readSync(fd, buf, 0, 8192, 0);
|
|
114
|
+
const m = buf.subarray(0, n).toString("utf8").match(/^#talk-entry ts=(\d+)/m);
|
|
115
|
+
return m ? Number(m[1]) : null;
|
|
116
|
+
} catch { return null; }
|
|
117
|
+
finally { if (fd != null) { try { closeSync(fd); } catch {} } }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 追加一条(正常路径只 append;到窗口或超预算才整写淘汰)。 */
|
|
121
|
+
export function appendTalkEntry({ file, block, atMs } = {}) {
|
|
122
|
+
try {
|
|
123
|
+
if (!file || !block) return 0;
|
|
124
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
125
|
+
const isNew = !existsSync(file);
|
|
126
|
+
appendFileSync(file, block);
|
|
127
|
+
// mode 只在新建时生效;存量文件补一次 chmod(POSIX;Windows 无意义)
|
|
128
|
+
if (isNew) { try { chmodSync(file, 0o600); } catch {} }
|
|
129
|
+
const now = Number.isFinite(atMs) && atMs > 0 ? atMs : Date.now();
|
|
130
|
+
let base = oldestCache.get(file);
|
|
131
|
+
if (base == null) { base = firstTs(file); if (base == null) base = now; }
|
|
132
|
+
const size = (() => { try { return statSync(file).size; } catch { return 0; } })();
|
|
133
|
+
if (now - base > windowMs() || size > maxBytes()) trimTalkLog(file, now);
|
|
134
|
+
else oldestCache.set(file, Math.min(base, now));
|
|
135
|
+
return 1;
|
|
136
|
+
} catch { return 0; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 按 `#talk-entry` 头行切块(内容行理论上可能撞前缀,故要求严格 ts= 形状才认头)。 */
|
|
140
|
+
export function splitEntries(txt) {
|
|
141
|
+
const entries = [];
|
|
142
|
+
const lines = String(txt ?? "").split("\n");
|
|
143
|
+
let start = -1;
|
|
144
|
+
const close = (end) => {
|
|
145
|
+
if (start < 0) return;
|
|
146
|
+
const chunk = lines.slice(start, end);
|
|
147
|
+
const m = chunk[0].match(/^#talk-entry ts=(\d+)/);
|
|
148
|
+
entries.push({ ts: m ? Number(m[1]) : 0, text: chunk.join("\n") + "\n" });
|
|
149
|
+
};
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
if (/^#talk-entry ts=\d+/.test(lines[i])) { close(i); start = i; }
|
|
152
|
+
}
|
|
153
|
+
close(lines.length);
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 环形淘汰:先按时间窗口丢,再按字节预算从最老的丢(至少留最新 1 条);tmp+rename 原子替换。 */
|
|
158
|
+
export function trimTalkLog(file, nowTs = Date.now()) {
|
|
159
|
+
try {
|
|
160
|
+
if (!existsSync(file)) return 0;
|
|
161
|
+
const limit = nowTs - windowMs();
|
|
162
|
+
const cap = maxBytes();
|
|
163
|
+
const all = splitEntries(readFileSync(file, "utf8"));
|
|
164
|
+
// 多进程(restart 交接窗口)可能交错追加 → 按 ts 重排,保证「环形」语义与阅读顺序一致
|
|
165
|
+
let kept = all.filter((e) => e.ts > 0 && e.ts >= limit).sort((a, b) => a.ts - b.ts);
|
|
166
|
+
if (kept.length === 0) { rmSync(file, { force: true }); oldestCache.delete(file); return all.length; }
|
|
167
|
+
let bytes = Buffer.byteLength(kept.map((e) => e.text).join(""), "utf8");
|
|
168
|
+
while (bytes > cap && kept.length > 1) bytes -= Buffer.byteLength(kept.shift().text, "utf8");
|
|
169
|
+
const tmp = `${file}.${process.pid}.trim`;
|
|
170
|
+
writeFileSync(tmp, kept.map((e) => e.text).join(""), { mode: 0o600 });
|
|
171
|
+
renameSync(tmp, file);
|
|
172
|
+
oldestCache.set(file, kept[0].ts);
|
|
173
|
+
return all.length - kept.length;
|
|
174
|
+
} catch { return 0; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* relay 落盘的唯一入口:把 relay() 累积的 detail.talk(思考/正文/工具分片)拼成一条对话。
|
|
179
|
+
* 空轮(零正文零思考零工具)不占环形窗口 —— 那是 relay-pipeline 已经另行报错的场景。
|
|
180
|
+
*/
|
|
181
|
+
export function recordRelayTalk({ reqId, model, via, hops, body, out, echo = {} } = {}) {
|
|
182
|
+
if (!talkLogEnabled()) return false;
|
|
183
|
+
try {
|
|
184
|
+
const talk = out?.detail?.talk;
|
|
185
|
+
const answer = (talk?.content || []).join("");
|
|
186
|
+
const thinking = (talk?.reasoning || []).join("");
|
|
187
|
+
const tools = talk?.tools?.length ? JSON.stringify(talk.tools) : "";
|
|
188
|
+
const status = out?.status;
|
|
189
|
+
if (!answer && !thinking && !tools && Number(status) !== 200) return false;
|
|
190
|
+
if (!answer && !thinking && !tools) return false;
|
|
191
|
+
const ts = Date.now();
|
|
192
|
+
const block = formatTalkEntry({
|
|
193
|
+
reqId, model, via, hops, status, stream: Boolean(body?.stream),
|
|
194
|
+
elapsedMs: out?.totalMs, question: lastUserQuestion(body || {}),
|
|
195
|
+
thinking, answer, tools, usage: out?.detail?.usage, ...echo, ts,
|
|
196
|
+
});
|
|
197
|
+
appendTalkEntry({ file: talkLogFile(model), block, atMs: ts });
|
|
198
|
+
return true;
|
|
199
|
+
} catch { return false; }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 读最近 N 条(倒序,最新在前)—— 给 CLI/排障用。 */
|
|
203
|
+
export function recentBlocks({ model, n = 10, file } = {}) {
|
|
204
|
+
try {
|
|
205
|
+
const target = file || talkLogFile(model);
|
|
206
|
+
if (!existsSync(target)) return [];
|
|
207
|
+
return splitEntries(readFileSync(target, "utf8")).map((e) => e.text).reverse().slice(0, n);
|
|
208
|
+
} catch { return []; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 最近活跃的文件清单(哪些供应商×模型在这一小时里说过话)。 */
|
|
212
|
+
export function listRecentFiles({ limit = 20 } = {}) {
|
|
213
|
+
try {
|
|
214
|
+
const dir = talkDir();
|
|
215
|
+
if (!existsSync(dir)) return [];
|
|
216
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
217
|
+
.filter((d) => d.isFile() && d.name.endsWith("-talk.log"))
|
|
218
|
+
.map((d) => {
|
|
219
|
+
let mtime = 0;
|
|
220
|
+
try { mtime = statSync(join(dir, d.name)).mtimeMs; } catch {}
|
|
221
|
+
return { name: d.name, mtime, time: fmtShanghaiYMDHMS(new Date(mtime)) };
|
|
222
|
+
})
|
|
223
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
224
|
+
.slice(0, limit);
|
|
225
|
+
} catch { return []; }
|
|
226
|
+
}
|
package/src/timeline.js
CHANGED
|
@@ -12,12 +12,15 @@ function outcome(status, reason) {
|
|
|
12
12
|
return `${status}${reason ? ` ${short(reason)}` : ""}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function formatTimeline({ reqId, model, direct = [], peers = [], retries = 0, result = null, totalMs = 0 } = {}) {
|
|
15
|
+
export function formatTimeline({ reqId, model, direct = [], peers = [], retries = 0, retryResult = null, result = null, totalMs = 0 } = {}) {
|
|
16
16
|
const d = direct.length ? direct[direct.length - 1] : null;
|
|
17
17
|
const ps = peers.map((p) => `[peer=${hostPort(p.peer)} ${p.ok ? "win" : "fail"} ${p.latencyMs != null ? `${p.latencyMs}ms` : "timing-"} ${short(p.message || p.status || "", 70)}]`);
|
|
18
18
|
const r = result || {};
|
|
19
19
|
const detail = r.detail || {};
|
|
20
20
|
const res = r.status == null ? "-" : `${r.status}${detail.sawFinishReason ? ` ${detail.sawFinishReason}` : ""}${detail.toolCalls ? ` tools=${detail.toolCalls}` : ""}${detail.chars != null ? ` chars=${detail.chars}` : ""}${detail.interrupted ? " interrupted=1" : ""}${detail.timedOut ? " timedOut=1" : ""}`;
|
|
21
21
|
const directText = d ? outcome(d.status, d.reason) : "-";
|
|
22
|
-
|
|
22
|
+
// retry= 次数(保持既有 token 可 grep);win=1 重试后救回,lost=1 重试用尽仍零正文。
|
|
23
|
+
const _rc = retryResult === "ok" ? " [retry_win=1]" : retryResult === "lost" ? " [retry_lost=1]" : "";
|
|
24
|
+
const retryTag = (retries || retryResult) ? ` [retry=${retries || 0}]${_rc}` : "";
|
|
25
|
+
return `[req=${reqId || "-"}] [model=${model || "-"}] [direct=${directText}]${ps.length ? ` ${ps.join(" ")}` : ""}${retryTag} [result=${res}] [total=${Math.max(0, Math.round(Number(totalMs) || 0))}ms]`;
|
|
23
26
|
}
|