mslxdff 0.1.213 → 0.1.214

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.213",
3
+ "version": "0.1.214",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/format.js CHANGED
@@ -76,6 +76,14 @@ export function fmtEvent(e) {
76
76
  return `${head} fallback! 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} 原因=${m(e.reason)} via=${m(e.via)} notice=${m(e.notice)}`;
77
77
  case "relay-try":
78
78
  return `${head} relay try 给宽带中继请求 target=${e.target} model=${m(e.model)} via=${e.via}`;
79
+ case "relay-miss":
80
+ return `${head} relay miss 宽带中继无候选(${e.reason || "-"}${e.candidates != null ? ` candidates=${e.candidates}` : ""})`;
81
+ case "relay-received":
82
+ return `${head} relay recv ${e.via === "broadband" ? "经中继" : "直连"}转给 target=${e.target} group=${e.group} model=${m(e.model)} msgs=${e.messages ?? "-"} tools=${e.tools ?? "-"} maxTok=${e.maxTokens ?? "-"}`;
83
+ case "relay-queued":
84
+ return `${head} relay queued 已入队交成员 target=${e.target} reqId=${e.reqId || "-"}`;
85
+ case "relay-member-result":
86
+ return `${head} relay result 成员回 target=${e.target} ${e.ok ? "ok" : "FAIL"} ${e.status ? `HTTP ${e.status}` : ""} ${e.ms ? `${e.ms}ms` : ""}${e.respChars ? ` resp=${e.respChars}B` : ""} ${m(e.error || e.respBody || "")}`;
79
87
  case "relay-fail":
80
88
  return `${head} relay fail ${e.target} ${e.status ? `HTTP ${e.status}` : ""} ${m(e.message)}`;
81
89
  case "relay-ip-change":
@@ -87,6 +87,10 @@ const DECISION_FIELDS = [
87
87
  "heldOpen", "terminalForm", "requestWaitedMs", "budgetMs", "budgetOut",
88
88
  // 终局判决(ADR-0058):取证时第一眼要看的三件——判的哪种、命中哪条判据、是否因此没问组员
89
89
  "kind", "marker", "peersSkipped",
90
+ // 中继/组员目标(2026-10-19):跨机链路排障第一眼要的是**这一跳打给了谁**——target= 目标成员
91
+ // (url 或 relay://id)、group= 组、leaderUrl= 经谁中继;缺了这几项,relay-try/fail 只剩状态码,
92
+ // 看日志根本认不出是哪个组员出的问题(用户实证要求「日志要全面」)。
93
+ "target", "group", "leaderUrl", "candidates", "tried",
90
94
  ];
91
95
 
92
96
  function decisionKv(data = {}) {
@@ -141,6 +145,22 @@ export function formatModelTrace({ type, reqId, model, data = {}, request = null
141
145
  detail = `upstream_response status=${data.status ?? "-"} finish=${d.sawFinishReason || "-"} chunks=${d.receivedChunks ?? "-"} bytes=${d.receivedBytes ?? "-"} tools=${d.toolCalls ?? 0} chars=${d.chars ?? 0} ${usageTxt} elapsed=${data.totalMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
142
146
  } else if (type === "result" || type === "client-response") {
143
147
  detail = `client status=${data.status ?? "-"} via=${data.via || "-"} actual=${data.actual || model || "-"} fallback=${data.fallback?.fallback ? 1 : 0} total=${totalMs ?? data.durationMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
148
+ } else if (type === "relay-try") {
149
+ detail = `指定宽带中继 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"}`;
150
+ } else if (type === "relay-received") {
151
+ detail = `中继收到 转给 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"} msgs=${data.messages ?? "-"} tools=${data.tools ?? "-"} maxTok=${data.maxTokens ?? "-"}`;
152
+ } else if (type === "relay-queued") {
153
+ detail = `已入队交成员 target=${data.target || "?"} reqId=${data.reqId || "-"}`;
154
+ } else if (type === "relay-member-result") {
155
+ detail = `成员回 target=${data.target || "?"} ${data.ok ? "ok" : "FAIL"}${data.status ? ` status=${data.status}` : ""}${data.ms ? ` ${data.ms}ms` : ""}${data.respChars ? ` resp=${data.respChars}B` : ""} ${safeText(data.error || data.respBody || "", 200)}`;
156
+ } else if (type === "relay-fail") {
157
+ detail = `中继失败 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"}${data.status ? ` status=${data.status}` : ""} ${safeText(data.message || "")}`;
158
+ } else if (type === "relay-miss") {
159
+ detail = `宽带中继无候选可试(全部候选未通/无存活成员,交回本地兜底)`;
160
+ } else if (type === "peer-race-start") {
161
+ detail = `组员竞速开始 peers=${data.peers ?? "-"}`;
162
+ } else if (type === "peer-budget-out") {
163
+ detail = `组员竞速预算用尽 reason=${safeText(data.reason || "", 80)} budgetMs=${data.budgetMs ?? "-"} remaining=${data.remaining ?? "-"}`;
144
164
  } else detail = safeText(`${decisionKv(data)} ${data.message || data.error || ""}`, 200);
145
165
  return `[${base.time}] [req=${base.req}] [model=${base.model}] [stage=${base.stage}] ${detail}`;
146
166
  }
@@ -3,6 +3,20 @@ import { DEFAULT_MAX_HOPS } from "../peers.js";
3
3
  import { enqueueRelay, dequeueRelayForPoll, resolveRelay, subscribeStream, unsubscribeStream } from "./relay-queue.js";
4
4
  import { compatFetch } from "../compat.js";
5
5
 
6
+ /** 请求规模摘要(给中继日志用):消息数/角色分布/工具数/maxTokens/model —— 排障第一眼要看清"转了什么"。 */
7
+ function relayReqSummary(b = {}) {
8
+ const msgs = Array.isArray(b?.messages) ? b.messages : [];
9
+ const roles = {};
10
+ for (const m of msgs) roles[String(m?.role || "?")] = (roles[String(m?.role || "?")] || 0) + 1;
11
+ return { model: b?.model || "-", stream: b?.stream === true, messages: msgs.length, roles, tools: Array.isArray(b?.tools) ? b.tools.length : 0, maxTokens: Number(b?.max_tokens ?? b?.max_completion_tokens) || null };
12
+ }
13
+
14
+ /** 响应正文摘要(供中继日志):单行化 + 截断,只为让人看出上游回的是什么。 */
15
+ function snippet(v, n = 300) {
16
+ const s = typeof v === "string" ? v : JSON.stringify(v);
17
+ return String(s || "").replace(/\s+/g, " ").slice(0, n);
18
+ }
19
+
6
20
  export async function heartbeatHandler({ req, res, groups, bus, logs }) {
7
21
  const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
8
22
  if (!auth) return json(res, 401, { error: "bearer token required" });
@@ -169,10 +183,15 @@ export async function forwardHandler({ req, res, groups, bus, logs, token }) {
169
183
  const targetMember = Object.values(members).find((m) => m.url === target) || Object.entries(members).find(([id]) => id === target)?.[1];
170
184
  if (!targetMember) return json(res, 404, { error: `target ${target} not found in group ${groupName}` });
171
185
  const isBb = targetMember.kind === "broadband" || String(targetMember.url).startsWith("relay://");
186
+ const reqSummary = relayReqSummary(body.body || body);
187
+ const fwdEvt = (type, extra) => { const e = { ts: Date.now(), type, group: groupName, target, hops, ...extra }; try { logs?.appendEvent?.(e); } catch {} try { bus?.emit?.(e); } catch {} };
188
+ // 组长「收到」:谁让我转给谁、转的是什么规模 —— 中继全流程的第一条日志
189
+ fwdEvt("relay-received", { via: isBb ? "broadband" : "direct", reqId: body.reqId || null, ...reqSummary });
172
190
  if (!isBb) {
173
191
  // 标记走直连:**在 fetch 之前**就设,这样直连失败(catch 里的 502)也带着它——
174
192
  // 那正是最需要区分的一跳(组长直连该成员不通 vs 宽带成员回报失败,两者的 body 可能一模一样)。
175
193
  res.setHeader("x-mslxdff-relay-via", "direct");
194
+ const t0 = Date.now();
176
195
  try {
177
196
  const fwdBody = body.body || body;
178
197
  const r = await compatFetch(`${targetMember.url}/v1/chat/completions`, {
@@ -180,41 +199,49 @@ export async function forwardHandler({ req, res, groups, bus, logs, token }) {
180
199
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${targetMember.token || ""}`, "x-mslxdff-hops": String(hops + 1), "x-mslxdff-model-lock": fwdBody.model || "", "Accept": "text/event-stream" },
181
200
  body: JSON.stringify(fwdBody),
182
201
  });
183
- const evtData = { ts: Date.now(), type: "relay-forward", target, via: "direct", group: groupName, hops };
184
- if (bus) bus.emit(evtData);
185
- logs?.appendEvent?.(evtData);
186
- res.statusCode = r.status;
187
- if (r.headers.get("content-type")?.includes("text/event-stream")) {
202
+ const isSSE = r.headers.get("content-type")?.includes("text/event-stream");
203
+ if (isSSE) {
204
+ // 流式直连:没法先读全 body 再转发,就记「已建立」+ 状态码
205
+ fwdEvt("relay-forward", { via: "direct", ok: r.status < 400, status: r.status, ms: Date.now() - t0, detail: "stream 直连建立" });
206
+ res.statusCode = r.status;
188
207
  res.setHeader("Content-Type", "text/event-stream");
189
208
  if (r.body) for await (const c of r.body) res.write(c);
190
209
  res.end();
191
210
  } else {
192
211
  const txt = await r.text();
212
+ // 直连非流式:把上游回的正文摘要也记下(这就是「组员回了什么」)
213
+ fwdEvt("relay-forward", { via: "direct", ok: r.status < 400, status: r.status, ms: Date.now() - t0, respChars: txt.length, respBody: snippet(txt) });
214
+ res.statusCode = r.status;
193
215
  res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
194
216
  res.end(txt);
195
217
  }
196
218
  return;
197
219
  } catch (err) {
220
+ fwdEvt("relay-forward", { via: "direct", ok: false, status: 502, ms: Date.now() - t0, error: errMsg(err) });
198
221
  return json(res, 502, { error: errMsg(err) });
199
222
  }
200
223
  }
201
224
  const reqId = body.reqId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
202
225
  const fwdBody = body.body || { model: body.model, messages: body.messages, stream: body.stream };
203
- const evtData = { ts: Date.now(), type: "relay-forward", target, via: "leader", group: groupName, hops, reqId, model: fwdBody.model };
204
- if (bus) bus.emit(evtData);
205
- logs?.appendEvent?.(evtData);
206
226
  const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
207
227
  if (typeof targetMember.lastSeen === "number" && Date.now() - targetMember.lastSeen > staleMs) {
208
228
  res.setHeader("x-mslxdff-relay-via", "broadband");
229
+ fwdEvt("relay-member-result", { reqId, ok: false, status: 502, error: "broadband member stale (no heartbeat)" });
209
230
  return json(res, 502, { error: "broadband member stale (no heartbeat)" });
210
231
  }
211
232
  // 标记走的是宽带中继:下游据此一眼分清「组长直连失败(502 fetch failed)」vs「宽带成员回报的失败」,
212
233
  // 两者响应体可能都是 {"error":"fetch failed"},只有这个头能区分(成员侧已把真实原因写进 errors.log)。
213
234
  res.setHeader("x-mslxdff-relay-via", "broadband");
235
+ const tFwd = Date.now();
236
+ // 组长「已交给成员」:入队即记 —— 成员有没有真正收到,由成员侧那条 relay-task-recv 佐证
237
+ fwdEvt("relay-queued", { reqId, via: "broadband" });
214
238
  try {
215
239
  const resultPromise = enqueueRelay({ group: groupName, target: targetMember.url, reqId, body: fwdBody, hops });
216
240
  const result = await resultPromise;
217
241
  if (result && typeof result.status === "number") {
242
+ const rb = result.body == null ? "" : (typeof result.body === "string" ? result.body : JSON.stringify(result.body));
243
+ // 组长「收到成员回什么」:状态码 + 正文摘要 + 耗时 —— 中继全流程的收尾日志
244
+ fwdEvt("relay-member-result", { reqId, ok: result.status < 400, status: result.status, ms: Date.now() - tFwd, respChars: rb.length, respBody: snippet(rb) });
218
245
  res.statusCode = result.status;
219
246
  if (result.status >= 500) res.setHeader("x-mslxdff-relay-member-status", String(result.status));
220
247
  if (result.headers) for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
@@ -224,8 +251,10 @@ export async function forwardHandler({ req, res, groups, bus, logs, token }) {
224
251
  } else res.end();
225
252
  return;
226
253
  }
254
+ fwdEvt("relay-member-result", { reqId, ok: true, status: 200, ms: Date.now() - tFwd, respBody: snippet(result) });
227
255
  return json(res, 200, result);
228
256
  } catch (err) {
257
+ fwdEvt("relay-member-result", { reqId, ok: false, status: 504, ms: Date.now() - tFwd, error: errMsg(err) || "relay timeout" });
229
258
  return json(res, 504, { error: errMsg(err) || "relay timeout" });
230
259
  }
231
260
  }
@@ -140,7 +140,10 @@ export async function tryBroadbandRelay({ groups, token: myToken, model, body, h
140
140
  }
141
141
  } catch {}
142
142
  }
143
- if (!allCandidates.length) return null;
143
+ if (!allCandidates.length) {
144
+ evt?.("relay-miss", { reqId, model, reason: "no-candidates", candidates: 0 });
145
+ return null;
146
+ }
144
147
  for (const cand of allCandidates) {
145
148
  try {
146
149
  evt?.("relay-try", { reqId, model, via: cand.via, target: cand.target, group: cand.group });
@@ -24,6 +24,11 @@ export function createBroadbandExec({ token, upstream, logs, log = console.log,
24
24
  const t0 = Date.now();
25
25
  const model = String(body?.model || "");
26
26
  const fl = withOutputFloor(body); // 首发输出额度托底:leader 转来的 body 不经 pipeline,成员侧自托(评审 P1)
27
+ // 成员「收到任务」:谁转来的、是什么规模、转给谁 —— 中继全流程在**成员这一跳**的入口日志。
28
+ // 没有它,组长侧只知道"入队了",无从确认成员是否真收到、收到的是什么(用户实证要求「日志要全面」)。
29
+ const nMsgs = Array.isArray(body?.messages) ? body.messages.length : 0;
30
+ const nTools = Array.isArray(body?.tools) ? body.tools.length : 0;
31
+ log(`broadband task recv: group=${g.name} req=${reqId} from leader=${g.leaderUrl} model=${model} messages=${nMsgs} tools=${nTools}`);
27
32
  if (fl !== body) log(`output-floor(broadband): req=${reqId} ${(body?.max_tokens ?? body?.max_completion_tokens)}→${(fl.max_tokens ?? fl.max_completion_tokens)}`);
28
33
  try {
29
34
  const upRes = await upstream.chat(fl);