mslxdff 0.1.126 → 0.1.128

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.126",
3
+ "version": "0.1.128",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,10 @@ export async function handleProviderModels(id, sub, args, rest) {
34
34
  let provider;
35
35
  if (id === "workbuddy") {
36
36
  provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
37
+ } else if (id === "cline" || id === "clinebot") {
38
+ // 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
39
+ const { createClineProvider } = await import("../../../providers/cline.js");
40
+ provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
37
41
  } else {
38
42
  if (!baseUrl) {
39
43
  console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
@@ -3,6 +3,8 @@ import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProv
3
3
  import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner } from "../base.js";
4
4
  import { compatFetch } from "../../compat.js";
5
5
  import { joinModelId } from "../model-id.js";
6
+ import { logDir } from "../../logs.js";
7
+ import { join } from "node:path";
6
8
  import { createAuthPool } from "./auth.js";
7
9
  import { clineHeaders, isRefreshToken } from "./headers.js";
8
10
  import { createChatService } from "./chat.js";
@@ -24,6 +26,7 @@ export function createClineProvider({
24
26
  apiKey,
25
27
  modelsPath,
26
28
  chatPath,
29
+ snapshotPath,
27
30
  connectTimeoutMs = Number(process.env.MSLXDFF_CLINE_TIMEOUT_MS) || 30_000,
28
31
  cooldownMs = envInt("MSLXDFF_CLINE_COOLDOWN_MS", 30_000),
29
32
  retry = { network: { attempts: 2, delayMs: 300 }, 429: { attempts: 1, delayMs: 100 }, 502: { attempts: 1, delayMs: 100 }, 503: { attempts: 1, delayMs: 100 }, 504: { attempts: 1, delayMs: 100 } },
@@ -105,7 +108,8 @@ export function createClineProvider({
105
108
  return oldRunner.runChat(body, tmpRing, "shared provider keys");
106
109
  }
107
110
 
108
- const modelsSvc = createModelsService({ id, baseUrl: resolvedBase, modelsPath: resolvedModelsPath, fetchImpl, dispatcher, ring, loadKeys: (pid) => loadProviderKeys(pid, file ? { file } : {}) });
111
+ // 快照文件供 daemon 启动自检 free 列表增删(默认 logDir 下,gitignored;测试可注入 tmp 路径)
112
+ const modelsSvc = createModelsService({ id, baseUrl: resolvedBase, modelsPath: resolvedModelsPath, fetchImpl, dispatcher, ring, loadKeys: (pid) => loadProviderKeys(pid, file ? { file } : {}), snapshotPath: snapshotPath || join(logDir(), "cline-free.json") });
109
113
 
110
114
  async function listModels() {
111
115
  const list = await modelsSvc.listModels();
@@ -113,10 +117,10 @@ export function createClineProvider({
113
117
  return list;
114
118
  }
115
119
 
116
- const { preheat } = (() => {
117
- try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }) }; }
120
+ const { preheat, checkFreeUpdates } = (() => {
121
+ try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }), checkFreeUpdates: async () => ({ ok: false }) }; }
118
122
  })();
119
123
 
120
124
  async function close() { if (agent?.close) try { await agent.close(); } catch {} }
121
- return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
125
+ return { id, chat, chatWithKeys, listModels, preheat, checkFreeUpdates, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
122
126
  }
@@ -1,6 +1,8 @@
1
1
  import { joinUrl, getUndici } from "../base.js";
2
2
  import { compatFetch } from "../../compat.js";
3
3
  import { joinModelId } from "../model-id.js";
4
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
4
6
 
5
7
  const { UndiciFetch } = getUndici();
6
8
 
@@ -8,7 +10,7 @@ function isClineBotHost(baseUrl) {
8
10
  try { const u = new URL(baseUrl); return u.hostname === "api.cline.bot" || u.hostname.endsWith(".cline.bot"); } catch { return String(baseUrl).includes("cline.bot"); }
9
11
  }
10
12
 
11
- export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispatcher, ring, loadKeys } = {}) {
13
+ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispatcher, ring, loadKeys, snapshotPath } = {}) {
12
14
  if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
13
15
  const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
14
16
  const resolvedPath = modelsPath || "/ai/cline/recommended-models";
@@ -16,11 +18,13 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
16
18
  let cache = null;
17
19
  let fetchedAt = 0;
18
20
 
19
- // 离线兜底:对标官方 FALLBACK(free 数组实证含 z-ai/glm-5.3-flash)。
21
+ // 离线兜底:与上游 recommended-models 的 free 对齐(2026-09-16 实测 5 个)。
20
22
  // 上游挂了/401 时也不返回空数组,保证 -provider clinebot models 与 picks 仍有免费可用。
21
23
  const FALLBACK_FREE = [
22
- { id: "deepseek/deepseek-v4-flash", name: "deepseek-v4-flash" },
24
+ { id: "cline-free/deepseek-v4.1-flash", name: "deepseek-v4.1-flash" },
25
+ { id: "cline-free/muse-spark-1.3-contributor", name: "muse-spark-1.3-contributor" },
23
26
  { id: "z-ai/glm-5.3-flash", name: "glm-5.3-flash" },
27
+ { id: "cline-free/solar-pro4", name: "solar-pro4" },
24
28
  { id: "poolside/laguna-s-2.1:free", name: "laguna-s-2.1:free" },
25
29
  ];
26
30
 
@@ -67,7 +71,41 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
67
71
  } catch { return fallbackList(); } finally { clearTimeout(timer); }
68
72
  }
69
73
 
70
- async function preheat() {
74
+ // 每次 daemon 启动自检 free 列表(server-lifecycle 显式调 checkFreeUpdates,不经 dispatcher.preheat):
75
+ // 对比快照报增删并留痕(daemon.log),顺带把结果填缓存(省一次 listModels 请求)。
76
+ // 快照路径由 index.js 注入 logDir 下文件;未注入时仅跳过自检,不影响拉取。
77
+ function readSnapshotFree() {
78
+ try { const j = JSON.parse(readFileSync(snapshotPath, "utf8")); return Array.isArray(j?.free) ? j.free : null; } catch { return null; }
79
+ }
80
+
81
+ function writeSnapshotFree(ids) {
82
+ try {
83
+ mkdirSync(dirname(snapshotPath), { recursive: true });
84
+ writeFileSync(snapshotPath, JSON.stringify({ ts: Date.now(), free: ids }, null, 2));
85
+ } catch {}
86
+ }
87
+
88
+ // Note: clinebot free 目录三处同源(CLI 直查/聚合目录/兜底)+ daemon 启动自检快照(diff 写 daemon.log,不自动改 picks)— 见 .agents/notes/implemented/bug-fix/2026-09-16-clinebot-free-catalog-unify.md
89
+ function detectFreeChanges(ids) {
90
+ const prev = readSnapshotFree();
91
+ if (!prev) {
92
+ writeSnapshotFree(ids);
93
+ console.log(`[${id}] free snapshot created (${ids.length} models)`);
94
+ return { first: true };
95
+ }
96
+ const added = ids.filter((x) => !prev.includes(x));
97
+ const removed = prev.filter((x) => !ids.includes(x));
98
+ if (!added.length && !removed.length) return { unchanged: true };
99
+ writeSnapshotFree(ids);
100
+ const parts = [];
101
+ if (added.length) parts.push(`+${added.join(" +")}`);
102
+ if (removed.length) parts.push(`-${removed.join(" -")}`);
103
+ console.log(`[${id}] free models updated (${ids.length} total): ${parts.join(" ")}`);
104
+ return { added, removed };
105
+ }
106
+
107
+ // 启动自检入口:拉 recommended-models → 填缓存 → 对比快照报 free 增删。
108
+ async function checkFreeUpdates() {
71
109
  const url = resolveModelsUrl();
72
110
  const t0 = performance.now();
73
111
  try {
@@ -77,12 +115,38 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
77
115
  const opts = { headers };
78
116
  if (dispatcher) opts.dispatcher = dispatcher;
79
117
  const res = await fetchImpl(url, opts);
80
- try { if (res.body) await res.text().catch(() => {}); } catch {}
81
- return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
118
+ let freeIds = null;
119
+ try {
120
+ const text = await res.text().catch(() => "");
121
+ if (res.ok && text && isClineBotHost(resolvedBase)) {
122
+ const json = JSON.parse(text);
123
+ if (Array.isArray(json.free)) {
124
+ const valid = json.free.filter((m) => m && typeof m.id === "string");
125
+ if (valid.length) {
126
+ cache = valid.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
127
+ fetchedAt = Date.now();
128
+ freeIds = valid.map((m) => m.id);
129
+ }
130
+ }
131
+ }
132
+ } catch {}
133
+ let change = null;
134
+ if (freeIds && snapshotPath) { try { change = detectFreeChanges(freeIds); } catch {} }
135
+ return {
136
+ ok: res.ok,
137
+ status: res.status,
138
+ ms: Math.round(performance.now() - t0),
139
+ ...(change?.added ? { freeAdded: change.added, freeRemoved: change.removed } : {}),
140
+ };
82
141
  } catch (err) {
83
142
  return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
84
143
  }
85
144
  }
86
145
 
87
- return { listModels, preheat };
146
+ // preheat 保留为别名:dispatcher 已不再调 clinebot,手动/测试/未来钩子仍可用,行为与自检一致
147
+ async function preheat() {
148
+ return checkFreeUpdates();
149
+ }
150
+
151
+ return { listModels, preheat, checkFreeUpdates };
88
152
  }
@@ -92,17 +92,18 @@ export function createProviderDispatcher(providers = [], opts = {}) {
92
92
  return out;
93
93
  }
94
94
 
95
+ // 只预热默认供应商(opencode):连接池与模型缓存预热是其主链路收益;
96
+ // 其他供应商按需在首次请求时自拉(10min 缓存)。不再逐家预热,避免 daemon 每次启动
97
+ // 对所有上游各发一次 GET;MSLXDFF_PREHEAT=0 的关闭由唯一被调的 opencode preheat 自行尊重。
98
+ // 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
95
99
  async function preheat() {
96
- const results = [];
97
- for (const p of providers) {
98
- if (typeof p.preheat !== "function") continue;
99
- try {
100
- results.push(await p.preheat());
101
- } catch {
102
- results.push({ ok: false, error: "preheat failed" });
103
- }
100
+ const p = byId.get(DEFAULT_PROVIDER);
101
+ if (!p || typeof p.preheat !== "function") return { ok: false, skipped: true };
102
+ try {
103
+ return await p.preheat();
104
+ } catch {
105
+ return { ok: false, error: "preheat failed" };
104
106
  }
105
- return results.length ? results[0] : { ok: false, skipped: true };
106
107
  }
107
108
 
108
109
  async function close() {
@@ -123,6 +123,12 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
123
123
  else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
124
124
  else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
125
125
  }).catch(() => {});
126
+ // clinebot free 目录自检(独立于 opencode 连接预热:dispatcher.preheat 只做默认供应商)
127
+ // 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
128
+ try {
129
+ const cline = upstream?.byId?.get?.("clinebot") ?? upstream?.byId?.get?.("cline");
130
+ if (cline?.checkFreeUpdates) void cline.checkFreeUpdates().catch(() => {});
131
+ } catch {}
126
132
  }, 100).unref?.();
127
133
 
128
134
  // responses 模型判定改元数据驱动(models.dev provider.npm):就绪后注入,每小时重查使新模型自动识别