mslxdff 0.1.211 → 0.1.213

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.211",
3
+ "version": "0.1.213",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -75,7 +75,7 @@ export async function handleV6Pool(args = []) {
75
75
  console.log("");
76
76
  console.log("-".repeat(66));
77
77
  console.log(` 基线(IPv4/直连):${line(r.baseline)}`);
78
- console.log(` v6 采样:有额度 ${r.v6OkCount}|429 ${r.v6LimitedCount}|不可达 ${r.v6NetCount}(共 ${r.requested})`);
78
+ console.log(` v6 采样:有额度 ${r.v6OkCount}|受限 ${r.v6LimitedCount}|不可达 ${r.v6NetCount}(共 ${r.requested})`);
79
79
  console.log("");
80
80
  const verdicts = {
81
81
  "v6-works": {
@@ -85,7 +85,7 @@ export async function handleV6Pool(args = []) {
85
85
  },
86
86
  "v6-also-limited": {
87
87
  mark: "⛔",
88
- text: `IPv6 **也全 429**:换 IP 没换到新额度`,
88
+ text: `IPv6 **也全受限**(换 IP 没换到新额度)`,
89
89
  next: ` 结论:v6 池对这台机器**无意义**(要么上游不按出口 IP 计额,要么该段没真路由出去)。建议 MSLXDFF_V6_POOL=0。`,
90
90
  },
91
91
  "v6-unreachable": {
@@ -2,13 +2,13 @@
2
2
  //
3
3
  // 为什么需要它:这条链路跨 3 台机器 + 1 道鉴权,坏了只会看到一句 `502 fetch failed`,
4
4
  // 完全无法定位是哪一跳断的。本模块**按阶段**记录,把断点直接指出来:
5
- // ① 定位本机组/组长 → ② 取组长 token(调组长 forward 要**组长的** token,见下)→
5
+ // ① 定位本机组/组长 → ② 取组长 token(调组长 forward 用组长自己的 token 恒可)→
6
6
  // ③ POST 组长 /v1/groups/relay/forward → ④ 判读回包(200 拿内容 / 401 鉴权 / 404 成员不存在 / 502·504 转发失败)
7
7
  //
8
- // ⚠ 鉴权事实(2026-10-19 实测):`authorized()` 比的是**接收方自己的 token**,
9
- // 而 `/v1/groups/relay/forward` 在路由层 requiresAuth=true ⇒ 要**组长 token**。
10
- // 普通组员只有自己的 token ⇒ 调不动组长 forward(这就是"组员经组长"从没通的原因)。
11
- // 本命令调用时用组长 token;若拿不到组长 token(如你只是组员且未持有),会明确报出这一步。
8
+ // 鉴权(2026-10-19 修正):forward 路由由 `requiresAuth:true` 改为鉴权下沉到 handler——
9
+ // 接受**组长 token 或本组成员 token**。旧实现只认组长 token,导致生产里成员经组长转发一律 401、
10
+ // 组员接力从未通过(deepseek-v4.1-flash-free.log 所有 relay-fail 均 401)。本命令仍用组长 token 调
11
+ // (组长 token 恒可),若拿不到则明确报出这一步。
12
12
  import { loadPeers, loadGroupsJoined, loadToken } from "../state.js";
13
13
  import { compatFetch, timeoutSignal } from "../compat.js";
14
14
 
@@ -87,8 +87,8 @@ export async function runGroupLinkTest({
87
87
  // 按阶段归类失败 —— 这是本命令存在的意义:一眼看出断在哪一跳
88
88
  if (res.status === 401) {
89
89
  return { status: "error", stage: "auth", httpStatus: 401, target, leaderUrl: lead.leaderUrl, group: groupName, ms,
90
- error: "组长拒了鉴权(401):调 /v1/groups/relay/forward 需要**组长的** token,本机没有",
91
- hint: "这是已知缺陷:组员中继链路的鉴权断了。修复 = 组长侧 forward 应接受成员 token(见 -group test 的 PR)" };
90
+ error: "组长拒了鉴权(401):本机持有的 token 既不是组长 token、也不在该组成员表里",
91
+ hint: "检查本机是否真的已加入该组(state.peers[leader].token / groupsJoined 是否过期);用 -addtogroup 重新入组。注:0.1.213+ 组长已接受成员 token,正常入组的节点不该再 401。" };
92
92
  }
93
93
  if (res.status === 404) {
94
94
  return { status: "error", stage: "target-missing", httpStatus: 404, target, leaderUrl: lead.leaderUrl, group: groupName, ms, error: txt.slice(0, 200), hint: "该目标不在组内(名称/url 对不上);用 -group list 核对" };
@@ -1,4 +1,4 @@
1
- import { clientIp, json, readBody, parseHops, errMsg } from "./helpers.js";
1
+ import { clientIp, json, readBody, parseHops, errMsg, authorized } from "./helpers.js";
2
2
  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";
@@ -148,11 +148,20 @@ export async function streamHandler({ req, res, groups, bus, logs }) {
148
148
  await new Promise(() => {});
149
149
  }
150
150
 
151
- export async function forwardHandler({ req, res, groups, bus, logs }) {
151
+ export async function forwardHandler({ req, res, groups, bus, logs, token }) {
152
152
  let body;
153
153
  try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
154
154
  const groupName = body?.group || body?.name;
155
155
  const target = body?.target || body?.url;
156
+ // 鉴权(对齐 heartbeat/poll/result):接受**组长自己的 token**(本机 forward 自用)**或本组的任一成员 token**
157
+ // (成员经组长转发用的就是成员 token)。路由层 authorized() 只认组长那一个 token,故本路由 requiresAuth:false,
158
+ // 校验下沉到这里——否则成员中继一律 401(现网 2026-10-19:所有 relay-fail 均 status=401)。
159
+ const bearer = (/^Bearer (.+)$/.exec(req.headers["authorization"] || "") || [])[1] || "";
160
+ const authed = Boolean(bearer) && (authorized(req, token) || Boolean(groupName && groups?.membersForToken?.(groupName, bearer)));
161
+ if (!authed) {
162
+ res.setHeader("WWW-Authenticate", "Bearer");
163
+ return json(res, 401, { error: "Unauthorized" });
164
+ }
156
165
  const hops = parseHops(req.headers["x-mslxdff-hops"] || body?.hops);
157
166
  if (!groupName || !target) return json(res, 400, { error: "group and target required" });
158
167
  if (hops >= DEFAULT_MAX_HOPS) return json(res, 429, { error: "max hops exceeded" });
@@ -110,7 +110,11 @@ const ROUTES = [
110
110
  {
111
111
  method: "POST",
112
112
  path: "/v1/groups/relay/forward",
113
- requiresAuth: true,
113
+ // 鉴权下沉到 forwardHandler:接受**组长 token 或本组成员 token**(成员经组长转发的正是成员 token)。
114
+ // 路由层 authorized(req, token) 只认组长自己那一个 token ⇒ 用 requiresAuth:true 会把成员中继一律 401
115
+ // (现网 2026-10-19 实证:deepseek-v4.1-flash-free.log 里所有 relay-fail 都是 status=401,组员接力从未通过)。
116
+ // 同族 heartbeat/poll/result 同理走 requiresAuth:false + membersForToken 自校验。
117
+ requiresAuth: false,
114
118
  handler: forwardHandler,
115
119
  },
116
120
  {
@@ -4,14 +4,15 @@
4
4
  // (打上游回 429 FreeUsageLimitError),而某个 IPv6 出口还能 200 ⇒ 池有用(换 IP 换到新额度);
5
5
  // 若 IPv6 出口也 429 ⇒ 要么上游按别的东西计额(账号/session),要么该段没真路由出去。
6
6
  //
7
- // 对照纪律:**除"源地址"外一切相同** —— 同 URL、同 UA、同模型、同 `stream:true`+五核心工具
8
- // (否则会 403 FreeTierError 而非 429,测出来的不是额度);差异只允许来自"哪个源地址出网"。
7
+ // 对照纪律:**除"源地址"外一切相同** —— 同 URL、同 UA、同**身份四件套 header**、同模型、同 `stream:true`+五核心工具
8
+ // (免费层要流式+五核心工具+身份头,缺任一项都会 403 FreeTierError 而非 429,测出来的不是额度);
9
+ // 整轮基线+v6 共用同一个 session。差异只允许来自"哪个源地址出网"。
9
10
  // 判定复用 `warp-pool/verdict.js` 的 classifyWarpSignal(与生产换址判决同源,不自造一套)。
10
11
  //
11
12
  // IO 全部注入(agentFactory/fetch/detect/claimer),零真机依赖,可单测。
12
13
  import { getUndici, timeoutSignal } from "../compat.js";
13
14
  import { classifyWarpSignal } from "../warp-pool/verdict.js";
14
- import { opencodeUa } from "../opencode-identity.js";
15
+ import { opencodeUa, genId } from "../opencode-identity.js";
15
16
  import { CORE_AGENT_TOOL_NAMES } from "../free-lane.js";
16
17
  import { detectPool } from "./detect.js";
17
18
  import { resolveV6Config } from "./config.js";
@@ -49,12 +50,29 @@ async function readBody(res, max = 2048) {
49
50
  }
50
51
  }
51
52
 
53
+ /** 免费层身份头(与生产 buildHeaders 同源,见 src/upstream.js):**缺了必 403 FreeTierError**。
54
+ * 实测 2026-10-19:只发 User-Agent → 403;补上 x-opencode-* 四件套 → 200。
55
+ * session 用随机 genId(对照测试测的是"出口 IP"不是"会话亲和",每个探针独立即可)。 */
56
+ export function freeLaneHeaders({ session = genId("ses_") } = {}) {
57
+ return {
58
+ "Content-Type": "application/json",
59
+ Accept: "text/event-stream",
60
+ "User-Agent": opencodeUa(),
61
+ "x-opencode-client": "desktop",
62
+ "x-opencode-session": session,
63
+ "x-opencode-request": genId("msg_"),
64
+ "x-opencode-project": "global",
65
+ "HTTP-Referer": "https://hermes-agent.nousresearch.com",
66
+ "X-Title": "Hermes Agent",
67
+ };
68
+ }
69
+
52
70
  /**
53
71
  * 从某个源地址打一次免费层 chat,返回判决。
54
72
  * `addr=""` 表示**直连**(走本机默认路由 = IPv4 出口,或双栈默认)。
55
73
  * @returns {Promise<{addr, ok, status, kind, rotate, ms, body, error}>}
56
74
  */
57
- export async function probeUpstreamFrom({ addr = "", model, baseUrl = "https://opencode.ai", agentFactory, fetchImpl, timeoutMs = 20_000, prompt } = {}) {
75
+ export async function probeUpstreamFrom({ addr = "", model, baseUrl = "https://opencode.ai", agentFactory, fetchImpl, timeoutMs = 20_000, prompt, session } = {}) {
58
76
  const t0 = Date.now();
59
77
  let agent = null;
60
78
  try {
@@ -62,7 +80,7 @@ export async function probeUpstreamFrom({ addr = "", model, baseUrl = "https://o
62
80
  const f = fetchImpl || getUndici().fetch;
63
81
  const res = await f(`${baseUrl}/zen/v1/chat/completions`, {
64
82
  method: "POST",
65
- headers: { "Content-Type": "application/json", "User-Agent": opencodeUa(), Accept: "text/event-stream" },
83
+ headers: freeLaneHeaders({ session }),
66
84
  body: JSON.stringify(freeLaneBody(model, prompt)),
67
85
  ...(agent ? { dispatcher: agent } : {}),
68
86
  signal: timeoutSignal(timeoutMs),
@@ -105,10 +123,13 @@ export async function runV6PoolCompare({
105
123
  const agentFactory = deps.agentFactory || ((addr) => new Agent({ connect: { localAddress: addr }, connections: 1, keepAliveTimeout: 5_000 }));
106
124
  const probe = deps.probe || probeUpstreamFrom;
107
125
  const pick = deps.randomAddress || ((p) => randomAddress(p));
126
+ // 整轮共用一个 session:对照纪律「除源地址外一切相同」——让差异只可能来自出口 IP,
127
+ // 而不是会话随机性(免费层按出口 IP 计额,但共用 session 才能把这一点测干净)。
128
+ const session = deps.session || genId("ses_");
108
129
 
109
130
  // ① 基准:直连(IPv4 / 系统默认出口),记下它的额度状态作为对照基线
110
131
  tick({ phase: "baseline-start", prefix: d.prefix, model });
111
- const baseline = await probe({ addr: "", model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl });
132
+ const baseline = await probe({ addr: "", model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl, session });
112
133
  tick({ phase: "baseline-done", ...baseline });
113
134
 
114
135
  // ② IPv6 池采样:每个随机源地址打同一条请求
@@ -122,7 +143,7 @@ export async function runV6PoolCompare({
122
143
  for (let k = 0; k < 16 && mineSet.has(addr); k++) addr = pick(prefix);
123
144
  await claimer.ensure(addr);
124
145
  tick({ phase: "v6-start", i: i + 1, n, addr });
125
- const r = await probe({ addr, model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl });
146
+ const r = await probe({ addr, model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl, session });
126
147
  v6.push(r);
127
148
  tick({ phase: "v6-done", i: i + 1, n, ...r });
128
149
  }
@@ -133,13 +154,16 @@ export async function runV6PoolCompare({
133
154
  // ③ 判语:对照基线
134
155
  const baseLimited = baseline.status === 429 || baseline.kind === "free-quota" || baseline.kind === "free-tier" || baseline.kind === "ip-limit";
135
156
  const v6Ok = v6.filter((r) => r.ok);
136
- const v6Limited = v6.filter((r) => r.status === 429);
157
+ // 受限 = 有 HTTP 响应但拿不到额度(403 FreeTierError / 429 额度 / 其余 4xx-5xx);
158
+ // 只有网络层失败(status=0)才算"够不着"。旧实现只认 429,403 全 0/4 时会掉进兜底 "mixed"
159
+ // 误判成「部分可用」(真机 2026-10-19 实测:4 个 v6 全 403 → 却报 0/4 部分可用)。
160
+ const v6Limited = v6.filter((r) => !r.ok && r.status);
137
161
  const v6Net = v6.filter((r) => !r.status);
138
162
 
139
163
  let verdict;
140
164
  const allOk = v6Ok.length === v6.length && v6.length > 0;
141
165
  if (allOk) verdict = "v6-works"; // 全部 v6 出口都有额度
142
- else if (v6Limited.length === v6.length) verdict = "v6-also-limited"; // 全部 429 ⇒ 没换到新额度
166
+ else if (v6Limited.length === v6.length) verdict = "v6-also-limited"; // 全部受限 ⇒ 没换到新额度
143
167
  else if (v6Net.length === v6.length) verdict = "v6-unreachable"; // 全部够不着 ⇒ 段没路由过来
144
168
  else verdict = "mixed"; // 有的成有的不成
145
169