mslxdff 0.1.104 → 0.1.106

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/auto.js +3 -3
  3. package/src/chat/engine.js +2 -2
  4. package/src/chat/gateway.js +6 -2
  5. package/src/chat/orchestrator.js +7 -0
  6. package/src/chat/prompt.js +2 -2
  7. package/src/chat/repl.js +11 -3
  8. package/src/chat/terminal.js +40 -0
  9. package/src/chat-pipeline/index.js +14 -3
  10. package/src/chat-pipeline/policy.js +3 -0
  11. package/src/chat-pipeline/serial-trial.js +15 -5
  12. package/src/cli/commands/provider/bench-via.js +0 -9
  13. package/src/cli/commands/provider/bench.js +0 -8
  14. package/src/cli/commands/provider/index.js +0 -4
  15. package/src/cli/commands/provider/models.js +0 -3
  16. package/src/cli/commands/use-group.js +56 -0
  17. package/src/cli/help.js +2 -0
  18. package/src/cli/index.js +2 -0
  19. package/src/providers/registry.js +0 -5
  20. package/src/routes/hedge.js +1 -2
  21. package/src/runtime/providers-setup.js +1 -2
  22. package/src/state/facade.js +1 -0
  23. package/src/state/schemas/use-group.js +56 -0
  24. package/src/cli/commands/provider/deepseek-health.js +0 -46
  25. package/src/cli/commands/provider/deepseek-login.js +0 -89
  26. package/src/providers/deepseek/auth.js +0 -194
  27. package/src/providers/deepseek/bridge.js +0 -158
  28. package/src/providers/deepseek/chat.js +0 -233
  29. package/src/providers/deepseek/debug.js +0 -44
  30. package/src/providers/deepseek/hash-reference.js +0 -117
  31. package/src/providers/deepseek/hash.js +0 -245
  32. package/src/providers/deepseek/health.js +0 -104
  33. package/src/providers/deepseek/index.js +0 -180
  34. package/src/providers/deepseek/pow.js +0 -99
  35. package/src/providers/deepseek/session.js +0 -117
  36. package/src/providers/deepseek/sse-decoder.js +0 -160
  37. package/src/providers/deepseek.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auto.js CHANGED
@@ -29,10 +29,10 @@ export const DEFAULT_AUTO_MODELS = [
29
29
  PREFERRED_MODEL,
30
30
  "mimo-v2.5-free",
31
31
  "deepseek-v4-flash-free",
32
- "ling-3.0-flash-free",
32
+ "ling-3.0-flash-fin-free",
33
33
  "nemotron-3-ultra-free",
34
- "north-mini-code-free",
35
- "laguna-s-2.1-free",
34
+ "nemotron-3.5-lightning-free",
35
+ "muse-spark-1.3-contributor-free",
36
36
  ].filter((id, i, arr) => id && arr.indexOf(id) === i);
37
37
 
38
38
  export function isAutoModel(model) {
@@ -43,7 +43,7 @@ export function createEngine({
43
43
  return next;
44
44
  }
45
45
 
46
- async function runTurn(userText, messages) {
46
+ async function runTurn(userText, messages, modelOverride) {
47
47
  const tools = GTOOLS();
48
48
  messages.push({ role: "user", content: userText });
49
49
  let loops = 0;
@@ -70,7 +70,7 @@ export function createEngine({
70
70
  const tCall = performance.now();
71
71
  let res;
72
72
  const activeTools = forceNoTools ? [] : tools;
73
- res = await CWF({ messages, tools: activeTools });
73
+ res = await CWF({ messages, tools: activeTools, model: modelOverride || undefined });
74
74
  if (forceNoTools && res.ok && res.message?.tool_calls?.length) {
75
75
  onTrace(`[guard] 禁工具模式下仍收到 tool_calls,已拦截`);
76
76
  res.message.tool_calls = [];
@@ -41,7 +41,7 @@ export function createGatewayClient({
41
41
  return { data: [] };
42
42
  });
43
43
 
44
- async function chatViaGateway({ messages, tools }) {
44
+ async function chatViaGateway({ messages, tools, autoProvider = "opencode" }) {
45
45
  const TRACE = env.MSLXDFF_CHAT_TRACE !== "0";
46
46
  const t0 = TRACE ? performance.now() : 0;
47
47
  let port = defaultPort;
@@ -72,7 +72,11 @@ export function createGatewayClient({
72
72
  const timer = setTimeout(() => controller.abort(), gatewayTimeoutMs);
73
73
  const res = await _fetch(url, {
74
74
  method: "POST",
75
- headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ ...(autoProvider ? { "x-mslxdff-auto-provider": autoProvider } : {}),
78
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
79
+ },
76
80
  body: JSON.stringify(body),
77
81
  signal: controller.signal,
78
82
  });
@@ -53,6 +53,13 @@ export function createOrchestrator({
53
53
 
54
54
  async function chatWithFallback(opts) {
55
55
  if (chatWithFallbackImpl) return chatWithFallbackImpl(opts);
56
+ // 严格模式:显式指定了具体模型(非 auto/空)→ 只用该模型,失败即报错,绝不降级到其他模型
57
+ const pinned = String(opts?.model || "").trim();
58
+ if (pinned && pinned.toLowerCase() !== "auto") {
59
+ const r = await safeChatOnce(opts, pinned);
60
+ if (r.ok) return { ...r, model: pinned };
61
+ return { ok: false, error: `指定模型 ${pinned} 失败:${r.error}(已锁定不自动换模型;如需自动择优请输入 /model auto)`, status: r.status || 502, pinnedModel: pinned };
62
+ }
56
63
  const TRACE = env.MSLXDFF_CHAT_TRACE !== "0";
57
64
  const HEDGE_MS = (() => {
58
65
  const v = Number(env.MSLXDFF_HEDGE_DELAY_MS);
@@ -22,8 +22,8 @@ function readModels() {
22
22
  if (ids.length) return ids;
23
23
  }
24
24
  } catch {}
25
- // 兜底:硬编码常见 free 模型,供离线时参考
26
- return ["mimo-v2.5-free", "big-pickle", "deepseek-v4-flash-free", "hy3-free", "laguna-free", "kimi-k2-free", "nemotron-3-nano-free", "big-pickle"];
25
+ // 兜底:硬编码常见 free 模型,供离线时参考(2026-09 实测 /zen/v1/models free 池)
26
+ return ["big-pickle", "mimo-v2.5-free", "ling-3.0-flash-fin-free", "deepseek-v4-flash-free", "nemotron-3.5-lightning-free", "nemotron-3-ultra-free", "muse-spark-1.3-contributor-free", "muse-spark-1.2-contributor-free"];
27
27
  }
28
28
 
29
29
  export function buildSystemPrompt({ modelsOverride } = {}) {
package/src/chat/repl.js CHANGED
@@ -31,6 +31,12 @@ export async function startRepl({ singleShot } = {}) {
31
31
  curlTool,
32
32
  onTrace: trace,
33
33
  });
34
+ let pinnedModel = null; // /model 锁定的模型:严格单模型,不通即报错(null=默认链)
35
+
36
+ function applyPrompt() {
37
+ const label = pinnedModel || CHAT_PREFERRED.split("-")[0];
38
+ rl.setPrompt(`\x1b[36m${label}>\x1b[0m `);
39
+ }
34
40
 
35
41
  if (singleShot) {
36
42
  const text = String(singleShot).trim();
@@ -47,22 +53,24 @@ export async function startRepl({ singleShot } = {}) {
47
53
 
48
54
  await printBanner();
49
55
  const rl = createReadline(`\x1b[36m${CHAT_PREFERRED.split("-")[0]}>\x1b[0m `);
56
+ applyPrompt();
50
57
  rl.prompt();
51
58
  for await (const line of rl) {
52
59
  const raw = String(line || "").trim();
53
60
  if (!raw) { rl.prompt(); continue; }
54
- const slash = handleSlash(raw, { messages, system });
61
+ const slash = handleSlash(raw, { messages, system, pinnedModel });
55
62
  if (slash.handled) {
56
63
  if (slash.exit) { console.log("再见"); saveHistory(messages.slice(1)); rl.close(); return; }
57
64
  if (slash.messages) messages = slash.messages;
65
+ if ("pinnedModel" in slash) { pinnedModel = slash.pinnedModel; applyPrompt(); }
58
66
  rl.prompt();
59
67
  continue;
60
68
  }
61
69
  try {
62
- const spinner = createSpinner("已发送给 AI,等待回复中");
70
+ const spinner = createSpinner(pinnedModel ? `已发送给 ${pinnedModel},等待回复中` : "已发送给 AI,等待回复中");
63
71
  spinner.start();
64
72
  let r;
65
- try { r = await engine.runTurn(raw, messages); }
73
+ try { r = await engine.runTurn(raw, messages, pinnedModel); }
66
74
  finally { spinner.stop(`\x1b[90m✓ AI 已回复\x1b[0m`); }
67
75
  if (r.text && !r.text.startsWith("OK") && !r.text.startsWith("FAIL")) console.log(r.text);
68
76
  if (r.model || r.latency) printFooter(r);
@@ -3,10 +3,25 @@ import { stdin, stdout } from "node:process";
3
3
  import { formatBannerLines, formatStatsDetail, collectStats, probeGateway } from "./stats.js";
4
4
  import { createSpinner } from "./spinner.js";
5
5
  import { normalizeFullId } from "../providers/model-id.js";
6
+ import { isFreeModel } from "../models.js";
7
+ import { getModelsForPrompt } from "./prompt.js";
6
8
  import { loadHistory, saveHistory, clearHistory, histPath, estimateChars } from "./store.js";
7
9
 
10
+ // -chat 只用 opencode 上游的免费模型:裸 id(无供应商前缀)+ free 池
11
+ const FALLBACK_FREE_POOL = ["big-pickle", "mimo-v2.5-free", "ling-3.0-flash-fin-free", "deepseek-v4-flash-free", "nemotron-3.5-lightning-free", "nemotron-3-ultra-free", "muse-spark-1.3-contributor-free", "muse-spark-1.2-contributor-free"];
12
+
13
+ function freePoolNow() {
14
+ try {
15
+ const ids = getModelsForPrompt().filter((id) => typeof id === "string" && !id.includes("/") && isFreeModel(id));
16
+ if (ids.length) return ids;
17
+ } catch {}
18
+ return FALLBACK_FREE_POOL;
19
+ }
20
+
8
21
  export const SLASH_HELP = `自然语言直接说,斜杠快捷:
9
22
  /help 本帮助
23
+ /model 查看/锁定模型(仅限 opencode free 池;锁定后严格只用该模型,不通即报错)
24
+ /model mimo-v2.5-free 锁定 · /model auto 解除锁定(回默认 mimo→pickle→auto 链)
10
25
  /stats 详细统计(网关 -d 的请求/延迟/模型)
11
26
  /history 查看对话历史
12
27
  /clear 清空历史
@@ -67,6 +82,31 @@ export function handleSlash(line, ctx) {
67
82
  console.log(SLASH_HELP);
68
83
  return { handled: true };
69
84
  }
85
+ if (low.startsWith("/model")) {
86
+ const arg = raw.slice(6).trim();
87
+ if (!arg) {
88
+ if (ctx.pinnedModel) console.log(`\x1b[36m当前锁定模型:${ctx.pinnedModel}(严格单模型,不通即报错)\x1b[0m`);
89
+ else console.log(`\x1b[90m未锁定模型 · 默认链:mimo-v2.5-free → big-pickle → 网关 auto\x1b[0m`);
90
+ return { handled: true };
91
+ }
92
+ if (arg === "auto" || arg === "clear" || arg === "off" || arg === "解锁" || arg === "解除") {
93
+ console.log(`\x1b[90m已解除模型锁定 · 回默认链:mimo-v2.5-free → big-pickle → 网关 auto\x1b[0m`);
94
+ return { handled: true, pinnedModel: null };
95
+ }
96
+ const id = arg;
97
+ if (id.includes("/")) {
98
+ console.log(`\x1b[31m[拒绝] ${id} 带供应商前缀 — -chat 只支持 opencode 上游模型(裸 id,直连 opencode.ai 免费池)\x1b[0m`);
99
+ console.log(`\x1b[90m其他供应商(deepseek/ workbuddy/ clinebot/ 等)请走网关:mslxdff -model set <id> 或 curl 本机 /v1/chat/completions\x1b[0m`);
100
+ return { handled: true };
101
+ }
102
+ if (!isFreeModel(id) || !freePoolNow().includes(id)) {
103
+ console.log(`\x1b[31m[拒绝] ${id} 不在 opencode free 池 — -chat 只能用 opencode 免费模型\x1b[0m`);
104
+ console.log(`\x1b[90m当前 free 池:${freePoolNow().join(", ")}\x1b[0m`);
105
+ return { handled: true };
106
+ }
107
+ console.log(`\x1b[36m[已锁定] ${id} · 严格只用该模型,不通即报错(不自动换模型)· 解除:/model auto\x1b[0m`);
108
+ return { handled: true, pinnedModel: id };
109
+ }
70
110
  if (["/stats", "/status", "stats", "status"].includes(low)) {
71
111
  console.log(formatStatsDetail());
72
112
  return { handled: true };
@@ -3,6 +3,7 @@ import { analyzePolicy } from "./policy.js";
3
3
  import { planRoute } from "./planner.js";
4
4
  import { createEngine } from "./engine.js";
5
5
  import { runHook } from "../plugins.js";
6
+ import { isFreeModel } from "../models.js";
6
7
  import { clientIp, summarizePrompt } from "../routes/helpers.js";
7
8
 
8
9
  /**
@@ -21,7 +22,7 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
21
22
  const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
22
23
 
23
24
  const policy = analyzePolicy({ headers: req?.headers || {}, body: req?.body || {} });
24
- const { requested, useAuto, lockModel, hops, shareKeys, workbuddyUid, aliasInfo } = policy;
25
+ const { requested, useAuto, autoProvider, lockModel, hops, shareKeys, workbuddyUid, aliasInfo } = policy;
25
26
  mark("parsed");
26
27
  if (aliasInfo) { try { res?.setHeader?.("x-mslxdff-alias", aliasInfo); } catch {} }
27
28
  // mslxdff/ 前缀或 alias 命中时,把 body.model 改写为还原后的模型(与原 gateway 语义一致)
@@ -31,10 +32,20 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
31
32
 
32
33
  // order 推导 + plugin model:select 可改
33
34
  // 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
35
+ // x-mslxdff-auto-provider 头可把 auto 候选限定到单供应商(opencode=裸 id 免费池),-chat 默认带
34
36
  let order;
35
37
  if (lockModel) order = [requested];
36
- else if (useAuto) order = auto ? await auto.candidates() : [""];
37
- else {
38
+ else if (useAuto) {
39
+ let cands = auto ? await auto.candidates() : [""];
40
+ if (autoProvider) {
41
+ const before = cands.length;
42
+ cands = cands.filter((m) => (autoProvider === "opencode"
43
+ ? !String(m).includes("/") && isFreeModel(m)
44
+ : String(m).startsWith(`${autoProvider}/`)));
45
+ evt("auto-scope", { reqId, provider: autoProvider, before, after: cands.length });
46
+ }
47
+ order = cands;
48
+ } else {
38
49
  if (auto && requested) { try { await auto.candidatesFor(requested); } catch {} }
39
50
  order = [requested];
40
51
  }
@@ -13,6 +13,8 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
13
13
  const shareKeys = parseShareKeysHeader(headers[SHARE_KEYS_HEADER] || headers["x-mslxdff-share-keys"] || "");
14
14
  const workbuddyUid = (headers["x-mslxdff-workbuddy-uid"] || headers["x-workbuddy-uid"] || "").toString().trim();
15
15
  const lockModel = (headers["x-mslxdff-model-lock"] || headers["X-Mslxdff-Model-Lock"] || "").toString();
16
+ // auto 范围限定:x-mslxdff-auto-provider: opencode → auto 候选只留该供应商(opencode=裸 id 免费池)
17
+ const autoProvider = (headers["x-mslxdff-auto-provider"] || headers["X-Mslxdff-Auto-Provider"] || "").toString().trim().toLowerCase() || null;
16
18
  const rawModel = body.model || "";
17
19
 
18
20
  let normalizedRequested = normalizeModel(lockModel || rawModel || "");
@@ -64,6 +66,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
64
66
  normalizedForUpstream,
65
67
  aliasInfo,
66
68
  useAuto,
69
+ autoProvider,
67
70
  shareKeys,
68
71
  workbuddyUid: extractedUid,
69
72
  lockModel,
@@ -9,6 +9,7 @@ import { handlePeerRelay } from "../routes/chat/peer-handler.js";
9
9
  import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
10
10
  import { handleViaRoute } from "../routes/chat/via-route-handler.js";
11
11
  import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
12
+ import { shouldUseGroupForModel } from "../state/schemas/use-group.js";
12
13
 
13
14
  /**
14
15
  * 串行 trial — 从 engine.js 抽出的第二段:via-route 单路径 → 串行 trial →
@@ -112,7 +113,8 @@ export async function runSerialTrial(ctx, deps = {}) {
112
113
  const isStream = Boolean(body.stream);
113
114
  const d = hedgeDelayMs();
114
115
  const hasPeers = Boolean(peers) && peers.ordered().length > 0;
115
- const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, model }) && upRes.status === 200 && upRes.body;
116
+ const canUseGroup = shouldUseGroupForModel(model);
117
+ const doHedge = canUseGroup && shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, model }) && upRes.status === 200 && upRes.body;
116
118
  if (doHedge) {
117
119
  const hr = await hedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
118
120
  if (hr.handled) return { done: true };
@@ -128,12 +130,20 @@ export async function runSerialTrial(ctx, deps = {}) {
128
130
  }
129
131
  }
130
132
  if (canForwardPeers) {
131
- const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
132
- if (pr.handled) return { done: true };
133
+ if (!shouldUseGroupForModel(model)) {
134
+ evt("group-skip", { reqId, model, reason: "useGroup=off for opencode (peer)" });
135
+ } else {
136
+ const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
137
+ if (pr.handled) return { done: true };
138
+ }
133
139
  }
134
140
  if (groups) {
135
- const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
136
- if (br.handled) return { done: true };
141
+ if (!shouldUseGroupForModel(model)) {
142
+ evt("group-skip", { reqId, model, reason: "useGroup=off for opencode (broadband)" });
143
+ } else {
144
+ const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
145
+ if (br.handled) return { done: true };
146
+ }
137
147
  }
138
148
  if (canFallback) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
139
149
  await exhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
@@ -54,13 +54,6 @@ export function filterBenchModels({ providerId, allowed, picks, allowAny = false
54
54
  }
55
55
 
56
56
  export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, loadKeys, loadAllowed, loadBaseUrl, loadAllowAny, loadModelPicks, getOnlinePeersFn }) {
57
- // DeepSeek 防禁言:via 也不测(显式传入直接返回;all 形态在 targetIds 收集处排除)
58
- if (String(providerId || "").trim().toLowerCase() === "deepseek") {
59
- const msg = "跳过 deepseek bench-via —— 网页通道易触发禁言/频率风控,体检用 mslxdff -provider deepseek health";
60
- if (opts.json) console.log(JSON.stringify({ meta: { at: new Date().toISOString(), samples: opts.samples, timeout: opts.timeoutMs, includeOpencode: false, peers: [], opencodeSkipped: true, deepseekSkipped: true }, results: [], advice: msg }, null, 2));
61
- else console.log(msg);
62
- return;
63
- }
64
57
  const { getOnlinePeers, orchestrateVia, resolveIncludeOpencode } = await import("../../../bench/via.js");
65
58
  const peers = await (typeof getOnlinePeersFn === "function" ? getOnlinePeersFn() : getOnlinePeers());
66
59
  if (!peers.length) {
@@ -106,8 +99,6 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
106
99
  const ids = new Set(Object.keys(configs));
107
100
  ids.add("opencode"); ids.add("openrouter");
108
101
  for (const pid of [...ids]) {
109
- // DeepSeek 网页通道防禁言:all 形态直接排除,即使 allowlist 非空也不进候选
110
- if (String(pid).toLowerCase() === "deepseek") continue;
111
102
  const allowed = loadAllowed(pid) || [];
112
103
  if (allowed.length) targetIds.push(pid);
113
104
  }
@@ -36,14 +36,6 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
36
36
  const isBench = sub === "bench" || sub === "benchmark" || sub === "eval" || sub === "test" || _isBenchViaAll;
37
37
  if (!isBench) return false;
38
38
  const opts = parseBenchArgs(_restArr);
39
- // DeepSeek 网页通道防禁言:bench 轰炸易触发 muted/频率风控,一律跳过(体检用 health 探活代替)
40
- const benchPidLower = String(id || "").trim().toLowerCase();
41
- if (benchPidLower === "deepseek" || benchPidLower === "ds") {
42
- const advice = "DeepSeek 网页通道(本机私有凭据)易触发禁言/频率风控,不支持 bench 测速。请用轻量体检:mslxdff -provider deepseek health";
43
- if (opts.json) console.log(JSON.stringify({ ok: false, skipped: "deepseek", advice }, null, 2));
44
- else console.log(`跳过 deepseek bench —— ${advice}`);
45
- return true;
46
- }
47
39
  if (opts.via) {
48
40
  const stateMod = await import("../../../state.js");
49
41
  const loadConfigs = deps.loadProviderConfigs || stateMod.loadProviderConfigs;
@@ -65,10 +65,6 @@ export async function handleProvider(args) {
65
65
  }
66
66
  const { handleClineLogin } = await import("./cline-login.js");
67
67
  if (await handleClineLogin(id, sub)) return true;
68
- const { handleDeepseekLogin } = await import("./deepseek-login.js");
69
- if (await handleDeepseekLogin(id, sub, rest)) return true;
70
- const { handleDeepseekHealth } = await import("./deepseek-health.js");
71
- if (await handleDeepseekHealth(id, sub, rest)) return true;
72
68
  const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
73
69
  if (await handleWorkbuddyLogin(id, sub, rest)) return true;
74
70
  const { handleProviderConfig } = await import("./config.js");
@@ -28,15 +28,12 @@ export async function handleProviderModels(id, sub, args, rest) {
28
28
  try {
29
29
  const { createGenericProvider } = await import("../../../providers/generic.js");
30
30
  const { createWorkbuddyProvider } = await import("../../../providers/workbuddy.js");
31
- const { createDeepseekProvider } = await import("../../../providers/deepseek.js");
32
31
  const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
33
32
  const keys = loadProviderKeys(id);
34
33
  const auths = cfg?.auths || [];
35
34
  let provider;
36
35
  if (id === "workbuddy") {
37
36
  provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
38
- } else if (id === "deepseek") {
39
- provider = createDeepseekProvider({ apiKeys: keys, file: defaultStateFile() });
40
37
  } else {
41
38
  if (!baseUrl) {
42
39
  console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
@@ -0,0 +1,56 @@
1
+ import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv } from "../../state/schemas/use-group.js";
2
+ import { argValue } from "../policy.js";
3
+
4
+ function parseInput(v) {
5
+ if (v === undefined || v === null || v === "") return null;
6
+ const s = String(v).trim().toLowerCase();
7
+ if (["1", "true", "on", "yes", "enable", "enabled", "open"].includes(s)) return true;
8
+ if (["0", "false", "off", "no", "disable", "disabled", "close", "closed"].includes(s)) return false;
9
+ return null;
10
+ }
11
+
12
+ export async function handleUseGroup(args) {
13
+ const hasFlag = args.some((a) => ["-use-group", "--use-group", "-use_group", "--use_group", "-usegroup", "--usegroup"].includes(a));
14
+ if (!hasFlag) return false;
15
+
16
+ // 取值:支持 -use-group true / -use-group=true / --use-group=off
17
+ let raw = argValue(args, "-use-group", "--use-group", "-use_group", "--use_group", "-usegroup", "--usegroup");
18
+ // 兼容 -use-group=true 这种等号形态
19
+ if (raw === null || raw === undefined) {
20
+ const eq = args.find((a) => a.startsWith("-use-group=") || a.startsWith("--use-group=") || a.startsWith("-use_group=") || a.startsWith("--use_group="));
21
+ if (eq) raw = eq.split("=")[1];
22
+ }
23
+ // 如果 flag 后面紧跟的不是另一个 flag,则视为值;否则视为查询
24
+ if (raw !== null && raw !== undefined && String(raw).startsWith("-")) raw = null;
25
+
26
+ const envVal = getUseGroupEnv();
27
+ const effective = getEffectiveUseGroup();
28
+ const stored = loadUseGroup();
29
+
30
+ if (raw === null || raw === undefined || raw === "") {
31
+ // 查询模式
32
+ console.log(`use-group: ${effective ? "on" : "off"} (effective)`);
33
+ console.log(` stored: ${stored ? "on" : "off"} (state.json useGroup)`);
34
+ if (envVal !== null) console.log(` env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} (overrides stored)`);
35
+ console.log(` default: on`);
36
+ console.log(` usage: mslxdff -use-group on|off (opencode 供应商本机失败时是否走组员网络,默认 on)`);
37
+ console.log(` env: MSLXDFF_USE_GROUP=0|1 (优先级高于 state)`);
38
+ process.exit(0);
39
+ }
40
+
41
+ const parsed = parseInput(raw);
42
+ if (parsed === null) {
43
+ console.error(`invalid value for -use-group: ${raw} (expected on/off/true/false/1/0)`);
44
+ process.exit(1);
45
+ }
46
+
47
+ if (envVal !== null) {
48
+ console.log(`note: env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} is set and overrides stored value; unset env to use stored value`);
49
+ }
50
+
51
+ saveUseGroup(parsed);
52
+ console.log(`use-group set to ${parsed ? "on" : "off"} (stored in state.json)`);
53
+ console.log(` opencode 供应商:本机失败时 ${parsed ? "允许" : "不再"} 通过组员网络请求上游`);
54
+ if (!parsed) console.log(` 提示:opencode 请求将仅在本机重试,不再走 peer/broadband 组员中继`);
55
+ process.exit(0);
56
+ }
package/src/cli/help.js CHANGED
@@ -43,6 +43,7 @@ Usage:
43
43
  mslxdff -autostart status 查看自启状态
44
44
  mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
45
45
  mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
46
+ mslxdff -use-group [on|off] opencode 供应商本机失败时是否走组员网络(默认 on;off 则仅本机,MSLXDFF_USE_GROUP 环境变量可覆盖)
46
47
  mslxdff -help show this help
47
48
 
48
49
  Environment:
@@ -62,6 +63,7 @@ Environment:
62
63
  MSLXDFF_BAN_THRESHOLD failed joins before an ip is banned (default 5)
63
64
  MSLXDFF_BAN_WINDOW_MS ban duration after too many failures (default 48h)
64
65
  MSLXDFF_HEDGE_DELAY_MS hedge peer race when local stream first chunk slow (default 1000, 0/off to disable)
66
+ MSLXDFF_USE_GROUP opencode 组员中继开关(默认 on;0/off/false 关闭后 opencode 仅本机,不走 peer/broadband)
65
67
  MSLXDFF_AUTO_UPDATE auto-update: hourly by default, 0/off/false to disable, 1/true or ms
66
68
  MSLXDFF_AUTO_UPDATE_MS same as above, explicit ms (overrides AUTO_UPDATE)
67
69
  `);
package/src/cli/index.js CHANGED
@@ -49,6 +49,8 @@ export async function run(args = process.argv.slice(2)) {
49
49
  if (await handleLeaveGroup(args)) return;
50
50
  if (await handleDelGroup(args)) return;
51
51
 
52
+ const { handleUseGroup } = await import("./commands/use-group.js");
53
+ if (await handleUseGroup(args)) return;
52
54
  const { handlePort, handleDaemonFlag, handleBareRun } = await import("./commands/daemon.js");
53
55
  if (await handlePort(args)) return;
54
56
  if (await handleDaemonFlag(args, VERSION)) return;
@@ -15,11 +15,6 @@ export const customProviders = [
15
15
  match: (id, baseUrl) => id === "cline" || id === "clinebot" || String(baseUrl).includes("cline.bot"),
16
16
  load: () => import("./cline.js").then((m) => m.createClineProvider),
17
17
  },
18
- {
19
- id: "deepseek",
20
- match: (id, baseUrl) => id === "deepseek" || String(baseUrl).includes("chat.deepseek.com"),
21
- load: () => import("./deepseek.js").then((m) => m.createDeepseekProvider),
22
- },
23
18
  ];
24
19
 
25
20
  // 供 bench/probe 等需要定制化解析模型列表的场景
@@ -18,8 +18,7 @@ function shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, mod
18
18
  if (!canForwardPeers) return false;
19
19
  if (!hasPeers) return false;
20
20
  if (!d || d <= 0) return false;
21
- // deepseek = 本机私有凭据供应商(组员节点没有凭据),对冲必败且 both-fail 会误杀本地慢首块流(reasoner 思考 3-30s),不走组员
22
- if (String(model || "").startsWith("deepseek/")) return false;
21
+ // 不做供应商级禁对冲特判:对冲仅按 useGroup/流式/peer 可用性决定
23
22
  // muse-spark 走 /responses 流式(event: 包装 + 加密 reasoning),组员旧版无此整形且聚合 JSON 带错 header,必抢赢本地慢首块,需本地直出
24
23
  if (String(model || "").toLowerCase().startsWith("muse-spark")) return false;
25
24
  return true;
@@ -76,8 +76,7 @@ export async function setupProviders() {
76
76
  const customFactory = await getCustomProviderFactory(gid, base);
77
77
  if (customFactory) {
78
78
  if (gid === "workbuddy" && !keys.length) continue;
79
- if (gid === "deepseek" && !keys.length) continue;
80
- if (gid !== "workbuddy" && gid !== "deepseek" && (!base || !keys.length)) continue;
79
+ if (gid !== "workbuddy" && (!base || !keys.length)) continue;
81
80
  try {
82
81
  const provider = gid === "workbuddy"
83
82
  ? await customFactory({ baseUrl: base || "https://copilot.tencent.com", apiKeys: keys, auths })
@@ -57,3 +57,4 @@ export {
57
57
  export { loadPeers, savePeers, loadPeerErrors, savePeerErrors, loadPeerStats, savePeerStats } from "./schemas/peer.js";
58
58
  export { loadGroups, loadGroupsJoined, saveGroupsJoined, loadBans, saveBans, saveGroups } from "./schemas/group.js";
59
59
  export { loadTimezone, loadTimezoneState, saveTimezone, clearTimezone, getTimezoneEnv, DEFAULT_TZ, isValidTimezone } from "./schemas/timezone.js";
60
+ export { loadUseGroup, saveUseGroup, getEffectiveUseGroup, shouldUseGroupForModel, isUseGroupEnabled, getUseGroupEnv } from "./schemas/use-group.js";
@@ -0,0 +1,56 @@
1
+ import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
2
+
3
+ function parseBool(v) {
4
+ if (typeof v === "boolean") return v;
5
+ if (typeof v === "number") return v !== 0;
6
+ if (typeof v === "string") {
7
+ const s = v.trim().toLowerCase();
8
+ if (["0", "false", "off", "no", "disable", "disabled", "close", "closed"].includes(s)) return false;
9
+ if (["1", "true", "on", "yes", "enable", "enabled", "open"].includes(s)) return true;
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export function loadUseGroup({ file = defaultStateFile() } = {}) {
15
+ const raw = readState(file).useGroup;
16
+ const parsed = parseBool(raw);
17
+ return parsed === null ? true : parsed; // 默认开
18
+ }
19
+
20
+ export function saveUseGroup(value, { file = defaultStateFile() } = {}) {
21
+ const b = Boolean(value);
22
+ writeStateImmediate(file, { useGroup: b });
23
+ return b;
24
+ }
25
+
26
+ export function getUseGroupEnv() {
27
+ const raw = process.env.MSLXDFF_USE_GROUP;
28
+ if (raw === undefined || raw === null || raw === "") return null;
29
+ return parseBool(raw);
30
+ }
31
+
32
+ export function getEffectiveUseGroup({ file = defaultStateFile() } = {}) {
33
+ const env = getUseGroupEnv();
34
+ if (env !== null) return env;
35
+ return loadUseGroup({ file });
36
+ }
37
+
38
+ // 仅对 opencode 供应商生效:opencode 的模型为裸 id 或 opencode/ 前缀
39
+ export function shouldUseGroupForModel(model, { file = defaultStateFile() } = {}) {
40
+ const m = String(model || "").trim();
41
+ if (!m) return getEffectiveUseGroup({ file });
42
+ // 带前缀:判断是否为 opencode
43
+ if (m.includes("/")) {
44
+ const head = m.split("/")[0].trim().toLowerCase();
45
+ if (head === "opencode" || head === "oc") {
46
+ return getEffectiveUseGroup({ file });
47
+ }
48
+ return true; // 其他供应商不受此开关限制
49
+ }
50
+ // 裸 id 视为 opencode
51
+ return getEffectiveUseGroup({ file });
52
+ }
53
+
54
+ export function isUseGroupEnabled({ file } = {}) {
55
+ return getEffectiveUseGroup({ file });
56
+ }
@@ -1,46 +0,0 @@
1
- // -provider deepseek health:逐账号探活(防禁言体系)
2
- // 用法:mslxdff -provider deepseek health [--json]
3
- // 对池内每个账号发最小真实请求(Hello world + PoW),检测 muted/限频/凭据坏;
4
- // 禁言账号自动冷却 5min(解封后再次探活即自动恢复)。不经过组员、直连本机凭据。
5
- export async function handleDeepseekHealth(id, sub, rest) {
6
- if (id !== "deepseek" && id !== "ds") return false;
7
- if (sub !== "health" && sub !== "check") return false;
8
-
9
- const args = rest || [];
10
- const jsonOut = args.some((a) => a === "--json" || a === "-json");
11
-
12
- const { loadProviderKeys } = await import("../../../state.js");
13
- const keys = loadProviderKeys("deepseek") || [];
14
- if (!keys.length) {
15
- console.error("❌ 没有 DeepSeek 凭据,先登录:");
16
- console.error(" mslxdff -provider deepseek login --token <userToken>");
17
- process.exit(1);
18
- }
19
-
20
- const { createAuthPool } = await import("../../../providers/deepseek/auth.js");
21
- const { deepseekHealth } = await import("../../../providers/deepseek/health.js");
22
- const authPool = createAuthPool({ tokens: keys });
23
-
24
- if (!jsonOut) {
25
- console.log(`🔍 DeepSeek 探活中(${keys.length} 个账号,逐个发最小请求)...`);
26
- }
27
- const report = await deepseekHealth({ authPool });
28
-
29
- if (jsonOut) {
30
- console.log(JSON.stringify(report, null, 2));
31
- } else {
32
- for (const r of report) {
33
- console.log(` ${r.ok ? "✓" : "✗"} ...${r.tokenTail} ${r.detail}`);
34
- }
35
- const okCount = report.filter((r) => r.ok).length;
36
- console.log(`\n结果:${okCount}/${report.length} 健康`);
37
- if (okCount === 0) {
38
- console.log("全部账号异常 —— 修复建议:");
39
- console.log(" 1. chat.deepseek.com 登录后 F12 → Application → Local Storage → 复制 userToken");
40
- console.log(" 2. mslxdff -provider deepseek login --token <userToken> 追加账号");
41
- console.log(" 3. 禁言账号等待解除后再次探活即自动恢复");
42
- }
43
- }
44
- if (report.length && report.every((r) => !r.ok)) process.exitCode = 1;
45
- return true;
46
- }