mslxdff 0.1.105 → 0.1.107

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.105",
3
+ "version": "0.1.107",
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,
@@ -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>`);
@@ -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 })
@@ -25,7 +25,9 @@ export function chatToResponsesBody(chatBody) {
25
25
  return base;
26
26
  });
27
27
  const input = inputParts.join("\n\n") || "hi";
28
- const out = { model: chatBody.model, input, stream: false };
28
+ // 流式意图透传:客户端要 SSE 就向上游要 SSE(reshapeResponsesSse 负责转回 chat SSE)。
29
+ // 写死 stream:false 是历史折衷(当时聚合 JSON 直回),已由完整 SSE 转换取代。
30
+ const out = { model: chatBody.model, input, stream: chatBody?.stream === true };
29
31
  if (system) out.instructions = system;
30
32
  // responses 的 tools 形状为平铺 {type,name,description,parameters},而 chat 为 {type,function:{name,...}}
31
33
  if (Array.isArray(chatBody.tools) && chatBody.tools.length) {
@@ -120,7 +122,6 @@ export function reshapeResponsesSse(res, fallbackModel) {
120
122
  const ct = res.headers?.get?.("content-type") || "";
121
123
  if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
122
124
  } catch { return res; }
123
- const reader = res.body.getReader();
124
125
  const decoder = new TextDecoder();
125
126
  const encoder = new TextEncoder();
126
127
  let buf = "";
@@ -143,22 +144,23 @@ export function reshapeResponsesSse(res, fallbackModel) {
143
144
  return `data: ${JSON.stringify(payload)}\n\n`;
144
145
  }
145
146
 
146
- let closed = false;
147
- const body = new ReadableStream({
148
- async pull(controller) {
149
- if (closed) { try { controller.close(); } catch {} return; }
150
- try {
151
- const { done, value } = await reader.read();
152
- if (done) {
153
- closed = true;
154
- if (buf.trim()) {
155
- // 残余缓冲尝试处理
156
- }
157
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
158
- controller.close();
159
- return;
160
- }
161
- buf += decoder.decode(value, { stream: true });
147
+ function sendRole(out) {
148
+ if (!hasSentRole) {
149
+ hasSentRole = true;
150
+ out += chatChunk({ role: "assistant" }, null);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ // TransformStream 泵:for await 直接驱动上游流,writer.write 背压回压。
156
+ // (自建 ReadableStream 的 pull 调度在本机 daemon 下出现"pull resolve 后不再续拉"
157
+ // 导致 muse SSE 卡死;async-iterator 泵是 undici 流已验证畅通的姿势)
158
+ const { readable, writable } = new TransformStream();
159
+ const writer = writable.getWriter();
160
+ (async () => {
161
+ try {
162
+ for await (const chunk of res.body) {
163
+ buf += decoder.decode(chunk, { stream: true });
162
164
  let out = "";
163
165
  // 按 \n\n 分事件
164
166
  while (true) {
@@ -181,92 +183,75 @@ export function reshapeResponsesSse(res, fallbackModel) {
181
183
  if (data.response?.id) respId = data.response.id;
182
184
  if (data.response?.model) respModel = data.response.model;
183
185
  if (data.response?.created_at) created = Math.floor(data.response.created_at);
184
- if (data.response?.id && !respId) respId = data.response.id;
185
- // 关注 output_text.delta
186
- if (curEvent === "response.output_text.delta" || data.type === "response.output_text.delta") {
187
- const deltaText = data.delta || "";
188
- if (deltaText) {
189
- if (!hasSentRole) {
190
- hasSentRole = true;
191
- out += chatChunk({ role: "assistant" }, null);
192
- }
193
- out += chatChunk({ content: deltaText }, null);
194
- }
195
- } else if (curEvent === "response.completed" || data.type === "response.completed") {
196
- const usage = data.response?.usage || null;
197
- // 若有 tool_calls,finish 应为 tool_calls
198
- const hasTools = toolMap.size > 0;
199
- const finish = hasTools ? "tool_calls" : (data.response?.status === "completed" ? "stop" : null);
200
- // 末帧带 usage
201
- const id = respId || `resp_${Date.now()}`;
202
- const payload = {
203
- id,
204
- object: "chat.completion.chunk",
205
- created,
206
- model: respModel,
207
- choices: [{ index: 0, delta: {}, finish_reason: finish }],
208
- usage: usage || undefined,
209
- };
210
- out += `data: ${JSON.stringify(payload)}\n\n`;
211
- } else if (data.type === "response.output_item.added" && data.item?.type === "message") {
212
- // message 开始,可发送 role
213
- if (!hasSentRole) {
214
- hasSentRole = true;
215
- out += chatChunk({ role: "assistant" }, null);
216
- }
217
- } else if (data.type === "response.output_item.added" && data.item?.type === "function_call") {
218
- const outIdx = Number(data.output_index ?? 1);
219
- const toolIdx = Math.max(0, outIdx - 1);
220
- const callId = data.item?.call_id || data.item?.id || "";
221
- const name = data.item?.name || "";
222
- toolMap.set(outIdx, { idx: toolIdx, id: callId, name });
223
- if (!hasSentRole) {
224
- hasSentRole = true;
225
- out += chatChunk({ role: "assistant" }, null);
226
- }
227
- const tc = { index: toolIdx, id: callId, type: "function", function: { name, arguments: "" } };
228
- // 清理空字符串,避免 undefined
229
- if (!callId) delete tc.id;
230
- if (!name) delete tc.function.name;
231
- out += chatChunk({ tool_calls: [tc] }, null);
232
- } else if (data.type === "response.function_call_arguments.delta") {
233
- const outIdx = Number(data.output_index ?? 1);
234
- const entry = toolMap.get(outIdx) || { idx: Math.max(0, outIdx - 1) };
235
- const deltaArgs = data.delta || "";
236
- if (deltaArgs) {
237
- if (!hasSentRole) {
238
- hasSentRole = true;
239
- out += chatChunk({ role: "assistant" }, null);
240
- }
241
- out += chatChunk({ tool_calls: [{ index: entry.idx, function: { arguments: deltaArgs } }] }, null);
242
- }
243
- } else if (data.type === "response.function_call_arguments.done") {
244
- const outIdx = Number(data.output_index ?? 1);
245
- const entry = toolMap.get(outIdx) || { idx: Math.max(0, outIdx - 1) };
246
- const args = data.arguments || "";
247
- if (args && !toolMap.get(outIdx)?._done) {
248
- // done 可能带全量,若未通过 delta 发送过,补发
249
- // 已通过 delta 流式发送则忽略,避免重复
250
- }
251
- } else if (data.type === "response.output_item.done" && data.item?.type === "function_call") {
252
- // 可忽略,已通过 added+delta 完整
253
- }
254
- // reasoning 加密块忽略
186
+ // created/in_progress 即发 role 帧:muse reasoning 阶段可达数十秒,
187
+ // 尽早产出首帧避免 relay 的首块超时(25s)误杀
188
+ if (data.type === "response.created" || data.type === "response.in_progress") {
189
+ out = sendRole(out);
190
+ }
191
+ if (curEvent === "response.output_text.delta" || data.type === "response.output_text.delta") {
192
+ const deltaText = data.delta || "";
193
+ if (deltaText) {
194
+ out = sendRole(out);
195
+ out += chatChunk({ content: deltaText }, null);
196
+ }
197
+ } else if (curEvent === "response.completed" || data.type === "response.completed") {
198
+ const usage = data.response?.usage || null;
199
+ // 若有 tool_calls,finish 应为 tool_calls
200
+ const hasTools = toolMap.size > 0;
201
+ const finish = hasTools ? "tool_calls" : (data.response?.status === "completed" ? "stop" : null);
202
+ // 末帧带 usage
203
+ const id = respId || `resp_${Date.now()}`;
204
+ const payload = {
205
+ id,
206
+ object: "chat.completion.chunk",
207
+ created,
208
+ model: respModel,
209
+ choices: [{ index: 0, delta: {}, finish_reason: finish }],
210
+ usage: usage || undefined,
211
+ };
212
+ out += `data: ${JSON.stringify(payload)}\n\n`;
213
+ } else if (data.type === "response.output_item.added" && data.item?.type === "message") {
214
+ // message 开始,可发送 role
215
+ out = sendRole(out);
216
+ } else if (data.type === "response.output_item.added" && data.item?.type === "function_call") {
217
+ const outIdx = Number(data.output_index ?? 1);
218
+ const toolIdx = Math.max(0, outIdx - 1);
219
+ const callId = data.item?.call_id || data.item?.id || "";
220
+ const name = data.item?.name || "";
221
+ toolMap.set(outIdx, { idx: toolIdx, id: callId, name });
222
+ out = sendRole(out);
223
+ const tc = { index: toolIdx, id: callId, type: "function", function: { name, arguments: "" } };
224
+ // 清理空字符串,避免 undefined
225
+ if (!callId) delete tc.id;
226
+ if (!name) delete tc.function.name;
227
+ out += chatChunk({ tool_calls: [tc] }, null);
228
+ } else if (data.type === "response.function_call_arguments.delta") {
229
+ const outIdx = Number(data.output_index ?? 1);
230
+ const entry = toolMap.get(outIdx) || { idx: Math.max(0, outIdx - 1) };
231
+ const deltaArgs = data.delta || "";
232
+ if (deltaArgs) {
233
+ out = sendRole(out);
234
+ out += chatChunk({ tool_calls: [{ index: entry.idx, function: { arguments: deltaArgs } }] }, null);
235
+ }
236
+ } else if (data.type === "response.function_call_arguments.done") {
237
+ // done 可能带全量,若未通过 delta 发送过则补发;已通过 delta 发送则忽略,避免重复
238
+ } else if (data.type === "response.output_item.done" && data.item?.type === "function_call") {
239
+ // 可忽略,已通过 added+delta 完整
240
+ }
241
+ // reasoning 加密块忽略
255
242
  }
256
- if (out) controller.enqueue(encoder.encode(out));
257
- } catch {
258
- closed = true;
259
- try { controller.close(); } catch {}
243
+ if (out) await writer.write(encoder.encode(out));
260
244
  }
261
- },
262
- cancel() {
263
- closed = true;
264
- try { reader.cancel(); } catch {}
265
- },
266
- });
245
+ await writer.write(encoder.encode("data: [DONE]\n\n"));
246
+ await writer.close();
247
+ } catch (e) {
248
+ try { await writer.abort(e instanceof Error ? e : new Error(String(e))); } catch { try { writer.close(); } catch {} }
249
+ }
250
+ })();
267
251
  const headers = new Headers(res.headers);
268
252
  headers.set("content-type", "text/event-stream");
269
- const out = new Response(body, { status: res.status, statusText: res.statusText, headers });
253
+ const out = new Response(readable, { status: res.status, statusText: res.statusText, headers });
270
254
  try { out._t = res._t; } catch {}
271
255
  return out;
272
256
  }
257
+
@@ -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
- }