mslxdff 0.1.236 → 0.1.237
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/bin/mslxdff.js +3 -3
- package/package.json +1 -1
- package/src/auto.js +264 -264
- package/src/autostart.js +62 -15
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +155 -155
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +282 -282
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +293 -293
- package/src/peers.js +389 -389
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +292 -292
- package/src/providers/qoder/index.js +311 -311
- package/src/providers/qoder/sse.js +233 -233
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/const.js +219 -219
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/chat.js +298 -298
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +158 -158
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -364
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/runtime/server-lifecycle.js +34 -10
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +355 -355
- package/docs/cli_help_mini.md +0 -149
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/cli/commands/sync.js
CHANGED
|
@@ -1,310 +1,310 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
|
|
3
|
-
import { getPreferredModel as getPref } from "../../auto.js";
|
|
4
|
-
import { normalizeModel } from "../../reasoning.js";
|
|
5
|
-
import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
|
|
6
|
-
import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
|
|
7
|
-
import { syncToCodex, codexConfigPath } from "../../sync-codex.js";
|
|
8
|
-
import { syncToClaude, claudeSettingsPath } from "../../sync-claude.js";
|
|
9
|
-
import { createModelsService } from "../../models.js";
|
|
10
|
-
import { createUpstreamClient } from "../../upstream.js";
|
|
11
|
-
import { logDir } from "../../logs.js";
|
|
12
|
-
|
|
13
|
-
// 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
|
|
14
|
-
// picks 为空(=不筛选)时返回 null,sync 侧一个不动。
|
|
15
|
-
function pruneKeep() {
|
|
16
|
-
const picks = loadModelPicks();
|
|
17
|
-
return picks.length ? picks : null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function handleSetto(args) {
|
|
21
|
-
if (!(args.includes("-setto") || args.includes("--setto"))) return false;
|
|
22
|
-
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
23
|
-
const target = args[idx + 1];
|
|
24
|
-
if (!["workbuddy", "opencode", "chatgpt", "codex", "claude", "claude-desktop", "claudedesktop"].includes(target)) {
|
|
25
|
-
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias,--1m 缺省 auto=有据才打 1M 标,详见 ADR-0056)");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
if (target === "claude-desktop" || target === "claudedesktop") {
|
|
29
|
-
// Claude Desktop on 3P(ADR-0048):写 profile + 角色槽 alias。sync.js 只做转发——
|
|
30
|
-
// 本文件已近 20KB 体积门,实现放 src/cli/commands/claude-desktop.js。
|
|
31
|
-
const { handleClaudeDesktopSetto } = await import("./claude-desktop.js");
|
|
32
|
-
return handleClaudeDesktopSetto(args, idx);
|
|
33
|
-
}
|
|
34
|
-
if (target === "chatgpt" || target === "codex") {
|
|
35
|
-
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
|
|
36
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
37
|
-
let id;
|
|
38
|
-
if (raw) {
|
|
39
|
-
if (raw === "auto" || !raw) {
|
|
40
|
-
console.error("modelId 不能为 auto 或空");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
const norm = normalizeModel(raw);
|
|
44
|
-
if (!norm) {
|
|
45
|
-
console.error("modelId 不能为空");
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
savePreferredModel(norm);
|
|
49
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
50
|
-
id = norm;
|
|
51
|
-
} else {
|
|
52
|
-
id = loadPreferredModel() || getPref();
|
|
53
|
-
if (!id) {
|
|
54
|
-
console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
const persisted = getPort();
|
|
60
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
61
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
62
|
-
const file = codexConfigPath();
|
|
63
|
-
const r = syncToCodex({ id, port, file });
|
|
64
|
-
console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
|
|
65
|
-
console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
|
|
66
|
-
console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error(`failed to sync to codex: ${String(err?.message || err)}`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
process.exit(0);
|
|
72
|
-
}
|
|
73
|
-
if (target === "claude") {
|
|
74
|
-
// Claude Code 用户设置 ~/.claude/settings.json:走本机 /v1/messages 外壳(ADR-0047)
|
|
75
|
-
// 参数校验一律先于任何写盘(含 savePreferredModel):校验不过不得碰 state.json 与 settings.json
|
|
76
|
-
// --behaves-as <已知 claude-* id> 覆盖能力锚;`--behaves-as ""` 显式关掉(关掉后本机 Claude Code 可能对未知 id 拒跑)
|
|
77
|
-
const baIdx = args.findIndex((x) => x === "--behaves-as");
|
|
78
|
-
if (baIdx >= 0 && args[baIdx + 1] === undefined) {
|
|
79
|
-
console.error("--behaves-as 需要取值:给一个已知 claude-* id,或显式传空串关闭(--behaves-as \"\")");
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
const behavesAs = baIdx >= 0 ? String(args[baIdx + 1]) : undefined;
|
|
83
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") && args[idx + 2] !== "all" ? String(args[idx + 2]).trim() : null;
|
|
84
|
-
const picksAll = loadModelPicks();
|
|
85
|
-
let id;
|
|
86
|
-
let batch = false;
|
|
87
|
-
if (raw) {
|
|
88
|
-
if (raw === "auto" || !normalizeModel(raw)) {
|
|
89
|
-
console.error("modelId 不能为 auto 或空");
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
const norm = normalizeModel(raw);
|
|
93
|
-
if (!norm) {
|
|
94
|
-
console.error("modelId 不能为空");
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
if (picksAll.length && !picksAll.includes(norm)) {
|
|
98
|
-
console.log(` ⚠ "${norm}" 不在勾选集 modelPicks 内 —— 可能是拼错或已失效;仍按你显式指定的写入`);
|
|
99
|
-
}
|
|
100
|
-
savePreferredModel(norm);
|
|
101
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
102
|
-
id = norm;
|
|
103
|
-
} else {
|
|
104
|
-
// 不带参数(含 --all / 裸 all)= 写入全部勾选模型:modelPicker.options = modelPicks 全集,
|
|
105
|
-
// 顶层 model 也只在勾选集内取。**不再兜底 preferredModel** —— 它可能是一个已从池子消失的 id
|
|
106
|
-
// (真实踩坑:preferredModel=mimo-v2.5-free 被写进 Claude Code 后,每次请求只会得到 502)。
|
|
107
|
-
batch = true;
|
|
108
|
-
if (!picksAll.length) {
|
|
109
|
-
console.error("modelPicks 勾选集为空 → 无可写入的模型;先跑 mslxdff -model pick 勾选,或直接 mslxdff -setto claude <modelId>");
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
const pref = normalizeModel(loadPreferredModel() || getPref() || "");
|
|
113
|
-
if (pref && picksAll.includes(pref)) {
|
|
114
|
-
id = pref;
|
|
115
|
-
} else {
|
|
116
|
-
if (pref) console.log(` ⚠ 网关首选模型 "${pref}" 不在勾选集内(可能已失效)→ claude 默认改用勾选集首个 "${picksAll[0]}"`);
|
|
117
|
-
id = normalizeModel(picksAll[0]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const { token } = await loadToken();
|
|
122
|
-
const persisted = getPort();
|
|
123
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
124
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
125
|
-
// options 由本命令全权管理:默认/`--all` 用勾选集整体替换,显式单模型则只留一条。
|
|
126
|
-
const picks = batch ? picksAll : null;
|
|
127
|
-
const file = claudeSettingsPath();
|
|
128
|
-
const r = syncToClaude({ id, token, port, picks, file, behavesAs });
|
|
129
|
-
console.log(`synced to claude: ${r.action} "${r.id}" @ ${r.file}${r.changed ? "" : "(已是目标状态,未改字节)"}`);
|
|
130
|
-
if (r.backup) { console.log(` backup: ${r.backup}`); console.log(` 还原: Copy-Item "${r.backup}" "${r.file}"`); }
|
|
131
|
-
else console.log(` backup: 未生成(本次无变化,或同目录已有更早的 settings.pre-mslxdff.json —— 那份是首次接管前的原文)`);
|
|
132
|
-
if (r.tmpLeftover) console.error(` ⚠ 临时文件清理失败:${r.tmpLeftover}(内含明文 token,请手动删除)`);
|
|
133
|
-
console.log(` url: http://127.0.0.1:${port} (不带 /v1 —— Claude Code 自己拼 /v1/messages)`);
|
|
134
|
-
console.log(` modelPicker: ${r.rows} 行${batch ? "(勾选集全集)" : "(单模型)"}`);
|
|
135
|
-
console.log(` 冒烟: curl -H "Authorization: Bearer $(mslxdff -showtoken)" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"${r.id}","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}' http://127.0.0.1:${port}/v1/messages (max_tokens 别给太小:思考型模型会把额度全花在 reasoning 上而吐空正文轮)`);
|
|
136
|
-
console.log(` ⚠ env 与 model 均为 Claude Code 启动时读取 → 重启 claude 生效;token 轮换后需重跑本命令`);
|
|
137
|
-
console.log(` ℹ 客户端不认识的模型 id 一律按 200K 窗口假设(带 \`[1m]\` 后缀才按 1M),需要更大窗口另设 env.CLAUDE_CODE_MAX_CONTEXT_TOKENS`);
|
|
138
|
-
} catch (err) {
|
|
139
|
-
console.error(`failed to sync to claude: ${String(err?.message || err)}`);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
process.exit(0);
|
|
143
|
-
}
|
|
144
|
-
if (target === "opencode") {
|
|
145
|
-
const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
|
|
146
|
-
if (wantsAll) {
|
|
147
|
-
const picks = loadModelPicks();
|
|
148
|
-
const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
|
|
149
|
-
if (!list.length) {
|
|
150
|
-
console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
const { token } = await loadToken();
|
|
155
|
-
const persisted = getPort();
|
|
156
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
157
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
158
|
-
const file = opencodeConfigPath();
|
|
159
|
-
let inserted = 0, updated = 0, prunedTotal = 0;
|
|
160
|
-
for (const rawId of list) {
|
|
161
|
-
const norm = normalizeModel(rawId);
|
|
162
|
-
if (!norm || norm === "auto") continue;
|
|
163
|
-
// 首次循环也同步 preferred(保持 daemon 热重载语义)
|
|
164
|
-
if (rawId === list[0]) savePreferredModel(norm);
|
|
165
|
-
const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
166
|
-
if (r.action === "inserted") inserted++; else updated++;
|
|
167
|
-
prunedTotal += r.pruned || 0;
|
|
168
|
-
console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
|
|
169
|
-
if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
|
|
170
|
-
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
|
|
171
|
-
}
|
|
172
|
-
console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
|
|
173
|
-
if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
174
|
-
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
175
|
-
console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
|
|
176
|
-
console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
process.exit(0);
|
|
182
|
-
}
|
|
183
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
184
|
-
let id;
|
|
185
|
-
if (raw) {
|
|
186
|
-
if (raw === "auto" || !raw) {
|
|
187
|
-
console.error("modelId 不能为 auto 或空");
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
const norm = normalizeModel(raw);
|
|
191
|
-
if (!norm) {
|
|
192
|
-
console.error("modelId 不能为空");
|
|
193
|
-
process.exit(1);
|
|
194
|
-
}
|
|
195
|
-
savePreferredModel(norm);
|
|
196
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
197
|
-
id = norm;
|
|
198
|
-
} else {
|
|
199
|
-
const pref = loadPreferredModel() || getPref();
|
|
200
|
-
if (!pref) {
|
|
201
|
-
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
const norm = normalizeModel(pref);
|
|
205
|
-
if (!norm) {
|
|
206
|
-
console.error("modelId 不能为空");
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
209
|
-
id = norm;
|
|
210
|
-
}
|
|
211
|
-
// 可选:校验是否在 free 列表
|
|
212
|
-
try {
|
|
213
|
-
const cacheFile = join(logDir(), "models.json");
|
|
214
|
-
const models = createModelsService({
|
|
215
|
-
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
216
|
-
headers: createUpstreamClient({}).headers,
|
|
217
|
-
refreshMs: 0,
|
|
218
|
-
cacheFile,
|
|
219
|
-
});
|
|
220
|
-
const fresh = await Promise.race([
|
|
221
|
-
models.get(),
|
|
222
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
223
|
-
]);
|
|
224
|
-
if (fresh?.data?.length) {
|
|
225
|
-
const ids = fresh.data.map((m) => m.id);
|
|
226
|
-
// 对 slash 形态也做 dash 兼容检查
|
|
227
|
-
const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
|
|
228
|
-
if (!ids.includes(id) && !ids.includes(dashId)) {
|
|
229
|
-
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} catch {}
|
|
233
|
-
try {
|
|
234
|
-
const { token } = await loadToken();
|
|
235
|
-
const persisted = getPort();
|
|
236
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
237
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
238
|
-
const file = opencodeConfigPath();
|
|
239
|
-
const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
240
|
-
console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
|
|
241
|
-
if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
|
|
242
|
-
else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
|
|
243
|
-
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
|
|
244
|
-
if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
|
|
245
|
-
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
246
|
-
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
247
|
-
console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
|
|
248
|
-
} catch (err) {
|
|
249
|
-
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
250
|
-
process.exit(1);
|
|
251
|
-
}
|
|
252
|
-
process.exit(0);
|
|
253
|
-
}
|
|
254
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
255
|
-
let id;
|
|
256
|
-
if (raw) {
|
|
257
|
-
if (raw === "auto" || !raw) {
|
|
258
|
-
console.error("modelId 不能为 auto 或空");
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
261
|
-
const norm = normalizeModel(raw);
|
|
262
|
-
if (!norm) {
|
|
263
|
-
console.error("modelId 不能为空");
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
savePreferredModel(norm);
|
|
267
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
268
|
-
id = norm;
|
|
269
|
-
} else {
|
|
270
|
-
id = loadPreferredModel() || getPref();
|
|
271
|
-
if (!id) {
|
|
272
|
-
console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const cacheFile = join(logDir(), "models.json");
|
|
278
|
-
const models = createModelsService({
|
|
279
|
-
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
280
|
-
headers: createUpstreamClient({}).headers,
|
|
281
|
-
refreshMs: 0,
|
|
282
|
-
cacheFile,
|
|
283
|
-
});
|
|
284
|
-
const fresh = await Promise.race([
|
|
285
|
-
models.get(),
|
|
286
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
287
|
-
]);
|
|
288
|
-
if (fresh?.data?.length) {
|
|
289
|
-
const ids = fresh.data.map((m) => m.id);
|
|
290
|
-
if (!ids.includes(id)) {
|
|
291
|
-
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
} catch {}
|
|
295
|
-
try {
|
|
296
|
-
const { token } = await loadToken();
|
|
297
|
-
const persisted = getPort();
|
|
298
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
299
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
300
|
-
const file = workbuddyModelsPath();
|
|
301
|
-
const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
|
|
302
|
-
console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
|
|
303
|
-
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
|
|
304
|
-
console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
|
|
307
|
-
process.exit(1);
|
|
308
|
-
}
|
|
309
|
-
process.exit(0);
|
|
310
|
-
}
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
|
|
3
|
+
import { getPreferredModel as getPref } from "../../auto.js";
|
|
4
|
+
import { normalizeModel } from "../../reasoning.js";
|
|
5
|
+
import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
|
|
6
|
+
import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
|
|
7
|
+
import { syncToCodex, codexConfigPath } from "../../sync-codex.js";
|
|
8
|
+
import { syncToClaude, claudeSettingsPath } from "../../sync-claude.js";
|
|
9
|
+
import { createModelsService } from "../../models.js";
|
|
10
|
+
import { createUpstreamClient } from "../../upstream.js";
|
|
11
|
+
import { logDir } from "../../logs.js";
|
|
12
|
+
|
|
13
|
+
// 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
|
|
14
|
+
// picks 为空(=不筛选)时返回 null,sync 侧一个不动。
|
|
15
|
+
function pruneKeep() {
|
|
16
|
+
const picks = loadModelPicks();
|
|
17
|
+
return picks.length ? picks : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleSetto(args) {
|
|
21
|
+
if (!(args.includes("-setto") || args.includes("--setto"))) return false;
|
|
22
|
+
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
23
|
+
const target = args[idx + 1];
|
|
24
|
+
if (!["workbuddy", "opencode", "chatgpt", "codex", "claude", "claude-desktop", "claudedesktop"].includes(target)) {
|
|
25
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias,--1m 缺省 auto=有据才打 1M 标,详见 ADR-0056)");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (target === "claude-desktop" || target === "claudedesktop") {
|
|
29
|
+
// Claude Desktop on 3P(ADR-0048):写 profile + 角色槽 alias。sync.js 只做转发——
|
|
30
|
+
// 本文件已近 20KB 体积门,实现放 src/cli/commands/claude-desktop.js。
|
|
31
|
+
const { handleClaudeDesktopSetto } = await import("./claude-desktop.js");
|
|
32
|
+
return handleClaudeDesktopSetto(args, idx);
|
|
33
|
+
}
|
|
34
|
+
if (target === "chatgpt" || target === "codex") {
|
|
35
|
+
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
|
|
36
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
37
|
+
let id;
|
|
38
|
+
if (raw) {
|
|
39
|
+
if (raw === "auto" || !raw) {
|
|
40
|
+
console.error("modelId 不能为 auto 或空");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const norm = normalizeModel(raw);
|
|
44
|
+
if (!norm) {
|
|
45
|
+
console.error("modelId 不能为空");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
savePreferredModel(norm);
|
|
49
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
50
|
+
id = norm;
|
|
51
|
+
} else {
|
|
52
|
+
id = loadPreferredModel() || getPref();
|
|
53
|
+
if (!id) {
|
|
54
|
+
console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const persisted = getPort();
|
|
60
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
61
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
62
|
+
const file = codexConfigPath();
|
|
63
|
+
const r = syncToCodex({ id, port, file });
|
|
64
|
+
console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
|
|
65
|
+
console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
|
|
66
|
+
console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`failed to sync to codex: ${String(err?.message || err)}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
if (target === "claude") {
|
|
74
|
+
// Claude Code 用户设置 ~/.claude/settings.json:走本机 /v1/messages 外壳(ADR-0047)
|
|
75
|
+
// 参数校验一律先于任何写盘(含 savePreferredModel):校验不过不得碰 state.json 与 settings.json
|
|
76
|
+
// --behaves-as <已知 claude-* id> 覆盖能力锚;`--behaves-as ""` 显式关掉(关掉后本机 Claude Code 可能对未知 id 拒跑)
|
|
77
|
+
const baIdx = args.findIndex((x) => x === "--behaves-as");
|
|
78
|
+
if (baIdx >= 0 && args[baIdx + 1] === undefined) {
|
|
79
|
+
console.error("--behaves-as 需要取值:给一个已知 claude-* id,或显式传空串关闭(--behaves-as \"\")");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const behavesAs = baIdx >= 0 ? String(args[baIdx + 1]) : undefined;
|
|
83
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") && args[idx + 2] !== "all" ? String(args[idx + 2]).trim() : null;
|
|
84
|
+
const picksAll = loadModelPicks();
|
|
85
|
+
let id;
|
|
86
|
+
let batch = false;
|
|
87
|
+
if (raw) {
|
|
88
|
+
if (raw === "auto" || !normalizeModel(raw)) {
|
|
89
|
+
console.error("modelId 不能为 auto 或空");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const norm = normalizeModel(raw);
|
|
93
|
+
if (!norm) {
|
|
94
|
+
console.error("modelId 不能为空");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (picksAll.length && !picksAll.includes(norm)) {
|
|
98
|
+
console.log(` ⚠ "${norm}" 不在勾选集 modelPicks 内 —— 可能是拼错或已失效;仍按你显式指定的写入`);
|
|
99
|
+
}
|
|
100
|
+
savePreferredModel(norm);
|
|
101
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
102
|
+
id = norm;
|
|
103
|
+
} else {
|
|
104
|
+
// 不带参数(含 --all / 裸 all)= 写入全部勾选模型:modelPicker.options = modelPicks 全集,
|
|
105
|
+
// 顶层 model 也只在勾选集内取。**不再兜底 preferredModel** —— 它可能是一个已从池子消失的 id
|
|
106
|
+
// (真实踩坑:preferredModel=mimo-v2.5-free 被写进 Claude Code 后,每次请求只会得到 502)。
|
|
107
|
+
batch = true;
|
|
108
|
+
if (!picksAll.length) {
|
|
109
|
+
console.error("modelPicks 勾选集为空 → 无可写入的模型;先跑 mslxdff -model pick 勾选,或直接 mslxdff -setto claude <modelId>");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const pref = normalizeModel(loadPreferredModel() || getPref() || "");
|
|
113
|
+
if (pref && picksAll.includes(pref)) {
|
|
114
|
+
id = pref;
|
|
115
|
+
} else {
|
|
116
|
+
if (pref) console.log(` ⚠ 网关首选模型 "${pref}" 不在勾选集内(可能已失效)→ claude 默认改用勾选集首个 "${picksAll[0]}"`);
|
|
117
|
+
id = normalizeModel(picksAll[0]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const { token } = await loadToken();
|
|
122
|
+
const persisted = getPort();
|
|
123
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
124
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
125
|
+
// options 由本命令全权管理:默认/`--all` 用勾选集整体替换,显式单模型则只留一条。
|
|
126
|
+
const picks = batch ? picksAll : null;
|
|
127
|
+
const file = claudeSettingsPath();
|
|
128
|
+
const r = syncToClaude({ id, token, port, picks, file, behavesAs });
|
|
129
|
+
console.log(`synced to claude: ${r.action} "${r.id}" @ ${r.file}${r.changed ? "" : "(已是目标状态,未改字节)"}`);
|
|
130
|
+
if (r.backup) { console.log(` backup: ${r.backup}`); console.log(` 还原: Copy-Item "${r.backup}" "${r.file}"`); }
|
|
131
|
+
else console.log(` backup: 未生成(本次无变化,或同目录已有更早的 settings.pre-mslxdff.json —— 那份是首次接管前的原文)`);
|
|
132
|
+
if (r.tmpLeftover) console.error(` ⚠ 临时文件清理失败:${r.tmpLeftover}(内含明文 token,请手动删除)`);
|
|
133
|
+
console.log(` url: http://127.0.0.1:${port} (不带 /v1 —— Claude Code 自己拼 /v1/messages)`);
|
|
134
|
+
console.log(` modelPicker: ${r.rows} 行${batch ? "(勾选集全集)" : "(单模型)"}`);
|
|
135
|
+
console.log(` 冒烟: curl -H "Authorization: Bearer $(mslxdff -showtoken)" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"${r.id}","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}' http://127.0.0.1:${port}/v1/messages (max_tokens 别给太小:思考型模型会把额度全花在 reasoning 上而吐空正文轮)`);
|
|
136
|
+
console.log(` ⚠ env 与 model 均为 Claude Code 启动时读取 → 重启 claude 生效;token 轮换后需重跑本命令`);
|
|
137
|
+
console.log(` ℹ 客户端不认识的模型 id 一律按 200K 窗口假设(带 \`[1m]\` 后缀才按 1M),需要更大窗口另设 env.CLAUDE_CODE_MAX_CONTEXT_TOKENS`);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(`failed to sync to claude: ${String(err?.message || err)}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
if (target === "opencode") {
|
|
145
|
+
const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
|
|
146
|
+
if (wantsAll) {
|
|
147
|
+
const picks = loadModelPicks();
|
|
148
|
+
const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
|
|
149
|
+
if (!list.length) {
|
|
150
|
+
console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const { token } = await loadToken();
|
|
155
|
+
const persisted = getPort();
|
|
156
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
157
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
158
|
+
const file = opencodeConfigPath();
|
|
159
|
+
let inserted = 0, updated = 0, prunedTotal = 0;
|
|
160
|
+
for (const rawId of list) {
|
|
161
|
+
const norm = normalizeModel(rawId);
|
|
162
|
+
if (!norm || norm === "auto") continue;
|
|
163
|
+
// 首次循环也同步 preferred(保持 daemon 热重载语义)
|
|
164
|
+
if (rawId === list[0]) savePreferredModel(norm);
|
|
165
|
+
const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
166
|
+
if (r.action === "inserted") inserted++; else updated++;
|
|
167
|
+
prunedTotal += r.pruned || 0;
|
|
168
|
+
console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
|
|
169
|
+
if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
|
|
170
|
+
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
|
|
171
|
+
}
|
|
172
|
+
console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
|
|
173
|
+
if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
174
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
175
|
+
console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
|
|
176
|
+
console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
184
|
+
let id;
|
|
185
|
+
if (raw) {
|
|
186
|
+
if (raw === "auto" || !raw) {
|
|
187
|
+
console.error("modelId 不能为 auto 或空");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const norm = normalizeModel(raw);
|
|
191
|
+
if (!norm) {
|
|
192
|
+
console.error("modelId 不能为空");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
savePreferredModel(norm);
|
|
196
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
197
|
+
id = norm;
|
|
198
|
+
} else {
|
|
199
|
+
const pref = loadPreferredModel() || getPref();
|
|
200
|
+
if (!pref) {
|
|
201
|
+
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const norm = normalizeModel(pref);
|
|
205
|
+
if (!norm) {
|
|
206
|
+
console.error("modelId 不能为空");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
id = norm;
|
|
210
|
+
}
|
|
211
|
+
// 可选:校验是否在 free 列表
|
|
212
|
+
try {
|
|
213
|
+
const cacheFile = join(logDir(), "models.json");
|
|
214
|
+
const models = createModelsService({
|
|
215
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
216
|
+
headers: createUpstreamClient({}).headers,
|
|
217
|
+
refreshMs: 0,
|
|
218
|
+
cacheFile,
|
|
219
|
+
});
|
|
220
|
+
const fresh = await Promise.race([
|
|
221
|
+
models.get(),
|
|
222
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
223
|
+
]);
|
|
224
|
+
if (fresh?.data?.length) {
|
|
225
|
+
const ids = fresh.data.map((m) => m.id);
|
|
226
|
+
// 对 slash 形态也做 dash 兼容检查
|
|
227
|
+
const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
|
|
228
|
+
if (!ids.includes(id) && !ids.includes(dashId)) {
|
|
229
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
try {
|
|
234
|
+
const { token } = await loadToken();
|
|
235
|
+
const persisted = getPort();
|
|
236
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
237
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
238
|
+
const file = opencodeConfigPath();
|
|
239
|
+
const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
240
|
+
console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
|
|
241
|
+
if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
|
|
242
|
+
else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
|
|
243
|
+
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
|
|
244
|
+
if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
|
|
245
|
+
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
246
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
247
|
+
console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
255
|
+
let id;
|
|
256
|
+
if (raw) {
|
|
257
|
+
if (raw === "auto" || !raw) {
|
|
258
|
+
console.error("modelId 不能为 auto 或空");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const norm = normalizeModel(raw);
|
|
262
|
+
if (!norm) {
|
|
263
|
+
console.error("modelId 不能为空");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
savePreferredModel(norm);
|
|
267
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
268
|
+
id = norm;
|
|
269
|
+
} else {
|
|
270
|
+
id = loadPreferredModel() || getPref();
|
|
271
|
+
if (!id) {
|
|
272
|
+
console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const cacheFile = join(logDir(), "models.json");
|
|
278
|
+
const models = createModelsService({
|
|
279
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
280
|
+
headers: createUpstreamClient({}).headers,
|
|
281
|
+
refreshMs: 0,
|
|
282
|
+
cacheFile,
|
|
283
|
+
});
|
|
284
|
+
const fresh = await Promise.race([
|
|
285
|
+
models.get(),
|
|
286
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
287
|
+
]);
|
|
288
|
+
if (fresh?.data?.length) {
|
|
289
|
+
const ids = fresh.data.map((m) => m.id);
|
|
290
|
+
if (!ids.includes(id)) {
|
|
291
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
try {
|
|
296
|
+
const { token } = await loadToken();
|
|
297
|
+
const persisted = getPort();
|
|
298
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
299
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
300
|
+
const file = workbuddyModelsPath();
|
|
301
|
+
const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
|
|
302
|
+
console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
|
|
303
|
+
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
|
|
304
|
+
console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|