mslxdff 0.1.135 → 0.1.138
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 +2 -2
- package/src/chat/gateway.js +1 -1
- package/src/chat/prompt.js +1 -1
- package/src/chat/terminal.js +1 -1
- package/src/chat-pipeline/serial-trial.js +11 -3
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/list-render.js +2 -2
- package/src/cli/commands/provider/add.js +2 -1
- package/src/cli/commands/provider/bench-via.js +1 -1
- package/src/cli/commands/provider/cline-free.js +94 -0
- package/src/cli/commands/provider/cline-login.js +20 -15
- package/src/cli/commands/provider/index.js +22 -0
- package/src/cli/commands/provider/models.js +4 -1
- package/src/cli/commands/provider/workbuddy-login.js +1 -1
- package/src/cli/commands/use-group.js +8 -4
- package/src/cli/commands/workbuddy.js +14 -21
- package/src/cli/index.js +3 -0
- package/src/compat.js +23 -5
- package/src/model-capabilities/index.js +16 -3
- package/src/model-capabilities/merge.js +127 -0
- package/src/providers/classify.js +2 -2
- package/src/providers/cline/auth.js +1 -1
- package/src/providers/cline/chat.js +14 -11
- package/src/providers/cline/free-catalog.js +71 -0
- package/src/providers/cline/index.js +2 -16
- package/src/providers/cline/models.js +28 -51
- package/src/providers/dispatcher.js +1 -1
- package/src/providers/generic.js +47 -9
- package/src/providers/model-id.js +4 -4
- package/src/providers/registry.js +1 -1
- package/src/providers/share-keys.js +7 -2
- package/src/providers/workbuddy/account-store.js +73 -7
- package/src/providers/workbuddy/chat.js +1 -1
- package/src/providers/workbuddy/index.js +10 -20
- package/src/readline-compat.js +9 -10
- package/src/routes/models-route.js +11 -1
- package/src/runtime/providers-setup.js +9 -0
- package/src/runtime/server-lifecycle.js +2 -2
- package/src/state/migrations/cline-unify.js +94 -0
- package/src/state/migrations.js +33 -0
- package/src/state/schemas/token.js +6 -2
- package/src/state/schemas/use-group.js +47 -6
- package/src/sync-workbuddy.js +1 -1
- package/src/upstream-engine/index.js +1 -1
- package/src/upstream-probe/rotate.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mslxdff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.138",
|
|
4
4
|
"description": "测试项目,请勿使用。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"docs:check": "node scripts/docs-check.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=18"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/",
|
package/src/chat/gateway.js
CHANGED
|
@@ -138,7 +138,7 @@ export function createGatewayClient({
|
|
|
138
138
|
}
|
|
139
139
|
if (provider === "opencode" && rawModel.includes("/")) {
|
|
140
140
|
const maybe = rawModel.split("/")[0];
|
|
141
|
-
if (["workbuddy", "
|
|
141
|
+
if (["workbuddy", "cline", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
|
|
142
142
|
}
|
|
143
143
|
} catch {}
|
|
144
144
|
if (TRACE) {
|
package/src/chat/prompt.js
CHANGED
|
@@ -58,7 +58,7 @@ ${mini}
|
|
|
58
58
|
- 需要执行命令时调用 run_command,需要看文件时调用 read_file,需要检查网络/服务可用性时调用 curl。
|
|
59
59
|
- curl 简写:upstream(=上游 https://opencode.ai/zen/v1/models)、local/health(=本机 /health)、local/models(=本机 /v1/models),也支持完整 http(s) URL;会自动补上游头、本机 token 与已配置供应商 key(直连 https://api.b.ai/v1/models 会自动带 bai 的 key,无需手动加头)。
|
|
60
60
|
- 查“某供应商有哪些模型”**优先用 CLI 直查**:若上方“可用模型”已能回答,直接前缀过滤回答(如 workbuddy/ 即 workbuddy);需实时拉取时调用 run_command: "-provider <id> models"(如 -provider workbuddy models)或 "-model list --provider <id>",按 allowlist 过滤,--json 供脚本。**禁止**调 -provider <id> list(这是查配置,不是查模型!)。**错误示例**:workbuddy有哪些模型 → 调 -provider workbuddy list → 错。**正确**:-provider workbuddy models。禁止为此调用 -showtoken。
|
|
61
|
-
- 严禁幻觉命令:mslxdff "hi" --model X / mslxdff --model X "hi" / mslxdff -chat --model X 都不存在,输出只会是 status 页。探活任意模型(含
|
|
61
|
+
- 严禁幻觉命令:mslxdff "hi" --model X / mslxdff --model X "hi" / mslxdff -chat --model X 都不存在,输出只会是 status 页。探活任意模型(含 cline/*、workbuddy/*、bai/*)必须用 curl POST http://localhost:8989/v1/chat/completions,body 为 {"model":"<前缀/模型>","messages":[{"role":"user","content":"hi"}],"stream":false},成功 200 + x-mslxdff-via:local 即通;401 代表本机 token 陈旧需提示 mslxdff -stop && mslxdff;403 + x-mslxdff-allowlist:1 代表白名单未放行需 allowlist add。
|
|
62
62
|
- **禁止重复调用(最高优先级)**:同一 run_command/curl/read_file 在本轮只执行一次,重复会被工具侧 SKIPPED_DUP 拦截;查询类(-showtoken/-status/-provider list/-providers list/-model list/-group list/-log 等)**调用一次即答案**,拿到 OK 结果后必须**立即用中文直接回答用户**,禁止再发起任何工具调用。收到 SKIPPED_DUP 或“请直接回答/禁止再调用”提示时,必须 0 工具直接回答。
|
|
63
63
|
- 禁止调用 -uninstall,包含即拒绝;-showtoken 仅在用户明确要求查看/调试本机 token 时才用,查模型/查供应商严禁调用。
|
|
64
64
|
- 回复风格:简洁友好,执行前后用中文说明你在做什么。
|
package/src/chat/terminal.js
CHANGED
|
@@ -96,7 +96,7 @@ export function handleSlash(line, ctx) {
|
|
|
96
96
|
const id = arg;
|
|
97
97
|
if (id.includes("/")) {
|
|
98
98
|
console.log(`\x1b[31m[拒绝] ${id} 带供应商前缀 — -chat 只支持 opencode 上游模型(裸 id,直连 opencode.ai 免费池)\x1b[0m`);
|
|
99
|
-
console.log(`\x1b[90m其他供应商(deepseek/ workbuddy/
|
|
99
|
+
console.log(`\x1b[90m其他供应商(deepseek/ workbuddy/ cline/ 等)请走网关:mslxdff -model set <id> 或 curl 本机 /v1/chat/completions\x1b[0m`);
|
|
100
100
|
return { handled: true };
|
|
101
101
|
}
|
|
102
102
|
if (!isFreeModel(id) || !freePoolNow().includes(id)) {
|
|
@@ -9,7 +9,13 @@ 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, isHardLocalOnly } from "../state/schemas/use-group.js";
|
|
12
|
+
import { shouldUseGroupForModel, isHardLocalOnly, isKeyProviderDirectOnly } from "../state/schemas/use-group.js";
|
|
13
|
+
|
|
14
|
+
function groupSkipReason(model) {
|
|
15
|
+
if (isHardLocalOnly(model)) return "provider local-only(禁组员,仅本机直连)";
|
|
16
|
+
if (isKeyProviderDirectOnly(model)) return "key provider default direct(仅本机直连,MSLXDFF_USE_GROUP_KEYS=1 可开组员)";
|
|
17
|
+
return "useGroup=off";
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* 串行 trial — 从 engine.js 抽出的第二段:via-route 单路径 → 串行 trial →
|
|
@@ -43,6 +49,8 @@ export async function runSerialTrial(ctx, deps = {}) {
|
|
|
43
49
|
} catch (e) {
|
|
44
50
|
evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
|
|
45
51
|
}
|
|
52
|
+
} else if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
|
|
53
|
+
evt("group-skip", { reqId, model: requested, reason: `${groupSkipReason(requested)} (via-route)` });
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
let lastErr = viaRouteLastErr;
|
|
@@ -143,7 +151,7 @@ export async function runSerialTrial(ctx, deps = {}) {
|
|
|
143
151
|
}
|
|
144
152
|
if (canForwardPeers) {
|
|
145
153
|
if (!shouldUseGroupForModel(model)) {
|
|
146
|
-
evt("group-skip", { reqId, model, reason:
|
|
154
|
+
evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (peer)` });
|
|
147
155
|
} else {
|
|
148
156
|
const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
|
|
149
157
|
if (pr.handled) return { done: true };
|
|
@@ -152,7 +160,7 @@ export async function runSerialTrial(ctx, deps = {}) {
|
|
|
152
160
|
}
|
|
153
161
|
if (groups) {
|
|
154
162
|
if (!shouldUseGroupForModel(model)) {
|
|
155
|
-
evt("group-skip", { reqId, model, reason:
|
|
163
|
+
evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (broadband)` });
|
|
156
164
|
} else {
|
|
157
165
|
const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
|
|
158
166
|
if (br.handled) return { done: true };
|
|
@@ -8,7 +8,7 @@ export async function renderOtherProviders({ pickedIds, ids, fullAliases }) {
|
|
|
8
8
|
try { _la2(); } catch {}
|
|
9
9
|
const configs = loadProviderConfigs();
|
|
10
10
|
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
11
|
-
const order2 = ["workbuddy", "
|
|
11
|
+
const order2 = ["workbuddy", "cline", "openrouter", "bai"];
|
|
12
12
|
otherIds.sort((a, b) => {
|
|
13
13
|
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
14
14
|
if (ia !== -1 || ib !== -1) {
|
|
@@ -57,7 +57,7 @@ export function renderFreeList({ ids, at, pickedIds, sortedProvs, groups, aliasM
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** 按供应商分组 + 排序(opencode/workbuddy/
|
|
60
|
+
/** 按供应商分组 + 排序(opencode/workbuddy/cline/openrouter 优先) */
|
|
61
61
|
export function groupByProvider(ids) {
|
|
62
62
|
const groups = {};
|
|
63
63
|
for (const id of ids) {
|
|
@@ -65,7 +65,7 @@ export function groupByProvider(ids) {
|
|
|
65
65
|
if (!groups[prov]) groups[prov] = [];
|
|
66
66
|
groups[prov].push(id);
|
|
67
67
|
}
|
|
68
|
-
const order = ["opencode", "workbuddy", "
|
|
68
|
+
const order = ["opencode", "workbuddy", "cline", "openrouter"];
|
|
69
69
|
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
70
70
|
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
71
71
|
if (ia !== -1 || ib !== -1) {
|
|
@@ -69,7 +69,8 @@ export async function handleProviderAdd(id, sub, rest) {
|
|
|
69
69
|
try {
|
|
70
70
|
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
71
71
|
const { join } = await import("node:path");
|
|
72
|
-
const
|
|
72
|
+
const { resolveAuthDir } = await import("../../providers/workbuddy/account-store.js");
|
|
73
|
+
const dir = resolveAuthDir();
|
|
73
74
|
mkdirSync(dir, { recursive: true });
|
|
74
75
|
const fp = join(dir, `workbuddy-${uid}.json`);
|
|
75
76
|
const doc = { account: { uid, enterpriseId: "", nickname: "" }, auth: { accessToken: token, refreshToken: "", expiresAt: Math.floor(Date.now()/1000)+3600, domain: "www.codebuddy.cn" } };
|
|
@@ -120,7 +120,7 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
|
|
|
120
120
|
for (const k of Object.keys(raw.providerConfigs || {})) if (!targetIds.includes(k) && (loadAllowed(k) || []).length) targetIds.push(k);
|
|
121
121
|
for (const k of Object.keys(raw.providerKeys || {})) if (!targetIds.includes(k) && (loadAllowed(k) || []).length) targetIds.push(k);
|
|
122
122
|
} catch {}
|
|
123
|
-
if (!targetIds.length) targetIds = ["openrouter", "workbuddy", "
|
|
123
|
+
if (!targetIds.length) targetIds = ["openrouter", "workbuddy", "cline"].filter((p) => (loadAllowed(p) || []).length);
|
|
124
124
|
// opencode 默认 allowAny 但 allowlist 常空:有裸 picks 且确认包含 opencode 时纳入
|
|
125
125
|
if (includeOpencode && !targetIds.includes("opencode") && picks.some((p) => !String(p).includes("/") || String(p).toLowerCase().startsWith("opencode/"))) {
|
|
126
126
|
targetIds.push("opencode");
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mslxdff -provider cline free [sync] [--yes] [--json] [--keep-extra]
|
|
3
|
+
* 只读:直查上游免费目录(公开接口,无凭据)并与当前 allowlist 求差;只有 `free sync --yes` 才写 allowlist。
|
|
4
|
+
* allowlist 存裸 id(不带 cline/ 前缀),与 normalizeAllowedModel 语义一致;落盘走 saveProviderAllowedModels 保留 keys/baseUrl。
|
|
5
|
+
*/
|
|
6
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
7
|
+
import { fetchFreeCatalog, FALLBACK_FREE, FREE_CATALOG_PATH } from "../../../providers/cline/free-catalog.js";
|
|
8
|
+
|
|
9
|
+
/** 上游不可达时退回内置兜底目录(标注 source=fallback,供人判断是否该落盘) */
|
|
10
|
+
async function loadCatalog() {
|
|
11
|
+
const cat = await fetchFreeCatalog({});
|
|
12
|
+
if (cat.ok) return { ...cat, source: "upstream" };
|
|
13
|
+
return {
|
|
14
|
+
...cat,
|
|
15
|
+
source: "fallback",
|
|
16
|
+
models: FALLBACK_FREE.map((m) => ({ ...m })),
|
|
17
|
+
ids: FALLBACK_FREE.map((m) => m.id),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function computeNext(current, freeIds, keepExtra) {
|
|
22
|
+
const next = keepExtra ? [...new Set([...current, ...freeIds])] : [...freeIds];
|
|
23
|
+
return {
|
|
24
|
+
next,
|
|
25
|
+
added: next.filter((m) => !current.includes(m)),
|
|
26
|
+
removed: current.filter((m) => !next.includes(m)),
|
|
27
|
+
kept: next.filter((m) => current.includes(m)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderList(cat, current, next, plan) {
|
|
32
|
+
console.log(`cline free 目录(GET ${FREE_CATALOG_PATH} → free):${cat.ids.length} 个${cat.source === "fallback" ? "(内置兜底,上游不可达)" : ""}`);
|
|
33
|
+
const w = Math.max(20, ...cat.ids.map((s) => s.length)) + 2;
|
|
34
|
+
for (const m of cat.models) console.log(` + ${String(m.id).padEnd(w)}${m.name && m.name !== m.id ? m.name : ""}`);
|
|
35
|
+
console.log(`当前 allowlist:${current.length} 个`);
|
|
36
|
+
for (const id of current) console.log(` ${next.includes(id) ? "✓" : "-"} ${id}${next.includes(id) ? "" : " (不在 free 目录)"}`);
|
|
37
|
+
console.log(`结果:allowlist ${current.length} → ${next.length}(+${plan.added.length} 新增 / -${plan.removed.length} 移除 / ${plan.kept.length} 保留)`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handleClineFree(id, sub, rest = [], args = []) {
|
|
41
|
+
if (normalizeProviderId(id) !== "cline") return false;
|
|
42
|
+
if (String(sub || "").toLowerCase() !== "free") return false;
|
|
43
|
+
const isSync = String(rest[1] || "").toLowerCase() === "sync";
|
|
44
|
+
const wantsJson = args.includes("--json") || args.includes("-json");
|
|
45
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
46
|
+
const keepExtra = args.includes("--keep-extra");
|
|
47
|
+
|
|
48
|
+
const { loadProviderAllowedModels, saveProviderAllowedModels } = await import("../../../state.js");
|
|
49
|
+
const cat = await loadCatalog();
|
|
50
|
+
const current = loadProviderAllowedModels("cline");
|
|
51
|
+
const plan = computeNext(current, cat.ids, keepExtra);
|
|
52
|
+
const dryRun = !(isSync && yes);
|
|
53
|
+
let written = [];
|
|
54
|
+
if (isSync && !dryRun) {
|
|
55
|
+
try {
|
|
56
|
+
written = saveProviderAllowedModels("cline", plan.next);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(`❌ 写入 allowlist 失败: ${String(e?.message || e)}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const warning = cat.source === "fallback" ? `上游目录不可达(${cat.error || cat.status}),使用内置兜底目录,可能过期` : null;
|
|
63
|
+
|
|
64
|
+
if (wantsJson) {
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
provider: "cline",
|
|
67
|
+
mode: isSync ? "sync" : "list",
|
|
68
|
+
source: cat.source,
|
|
69
|
+
url: cat.url,
|
|
70
|
+
free: cat.ids,
|
|
71
|
+
current,
|
|
72
|
+
next: plan.next,
|
|
73
|
+
added: plan.added,
|
|
74
|
+
removed: plan.removed,
|
|
75
|
+
kept: plan.kept,
|
|
76
|
+
dryRun,
|
|
77
|
+
written,
|
|
78
|
+
...(warning ? { warning } : {}),
|
|
79
|
+
}, null, 2));
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (warning) console.error(`⚠️ ${warning}`);
|
|
84
|
+
renderList(cat, current, plan.next, plan);
|
|
85
|
+
if (!isSync) {
|
|
86
|
+
console.log(`\n预览(只读)。一键同步:mslxdff -provider cline free sync --yes · 只增不删:加 --keep-extra`);
|
|
87
|
+
} else if (dryRun) {
|
|
88
|
+
console.log(`\n预览模式,未写入。执行:mslxdff -provider cline free sync --yes${keepExtra ? " --keep-extra" : ""}`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`\n✅ 已写入 allowlist:${written.length} 个(providerConfigs.cline.allowedModels)`);
|
|
91
|
+
console.log(` 生效:mslxdff -restart(daemon 的 /v1/models 聚合有 10 分钟缓存,重启即刷新)`);
|
|
92
|
+
}
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { compatFetch, getUndici, timeoutSignal } from "../../../compat.js";
|
|
2
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
2
3
|
export async function handleClineLogin(id, sub) {
|
|
3
4
|
if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
|
|
4
|
-
if (id !== "cline"
|
|
5
|
+
if (normalizeProviderId(id) !== "cline") return false;
|
|
5
6
|
|
|
6
7
|
const CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR";
|
|
7
8
|
const WORKOS_DEVICE = "https://api.workos.com/user_management/authorize/device";
|
|
@@ -97,7 +98,7 @@ export async function handleClineLogin(id, sub) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
if (!workos) {
|
|
100
|
-
console.error("\n❌ 授权超时,请重新运行 mslxdff -provider
|
|
101
|
+
console.error("\n❌ 授权超时,请重新运行 mslxdff -provider cline login");
|
|
101
102
|
process.exit(1);
|
|
102
103
|
}
|
|
103
104
|
console.log("\n✅ WorkOS 授权成功!\n🔗 用 WorkOS token 在 Cline 注册...");
|
|
@@ -117,28 +118,32 @@ export async function handleClineLogin(id, sub) {
|
|
|
117
118
|
console.log("=".repeat(60));
|
|
118
119
|
console.log(`✅ 登录成功! 账号: ${email}`);
|
|
119
120
|
console.log(`🔑 refreshToken: ${rt.slice(0, 8)}…${rt.slice(-8)} (${rt.length} 字符)`);
|
|
120
|
-
// 落盘到 state
|
|
121
|
-
const { loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// 落盘到 state(供应商 id 恒为 cline;写盘前先跑一次性迁移,保证老用户不会再写出 clinebot)
|
|
122
|
+
const { defaultStateFile, loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
|
|
123
|
+
try {
|
|
124
|
+
const { runStateMigrations } = await import("../../../state/migrations.js");
|
|
125
|
+
const mig = await runStateMigrations({ file: defaultStateFile() });
|
|
126
|
+
if (mig.applied.length) console.log(` ℹ️ 已合并遗留供应商配置: ${mig.applied.join(", ")}`);
|
|
127
|
+
} catch {}
|
|
128
|
+
try {
|
|
129
|
+
const pid = "cline";
|
|
130
|
+
const cur = loadProviderKeys(pid);
|
|
131
|
+
if (cur.includes(rt)) {
|
|
132
|
+
console.log(` ℹ️ ${pid} 已存在相同 token,跳过`);
|
|
133
|
+
} else {
|
|
129
134
|
const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
|
|
130
135
|
const nextKeys = [...new Set([...(cfg.keys || cur), rt].filter(Boolean))];
|
|
131
136
|
saveProviderConfig(pid, { baseUrl: cfg.baseUrl || "https://api.cline.bot", keys: nextKeys });
|
|
132
137
|
console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
|
|
133
|
-
} catch (e) {
|
|
134
|
-
console.log(` ⚠️ 写入 ${pid} 失败: ${e.message}`);
|
|
135
138
|
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.log(` ⚠️ 写入 cline 失败: ${e.message}`);
|
|
136
141
|
}
|
|
137
142
|
console.log("=".repeat(60));
|
|
138
143
|
console.log("\n下一步:");
|
|
139
144
|
console.log(" mslxdff -restart 重启网关使新账号生效");
|
|
140
|
-
console.log(" mslxdff -provider
|
|
145
|
+
console.log(" mslxdff -provider cline bench --json 测速 deepseek 是否 200");
|
|
141
146
|
console.log(" mslxdff -chat 直接对话,模型选 deepseek/deepseek-v4-flash");
|
|
142
|
-
console.log("\n多账号:重复 `mslxdff -provider
|
|
147
|
+
console.log("\n多账号:重复 `mslxdff -provider cline login` 追加,二号自动做后备");
|
|
143
148
|
process.exit(0);
|
|
144
149
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
2
3
|
import { defaultStateFile } from "../../../state.js";
|
|
3
4
|
|
|
4
5
|
export async function handleProviders(args) {
|
|
@@ -74,6 +75,27 @@ export async function handleProvider(args) {
|
|
|
74
75
|
if (await handleProviderModels(id, sub, args, rest)) return true;
|
|
75
76
|
const { handleProviderBench } = await import("./bench.js");
|
|
76
77
|
if (await handleProviderBench(id, sub, rest, args)) return true;
|
|
78
|
+
const { handleClineFree } = await import("./cline-free.js");
|
|
79
|
+
if (await handleClineFree(id, sub, rest, args)) return true;
|
|
80
|
+
if (normalizeProviderId(id) === "cline" && sub === "migrate") {
|
|
81
|
+
const { runStateMigrations } = await import("../../../state/migrations.js");
|
|
82
|
+
const dryRun = args.includes("--dry-run") || args.includes("--dryrun");
|
|
83
|
+
const r = await runStateMigrations({ dryRun });
|
|
84
|
+
const d = r.details["cline-unify"] || {};
|
|
85
|
+
if (r.errors.length) console.error(`❌ 迁移失败: ${r.errors.map((e) => `${e.id}: ${e.error}`).join("; ")}`);
|
|
86
|
+
if (dryRun) {
|
|
87
|
+
console.log(d.changed
|
|
88
|
+
? `cline migrate 干跑:将合并遗留 cline id 配置(keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}),未写入`
|
|
89
|
+
: "cline migrate 干跑:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
|
|
90
|
+
} else if (d.applied) {
|
|
91
|
+
console.log(`✅ cline migrate 完成:keys ${d.before.keys} → ${d.after.keys} / allowlist ${d.before.allowedModels} → ${d.after.allowedModels}`);
|
|
92
|
+
if (d.backup) console.log(` 备份:${d.backup}`);
|
|
93
|
+
console.log(" 生效:mslxdff -restart");
|
|
94
|
+
} else {
|
|
95
|
+
console.log("cline migrate:无需迁移(未发现 providerConfigs.clinebot / cline-bot)");
|
|
96
|
+
}
|
|
97
|
+
process.exit(r.errors.length ? 1 : 0);
|
|
98
|
+
}
|
|
77
99
|
const { handleProviderKeys } = await import("./keys.js");
|
|
78
100
|
await handleProviderKeys(id, sub, rest, args);
|
|
79
101
|
return true;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { defaultStateFile } from "../../../state.js";
|
|
3
3
|
import { logDir } from "../../../logs.js";
|
|
4
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
4
5
|
|
|
5
6
|
export async function handleProviderModels(id, sub, args, rest) {
|
|
6
7
|
if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
|
|
8
|
+
// 供应商 id 归一(clinebot/cline-bot → cline):老脚本 `-provider clinebot models` 仍走 cline 直查
|
|
9
|
+
id = normalizeProviderId(id);
|
|
7
10
|
const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
|
|
8
11
|
const wantsJson = args.includes("--json") || args.includes("-json");
|
|
9
12
|
const cfg = loadProviderConfig(id);
|
|
@@ -34,7 +37,7 @@ export async function handleProviderModels(id, sub, args, rest) {
|
|
|
34
37
|
let provider;
|
|
35
38
|
if (id === "workbuddy") {
|
|
36
39
|
provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
|
|
37
|
-
} else if (id === "cline"
|
|
40
|
+
} else if (id === "cline") {
|
|
38
41
|
// 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
|
|
39
42
|
const { createClineProvider } = await import("../../../providers/cline.js");
|
|
40
43
|
provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compatFetch, timeoutSignal } from "../../../compat.js";
|
|
2
|
-
// mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标
|
|
2
|
+
// mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 cline login)。
|
|
3
3
|
// 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
|
|
4
4
|
// 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
|
|
5
5
|
// 不走 whistle/MITM,不依赖 codebuddy CLI,自然支持多账号追加。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv } from "../../state/schemas/use-group.js";
|
|
1
|
+
import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv, getUseGroupKeysEnv } from "../../state/schemas/use-group.js";
|
|
2
2
|
import { argValue } from "../policy.js";
|
|
3
3
|
|
|
4
4
|
function parseInput(v) {
|
|
@@ -26,15 +26,18 @@ export async function handleUseGroup(args) {
|
|
|
26
26
|
const envVal = getUseGroupEnv();
|
|
27
27
|
const effective = getEffectiveUseGroup();
|
|
28
28
|
const stored = loadUseGroup();
|
|
29
|
+
const keysEnv = getUseGroupKeysEnv();
|
|
29
30
|
|
|
30
31
|
if (raw === null || raw === undefined || raw === "") {
|
|
31
32
|
// 查询模式
|
|
32
|
-
console.log(`use-group: ${effective ? "on" : "off"} (effective)`);
|
|
33
|
+
console.log(`use-group: ${effective ? "on" : "off"} (effective, 仅 opencode 免费池)`);
|
|
33
34
|
console.log(` stored: ${stored ? "on" : "off"} (state.json useGroup)`);
|
|
34
35
|
if (envVal !== null) console.log(` env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} (overrides stored)`);
|
|
36
|
+
console.log(` keys: ${keysEnv ? "on" : "off"} (env MSLXDFF_USE_GROUP_KEYS, 默认 off: 带 key 上游恒直连)`);
|
|
35
37
|
console.log(` default: on`);
|
|
36
|
-
console.log(` usage: mslxdff -use-group on|off (
|
|
38
|
+
console.log(` usage: mslxdff -use-group on|off (opencode 失败时是否走组员网络,默认 on)`);
|
|
37
39
|
console.log(` env: MSLXDFF_USE_GROUP=0|1 (优先级高于 state)`);
|
|
40
|
+
console.log(` env: MSLXDFF_USE_GROUP_KEYS=1 (把 key 供应商开回组员,cline/workbuddy 仍硬禁)`);
|
|
38
41
|
process.exit(0);
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -50,7 +53,8 @@ export async function handleUseGroup(args) {
|
|
|
50
53
|
|
|
51
54
|
saveUseGroup(parsed);
|
|
52
55
|
console.log(`use-group set to ${parsed ? "on" : "off"} (stored in state.json)`);
|
|
53
|
-
console.log(` ${parsed ? "允许" : "
|
|
56
|
+
console.log(` ${parsed ? "允许 opencode" : "opencode 也不再"}走组员网络(via-route/hedge/peer/broadband)`);
|
|
57
|
+
console.log(` 带 key 上游默认恒直连(不受本开关影响,MSLXDFF_USE_GROUP_KEYS=1 可开回,cline/workbuddy 仍硬禁)`);
|
|
54
58
|
if (!parsed) console.log(` 提示:所有请求将仅在本机重试,不再走组员中继`);
|
|
55
59
|
process.exit(0);
|
|
56
60
|
}
|
|
@@ -126,12 +126,15 @@ export async function handleWorkbuddy(args) {
|
|
|
126
126
|
auths.splice(rmIdx,1); keys.splice(rmIdx,1);
|
|
127
127
|
saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
|
|
128
128
|
if (!keep) {
|
|
129
|
+
// 旧实现只删 cwd/auths 那一份,留下的旧副本会被读取兜底"复活"账号 → 扫所有候选目录
|
|
129
130
|
try {
|
|
130
131
|
const { existsSync, unlinkSync } = await import("node:fs");
|
|
131
132
|
const { join } = await import("node:path");
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
133
|
+
const { resolveAuthDirs } = await import("../../providers/workbuddy/account-store.js");
|
|
134
|
+
for (const dir of resolveAuthDirs()) {
|
|
135
|
+
const fp = join(dir, `workbuddy-${uid}.json`);
|
|
136
|
+
if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
|
|
137
|
+
}
|
|
135
138
|
} catch {}
|
|
136
139
|
}
|
|
137
140
|
try { const { getBalanceCache } = await import("../../providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
|
|
@@ -155,24 +158,14 @@ async function loadWorkbuddyAccounts() {
|
|
|
155
158
|
})).filter((it) => it.uid && it.key);
|
|
156
159
|
if (items.length) return items;
|
|
157
160
|
try {
|
|
158
|
-
const {
|
|
159
|
-
const {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (j?.account?.uid && j?.auth?.accessToken) {
|
|
167
|
-
const domain = j.auth.domain || "www.codebuddy.cn";
|
|
168
|
-
items.push({
|
|
169
|
-
uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "",
|
|
170
|
-
key: j.auth.accessToken,
|
|
171
|
-
auth: { uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "", refreshToken: j.auth.refreshToken || "" },
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
161
|
+
const { listAccountDocs } = await import("../../providers/workbuddy/account-store.js");
|
|
162
|
+
for (const { uid, doc } of listAccountDocs()) {
|
|
163
|
+
const domain = doc.auth.domain || "www.codebuddy.cn";
|
|
164
|
+
items.push({
|
|
165
|
+
uid, domain, enterpriseId: doc.account.enterpriseId || "",
|
|
166
|
+
key: doc.auth.accessToken,
|
|
167
|
+
auth: { uid, domain, enterpriseId: doc.account.enterpriseId || "", refreshToken: doc.auth.refreshToken || "" },
|
|
168
|
+
});
|
|
176
169
|
}
|
|
177
170
|
} catch {}
|
|
178
171
|
return items;
|
package/src/cli/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { assertMinNode } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8")).version;
|
|
7
8
|
|
|
8
9
|
// deep CLI entry — single inlet: run(args) — hides all command branching inside
|
|
9
10
|
export async function run(args = process.argv.slice(2)) {
|
|
11
|
+
// 运行环境门(强制):项目要求 Node >=18(ADR-0024),不满足则人话退出
|
|
12
|
+
if (!assertMinNode()) process.exit(1);
|
|
10
13
|
const { handleHelp, handleUpdate, handleRefreshToken, handleShowToken, handleUninstall, handleLog, handlePlugins, handleChat, handleStatus, handleFree, handleAutostart } = await import("./commands/system.js");
|
|
11
14
|
if (await handleHelp(args, VERSION)) return;
|
|
12
15
|
if (await handleUpdate(args, VERSION)) return;
|
package/src/compat.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// 运行时兼容层(项目要求 Node >=18,见 ADR-0024;源码里勿直接用裸 fetch/globalThis.fetch)。
|
|
2
|
+
// 仍保留本层的原因:① undici 是显式依赖(Agent 连接池/keep-alive 要用),
|
|
3
|
+
// 各家 fetch 实现统一从这里取,避免 globalThis.fetch 与 undici.fetch 混用;
|
|
4
|
+
// ② AbortSignal.timeout/structuredClone 在 18+ 已原生,这里的兜底只为可读报错,
|
|
5
|
+
// 不再承诺 <18 可用(<18 请走 assertMinNode 的人话提示)。
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
let _undici = null;
|
|
@@ -33,3 +33,21 @@ export function clone(v) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const uuid = () => randomUUID();
|
|
36
|
+
|
|
37
|
+
// ---- 运行环境门(强制):项目要求 Node >=18(ADR-0024)----
|
|
38
|
+
// Node <18 缺 Response/Headers/ReadableStream/TransformStream 等 Web 全局,
|
|
39
|
+
// 上游适配层有 29 处裸用,跑起来必 ReferenceError;与其半途炸掉,不如入口给人话。
|
|
40
|
+
export const MIN_NODE_MAJOR = 18;
|
|
41
|
+
|
|
42
|
+
export function nodeMajor() {
|
|
43
|
+
return Number(String(process.versions?.node || "0").split(".")[0]) || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertMinNode({ min = MIN_NODE_MAJOR, label = "mslxdff" } = {}) {
|
|
47
|
+
const major = nodeMajor();
|
|
48
|
+
if (major >= min) return true;
|
|
49
|
+
console.error(`[运行环境不满足] ${label} 需要 Node ${min}+,当前 v${process.versions.node}。`);
|
|
50
|
+
console.error("原因:Node <18 没有 Response/Headers/ReadableStream 等全局对象,上游转发必然失败。");
|
|
51
|
+
console.error("升级方式:nvm install 20 && nvm use 20,或到 https://nodejs.org/ 装 LTS(推荐 20/22)。");
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -117,8 +117,22 @@ export function createCapabilitiesService({
|
|
|
117
117
|
function providers() {
|
|
118
118
|
return [...capsIndex.keys()].sort();
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// readyWarm:仅用内存/磁盘缓存热身,绝不网络请求。/models 富化用它避免冷启动阻塞;
|
|
121
|
+
// 冷缓存时后台拉新(不 await),下次请求即富化。
|
|
122
|
+
async function readyWarm() {
|
|
123
|
+
const t = now();
|
|
124
|
+
if (raw && t - loadedAt < ttl) return true;
|
|
125
|
+
const disk = readCache();
|
|
126
|
+
if (disk) {
|
|
127
|
+
raw = disk;
|
|
128
|
+
capsIndex = buildIndex(disk);
|
|
129
|
+
loadedAt = t;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
ready().catch(() => {}); // 后台拉新,失败静默(/models 原样降级)
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return { ready, readyWarm, get, list, providers, npmIndex: () => new Map(npmIndex) };
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
// 模块级单例:HTTP handler 懒加载,测试 _reset 后注入
|
|
@@ -128,7 +142,6 @@ export function globalCapabilities() {
|
|
|
128
142
|
return _global;
|
|
129
143
|
}
|
|
130
144
|
export function _resetGlobalCapabilities() { _global = null; }
|
|
131
|
-
|
|
132
145
|
// 缓存落盘位置:MSLXDFF_MODELS_DEV_CACHE 覆盖 > ~/.config/mslxdff/models-dev.json(与 state 同目录)
|
|
133
146
|
function defaultCacheFile() {
|
|
134
147
|
const override = process.env.MSLXDFF_MODELS_DEV_CACHE;
|