mslxdff 0.1.235 → 0.1.236

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.235",
3
+ "version": "0.1.236",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/status.js CHANGED
@@ -229,6 +229,9 @@ export async function printStatus(VERSION) {
229
229
  const s = peers.stat(p.url);
230
230
  if (s?.latencyMs != null) tags.push(`${s.latencyMs}ms`);
231
231
  if (s?.fails) tags.push(`${s.fails} fail(s)`);
232
+ if (s?.wins) tags.push(`${s.wins} 成功`);
233
+ const today = s?.days?.[peers.todayKey()];
234
+ if (today && (today.ok || today.fail)) tags.push(`今天 ${today.ok || 0}成/${today.fail || 0}败`);
232
235
  const tag = tags.length ? ` [${tags.join(", ")}]` : "";
233
236
  console.log(` ${p.name || p.url} ${p.url}${tag}`);
234
237
  }
package/src/peers.js CHANGED
@@ -263,10 +263,39 @@ export function createPeersService({
263
263
  return avail[cursor++];
264
264
  }
265
265
 
266
+ // ——— 累计计数(排障读数,不参与选路) ———
267
+ // 为什么单列而不是复用 fails/okAt:那两个是**瞬时**量(成功把 fails 清零、失败把 okAt 晾着),
268
+ // 用来判"现在好不好用";而"这个节点一共替我扛了多少次"必须只增不减,否则永远读不出分布。
269
+ // day 分桶让「今天是不是某节点突然不干活了」一眼可见,历史桶只留 DAY_KEEP 天。
270
+ const DAY_KEEP = 14;
271
+
272
+ function todayKey() {
273
+ const d = new Date(now());
274
+ const p = (n) => String(n).padStart(2, "0");
275
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
276
+ }
277
+
278
+ // 只在 acc({wins,failTotal,days})上加计数,不碰 stats[url]:
279
+ // recordResult 的成功分支会**整体重建**该节点条目,早先那种"先写进 stats 再重建"
280
+ // 的写法会把刚累加的计数直接抹掉(成功一次 = 归零一次,统计恒为 0)。
281
+ function accOf(prev) {
282
+ return { wins: prev.wins || 0, failTotal: prev.failTotal || 0, days: prev.days || {} };
283
+ }
284
+
285
+ function bumpDayInto(acc, field) {
286
+ const days = { ...acc.days };
287
+ const k = todayKey();
288
+ const cur = days[k] || { ok: 0, fail: 0 };
289
+ days[k] = { ok: cur.ok || 0, fail: cur.fail || 0, [field]: (cur[field] || 0) + 1 };
290
+ const keys = Object.keys(days).sort();
291
+ for (const old of keys.slice(0, Math.max(0, keys.length - DAY_KEEP))) delete days[old];
292
+ acc.days = days;
293
+ }
294
+
266
295
  // Long-lived error memory: a peer keeps its last-error timestamp until a
267
296
  // subsequent success resets it (success clears the failure record) or the
268
297
  // error is no longer in the persist store on next load.
269
- async function recordError(url, { status, message } = {}) {
298
+ async function recordError(url, { status, message } = { }) {
270
299
  if (!url) return;
271
300
  lastErrorAt[url] = now();
272
301
  await persistErrors({ ...lastErrorAt });
@@ -287,7 +316,10 @@ export function createPeersService({
287
316
  // Outcome of a forwarded request: ok updates the hot-cache (EMA latency,
288
317
  // last successful model) and clears the error memory; failures keep the
289
318
  // error timestamp so the retry pass can order by recovery time.
290
- async function recordResult(url, { ok, latencyMs, model } = {}) {
319
+ // 累计计数(用户 2026-10-23):okAt/fails 是**瞬时**状态(成功清 fails、失败清 okAt),
320
+ // 看不出「这个节点一共替我扛了多少次」。故另立只增不减的 wins/failTotal 与今日分桶,
321
+ // 用于回答「各节点成功转发次数是否合理」——它是排障读数,不参与选路打分。
322
+ async function recordResult(url, { ok, latencyMs, model, provisional = false } = {}) {
291
323
  if (!url) return;
292
324
  if (ok) {
293
325
  if (lastErrorAt[url] != null) {
@@ -295,6 +327,9 @@ export function createPeersService({
295
327
  await persistErrors({ ...lastErrorAt });
296
328
  }
297
329
  const prev = stats[url] || {};
330
+ // ⚠ 成功分支用 next **整体重建**该节点条目(清 fails/streak429/deadFails 等瞬时量),
331
+ // 所以累计量 wins/failTotal/days 必须经 acc 显式带回去——早先"先写 stats 再重建"的
332
+ // 写法等于每次成功把累计抹掉(成功一次归零一次,统计恒为 0)。
298
333
  const next = {
299
334
  okAt: now(),
300
335
  latencyMs: prev.latencyMs != null && typeof latencyMs === "number"
@@ -307,11 +342,19 @@ export function createPeersService({
307
342
  };
308
343
  // 探测死亡标记:请求成功 = 节点活着,冷却窗口立即解除。
309
344
  // next 本身不含 probeDeadAt 键,天然清除;这里只兜底确保没有残留。
310
- stats[url] = next;
345
+ const acc = accOf(prev);
346
+ if (!provisional) {
347
+ acc.wins += 1;
348
+ bumpDayInto(acc, "ok");
349
+ }
350
+ stats[url] = { ...next, ...acc };
311
351
  delete stats[url].probeDeadAt;
312
352
  } else {
313
353
  const prev = stats[url] || {};
314
- stats[url] = { ...prev, fails: (prev.fails || 0) + 1 };
354
+ const acc = accOf(prev);
355
+ acc.failTotal += 1;
356
+ bumpDayInto(acc, "fail");
357
+ stats[url] = { ...prev, fails: (prev.fails || 0) + 1, ...acc };
315
358
  }
316
359
  await persistStats({ ...stats });
317
360
  }
@@ -341,5 +384,6 @@ export function createPeersService({
341
384
  }, isHot, stat, ordered, orderedByLastError, coolingByLastError, available, next,
342
385
  standbyChain, nextStandby, isLeaderPeer,
343
386
  recordError, recordResult, recordProbeDead, recordProbeOk, errors: () => ({ ...lastErrorAt }), stats: () => ({ ...stats }),
387
+ todayKey,
344
388
  };
345
389
  }
@@ -74,7 +74,8 @@ export async function handleHedge({
74
74
  if (hedged.winner === "peer" && hedged.peerInfo) {
75
75
  const win = hedged.peerInfo;
76
76
  evt("peer-race-win", { reqId: handlerCtx.reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs, hedged: true, ttfMs: hedged.ttfMs });
77
- await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
77
+ // 同 peer-handler:此处只拿到响应头,先记 provisional;送达后(r.handled)再记真正的成功。
78
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target, provisional: true });
78
79
  const bufferedPeerRes = { ...win.res, body: hedged.bufferedBody, headers: win.res.headers, status: win.res.status, _t: win.res._t };
79
80
  const pipeline = createPipeline({
80
81
  relay,
@@ -107,6 +108,8 @@ export async function handleHedge({
107
108
  startedAt,
108
109
  });
109
110
  if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
111
+ // 真正送达才算这个节点成功转发一次(口径见 peers.js bumpWins 注释)
112
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
110
113
  return { handled: true };
111
114
  }
112
115
  }
@@ -54,7 +54,9 @@ export async function handlePeerRelay({
54
54
  return { handled: false, lastErr: { model, upstream: null, status: allLimit ? 429 : 502, message: `peers failed: ${detail}` } };
55
55
  }
56
56
  evt("peer-race-win", { reqId: handlerCtx.reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
57
- await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
57
+ // 这里只拿到**响应头**,pipeline 后面还可能超时/空轮。故先记 provisional(只暖热缓存、参与选路),
58
+ // 真正的成功计数等 pipeline 报 handled:true 再记——否则"收到 200 但零正文"也被算成转发成功。
59
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target, provisional: true });
58
60
 
59
61
  const pipeline = createPipeline({
60
62
  relay,
@@ -88,5 +90,7 @@ export async function handlePeerRelay({
88
90
  });
89
91
  // Note: 超时/失败必须把 handled:false 交回上层换候选(丢弃 → 响应永不 end)— 见 .agents/notes/implemented/bug-fix/2026-09-17-relay-first-chunk-gate-real-cancel.md
90
92
  if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
93
+ // 真正送达才算这个节点成功转发一次(口径见 peers.js 的 bumpWins 注释)
94
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
91
95
  return { handled: true };
92
96
  }
@@ -116,8 +116,8 @@ export async function handleViaRoute({
116
116
  // 502/429 等可 fallback 到 direct
117
117
  return { handled: false, lastErr: { model, upstream: upRes, status, message: msg } };
118
118
  }
119
- // 成功:记录并走 pipeline 中继(复用 peer 的 streaming 逻辑)
120
- try { await peers.recordResult(peer.url, { ok: true, latencyMs: 0, model }); } catch {}
119
+ // 成功:只拿到响应头,先记 provisional(只暖热缓存);送达后(r.handled)再记真正的成功
120
+ try { await peers.recordResult(peer.url, { ok: true, latencyMs: 0, model, provisional: true }); } catch {}
121
121
  evt("via-route-win", { reqId: handlerCtx.reqId, peer: peer.url, model });
122
122
  const pipeline = createRelayPipeline({
123
123
  relay,
@@ -152,5 +152,7 @@ export async function handleViaRoute({
152
152
  streamTimeoutMs: LAST_CANDIDATE_TIMEOUT_MS,
153
153
  });
154
154
  if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
155
+ // 真正送达才算这个节点成功转发一次(口径见 peers.js bumpWins 注释)
156
+ try { await peers.recordResult(peer.url, { ok: true, latencyMs: 0, model }); } catch {}
155
157
  return { handled: true };
156
158
  }