mslxdff 0.1.136 → 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/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/workbuddy.js +14 -21
- package/src/cli/index.js +3 -0
- package/src/compat.js +23 -5
- package/src/model-capabilities/merge.js +2 -2
- package/src/providers/classify.js +1 -1
- 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/model-id.js +4 -4
- package/src/providers/registry.js +1 -1
- package/src/providers/share-keys.js +6 -1
- 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/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 +4 -4
- package/src/sync-workbuddy.js +1 -1
- package/src/upstream-engine/index.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)) {
|
|
@@ -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,自然支持多账号追加。
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 全程 best-effort:目录不可用 → 原样返回,绝不让 /models 因富化失败挂掉。
|
|
5
5
|
import { globalCapabilities } from "./index.js";
|
|
6
6
|
|
|
7
|
-
// "
|
|
7
|
+
// "cline/deepseek/deepseek-v4-flash" → { provider: "cline", raw: "deepseek/deepseek-v4-flash" };裸 id 归 opencode
|
|
8
8
|
export function splitModelId(id) {
|
|
9
9
|
const s = String(id || "").trim();
|
|
10
10
|
const i = s.indexOf("/");
|
|
@@ -55,7 +55,7 @@ export function capsPayloadFor(caps, raw) {
|
|
|
55
55
|
return payload;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// 单条 id 的目录匹配:精确裸 id → 剥 -free 后缀 → 二级厂商前缀(
|
|
58
|
+
// 单条 id 的目录匹配:精确裸 id → 剥 -free 后缀 → 二级厂商前缀(cline/deepseek/x → deepseek/x)
|
|
59
59
|
function lookupCaps(svc, provider, raw) {
|
|
60
60
|
return (
|
|
61
61
|
svc.get(provider, raw) ||
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// latency-compare:key/token 类,direct vs link+remote 比延迟
|
|
5
5
|
export function classifyProvider(id) {
|
|
6
6
|
const s = String(id || "").trim().toLowerCase();
|
|
7
|
-
if (s === "workbuddy" || s === "cline"
|
|
7
|
+
if (s === "workbuddy" || s === "cline") return "local-only";
|
|
8
8
|
if (s === "opencode") return "quota-pool";
|
|
9
9
|
return "latency-compare";
|
|
10
10
|
}
|
|
@@ -120,7 +120,7 @@ export function createAuthPool({
|
|
|
120
120
|
if (account.dead) throw new Error("invalid_grant");
|
|
121
121
|
if (account.cooldownUntil > now) throw new Error("account_cooldown");
|
|
122
122
|
if (account.accessToken && now < account.expiry) return account.accessToken;
|
|
123
|
-
// base 可能已含 /api/v1
|
|
123
|
+
// base 可能已含 /api/v1(老 clinebot 配置就是 …/api/v1),直接拼会 double 成
|
|
124
124
|
// …/api/v1/api/v1/auth/refresh → 上游回 401 Unauthorized(版本/重认证提示),绝不能当 token 死。
|
|
125
125
|
const baseNoV1 = resolvedBase.replace(/\/api\/v1\/?$/, "");
|
|
126
126
|
const url = joinUrl(baseNoV1, "/api/v1/auth/refresh");
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { joinUrl, sleep } from "../base.js";
|
|
2
2
|
import { clineHeaders } from "./headers.js";
|
|
3
3
|
import { createTransport } from "../../transport/index.js";
|
|
4
|
+
import { normalizeProviderId } from "../model-id.js";
|
|
4
5
|
import { createSdkDispatch } from "../../upstream-engine/sdk/dispatch.js";
|
|
5
6
|
|
|
6
7
|
function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
|
|
7
8
|
|
|
8
|
-
// stripProviderPrefix
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
9
|
+
// stripProviderPrefix 只剥「本供应商」前缀(cline/<裸 id> → 裸 id),非本前缀(含上游自带多段 id
|
|
10
|
+
// 如 meta/vendor/x)原样透传,不误削首段(旧实现按段数剥曾是潜在 bug)。
|
|
11
|
+
function stripProviderPrefix(model, providerId) {
|
|
12
|
+
const s = String(model || "");
|
|
13
|
+
const i = s.indexOf("/");
|
|
14
|
+
if (i <= 0) return s;
|
|
15
|
+
const low = (v) => normalizeProviderId(String(v || "").toLowerCase());
|
|
16
|
+
return low(s.slice(0, i)) === low(providerId) ? s.slice(i + 1) : s;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
18
|
function unwrapData(obj) {
|
|
19
19
|
if (obj && obj.data && typeof obj.data === "object") {
|
|
20
20
|
const d = obj.data;
|
|
@@ -169,7 +169,7 @@ export function createChatService({
|
|
|
169
169
|
const model = body?.model || "deepseek/deepseek-v4-flash";
|
|
170
170
|
const sessionId = genSessionId();
|
|
171
171
|
const isStream = body?.stream === true;
|
|
172
|
-
const upstreamModel = stripProviderPrefix(model);
|
|
172
|
+
const upstreamModel = stripProviderPrefix(model, id);
|
|
173
173
|
// token 口径双写:对标官方 withMaxCompletionTokensForReasoningModels——
|
|
174
174
|
// cline 上游默认 reasoning_effort high,推理模型认 max_completion_tokens,
|
|
175
175
|
// 只发 max_tokens 会被部分通道拒;双写兼容最稳。
|
|
@@ -182,7 +182,10 @@ export function createChatService({
|
|
|
182
182
|
reasoning_effort: body?.reasoning_effort || body?.reasoningEffort || "high",
|
|
183
183
|
messages: body?.messages || [],
|
|
184
184
|
};
|
|
185
|
-
|
|
185
|
+
// 部分免费通道(deepseek 家族,含 cline-free/deepseek-*)原生非流式不可靠
|
|
186
|
+
//(500 empty response):这类模型在内部走 stream+聚合成 JSON,对外仍按请求方
|
|
187
|
+
// stream 标志返回(true 直透,false 聚合)。其它模型完全尊重请求方,不强制。
|
|
188
|
+
const forceStream = !isStream && String(upstreamModel).toLowerCase().includes("deepseek");
|
|
186
189
|
if (isStream || forceStream) upstreamBody.stream = true;
|
|
187
190
|
for (const k of ["temperature", "top_p", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"]) {
|
|
188
191
|
if (body[k] !== undefined) upstreamBody[k] = body[k];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline 免费目录深模块:只认上游 `free` 数组(公开接口,无需鉴权、无凭据)。
|
|
3
|
+
* GET {bareHost}/api/v1/ai/cline/recommended-models → { recommended, free, clinePass, clineCloud }
|
|
4
|
+
* 供 cline/models.js(聚合目录 + 启动自检)与 `-provider cline free` CLI 共用;缓存由调用方持有。
|
|
5
|
+
*/
|
|
6
|
+
import { compatFetch } from "../../compat.js";
|
|
7
|
+
import { joinUrl } from "../base.js";
|
|
8
|
+
|
|
9
|
+
export const FREE_CATALOG_PATH = "/api/v1/ai/cline/recommended-models";
|
|
10
|
+
export const DEFAULT_CLINE_BASE = "https://api.cline.bot";
|
|
11
|
+
|
|
12
|
+
// 上游不可达时的内置兜底(2026-09-20 实测 5 个;只有 id 与短名,无凭据)
|
|
13
|
+
export const FALLBACK_FREE = Object.freeze([
|
|
14
|
+
{ id: "cline-free/deepseek-v4.1-flash", name: "deepseek-v4.1-flash" },
|
|
15
|
+
{ id: "cline-free/muse-spark-1.3-contributor", name: "muse-spark-1.3-contributor" },
|
|
16
|
+
{ id: "z-ai/glm-5.3-flash", name: "glm-5.3-flash" },
|
|
17
|
+
{ id: "cline-free/solar-pro4", name: "solar-pro4" },
|
|
18
|
+
{ id: "poolside/laguna-s-2.1:free", name: "laguna-s-2.1:free" },
|
|
19
|
+
]);
|
|
20
|
+
export const FALLBACK_FREE_IDS = FALLBACK_FREE.map((m) => m.id);
|
|
21
|
+
|
|
22
|
+
/** 目录 URL 归一:baseUrl 可能是裸 host 或带 /api/v1 的老 clinebot 形态;customPath 尊重用户自定义端点 */
|
|
23
|
+
export function catalogUrl(baseUrl, customPath) {
|
|
24
|
+
const raw = String(baseUrl || "").trim() || DEFAULT_CLINE_BASE;
|
|
25
|
+
const bare = raw.replace(/\/+$/, "").replace(/\/api\/v1\/?$/, "") || DEFAULT_CLINE_BASE;
|
|
26
|
+
const p = String(customPath || "").trim();
|
|
27
|
+
if (p && p !== "/models" && p !== FREE_CATALOG_PATH) return joinUrl(bare, p);
|
|
28
|
+
return joinUrl(bare, FREE_CATALOG_PATH);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 只取 json.free(对象 {id,name,...} 或裸字符串两种形态都容忍) */
|
|
32
|
+
function pickFree(json) {
|
|
33
|
+
const arr = Array.isArray(json?.free) ? json.free : [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const m of arr) {
|
|
36
|
+
if (typeof m === "string" && m.trim()) out.push({ id: m.trim() });
|
|
37
|
+
else if (m && typeof m.id === "string" && m.id.trim()) out.push({ ...m, id: m.id.trim(), name: String(m.name || "") });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 拉免费目录。
|
|
44
|
+
* @returns {Promise<{ok:boolean, status:number, url:string, models:object[], ids:string[], error?:string}>}
|
|
45
|
+
* ok=false 时 models/ids 为空(兜底由调用方决定);authorization 供私有/自定义目录可选透传。
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchFreeCatalog({ baseUrl, customPath, fetchImpl, dispatcher, authorization, timeoutMs = 15_000 } = {}) {
|
|
48
|
+
const url = catalogUrl(baseUrl, customPath);
|
|
49
|
+
const impl = fetchImpl || compatFetch;
|
|
50
|
+
const headers = { Accept: "application/json" };
|
|
51
|
+
if (authorization) headers.Authorization = authorization;
|
|
52
|
+
const opts = { headers };
|
|
53
|
+
if (dispatcher) opts.dispatcher = dispatcher;
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(new Error("cline free catalog timed out")), timeoutMs);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
opts.signal = controller.signal;
|
|
58
|
+
try {
|
|
59
|
+
const res = await impl(url, opts);
|
|
60
|
+
const status = Number(res?.status) || 0;
|
|
61
|
+
if (!res?.ok) return { ok: false, status, url, models: [], ids: [], error: `HTTP ${status}` };
|
|
62
|
+
const json = await res.json().catch(() => null);
|
|
63
|
+
const models = pickFree(json);
|
|
64
|
+
if (!models.length) return { ok: false, status, url, models: [], ids: [], error: "empty free list" };
|
|
65
|
+
return { ok: true, status, url, models, ids: models.map((m) => m.id) };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { ok: false, status: 0, url, models: [], ids: [], error: String(err?.message || err) };
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|