mslxdff 0.1.207 → 0.1.208

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.
@@ -111,6 +111,7 @@
111
111
  | 组员开关 | `-use-group [on\|off]` / `--use-group [on\|off]` | opencode 失败时是否走组员(默认 on,off 则所有供应商仅本机;key 供应商默认恒直连,`MSLXDFF_USE_GROUP_KEYS=1` 可开回;cline/workbuddy 恒禁) |
112
112
  | 白嫖雷达 | `-free` / `--free` / `-free-check` / `--free-check` | V2EX 单源白嫖雷达(`latest.json + hot.json` 按白嫖|限免|免费额度过滤) |
113
113
  | 白嫖 watch | `-free-watch` / `--free-watch` | V2EX 白嫖雷达 watch(每 5 分钟轮询) |
114
+ | IPv6 出口对照 | `-v6-pool [test] [--count N] [--model <id>]` | **Linux VPS** 专用:直连(IPv4)打一条 free 层请求取基线,再从 v6 段随机采 N 个源地址各打同一条(只换源地址)→ **基线 429 + v6 有 200 ⇒ 换 IP 能换到新额度**;v6 也 429 ⇒ 换 IP 抬不动。自造 `/128` 用完即撤;非 Linux 如实跳过 |
114
115
  | 自启开 | `-enable-autostart` / `--enable-autostart` | 开机自启(Windows 任务计划 / Linux systemd) |
115
116
  | 自启关 | `-disable-autostart` / `--disable-autostart` | 关闭开机自启 |
116
117
  | 自启状态 | `-autostart status` / `--autostart status` | 查看自启状态 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.207",
3
+ "version": "0.1.208",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,123 @@
1
+ // mslxdff -v6-pool [test|help] —— IPv6 出口额度对照测试(在 VPS 上验证 v6 池值不值得用)。
2
+ //
3
+ // 目的(用户 2026-10-18 明确):**当这台 VPS 的 IPv4 出口撞 429 时,对比测一下 IPv6 出口还有没有额度**。
4
+ // 所以本命令做 A/B:
5
+ // ① 直连(IPv4/系统默认出口)打一条免费层请求,记下额度状态作基线;
6
+ // ② 从 v6 段里随机采 N 个源地址,各打**同一条**请求(同 UA/模型/stream/tools,只换源地址);
7
+ // ③ 对照两者:v6 有 200 ⇒ 池有用(换 IP 换到新额度);v6 全 429 ⇒ 换 IP 抬不动。
8
+ //
9
+ // 纯可逆:自造的 /128 用完即撤(bind 模式不碰网卡)。非 Linux 如实跳过。
10
+ import { runV6PoolCompare, V6TEST_DEFAULT, DEFAULT_PROBE_MODEL } from "../../v6pool/selftest.js";
11
+ import { argValue } from "../policy.js";
12
+
13
+ const FLAG = "-v6-pool";
14
+
15
+ function flagValue(args, ...names) {
16
+ for (const name of names) {
17
+ const v = argValue(args, name);
18
+ if (v !== null && v !== undefined && !String(v).startsWith("-")) return v;
19
+ const eq = args.find((a) => a.startsWith(`${name}=`));
20
+ if (eq) return eq.slice(eq.indexOf("=") + 1);
21
+ }
22
+ return null;
23
+ }
24
+
25
+ const kindZh = { "free-quota": "429 额度耗尽", "free-tier": "403 免费层资格", "ip-limit": "429 限流", "black-quota": "账号日配额", network: "网络不可达", none: "非额度信号" };
26
+ const line = (r) => {
27
+ if (r.error) return `✗ 不可达(${String(r.error).slice(0, 50)})`;
28
+ if (r.ok) return `✅ HTTP ${r.status} 有额度`;
29
+ return `⛔ HTTP ${r.status} ${kindZh[r.kind] || r.kind}`;
30
+ };
31
+
32
+ export async function handleV6Pool(args = []) {
33
+ const at = args.findIndex((x) => x === FLAG || x.startsWith(`${FLAG}=`));
34
+ if (at < 0) return false;
35
+ const sub = (args[at].includes("=") ? args[at].split("=")[1] : args[at + 1] || "test").toLowerCase();
36
+ if (["-h", "--help", "help"].includes(sub)) return printUsage();
37
+
38
+ const count = (() => { const v = flagValue(args, "--count", "-n"); return v === null ? V6TEST_DEFAULT : Math.max(1, Math.min(64, Number(v) || V6TEST_DEFAULT)); })();
39
+ const model = flagValue(args, "--model", "-m") || DEFAULT_PROBE_MODEL;
40
+
41
+ console.log("=".repeat(66));
42
+ console.log(" IPv6 出口额度对照 —— 「IPv4 撞 429 时,IPv6 还有额度吗」");
43
+ console.log("=".repeat(66));
44
+ console.log("");
45
+ console.log(` 探测模型 ${model}|采样 ${count} 个 v6 源地址|只换源地址,其余请求完全相同`);
46
+ console.log("");
47
+
48
+ const onProgress = (ev) => {
49
+ if (ev.phase === "baseline-start") console.log(` ▸ 基线:直连(IPv4 / 系统默认出口)打一条免费层请求…`);
50
+ else if (ev.phase === "baseline-done") {
51
+ console.log(` 基线结果:${line(ev)}${ev.body ? `(${String(ev.body).slice(0, 80).replace(/\s+/g, " ")})` : ""}`);
52
+ console.log("");
53
+ } else if (ev.phase === "v6-start") process.stdout.write(` [${String(ev.i).padStart(2)}/${ev.n}] ${ev.addr} … `);
54
+ else if (ev.phase === "v6-done") process.stdout.write(`${line(ev)}\n`);
55
+ };
56
+
57
+ let r;
58
+ try {
59
+ r = await runV6PoolCompare({ count, model, onProgress });
60
+ } catch (e) {
61
+ console.log("");
62
+ console.log(`❌ 测试执行出错:${e?.message ?? e}`);
63
+ console.log(` 重试:mslxdff ${FLAG}`);
64
+ process.exit(1);
65
+ return true;
66
+ }
67
+
68
+ if (r.status === "skip") {
69
+ console.log(`❌ 无法测 v6 出口:${r.reason}`);
70
+ console.log(` 下一步:${r.hint}`);
71
+ process.exit(1);
72
+ return true;
73
+ }
74
+
75
+ console.log("");
76
+ console.log("-".repeat(66));
77
+ console.log(` 基线(IPv4/直连):${line(r.baseline)}`);
78
+ console.log(` v6 采样:有额度 ${r.v6OkCount}|429 ${r.v6LimitedCount}|不可达 ${r.v6NetCount}(共 ${r.requested})`);
79
+ console.log("");
80
+ const verdicts = {
81
+ "v6-works": {
82
+ mark: "✅",
83
+ text: `IPv6 **能换到新额度**:${r.v6OkCount}/${r.requested} 个 v6 出口拿到 200`,
84
+ next: ` 结论:v6 池对这台机器**有用** —— IPv4 耗尽时换 v6 出口能续。daemon 建池时会自动启用。`,
85
+ },
86
+ "v6-also-limited": {
87
+ mark: "⛔",
88
+ text: `IPv6 **也全 429**:换 IP 没换到新额度`,
89
+ next: ` 结论:v6 池对这台机器**无意义**(要么上游不按出口 IP 计额,要么该段没真路由出去)。建议 MSLXDFF_V6_POOL=0。`,
90
+ },
91
+ "v6-unreachable": {
92
+ mark: "❌",
93
+ text: `IPv6 源地址**全都够不着上游**(段没路由到本机)`,
94
+ next: ` 结论:本机这段 v6 不能用作出口。查机房是否把该段 routed 过来,或用 MSLXDFF_V6_PREFIX 指更准的段。`,
95
+ },
96
+ mixed: {
97
+ mark: "△",
98
+ text: `部分可用:${r.v6OkCount}/${r.requested} 个 v6 出口有额度`,
99
+ next: ` 结论:v6 池**部分可用**(可能有些 /128 被沿途丢弃)。够用即可开着。`,
100
+ },
101
+ }[r.verdict] || { mark: "?", text: "未判定", next: "" };
102
+ console.log(` ${verdicts.mark} ${verdicts.text}`);
103
+ if (verdicts.next) console.log(verdicts.next);
104
+ console.log("");
105
+ console.log(` 重试:mslxdff ${FLAG} --count ${r.requested}${model !== DEFAULT_PROBE_MODEL ? ` --model ${model}` : ""}`);
106
+ process.exit(r.verdict === "v6-works" || r.verdict === "mixed" ? 0 : 1);
107
+ }
108
+
109
+ function printUsage() {
110
+ console.log(`用法: mslxdff ${FLAG} [test] [--count N] [--model <id>]`);
111
+ console.log("");
112
+ console.log(" 在 Linux VPS 上做 A/B:直连(IPv4)打一条 free 层请求取基线,");
113
+ console.log(" 再从 v6 段随机采 N 个源地址各打同一条,对照 IPv6 出口还有没有额度。");
114
+ console.log(" 基线 429 + v6 有 200 ⇒ 池有用(换 IP 换到新额度)");
115
+ console.log(" 基线 429 + v6 也 429 ⇒ 换 IP 抬不动(上游不按出口 IP 计额 / 段没路由过来)");
116
+ console.log("");
117
+ console.log("相关 env(与 daemon 建池同源):");
118
+ console.log(" MSLXDFF_V6_PREFIX=<段>/<位数> 显式指定池前缀(缺省取本机全局地址前缀)");
119
+ console.log(" MSLXDFF_V6_IFACE=<网卡> 指定网卡");
120
+ console.log(" MSLXDFF_V6_CLAIM=auto|bind|add 认领方式(bind 需 ip_nonlocal_bind=1;add 需 root)");
121
+ process.exit(0);
122
+ return true;
123
+ }
package/src/cli/index.js CHANGED
@@ -41,6 +41,9 @@ export async function run(args = process.argv.slice(2)) {
41
41
  if (await handleAutostart(args)) return;
42
42
  if (await handleFree(args)) return;
43
43
 
44
+ const { handleV6Pool } = await import("./commands/v6-pool.js");
45
+ if (await handleV6Pool(args)) return;
46
+
44
47
  const { handleProviders, handleProvider } = await import("./commands/provider.js");
45
48
  if (await handleProviders(args)) return;
46
49
  if (await handleProvider(args)) return;
@@ -0,0 +1,154 @@
1
+ // v6pool · 出口对照测试:回答「这台 VPS 的 IPv4 出口撞 429 时,换成 IPv6 出口还有没有额度」。
2
+ //
3
+ // 这才是 v6 池的**唯一价值判据**:opencode 免费层按**出口 IP** 计额。若本机 IPv4 已耗尽
4
+ // (打上游回 429 FreeUsageLimitError),而某个 IPv6 出口还能 200 ⇒ 池有用(换 IP 换到新额度);
5
+ // 若 IPv6 出口也 429 ⇒ 要么上游按别的东西计额(账号/session),要么该段没真路由出去。
6
+ //
7
+ // 对照纪律:**除"源地址"外一切相同** —— 同 URL、同 UA、同模型、同 `stream:true`+五核心工具
8
+ // (否则会 403 FreeTierError 而非 429,测出来的不是额度);差异只允许来自"哪个源地址出网"。
9
+ // 判定复用 `warp-pool/verdict.js` 的 classifyWarpSignal(与生产换址判决同源,不自造一套)。
10
+ //
11
+ // IO 全部注入(agentFactory/fetch/detect/claimer),零真机依赖,可单测。
12
+ import { getUndici, timeoutSignal } from "../compat.js";
13
+ import { classifyWarpSignal } from "../warp-pool/verdict.js";
14
+ import { opencodeUa } from "../opencode-identity.js";
15
+ import { CORE_AGENT_TOOL_NAMES } from "../free-lane.js";
16
+ import { detectPool } from "./detect.js";
17
+ import { resolveV6Config } from "./config.js";
18
+ import { createClaimer } from "./claim.js";
19
+ import { randomAddress, parsePrefix, hostBitsOf } from "./prefix.js";
20
+
21
+ /** 采样上限:抽查,不扫全段。 */
22
+ export const V6TEST_MAX = 64;
23
+ export const V6TEST_DEFAULT = 6;
24
+
25
+ /** 默认探测模型:免费池里最有代表性、额度最容易耗的一个。 */
26
+ export const DEFAULT_PROBE_MODEL = "deepseek-v4.1-flash-free";
27
+
28
+ /** 免费层请求体形状(stream:true + 五核心工具名),缺了会 403 而非 429 ⇒ 测不出额度。 */
29
+ export function freeLaneBody(model, prompt = "hi") {
30
+ return {
31
+ model,
32
+ stream: true,
33
+ max_tokens: 16,
34
+ messages: [{ role: "user", content: prompt }],
35
+ tools: CORE_AGENT_TOOL_NAMES.map((name) => ({
36
+ type: "function",
37
+ function: { name, description: `The ${name} tool.`, parameters: { type: "object", properties: {} } },
38
+ })),
39
+ };
40
+ }
41
+
42
+ /** 读响应正文(有上界,失败给空串——不读就无法判决 403/429 的具名类名)。 */
43
+ async function readBody(res, max = 2048) {
44
+ try {
45
+ const t = await res.text();
46
+ return t.slice(0, max);
47
+ } catch {
48
+ return "";
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 从某个源地址打一次免费层 chat,返回判决。
54
+ * `addr=""` 表示**直连**(走本机默认路由 = IPv4 出口,或双栈默认)。
55
+ * @returns {Promise<{addr, ok, status, kind, rotate, ms, body, error}>}
56
+ */
57
+ export async function probeUpstreamFrom({ addr = "", model, baseUrl = "https://opencode.ai", agentFactory, fetchImpl, timeoutMs = 20_000, prompt } = {}) {
58
+ const t0 = Date.now();
59
+ let agent = null;
60
+ try {
61
+ agent = addr ? agentFactory(addr) : null; // 直连:不注入 dispatcher,走系统默认出口
62
+ const f = fetchImpl || getUndici().fetch;
63
+ const res = await f(`${baseUrl}/zen/v1/chat/completions`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json", "User-Agent": opencodeUa(), Accept: "text/event-stream" },
66
+ body: JSON.stringify(freeLaneBody(model, prompt)),
67
+ ...(agent ? { dispatcher: agent } : {}),
68
+ signal: timeoutSignal(timeoutMs),
69
+ });
70
+ const body = await readBody(res);
71
+ const v = classifyWarpSignal({ status: res.status, text: body });
72
+ return { addr, ok: res.status >= 200 && res.status < 300, status: res.status, kind: v.kind, rotate: v.rotate, ms: Date.now() - t0, body };
73
+ } catch (e) {
74
+ return { addr, ok: false, status: 0, kind: "network", rotate: true, ms: Date.now() - t0, body: "", error: String(e?.message || e) };
75
+ } finally {
76
+ if (agent) { try { await agent.close?.(); } catch { /* 一次性 Agent */ } }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 跑一次出口对照测试。返回结构化结果,由 CLI 渲染(本模块不打印)。
82
+ *
83
+ * @param {{ env?: object, count?: number, model?: string, baseUrl?: string,
84
+ * onProgress?: (ev: object) => void,
85
+ * deps?: { detect?, claimer?, agentFactory?, fetchImpl?, probe?, randomAddress? } }} opts
86
+ */
87
+ export async function runV6PoolCompare({
88
+ env = process.env,
89
+ count = V6TEST_DEFAULT,
90
+ model = DEFAULT_PROBE_MODEL,
91
+ baseUrl = "https://opencode.ai",
92
+ onProgress,
93
+ deps = {},
94
+ } = {}) {
95
+ const cfg = resolveV6Config(env);
96
+ const tick = (ev) => { try { onProgress?.(ev); } catch { /* 渲染出错不打断测试 */ } };
97
+
98
+ const detect = deps.detect || detectPool;
99
+ const d = detect({ cfg, env });
100
+ if (!d.ok) return { status: "skip", reason: d.reason, hint: d.hint };
101
+ const prefix = parsePrefix(`${d.prefix}`);
102
+ if (!prefix) return { status: "skip", reason: `前缀解析失败:${d.prefix}`, hint: "用 MSLXDFF_V6_PREFIX=<段>/<位数> 显式指定" };
103
+
104
+ const { Agent } = getUndici();
105
+ const agentFactory = deps.agentFactory || ((addr) => new Agent({ connect: { localAddress: addr }, connections: 1, keepAliveTimeout: 5_000 }));
106
+ const probe = deps.probe || probeUpstreamFrom;
107
+ const pick = deps.randomAddress || ((p) => randomAddress(p));
108
+
109
+ // ① 基准:直连(IPv4 / 系统默认出口),记下它的额度状态作为对照基线
110
+ tick({ phase: "baseline-start", prefix: d.prefix, model });
111
+ const baseline = await probe({ addr: "", model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl });
112
+ tick({ phase: "baseline-done", ...baseline });
113
+
114
+ // ② IPv6 池采样:每个随机源地址打同一条请求
115
+ const n = Math.max(1, Math.min(V6TEST_MAX, Number(count) || V6TEST_DEFAULT));
116
+ const claimer = deps.claimer ? deps.claimer(d, cfg) : createClaimer({ iface: d.iface, mode: d.mode, dir: cfg.dir });
117
+ const mineSet = new Set(d.mine || []);
118
+ const v6 = [];
119
+ try {
120
+ for (let i = 0; i < n; i++) {
121
+ let addr = pick(prefix);
122
+ for (let k = 0; k < 16 && mineSet.has(addr); k++) addr = pick(prefix);
123
+ await claimer.ensure(addr);
124
+ tick({ phase: "v6-start", i: i + 1, n, addr });
125
+ const r = await probe({ addr, model, baseUrl, agentFactory, fetchImpl: deps.fetchImpl });
126
+ v6.push(r);
127
+ tick({ phase: "v6-done", i: i + 1, n, ...r });
128
+ }
129
+ } finally {
130
+ try { await claimer.releaseAll(); } catch { /* 释放失败交给下次 sweepStale */ }
131
+ }
132
+
133
+ // ③ 判语:对照基线
134
+ const baseLimited = baseline.status === 429 || baseline.kind === "free-quota" || baseline.kind === "free-tier" || baseline.kind === "ip-limit";
135
+ const v6Ok = v6.filter((r) => r.ok);
136
+ const v6Limited = v6.filter((r) => r.status === 429);
137
+ const v6Net = v6.filter((r) => !r.status);
138
+
139
+ let verdict;
140
+ const allOk = v6Ok.length === v6.length && v6.length > 0;
141
+ if (allOk) verdict = "v6-works"; // 全部 v6 出口都有额度
142
+ else if (v6Limited.length === v6.length) verdict = "v6-also-limited"; // 全部 429 ⇒ 没换到新额度
143
+ else if (v6Net.length === v6.length) verdict = "v6-unreachable"; // 全部够不着 ⇒ 段没路由过来
144
+ else verdict = "mixed"; // 有的成有的不成
145
+
146
+ return {
147
+ status: "ok",
148
+ prefix: d.prefix, iface: d.iface, mode: d.mode, hostBits: hostBitsOf(prefix.bits), model, baseUrl,
149
+ baseline, baseLimited,
150
+ v6, requested: n,
151
+ v6OkCount: v6Ok.length, v6LimitedCount: v6Limited.length, v6NetCount: v6Net.length,
152
+ verdict,
153
+ };
154
+ }