mslxdff 0.1.200 → 0.1.203
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/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +126 -126
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/translate.js +287 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/serial-trial.js +283 -283
- package/src/chat-pipeline/terminal-verdict.js +147 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +149 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +70 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +62 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -251
- package/src/plugins.js +107 -107
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -165
- package/src/providers/raccoon/credits.js +185 -175
- package/src/providers/raccoon/crypto.js +43 -0
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -87
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +83 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +365 -365
- package/src/routes/peers.js +23 -1
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/bootstrap.js +8 -0
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -0
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/server.js +7 -0
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -74
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/usage/record.js +106 -106
- package/src/usage/report.js +195 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -263
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- package/src/warp-pool/wireproxy.js +162 -162
- package/docs/cli_help_mini.md +0 -146
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/sync-claude.js
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync, copyFileSync, chmodSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* -setto claude:把本机网关写进 Claude Code 的用户设置文件。
|
|
7
|
-
* 键面依据 code.claude.com/docs/en/settings-reference(model / modelPicker / env 三节):
|
|
8
|
-
* - `ANTHROPIC_BASE_URL` **不带 /v1** —— 客户端自己拼 `/v1/messages`(参考仓库 README 写 /v1 是它的错)
|
|
9
|
-
* - `env.ANTHROPIC_MODEL` 必须删 —— 它压过顶层 `model` 键,留着会让设置失效
|
|
10
|
-
* - `CLAUDE_CODE_ATTRIBUTION_HEADER=0` —— 我们把 system 折叠成单条,归因块必须在客户端就不发
|
|
11
|
-
* - `modelPicker.options[].behavesAs` 实测必需 —— 见下方 DEFAULT_BEHAVES_AS 注释
|
|
12
|
-
* 只动上述键,其余键与键序原样保留;解析失败一律拒写(那是用户手写的 hooks/permissions)。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const OUR_ENV_KEYS = [
|
|
16
|
-
"ANTHROPIC_BASE_URL",
|
|
17
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
18
|
-
"ANTHROPIC_MODEL", // 只删不写
|
|
19
|
-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
|
20
|
-
"CLAUDE_CODE_ATTRIBUTION_HEADER",
|
|
21
|
-
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
// 本机 Claude Code 对「不认识且无 behavesAs 映射」的 model id 直接拒跑,连请求都不发
|
|
25
|
-
// (实测 2026-10-09:`"big-pickle" isn't described by this version's model catalog; …map it with
|
|
26
|
-
// behavesAs on a modelPicker row`,加 behavesAs 后 `claude -p` 正常走通工具往返)。
|
|
27
|
-
// behavesAs 只改客户端的**本地能力推断**(effort / auto-compact 口径),不改变发给上游的模型;
|
|
28
|
-
// 上游不认的字段由 messagesToChatBody 统一丢,所以锚一个通用已知 id 即可。`--behaves-as ""` 可关。
|
|
29
|
-
const DEFAULT_BEHAVES_AS = "claude-sonnet-5";
|
|
30
|
-
|
|
31
|
-
/** Claude Code 用户设置文件路径(`CLAUDE_CONFIG_DIR` 优先,与 Claude Code 自身口径一致)。 */
|
|
32
|
-
export function claudeSettingsPath() {
|
|
33
|
-
const dir = process.env.CLAUDE_CONFIG_DIR;
|
|
34
|
-
if (typeof dir === "string" && dir.trim()) return join(dir.trim(), "settings.json");
|
|
35
|
-
return join(os.homedir(), ".claude", "settings.json");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 纯函数:existing 对象 + 上下文 → 新对象。除我们的键外一律原样保留(含键序,JSON 键序随插入顺序)。 */
|
|
39
|
-
export function buildClaudeSettings(existing, ctx = {}) {
|
|
40
|
-
const out = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
41
|
-
const env = { ...(out.env && typeof out.env === "object" && !Array.isArray(out.env) ? out.env : {}) };
|
|
42
|
-
env.ANTHROPIC_BASE_URL = String(ctx.baseUrl || "");
|
|
43
|
-
env.ANTHROPIC_AUTH_TOKEN = String(ctx.token || "");
|
|
44
|
-
env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
45
|
-
env.CLAUDE_CODE_ATTRIBUTION_HEADER = "0";
|
|
46
|
-
env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
|
|
47
|
-
delete env.ANTHROPIC_MODEL;
|
|
48
|
-
out.env = env;
|
|
49
|
-
out.model = String(ctx.model || "");
|
|
50
|
-
const picks = Array.isArray(ctx.picks) && ctx.picks.length ? ctx.picks : [ctx.model];
|
|
51
|
-
// behavesAs 的含义是「该 id 不在客户端模型目录里,请按 X 的能力口径推断」。
|
|
52
|
-
// 客户端本来就认识的 claude-* id 不该被这么标:把 claude-opus 标成 behavesAs=claude-sonnet-5,
|
|
53
|
-
// 客户端就会按 sonnet 的 effort / auto-compact 口径去推断它。显式 --behaves-as 仍覆盖一切。
|
|
54
|
-
const override = ctx.behavesAs == null ? null : String(ctx.behavesAs);
|
|
55
|
-
const anchorOf = (m) => (override != null ? override : /^claude[-.]/i.test(String(m)) ? "" : DEFAULT_BEHAVES_AS);
|
|
56
|
-
out.modelPicker = {
|
|
57
|
-
options: picks.filter(Boolean).map((m) => {
|
|
58
|
-
const row = { model: String(m), label: String(m) };
|
|
59
|
-
const anchor = anchorOf(m);
|
|
60
|
-
if (anchor) row.behavesAs = anchor;
|
|
61
|
-
return row;
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 归一化用于幂等比较(忽略换行符与首尾空白差异)
|
|
68
|
-
function norm(text) {
|
|
69
|
-
return String(text || "").replace(/\r\n/g, "\n").trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 写盘。返回 { action, file, id, backup, changed, rows }。
|
|
74
|
-
* @param {{id?:string, token?:string, port?:number, picks?:string[], file?:string, baseUrl?:string, behavesAs?:string}} p
|
|
75
|
-
*/
|
|
76
|
-
export function syncToClaude(p = {}) {
|
|
77
|
-
const targetFile = p.file || claudeSettingsPath();
|
|
78
|
-
const model = String(p.id ?? p.model ?? "").trim();
|
|
79
|
-
if (!model) throw new Error("claude settings: model id required");
|
|
80
|
-
const token = String(p.token || "");
|
|
81
|
-
if (!token) throw new Error("claude settings: token required(先跑 mslxdff -showtoken)");
|
|
82
|
-
const baseUrl = p.baseUrl || `http://127.0.0.1:${Number(p.port) || 8989}`;
|
|
83
|
-
const picks = Array.isArray(p.picks) && p.picks.length ? p.picks : [model];
|
|
84
|
-
|
|
85
|
-
let existed = false;
|
|
86
|
-
let originalText = "";
|
|
87
|
-
try {
|
|
88
|
-
originalText = readFileSync(targetFile, "utf8");
|
|
89
|
-
existed = true;
|
|
90
|
-
} catch {
|
|
91
|
-
existed = false;
|
|
92
|
-
}
|
|
93
|
-
let originalObj = null;
|
|
94
|
-
if (existed) {
|
|
95
|
-
try {
|
|
96
|
-
originalObj = JSON.parse(originalText);
|
|
97
|
-
} catch {
|
|
98
|
-
throw new Error(`claude settings 解析失败,拒绝覆盖:${targetFile}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const next = buildClaudeSettings(originalObj, { baseUrl, token, model, picks, behavesAs: p.behavesAs });
|
|
103
|
-
const nextText = `${JSON.stringify(next, null, 2)}\n`;
|
|
104
|
-
if (existed && norm(originalText) === norm(nextText)) {
|
|
105
|
-
return { action: "updated", file: targetFile, id: model, backup: null, changed: false, rows: next.modelPicker.options.length };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 备份只保留「mslxdff 首次接管前」那一份:已存在就不覆盖(代价:用户后来手改的内容不进备份)
|
|
109
|
-
let backup = null;
|
|
110
|
-
if (existed) {
|
|
111
|
-
const backupFile = join(dirname(targetFile), "settings.pre-mslxdff.json");
|
|
112
|
-
if (!existsSync(backupFile)) {
|
|
113
|
-
try { copyFileSync(targetFile, backupFile); backup = backupFile; } catch { /* 复制失败不阻塞写入,但绝不谎报「已备份」 */ }
|
|
114
|
-
}
|
|
115
|
-
// 只在**本次真复制过**时返回路径(spec R6「打印备份路径(若本次生成)」);已有旧备份时返回 null,
|
|
116
|
-
// 由 CLI 换成「已存在更早的备份」提示 —— 免得用户拿首份旧备份去覆盖自己后来的手改。
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
mkdirSync(dirname(targetFile), { recursive: true });
|
|
120
|
-
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
121
|
-
// 凭据落地红线:文件里有明文 token → 0600(mode 只在新建时生效,故存量补 chmod)
|
|
122
|
-
let written = false;
|
|
123
|
-
let tmpLeftover = null;
|
|
124
|
-
// 半截 tmp 里带明文 token:unlink 也可能被占用(Windows 上正是 rename 失败的同因),
|
|
125
|
-
// 所以先擦内容再试一次;仍清不掉就把路径如实交出去,让 CLI 告警,绝不静默。
|
|
126
|
-
const sweepTmp = () => {
|
|
127
|
-
if (!existsSync(tmp)) return;
|
|
128
|
-
try { unlinkSync(tmp); return; } catch { /* 占用,走擦除 */ }
|
|
129
|
-
try { writeFileSync(tmp, "", { encoding: "utf8" }); } catch { /* 擦不动也继续试删 */ }
|
|
130
|
-
try { unlinkSync(tmp); } catch { tmpLeftover = tmp; }
|
|
131
|
-
};
|
|
132
|
-
try {
|
|
133
|
-
try { writeFileSync(tmp, nextText, { encoding: "utf8", mode: 0o600 }); } catch { writeFileSync(tmp, nextText, "utf8"); }
|
|
134
|
-
try { chmodSync(tmp, 0o600); } catch { /* Windows 无 posix mode,忽略 */ }
|
|
135
|
-
try {
|
|
136
|
-
renameSync(tmp, targetFile);
|
|
137
|
-
written = true;
|
|
138
|
-
} catch {
|
|
139
|
-
// rename 失败多见于目标被别的进程占用(Windows 常态):退回直写;仍失败就报错,绝不谎报「已同步」
|
|
140
|
-
try { writeFileSync(targetFile, nextText, { encoding: "utf8", mode: 0o600 }); written = true; } catch { /* 交给下面的 throw */ }
|
|
141
|
-
}
|
|
142
|
-
} finally {
|
|
143
|
-
sweepTmp();
|
|
144
|
-
}
|
|
145
|
-
if (!written) {
|
|
146
|
-
throw new Error(`无法写入 ${targetFile}(文件可能被其他进程占用)——配置未更新;原文见同目录 settings.pre-mslxdff.json${tmpLeftover ? `;另有清理失败的临时文件 ${tmpLeftover}(内含明文 token,请手动删除)` : ""}`);
|
|
147
|
-
}
|
|
148
|
-
try { chmodSync(targetFile, 0o600); } catch { /* ignore */ }
|
|
149
|
-
return { action: existed ? "updated" : "inserted", file: targetFile, id: model, backup, changed: true, rows: next.modelPicker.options.length, tmpLeftover };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export { OUR_ENV_KEYS, DEFAULT_BEHAVES_AS };
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync, copyFileSync, chmodSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* -setto claude:把本机网关写进 Claude Code 的用户设置文件。
|
|
7
|
+
* 键面依据 code.claude.com/docs/en/settings-reference(model / modelPicker / env 三节):
|
|
8
|
+
* - `ANTHROPIC_BASE_URL` **不带 /v1** —— 客户端自己拼 `/v1/messages`(参考仓库 README 写 /v1 是它的错)
|
|
9
|
+
* - `env.ANTHROPIC_MODEL` 必须删 —— 它压过顶层 `model` 键,留着会让设置失效
|
|
10
|
+
* - `CLAUDE_CODE_ATTRIBUTION_HEADER=0` —— 我们把 system 折叠成单条,归因块必须在客户端就不发
|
|
11
|
+
* - `modelPicker.options[].behavesAs` 实测必需 —— 见下方 DEFAULT_BEHAVES_AS 注释
|
|
12
|
+
* 只动上述键,其余键与键序原样保留;解析失败一律拒写(那是用户手写的 hooks/permissions)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const OUR_ENV_KEYS = [
|
|
16
|
+
"ANTHROPIC_BASE_URL",
|
|
17
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
18
|
+
"ANTHROPIC_MODEL", // 只删不写
|
|
19
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
|
20
|
+
"CLAUDE_CODE_ATTRIBUTION_HEADER",
|
|
21
|
+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// 本机 Claude Code 对「不认识且无 behavesAs 映射」的 model id 直接拒跑,连请求都不发
|
|
25
|
+
// (实测 2026-10-09:`"big-pickle" isn't described by this version's model catalog; …map it with
|
|
26
|
+
// behavesAs on a modelPicker row`,加 behavesAs 后 `claude -p` 正常走通工具往返)。
|
|
27
|
+
// behavesAs 只改客户端的**本地能力推断**(effort / auto-compact 口径),不改变发给上游的模型;
|
|
28
|
+
// 上游不认的字段由 messagesToChatBody 统一丢,所以锚一个通用已知 id 即可。`--behaves-as ""` 可关。
|
|
29
|
+
const DEFAULT_BEHAVES_AS = "claude-sonnet-5";
|
|
30
|
+
|
|
31
|
+
/** Claude Code 用户设置文件路径(`CLAUDE_CONFIG_DIR` 优先,与 Claude Code 自身口径一致)。 */
|
|
32
|
+
export function claudeSettingsPath() {
|
|
33
|
+
const dir = process.env.CLAUDE_CONFIG_DIR;
|
|
34
|
+
if (typeof dir === "string" && dir.trim()) return join(dir.trim(), "settings.json");
|
|
35
|
+
return join(os.homedir(), ".claude", "settings.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 纯函数:existing 对象 + 上下文 → 新对象。除我们的键外一律原样保留(含键序,JSON 键序随插入顺序)。 */
|
|
39
|
+
export function buildClaudeSettings(existing, ctx = {}) {
|
|
40
|
+
const out = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
|
|
41
|
+
const env = { ...(out.env && typeof out.env === "object" && !Array.isArray(out.env) ? out.env : {}) };
|
|
42
|
+
env.ANTHROPIC_BASE_URL = String(ctx.baseUrl || "");
|
|
43
|
+
env.ANTHROPIC_AUTH_TOKEN = String(ctx.token || "");
|
|
44
|
+
env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
45
|
+
env.CLAUDE_CODE_ATTRIBUTION_HEADER = "0";
|
|
46
|
+
env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
|
|
47
|
+
delete env.ANTHROPIC_MODEL;
|
|
48
|
+
out.env = env;
|
|
49
|
+
out.model = String(ctx.model || "");
|
|
50
|
+
const picks = Array.isArray(ctx.picks) && ctx.picks.length ? ctx.picks : [ctx.model];
|
|
51
|
+
// behavesAs 的含义是「该 id 不在客户端模型目录里,请按 X 的能力口径推断」。
|
|
52
|
+
// 客户端本来就认识的 claude-* id 不该被这么标:把 claude-opus 标成 behavesAs=claude-sonnet-5,
|
|
53
|
+
// 客户端就会按 sonnet 的 effort / auto-compact 口径去推断它。显式 --behaves-as 仍覆盖一切。
|
|
54
|
+
const override = ctx.behavesAs == null ? null : String(ctx.behavesAs);
|
|
55
|
+
const anchorOf = (m) => (override != null ? override : /^claude[-.]/i.test(String(m)) ? "" : DEFAULT_BEHAVES_AS);
|
|
56
|
+
out.modelPicker = {
|
|
57
|
+
options: picks.filter(Boolean).map((m) => {
|
|
58
|
+
const row = { model: String(m), label: String(m) };
|
|
59
|
+
const anchor = anchorOf(m);
|
|
60
|
+
if (anchor) row.behavesAs = anchor;
|
|
61
|
+
return row;
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 归一化用于幂等比较(忽略换行符与首尾空白差异)
|
|
68
|
+
function norm(text) {
|
|
69
|
+
return String(text || "").replace(/\r\n/g, "\n").trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 写盘。返回 { action, file, id, backup, changed, rows }。
|
|
74
|
+
* @param {{id?:string, token?:string, port?:number, picks?:string[], file?:string, baseUrl?:string, behavesAs?:string}} p
|
|
75
|
+
*/
|
|
76
|
+
export function syncToClaude(p = {}) {
|
|
77
|
+
const targetFile = p.file || claudeSettingsPath();
|
|
78
|
+
const model = String(p.id ?? p.model ?? "").trim();
|
|
79
|
+
if (!model) throw new Error("claude settings: model id required");
|
|
80
|
+
const token = String(p.token || "");
|
|
81
|
+
if (!token) throw new Error("claude settings: token required(先跑 mslxdff -showtoken)");
|
|
82
|
+
const baseUrl = p.baseUrl || `http://127.0.0.1:${Number(p.port) || 8989}`;
|
|
83
|
+
const picks = Array.isArray(p.picks) && p.picks.length ? p.picks : [model];
|
|
84
|
+
|
|
85
|
+
let existed = false;
|
|
86
|
+
let originalText = "";
|
|
87
|
+
try {
|
|
88
|
+
originalText = readFileSync(targetFile, "utf8");
|
|
89
|
+
existed = true;
|
|
90
|
+
} catch {
|
|
91
|
+
existed = false;
|
|
92
|
+
}
|
|
93
|
+
let originalObj = null;
|
|
94
|
+
if (existed) {
|
|
95
|
+
try {
|
|
96
|
+
originalObj = JSON.parse(originalText);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(`claude settings 解析失败,拒绝覆盖:${targetFile}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const next = buildClaudeSettings(originalObj, { baseUrl, token, model, picks, behavesAs: p.behavesAs });
|
|
103
|
+
const nextText = `${JSON.stringify(next, null, 2)}\n`;
|
|
104
|
+
if (existed && norm(originalText) === norm(nextText)) {
|
|
105
|
+
return { action: "updated", file: targetFile, id: model, backup: null, changed: false, rows: next.modelPicker.options.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 备份只保留「mslxdff 首次接管前」那一份:已存在就不覆盖(代价:用户后来手改的内容不进备份)
|
|
109
|
+
let backup = null;
|
|
110
|
+
if (existed) {
|
|
111
|
+
const backupFile = join(dirname(targetFile), "settings.pre-mslxdff.json");
|
|
112
|
+
if (!existsSync(backupFile)) {
|
|
113
|
+
try { copyFileSync(targetFile, backupFile); backup = backupFile; } catch { /* 复制失败不阻塞写入,但绝不谎报「已备份」 */ }
|
|
114
|
+
}
|
|
115
|
+
// 只在**本次真复制过**时返回路径(spec R6「打印备份路径(若本次生成)」);已有旧备份时返回 null,
|
|
116
|
+
// 由 CLI 换成「已存在更早的备份」提示 —— 免得用户拿首份旧备份去覆盖自己后来的手改。
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
120
|
+
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
121
|
+
// 凭据落地红线:文件里有明文 token → 0600(mode 只在新建时生效,故存量补 chmod)
|
|
122
|
+
let written = false;
|
|
123
|
+
let tmpLeftover = null;
|
|
124
|
+
// 半截 tmp 里带明文 token:unlink 也可能被占用(Windows 上正是 rename 失败的同因),
|
|
125
|
+
// 所以先擦内容再试一次;仍清不掉就把路径如实交出去,让 CLI 告警,绝不静默。
|
|
126
|
+
const sweepTmp = () => {
|
|
127
|
+
if (!existsSync(tmp)) return;
|
|
128
|
+
try { unlinkSync(tmp); return; } catch { /* 占用,走擦除 */ }
|
|
129
|
+
try { writeFileSync(tmp, "", { encoding: "utf8" }); } catch { /* 擦不动也继续试删 */ }
|
|
130
|
+
try { unlinkSync(tmp); } catch { tmpLeftover = tmp; }
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
try { writeFileSync(tmp, nextText, { encoding: "utf8", mode: 0o600 }); } catch { writeFileSync(tmp, nextText, "utf8"); }
|
|
134
|
+
try { chmodSync(tmp, 0o600); } catch { /* Windows 无 posix mode,忽略 */ }
|
|
135
|
+
try {
|
|
136
|
+
renameSync(tmp, targetFile);
|
|
137
|
+
written = true;
|
|
138
|
+
} catch {
|
|
139
|
+
// rename 失败多见于目标被别的进程占用(Windows 常态):退回直写;仍失败就报错,绝不谎报「已同步」
|
|
140
|
+
try { writeFileSync(targetFile, nextText, { encoding: "utf8", mode: 0o600 }); written = true; } catch { /* 交给下面的 throw */ }
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
sweepTmp();
|
|
144
|
+
}
|
|
145
|
+
if (!written) {
|
|
146
|
+
throw new Error(`无法写入 ${targetFile}(文件可能被其他进程占用)——配置未更新;原文见同目录 settings.pre-mslxdff.json${tmpLeftover ? `;另有清理失败的临时文件 ${tmpLeftover}(内含明文 token,请手动删除)` : ""}`);
|
|
147
|
+
}
|
|
148
|
+
try { chmodSync(targetFile, 0o600); } catch { /* ignore */ }
|
|
149
|
+
return { action: existed ? "updated" : "inserted", file: targetFile, id: model, backup, changed: true, rows: next.modelPicker.options.length, tmpLeftover };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { OUR_ENV_KEYS, DEFAULT_BEHAVES_AS };
|
package/src/sync-codex.js
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
|
|
5
|
-
// TOML 字符串:含反斜杠(Windows 路径)时用字面单引号,避免转义被吃
|
|
6
|
-
function tomlStr(s) {
|
|
7
|
-
const v = String(s);
|
|
8
|
-
if (v.includes("\\") && !v.includes("'")) return `'${v}'`;
|
|
9
|
-
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// auth 命令解析:Codex 起子进程不保证继承终端 PATH,所以不用裸 `mslxdff`,
|
|
13
|
-
// 而用绝对路径 node + 绝对路径脚本(当前进程即 mslxdff 本体,最可信)。
|
|
14
|
-
export function buildAuthCommand({ execPath = process.execPath, argv1 = process.argv[1], cwd = process.cwd() } = {}) {
|
|
15
|
-
const script = argv1 ? (isAbsolute(argv1) ? argv1 : resolve(cwd, argv1)) : "";
|
|
16
|
-
if (execPath && script && /mslxdff(\.js)?$/i.test(script)) {
|
|
17
|
-
try { if (existsSync(script)) return { command: execPath, args: [script, "-showtoken"] }; } catch {}
|
|
18
|
-
}
|
|
19
|
-
return { command: "mslxdff", args: ["-showtoken"] }; // 兜底:走 PATH
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:自定义 provider 指向本地网关。
|
|
23
|
-
export function codexConfigPath() {
|
|
24
|
-
const home = process.env.CODEX_HOME;
|
|
25
|
-
if (typeof home === "string" && home.trim()) return join(home.trim(), "config.toml");
|
|
26
|
-
return join(os.homedir(), ".codex", "config.toml");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 顶层 key = "value" 行:有则替换,无则追加
|
|
30
|
-
function upsertTopKey(lines, key, value) {
|
|
31
|
-
const re = new RegExp(`^\\s*${key}\\s*=`);
|
|
32
|
-
const line = `${key} = "${value}"`;
|
|
33
|
-
const idx = lines.findIndex((l) => re.test(l) && !l.trim().startsWith("#"));
|
|
34
|
-
if (idx >= 0) {
|
|
35
|
-
if (lines[idx].trim() === line) return { lines, changed: false };
|
|
36
|
-
lines[idx] = line;
|
|
37
|
-
return { lines, changed: true };
|
|
38
|
-
}
|
|
39
|
-
// 插到首个 [section] 之前(顶层区),无 section 则末尾追加
|
|
40
|
-
const secIdx = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
41
|
-
if (secIdx >= 0) lines.splice(secIdx, 0, line);
|
|
42
|
-
else { if (lines.length && lines[lines.length - 1].trim()) lines.push(""); lines.push(line); }
|
|
43
|
-
return { lines, changed: true };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// [model_providers.mslxdff] 整段替换(段从表头到下个 ^[ 或 EOF),无则追加
|
|
47
|
-
function upsertProviderSection(lines, section) {
|
|
48
|
-
const head = "[model_providers.mslxdff]";
|
|
49
|
-
const authHead = "[model_providers.mslxdff.auth]";
|
|
50
|
-
let start = lines.findIndex((l) => l.trim() === head);
|
|
51
|
-
const main = [head, ...section.main];
|
|
52
|
-
const auth = ["[model_providers.mslxdff.auth]", ...section.auth];
|
|
53
|
-
const full = [...main, "", ...auth];
|
|
54
|
-
if (start >= 0) {
|
|
55
|
-
let end = lines.length;
|
|
56
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
57
|
-
const t = lines[i].trim();
|
|
58
|
-
// 自家子段([model_providers.mslxdff.*])不算边界,一并替换
|
|
59
|
-
if (/^\s*\[/.test(lines[i]) && !t.startsWith("[model_providers.mslxdff.")) { end = i; break; }
|
|
60
|
-
}
|
|
61
|
-
const old = lines.slice(start, end).join("\n").trim();
|
|
62
|
-
if (old === full.join("\n").trim()) return { lines, changed: false };
|
|
63
|
-
lines.splice(start, end - start, ...full);
|
|
64
|
-
return { lines, changed: true };
|
|
65
|
-
}
|
|
66
|
-
if (lines.length && lines[lines.length - 1].trim()) lines.push("");
|
|
67
|
-
lines.push(...full);
|
|
68
|
-
return { lines, changed: true };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function buildCodexProvider({ port, auth } = {}) {
|
|
72
|
-
const p = Number(port) || 8989;
|
|
73
|
-
const a = auth || buildAuthCommand();
|
|
74
|
-
return {
|
|
75
|
-
main: [
|
|
76
|
-
'name = "mslxdff local gateway"',
|
|
77
|
-
`base_url = "http://127.0.0.1:${p}/v1"`,
|
|
78
|
-
'wire_api = "responses"', // 显式锁定:现行 Codex 自定义 provider 只认 Responses
|
|
79
|
-
],
|
|
80
|
-
// command-backed auth:调 mslxdff -showtoken 取 Bearer,token 永不落盘
|
|
81
|
-
auth: [
|
|
82
|
-
`command = ${tomlStr(a.command)}`,
|
|
83
|
-
`args = [${a.args.map(tomlStr).join(", ")}]`,
|
|
84
|
-
"timeout_ms = 5000",
|
|
85
|
-
"refresh_interval_ms = 300000",
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function syncToCodex({ id, port, file, auth } = {}) {
|
|
91
|
-
const targetFile = file || codexConfigPath();
|
|
92
|
-
const model = String(id || "").trim();
|
|
93
|
-
if (!model) throw new Error("model id required");
|
|
94
|
-
let text = "";
|
|
95
|
-
try { text = readFileSync(targetFile, "utf8"); } catch { text = ""; }
|
|
96
|
-
const lines = text ? text.split("\n") : [];
|
|
97
|
-
let changed = false;
|
|
98
|
-
let r = upsertTopKey(lines, "model", model);
|
|
99
|
-
changed = changed || r.changed;
|
|
100
|
-
r = upsertTopKey(r.lines, "model_provider", "mslxdff");
|
|
101
|
-
changed = changed || r.changed;
|
|
102
|
-
r = upsertProviderSection(r.lines, buildCodexProvider({ port, auth }));
|
|
103
|
-
changed = changed || r.changed;
|
|
104
|
-
const next = r.lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
|
|
105
|
-
if (!changed && text === next) return { action: "updated", file: targetFile, id: model };
|
|
106
|
-
mkdirSync(dirname(targetFile), { recursive: true });
|
|
107
|
-
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
108
|
-
writeFileSync(tmp, next, "utf8");
|
|
109
|
-
try {
|
|
110
|
-
renameSync(tmp, targetFile);
|
|
111
|
-
} catch {
|
|
112
|
-
try { writeFileSync(targetFile, next, "utf8"); } catch {}
|
|
113
|
-
}
|
|
114
|
-
try { if (existsSync(tmp)) unlinkSync(tmp); } catch {}
|
|
115
|
-
return { action: text ? "updated" : "inserted", file: targetFile, id: model };
|
|
116
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
// TOML 字符串:含反斜杠(Windows 路径)时用字面单引号,避免转义被吃
|
|
6
|
+
function tomlStr(s) {
|
|
7
|
+
const v = String(s);
|
|
8
|
+
if (v.includes("\\") && !v.includes("'")) return `'${v}'`;
|
|
9
|
+
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// auth 命令解析:Codex 起子进程不保证继承终端 PATH,所以不用裸 `mslxdff`,
|
|
13
|
+
// 而用绝对路径 node + 绝对路径脚本(当前进程即 mslxdff 本体,最可信)。
|
|
14
|
+
export function buildAuthCommand({ execPath = process.execPath, argv1 = process.argv[1], cwd = process.cwd() } = {}) {
|
|
15
|
+
const script = argv1 ? (isAbsolute(argv1) ? argv1 : resolve(cwd, argv1)) : "";
|
|
16
|
+
if (execPath && script && /mslxdff(\.js)?$/i.test(script)) {
|
|
17
|
+
try { if (existsSync(script)) return { command: execPath, args: [script, "-showtoken"] }; } catch {}
|
|
18
|
+
}
|
|
19
|
+
return { command: "mslxdff", args: ["-showtoken"] }; // 兜底:走 PATH
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:自定义 provider 指向本地网关。
|
|
23
|
+
export function codexConfigPath() {
|
|
24
|
+
const home = process.env.CODEX_HOME;
|
|
25
|
+
if (typeof home === "string" && home.trim()) return join(home.trim(), "config.toml");
|
|
26
|
+
return join(os.homedir(), ".codex", "config.toml");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 顶层 key = "value" 行:有则替换,无则追加
|
|
30
|
+
function upsertTopKey(lines, key, value) {
|
|
31
|
+
const re = new RegExp(`^\\s*${key}\\s*=`);
|
|
32
|
+
const line = `${key} = "${value}"`;
|
|
33
|
+
const idx = lines.findIndex((l) => re.test(l) && !l.trim().startsWith("#"));
|
|
34
|
+
if (idx >= 0) {
|
|
35
|
+
if (lines[idx].trim() === line) return { lines, changed: false };
|
|
36
|
+
lines[idx] = line;
|
|
37
|
+
return { lines, changed: true };
|
|
38
|
+
}
|
|
39
|
+
// 插到首个 [section] 之前(顶层区),无 section 则末尾追加
|
|
40
|
+
const secIdx = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
41
|
+
if (secIdx >= 0) lines.splice(secIdx, 0, line);
|
|
42
|
+
else { if (lines.length && lines[lines.length - 1].trim()) lines.push(""); lines.push(line); }
|
|
43
|
+
return { lines, changed: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// [model_providers.mslxdff] 整段替换(段从表头到下个 ^[ 或 EOF),无则追加
|
|
47
|
+
function upsertProviderSection(lines, section) {
|
|
48
|
+
const head = "[model_providers.mslxdff]";
|
|
49
|
+
const authHead = "[model_providers.mslxdff.auth]";
|
|
50
|
+
let start = lines.findIndex((l) => l.trim() === head);
|
|
51
|
+
const main = [head, ...section.main];
|
|
52
|
+
const auth = ["[model_providers.mslxdff.auth]", ...section.auth];
|
|
53
|
+
const full = [...main, "", ...auth];
|
|
54
|
+
if (start >= 0) {
|
|
55
|
+
let end = lines.length;
|
|
56
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
57
|
+
const t = lines[i].trim();
|
|
58
|
+
// 自家子段([model_providers.mslxdff.*])不算边界,一并替换
|
|
59
|
+
if (/^\s*\[/.test(lines[i]) && !t.startsWith("[model_providers.mslxdff.")) { end = i; break; }
|
|
60
|
+
}
|
|
61
|
+
const old = lines.slice(start, end).join("\n").trim();
|
|
62
|
+
if (old === full.join("\n").trim()) return { lines, changed: false };
|
|
63
|
+
lines.splice(start, end - start, ...full);
|
|
64
|
+
return { lines, changed: true };
|
|
65
|
+
}
|
|
66
|
+
if (lines.length && lines[lines.length - 1].trim()) lines.push("");
|
|
67
|
+
lines.push(...full);
|
|
68
|
+
return { lines, changed: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildCodexProvider({ port, auth } = {}) {
|
|
72
|
+
const p = Number(port) || 8989;
|
|
73
|
+
const a = auth || buildAuthCommand();
|
|
74
|
+
return {
|
|
75
|
+
main: [
|
|
76
|
+
'name = "mslxdff local gateway"',
|
|
77
|
+
`base_url = "http://127.0.0.1:${p}/v1"`,
|
|
78
|
+
'wire_api = "responses"', // 显式锁定:现行 Codex 自定义 provider 只认 Responses
|
|
79
|
+
],
|
|
80
|
+
// command-backed auth:调 mslxdff -showtoken 取 Bearer,token 永不落盘
|
|
81
|
+
auth: [
|
|
82
|
+
`command = ${tomlStr(a.command)}`,
|
|
83
|
+
`args = [${a.args.map(tomlStr).join(", ")}]`,
|
|
84
|
+
"timeout_ms = 5000",
|
|
85
|
+
"refresh_interval_ms = 300000",
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function syncToCodex({ id, port, file, auth } = {}) {
|
|
91
|
+
const targetFile = file || codexConfigPath();
|
|
92
|
+
const model = String(id || "").trim();
|
|
93
|
+
if (!model) throw new Error("model id required");
|
|
94
|
+
let text = "";
|
|
95
|
+
try { text = readFileSync(targetFile, "utf8"); } catch { text = ""; }
|
|
96
|
+
const lines = text ? text.split("\n") : [];
|
|
97
|
+
let changed = false;
|
|
98
|
+
let r = upsertTopKey(lines, "model", model);
|
|
99
|
+
changed = changed || r.changed;
|
|
100
|
+
r = upsertTopKey(r.lines, "model_provider", "mslxdff");
|
|
101
|
+
changed = changed || r.changed;
|
|
102
|
+
r = upsertProviderSection(r.lines, buildCodexProvider({ port, auth }));
|
|
103
|
+
changed = changed || r.changed;
|
|
104
|
+
const next = r.lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
|
|
105
|
+
if (!changed && text === next) return { action: "updated", file: targetFile, id: model };
|
|
106
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
107
|
+
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
108
|
+
writeFileSync(tmp, next, "utf8");
|
|
109
|
+
try {
|
|
110
|
+
renameSync(tmp, targetFile);
|
|
111
|
+
} catch {
|
|
112
|
+
try { writeFileSync(targetFile, next, "utf8"); } catch {}
|
|
113
|
+
}
|
|
114
|
+
try { if (existsSync(tmp)) unlinkSync(tmp); } catch {}
|
|
115
|
+
return { action: text ? "updated" : "inserted", file: targetFile, id: model };
|
|
116
|
+
}
|