mslxdff 0.1.96 → 0.1.98

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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/chat-pipeline/serial-trial.js +1 -1
  3. package/src/cli/commands/model/picks.js +7 -2
  4. package/src/cli/commands/provider/bench-via.js +9 -0
  5. package/src/cli/commands/provider/bench.js +8 -0
  6. package/src/cli/commands/provider/deepseek-health.js +46 -0
  7. package/src/cli/commands/provider/deepseek-login.js +89 -0
  8. package/src/cli/commands/provider/index.js +4 -0
  9. package/src/cli/commands/provider/models.js +3 -0
  10. package/src/groups.js +2 -1
  11. package/src/providers/cline/index.js +2 -1
  12. package/src/providers/cline/models.js +2 -1
  13. package/src/providers/deepseek/auth.js +111 -0
  14. package/src/providers/deepseek/bridge.js +158 -0
  15. package/src/providers/deepseek/chat.js +220 -0
  16. package/src/providers/deepseek/debug.js +44 -0
  17. package/src/providers/deepseek/hash-reference.js +117 -0
  18. package/src/providers/deepseek/hash.js +245 -0
  19. package/src/providers/deepseek/health.js +104 -0
  20. package/src/providers/deepseek/index.js +180 -0
  21. package/src/providers/deepseek/pow.js +99 -0
  22. package/src/providers/deepseek/session.js +114 -0
  23. package/src/providers/deepseek/sse-decoder.js +160 -0
  24. package/src/providers/deepseek.js +1 -0
  25. package/src/providers/generic.js +2 -1
  26. package/src/providers/registry.js +5 -0
  27. package/src/providers/workbuddy/auth.js +4 -7
  28. package/src/providers/workbuddy/balance.js +3 -1
  29. package/src/providers/workbuddy/index.js +2 -1
  30. package/src/providers/workbuddy-balance.js +3 -1
  31. package/src/routes/hedge.js +3 -1
  32. package/src/routes/peers.js +1 -1
  33. package/src/runtime/providers-setup.js +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -112,7 +112,7 @@ export async function runSerialTrial(ctx, deps = {}) {
112
112
  const isStream = Boolean(body.stream);
113
113
  const d = hedgeDelayMs();
114
114
  const hasPeers = Boolean(peers) && peers.ordered().length > 0;
115
- const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
115
+ const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, model }) && upRes.status === 200 && upRes.body;
116
116
  if (doHedge) {
117
117
  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
118
  if (hr.handled) return { done: true };
@@ -15,8 +15,11 @@ export async function handlePicksCommand(args, idx, sub) {
15
15
  process.exit(0);
16
16
  }
17
17
  if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
18
- const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
18
+ // id 勾选:`-model pick a b c`(空格分隔)与 `-model pick a,b`(逗号)均支持
19
+ const ids = args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean);
20
+ const picks = [...new Set([...loadModelPicks(), ...ids])];
19
21
  saveModelPicks(picks);
22
+ console.log(`picked ${ids.length} model(s): ${ids.join(", ")}`);
20
23
  console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
21
24
  process.exit(0);
22
25
  }
@@ -26,8 +29,10 @@ export async function handlePicksCommand(args, idx, sub) {
26
29
  process.exit(0);
27
30
  }
28
31
  if (sub === "unpick" && args[idx + 2]) {
29
- const picks = loadModelPicks().filter((x) => x !== args[idx + 2]);
32
+ const removeSet = new Set(args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean));
33
+ const picks = loadModelPicks().filter((x) => !removeSet.has(x));
30
34
  saveModelPicks(picks);
35
+ console.log(`unpicked ${removeSet.size}: ${[...removeSet].join(", ")}`);
31
36
  console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
32
37
  process.exit(0);
33
38
  }
@@ -54,6 +54,13 @@ 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
+ }
57
64
  const { getOnlinePeers, orchestrateVia, resolveIncludeOpencode } = await import("../../../bench/via.js");
58
65
  const peers = await (typeof getOnlinePeersFn === "function" ? getOnlinePeersFn() : getOnlinePeers());
59
66
  if (!peers.length) {
@@ -99,6 +106,8 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
99
106
  const ids = new Set(Object.keys(configs));
100
107
  ids.add("opencode"); ids.add("openrouter");
101
108
  for (const pid of [...ids]) {
109
+ // DeepSeek 网页通道防禁言:all 形态直接排除,即使 allowlist 非空也不进候选
110
+ if (String(pid).toLowerCase() === "deepseek") continue;
102
111
  const allowed = loadAllowed(pid) || [];
103
112
  if (allowed.length) targetIds.push(pid);
104
113
  }
@@ -36,6 +36,14 @@ 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
+ }
39
47
  if (opts.via) {
40
48
  const stateMod = await import("../../../state.js");
41
49
  const loadConfigs = deps.loadProviderConfigs || stateMod.loadProviderConfigs;
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,89 @@
1
+ // -provider deepseek login:把 chat.deepseek.com 凭据落盘 providerConfigs.deepseek.keys
2
+ // 三种用法:
3
+ // login --token <userToken> 浏览器贴 token(F12 → Application → Local Storage → userToken)
4
+ // login <email|mobile> <password> 账密登录换 token(Android 协议)
5
+ // login 打印用法引导
6
+ export async function handleDeepseekLogin(id, sub, rest) {
7
+ if (id !== "deepseek" && id !== "ds") return false;
8
+ if (sub !== "login" && sub !== "auth") return false;
9
+
10
+ const args = rest || [];
11
+ const tokenFlagIdx = args.findIndex((a) => a === "--token" || a === "-token" || a === "token");
12
+
13
+ if (tokenFlagIdx < 0 && args.filter((a) => !String(a).startsWith("-")).length < 2) {
14
+ printUsage();
15
+ process.exit(0);
16
+ }
17
+
18
+ const { loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
19
+
20
+ let token = null;
21
+ let label = "";
22
+
23
+ if (tokenFlagIdx >= 0) {
24
+ token = String(args[tokenFlagIdx + 1] || "").trim();
25
+ if (!token) {
26
+ console.error("❌ --token 后面要贴 userToken 值");
27
+ printUsage();
28
+ process.exit(1);
29
+ }
30
+ label = "userToken(浏览器)";
31
+ } else {
32
+ const [loginValue, password] = args.filter((a) => !String(a).startsWith("-"));
33
+ console.log("🚀 DeepSeek 账密登录中(Android 协议)...");
34
+ try {
35
+ const { loginDeepseek } = await import("../../../providers/deepseek/auth.js");
36
+ const out = await loginDeepseek({ loginValue, password });
37
+ token = out.token;
38
+ label = `账密(${String(loginValue).slice(0, 3)}***)`;
39
+ console.log(`✅ 登录成功,拿到 token`);
40
+ } catch (e) {
41
+ console.error(`❌ ${e.message}`);
42
+ console.error(`\n换浏览器贴 token 方式:chat.deepseek.com 登录后 F12 → Application → Local Storage → 复制 userToken →`);
43
+ console.error(` mslxdff -provider deepseek login --token <userToken>`);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ if (!token) {
49
+ console.error("❌ 未拿到 token");
50
+ process.exit(1);
51
+ }
52
+
53
+ const cur = loadProviderKeys("deepseek");
54
+ if (cur.includes(token)) {
55
+ console.log(`ℹ️ 该 token 已存在(现 ${cur.length} 个账号),跳过`);
56
+ } else {
57
+ const cfg = loadProviderConfig("deepseek") || {};
58
+ const nextKeys = [...new Set([...(cfg.keys || cur), token].filter(Boolean))];
59
+ saveProviderConfig("deepseek", { ...cfg, keys: nextKeys });
60
+ console.log(`✅ 已写入 deepseek(现 ${nextKeys.length} 个账号)· 来源: ${label}`);
61
+ }
62
+
63
+ console.log("=".repeat(60));
64
+ console.log("\n下一步:");
65
+ console.log(" mslxdff -provider deepseek allowAny on 放行模型(默认 allowlist 空=全 blocked)");
66
+ console.log(" mslxdff -provider deepseek models 查看支持的模型");
67
+ console.log(" mslxdff -restart 重启网关生效");
68
+ console.log("\n用法:model 字段填 deepseek/deepseek-chat 或 deepseek/deepseek-reasoner");
69
+ console.log(" (-search 后缀开启联网搜索:deepseek/deepseek-chat-search)");
70
+ console.log("\n多账号:重复 login 追加,自动轮换 + 401/429/风控切号");
71
+ console.log("注意:单账号同时仅 1 路输出;触发验证码时换号或稍后再试");
72
+ process.exit(0);
73
+ }
74
+
75
+ function printUsage() {
76
+ console.log("DeepSeek 登录(两种方式任选):");
77
+ console.log("");
78
+ console.log(" 方式 1(推荐,免密码):浏览器贴 token");
79
+ console.log(" 1. 浏览器登录 https://chat.deepseek.com");
80
+ console.log(" 2. F12 → Application → Local Storage → https://chat.deepseek.com");
81
+ console.log(" 3. 找到 userToken,复制值");
82
+ console.log(" 4. mslxdff -provider deepseek login --token <userToken>");
83
+ console.log("");
84
+ console.log(" 方式 2:账号密码(邮箱或手机号)");
85
+ console.log(" mslxdff -provider deepseek login you@example.com yourpassword");
86
+ console.log(" mslxdff -provider deepseek login 13800138000 yourpassword");
87
+ console.log("");
88
+ console.log(" 登录后:mslxdff -provider deepseek allowAny on && mslxdff -restart");
89
+ }
@@ -65,6 +65,10 @@ 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;
68
72
  const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
69
73
  if (await handleWorkbuddyLogin(id, sub, rest)) return true;
70
74
  const { handleProviderConfig } = await import("./config.js");
@@ -28,12 +28,15 @@ 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");
31
32
  const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
32
33
  const keys = loadProviderKeys(id);
33
34
  const auths = cfg?.auths || [];
34
35
  let provider;
35
36
  if (id === "workbuddy") {
36
37
  provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
38
+ } else if (id === "deepseek") {
39
+ provider = createDeepseekProvider({ apiKeys: keys, file: defaultStateFile() });
37
40
  } else {
38
41
  if (!baseUrl) {
39
42
  console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
package/src/groups.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { timingSafeEqual, createHash } from "node:crypto";
2
+ import { compatFetch } from "./compat.js";
2
3
  import { loadGroups, saveGroups, loadBans, saveBans } from "./state.js";
3
4
  import { normalizePeerUrl } from "./peers.js";
4
5
 
@@ -159,7 +160,7 @@ export function createGroupsService({ file } = {}) {
159
160
 
160
161
  // Re-register with the leader (join is idempotent) and return the fresh member list.
161
162
  // No key is needed once registered: the leader verifies our bearer token.
162
- export async function refreshGroupMembers(name, { leaderUrl, memberName, url, token, kind, fetchImpl = fetch } = {}) {
163
+ export async function refreshGroupMembers(name, { leaderUrl, memberName, url, token, kind, fetchImpl = compatFetch } = {}) {
163
164
  const controller = new AbortController();
164
165
  const timer = setTimeout(() => controller.abort(), SYNC_TIMEOUT_MS);
165
166
  try {
@@ -1,6 +1,7 @@
1
1
  import { createKeyRing } from "../keyring.js";
2
2
  import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath, saveProviderConfig } from "../../state.js";
3
3
  import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner } from "../base.js";
4
+ import { compatFetch } from "../../compat.js";
4
5
  import { joinModelId } from "../model-id.js";
5
6
  import { createAuthPool } from "./auth.js";
6
7
  import { clineHeaders, isRefreshToken } from "./headers.js";
@@ -34,7 +35,7 @@ export function createClineProvider({
34
35
  const resolvedModelsPath = modelsPath || (_cfgModels && _cfgModels !== "/models" ? _cfgModels : null) || "/ai/cline/recommended-models";
35
36
  const defaultChat = String(resolvedBase).includes("/api/v1") ? "/chat/completions" : "/api/v1/chat/completions";
36
37
  const resolvedChatPath = chatPath || loadProviderChatPath(id, file ? { file } : {}) || defaultChat;
37
- if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
38
+ if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
38
39
 
39
40
  const rawKeys = collectApiKeysGeneric(id, apiKeys, apiKey, (pid) => loadProviderKeys(pid, file ? { file } : {}));
40
41
  // 同时兼容 cline 与 clinebot 两个 id 的 keys(用户可能配在任一)
@@ -1,4 +1,5 @@
1
1
  import { joinUrl, getUndici } from "../base.js";
2
+ import { compatFetch } from "../../compat.js";
2
3
  import { joinModelId } from "../model-id.js";
3
4
 
4
5
  const { UndiciFetch } = getUndici();
@@ -8,7 +9,7 @@ function isClineBotHost(baseUrl) {
8
9
  }
9
10
 
10
11
  export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispatcher, ring, loadKeys } = {}) {
11
- if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
12
+ if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
12
13
  const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
13
14
  const resolvedPath = modelsPath || "/ai/cline/recommended-models";
14
15
  const CACHE_TTL = 10 * 60 * 1000;
@@ -0,0 +1,111 @@
1
+ // DeepSeek 账号池 + 登录(Android 协议)
2
+ // 上游协议参考 iidamie/deepseek2api(GPL-3.0,协议事实)
3
+ import { compatFetch } from "../../compat.js";
4
+ import { androidHeaders } from "./pow.js";
5
+ import { DEEPSEEK_DEFAULT_BASE, DEEPSEEK_API_PREFIX } from "./pow.js";
6
+ import { dsDebug } from "./debug.js";
7
+
8
+ export { androidHeaders };
9
+
10
+ const DEFAULT_COOLDOWN_MS = 30_000;
11
+ // 差异化冷却(防禁言体系):频率前兆 60s(TQZHR 重试间隔同量级)、禁言 5min;防同号轰炸拖成禁言
12
+ export const COOLDOWN_PRESETS = Object.freeze({ default: DEFAULT_COOLDOWN_MS, frequency: 60_000, muted: 300_000 });
13
+
14
+ function isEmail(v) {
15
+ return String(v || "").includes("@");
16
+ }
17
+
18
+ export async function loginDeepseek({ loginValue, password, areaCode = "+86", fetchImpl, dispatcher, baseUrl = DEEPSEEK_DEFAULT_BASE, connectTimeoutMs = 30_000 } = {}) {
19
+ const doFetch = fetchImpl || compatFetch;
20
+ const email = isEmail(loginValue);
21
+ const payload = {
22
+ email: email ? String(loginValue).trim() : "",
23
+ mobile: email ? "" : String(loginValue).trim(),
24
+ area_code: email ? "" : areaCode,
25
+ password: String(password ?? ""),
26
+ device_id: "mslxdff",
27
+ os: "android",
28
+ };
29
+ const url = `${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/users/login`;
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(new Error("DeepSeek 登录超时")), connectTimeoutMs);
32
+ let res;
33
+ try {
34
+ res = await doFetch(url, {
35
+ method: "POST",
36
+ headers: androidHeaders(null),
37
+ body: JSON.stringify(payload),
38
+ ...(dispatcher ? { dispatcher } : {}),
39
+ signal: controller.signal,
40
+ });
41
+ } catch (err) {
42
+ throw new Error(`DeepSeek 登录失败: ${String(err?.message || err)}`);
43
+ } finally {
44
+ clearTimeout(timer);
45
+ }
46
+
47
+ let data = null;
48
+ try { data = await res.json(); } catch {}
49
+ const biz = data?.data;
50
+ const bizCode = biz?.biz_code ?? data?.code;
51
+ if (!res.ok || bizCode !== 0 || !biz?.biz_data?.user?.token) {
52
+ const msg = biz?.biz_msg || data?.msg || (biz?.biz_data?.user ? "响应缺少 token" : "登录响应格式错误");
53
+ throw new Error(`DeepSeek 登录失败: ${msg}${res.ok ? "" : ` (http ${res.status})`}`);
54
+ }
55
+ return { token: biz.biz_data.user.token, userId: biz.biz_data.user.id };
56
+ }
57
+
58
+ // 多账号轮换池:空闲最久优先 + 冷却 + onError;轮换语义对齐 NIyueeE/ds-free-api(选空闲最久者,最大化每次使用间隔)
59
+ export function createAuthPool({ tokens = [], cooldownMs = DEFAULT_COOLDOWN_MS, clock = Date.now } = {}) {
60
+ const list = [...new Set((tokens || []).map((t) => String(t).trim()).filter(Boolean))];
61
+ const until = new Map(); // token → 冷却到期时刻
62
+ const lastUsedAt = new Map(); // token → 最近一次被取用时刻(空闲最久排序依据)
63
+
64
+ function isCooling(token) {
65
+ const u = until.get(token);
66
+ return u != null && clock() < u;
67
+ }
68
+
69
+ // 空闲最久优先:平局按 list 顺序(与 round-robin 常规场景兼容,间隔拉开时倾向复用久未用号)
70
+ function next() {
71
+ if (!list.length) return null;
72
+ let best = null;
73
+ let bestIdle = -1;
74
+ for (const token of list) {
75
+ if (isCooling(token)) continue;
76
+ const idle = clock() - (lastUsedAt.get(token) || 0);
77
+ if (idle > bestIdle) {
78
+ bestIdle = idle;
79
+ best = token;
80
+ }
81
+ }
82
+ if (best) lastUsedAt.set(best, clock());
83
+ return best;
84
+ }
85
+
86
+ // cooldownMsOverride 可覆盖池默认(frequency/muted 走差异化冷却)
87
+ function onError(token, { cooldownMs: cooldownMsOverride } = {}) {
88
+ if (!list.includes(token)) return;
89
+ const ms = Number(cooldownMsOverride) > 0 ? Number(cooldownMsOverride) : cooldownMs;
90
+ until.set(token, clock() + ms);
91
+ dsDebug("auth", { event: "rotate", tokenTail: String(token).slice(-6), cooldownMs: ms, cooling: list.length - available(), total: list.length });
92
+ }
93
+
94
+ function available() {
95
+ return list.filter((t) => !isCooling(t)).length;
96
+ }
97
+
98
+ function requireToken() {
99
+ const token = next();
100
+ if (!token) {
101
+ const err = new Error(list.length
102
+ ? `DeepSeek: 所有账号都在冷却中(${list.length} 个),稍后再试或 -provider deepseek login 追加`
103
+ : "缺少 DeepSeek 凭据,请先 -provider deepseek login 或设置 providerConfigs.deepseek.keys");
104
+ err._deepseekNoAuth = true;
105
+ throw err;
106
+ }
107
+ return token;
108
+ }
109
+
110
+ return { next, onError, available, requireToken, size: list.length, cooldownMs, keys: [...list] };
111
+ }
@@ -0,0 +1,158 @@
1
+ // DeepSeek bridge:OpenAI messages ↔ DeepSeek web 协议互转
2
+ // prompt 构造参考 iidamie/deepseek2api messages_prepare(协议事实)
3
+ // SSE 事件层(event/data 分块 + 流尾 flush)+ 调 sse-decoder.js 纯解码
4
+ import { createDeepseekDeltaParser } from "./sse-decoder.js";
5
+
6
+ const USER_TAG = "<|User|>";
7
+ const ASSISTANT_OPEN = "<|Assistant|>";
8
+ const ASSISTANT_CLOSE = "<|end▁of▁sentence|>";
9
+
10
+ export function buildPrompt(messages) {
11
+ const list = (messages || []).map((m) => {
12
+ let text = "";
13
+ if (Array.isArray(m?.content)) {
14
+ text = m.content
15
+ .filter((p) => p?.type === "text" && typeof p.text === "string")
16
+ .map((p) => p.text)
17
+ .join("\n");
18
+ } else if (m?.content != null) {
19
+ text = String(m.content);
20
+ }
21
+ return { role: String(m?.role || ""), text };
22
+ });
23
+ if (!list.length) return "";
24
+
25
+ const merged = [list[0]];
26
+ for (const msg of list.slice(1)) {
27
+ if (msg.role && msg.role === merged[merged.length - 1].role) {
28
+ merged[merged.length - 1].text += `\n\n${msg.text}`;
29
+ } else {
30
+ merged.push(msg);
31
+ }
32
+ }
33
+
34
+ const parts = [];
35
+ for (let idx = 0; idx < merged.length; idx++) {
36
+ const { role, text } = merged[idx];
37
+ if (role === "assistant") {
38
+ parts.push(`${ASSISTANT_OPEN}${text}${ASSISTANT_CLOSE}`);
39
+ } else if (role === "user" || role === "system") {
40
+ parts.push(idx > 0 ? `${USER_TAG}${text}` : text);
41
+ } else {
42
+ parts.push(text);
43
+ }
44
+ }
45
+ return parts.join("");
46
+ }
47
+
48
+ // 输入字符上限(ds-free-api 实测标定:default/vision≈1M tokens;expert≈64K tokens 与官方 API 一致)
49
+ // 超限时上游 HTTP 200 + event:hint(input_exceeds_limit) + 立即 close;阈值取上限 75% 留余量
50
+ const INPUT_CHAR_LIMITS = { expert: 163_840, default: 2_621_440, vision: 2_621_440 };
51
+
52
+ export function promptThresholdFor(flags = {}) {
53
+ const limit = flags?.expert ? INPUT_CHAR_LIMITS.expert : INPUT_CHAR_LIMITS.default;
54
+ return Math.floor(limit * 0.75);
55
+ }
56
+
57
+ // 硬切(对齐 ds-free-api split_prompt_chunks:不感知标签边界,按字符数)
58
+ export function splitPromptChunks(prompt, chunkSize) {
59
+ const s = String(prompt ?? "");
60
+ if (!chunkSize || chunkSize <= 0) return s ? [s] : [];
61
+ const chunks = [];
62
+ for (let i = 0; i < s.length; i += chunkSize) chunks.push(s.slice(i, i + chunkSize));
63
+ return chunks;
64
+ }
65
+
66
+ export function mapModelToFlags(modelId) {
67
+ const id = String(modelId || "");
68
+ // 官网「专家模式」:completion body 显式 model_type:"expert"(真机抓包,model_type 取值 default/chat/reasoner/vision/expert)
69
+ return {
70
+ thinking: id.includes("reasoner"),
71
+ search: id.includes("search"),
72
+ expert: id.includes("expert"),
73
+ };
74
+ }
75
+
76
+ export function buildUpstreamBody({ sessionId, prompt, thinking, search, expert, parentMessageId = null }) {
77
+ const body = {
78
+ chat_session_id: sessionId,
79
+ parent_message_id: parentMessageId ?? null,
80
+ prompt: String(prompt ?? ""),
81
+ ref_file_ids: [],
82
+ thinking_enabled: !!thinking,
83
+ search_enabled: !!search,
84
+ };
85
+ // 非 expert 不传 model_type(v2.0.0 实测缺省走 session 的 default;expert 模式显式声明)
86
+ if (expert) body.model_type = "expert";
87
+ return body;
88
+ }
89
+
90
+ // SSE 事件层:喂入文本(可跨块断行),产出标准 delta 事件数组。
91
+ // 上游形态(Android x-client-version 2.0.0 真机抓包):
92
+ // event: ready / update_session / title / close
93
+ // data: {"v":{"response":{fragments:[{type,content}]}}} → 快照(decoder 做 unseen 后缀去重)
94
+ // data: {"p":"response/fragments/-1/content","o":"APPEND","v":"字"} → 定向增量
95
+ // data: {"v":"字"} → 裸值:沿用 currentKind
96
+ // data: {"p":"response/status","o":"SET","v":"FINISHED"} → 结束
97
+ // startKind:thinking 请求首个 THINK fragment 可能以空对象 {} 下发(无 type),靠请求方兜底
98
+ export function createDeepseekSseParser({ startKind = "content" } = {}) {
99
+ let buffer = "";
100
+ let closeSeen = false;
101
+ const decoder = createDeepseekDeltaParser({ startKind });
102
+
103
+ function processEventBlock(rawEvent, out) {
104
+ let eventName = "";
105
+ const dataLines = [];
106
+ for (const line of rawEvent.split("\n")) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed) continue;
109
+ if (trimmed.startsWith("event:")) { eventName = trimmed.slice(6).trim(); continue; }
110
+ if (trimmed.startsWith("data:")) dataLines.push(trimmed.slice(5).trim());
111
+ }
112
+ if (eventName === "close" && !closeSeen && !decoder._state.finished) {
113
+ closeSeen = true;
114
+ out.push({ finish: "stop" });
115
+ return;
116
+ }
117
+ // 上游拒绝信号(真机抓包):event: hint + {"type":"error","content":"内容超长,请删减后再试","finish_reason":"input_exceeds_limit"}
118
+ // 后跟 event: close,HTTP 仍是 200 —— 不透传就是"成功但空回复"
119
+ if (eventName === "hint") {
120
+ for (const dataStr of dataLines) {
121
+ let d = null;
122
+ try { d = JSON.parse(dataStr); } catch {}
123
+ if (d?.type === "error") {
124
+ out.push({ error: String(d.content || "上游拒绝"), finishReason: d.finish_reason || null, clearResponse: d.clear_response === true });
125
+ return;
126
+ }
127
+ }
128
+ return;
129
+ }
130
+ for (const dataStr of dataLines) {
131
+ if (!dataStr || dataStr === "[DONE]") continue;
132
+ for (const delta of decoder.consume(dataStr)) {
133
+ if (delta.kind === "finish") out.push({ finish: "stop" });
134
+ else if (delta.kind === "reasoning") out.push({ reasoning: delta.text });
135
+ else out.push({ content: delta.text });
136
+ }
137
+ }
138
+ }
139
+
140
+ return function parse(chunk) {
141
+ buffer += String(chunk ?? "");
142
+ const out = [];
143
+ let idx;
144
+ while ((idx = buffer.indexOf("\n\n")) >= 0) {
145
+ const rawEvent = buffer.slice(0, idx);
146
+ buffer = buffer.slice(idx + 2);
147
+ processEventBlock(rawEvent, out);
148
+ }
149
+ // 流尾 flush:仅当剩余 buffer 以换行结尾(data 行已完整终止)才处理,
150
+ // 半截 JSON 保持缓冲等待下一块;否则 FINISHED/close 等尾部事件会永久卡在 buffer
151
+ if (buffer.endsWith("\n") && buffer.trim()) {
152
+ const rest = buffer;
153
+ buffer = "";
154
+ processEventBlock(rest, out);
155
+ }
156
+ return out;
157
+ };
158
+ }