mslxdff 0.1.203 → 0.1.207
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/docs/cli_help_mini.md +146 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -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/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -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 +158 -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 +71 -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 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- 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 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- 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 -102
- 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/AGENTS.md +37 -0
- 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 +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- 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/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 -120
- 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/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/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -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/src/plugins.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import { readdirSync, existsSync } from "node:fs";
|
|
2
|
-
import { join, basename, extname } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
|
|
6
|
-
// mslxdff 插件系统:约定目录下的 *.mjs / *.js 模块,default export:
|
|
7
|
-
// { name?, version?, description?, hooks?: { "<hook-name>": async (ctx) => nextValue? } }
|
|
8
|
-
// hook 返回数组时可替换流程值(如 model:select 的候选顺序);返回 undefined 表示只观察。
|
|
9
|
-
// 插件错误一律隔离:加载失败进 errors,hook 抛错不影响主链路。
|
|
10
|
-
|
|
11
|
-
export function pluginsDir() {
|
|
12
|
-
return process.env.MSLXDFF_PLUGINS_DIR ||
|
|
13
|
-
join(os.homedir(), ".config", "mslxdff", "plugins");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 安装目录下的 plugins/(随仓库/内置分发;auto-update 重装会丢,仅作开发/内置用)
|
|
17
|
-
export function bundledPluginsDir(pkgRoot) {
|
|
18
|
-
if (!pkgRoot) return null;
|
|
19
|
-
return join(pkgRoot, "plugins");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 解析扫描目录列表:env 完全接管;否则安装目录 plugins/ + 用户目录(同名文件用户目录优先)
|
|
23
|
-
export function resolvePluginDirs({ pkgRoot } = {}) {
|
|
24
|
-
const env = process.env.MSLXDFF_PLUGINS_DIR;
|
|
25
|
-
if (env) return [env];
|
|
26
|
-
const dirs = [];
|
|
27
|
-
const bundled = bundledPluginsDir(pkgRoot);
|
|
28
|
-
if (bundled) dirs.push(bundled);
|
|
29
|
-
dirs.push(pluginsDir());
|
|
30
|
-
return dirs;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const PLUGIN_EXTS = new Set([".mjs", ".js"]);
|
|
34
|
-
|
|
35
|
-
async function loadFromDir(dir, skipFiles) {
|
|
36
|
-
const plugins = [];
|
|
37
|
-
const errors = [];
|
|
38
|
-
if (!dir || !existsSync(dir)) return { plugins, errors };
|
|
39
|
-
let files = [];
|
|
40
|
-
try {
|
|
41
|
-
files = readdirSync(dir).filter((f) => PLUGIN_EXTS.has(extname(f).toLowerCase()));
|
|
42
|
-
} catch (err) {
|
|
43
|
-
errors.push({ file: dir, error: String(err?.message || err) });
|
|
44
|
-
return { plugins, errors };
|
|
45
|
-
}
|
|
46
|
-
files.sort();
|
|
47
|
-
for (const f of files) {
|
|
48
|
-
const file = join(dir, f);
|
|
49
|
-
if (skipFiles?.has(f)) continue; // 用户目录同名文件优先
|
|
50
|
-
try {
|
|
51
|
-
const mod = await import(pathToFileURL(file).href);
|
|
52
|
-
const plugin = mod?.default;
|
|
53
|
-
if (!plugin || typeof plugin !== "object") {
|
|
54
|
-
errors.push({ file, error: "no default export object" });
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
plugins.push({
|
|
58
|
-
name: typeof plugin.name === "string" && plugin.name ? plugin.name : basename(f, extname(f)),
|
|
59
|
-
version: typeof plugin.version === "string" ? plugin.version : "",
|
|
60
|
-
description: typeof plugin.description === "string" ? plugin.description : "",
|
|
61
|
-
hooks: plugin.hooks && typeof plugin.hooks === "object" ? plugin.hooks : {},
|
|
62
|
-
file,
|
|
63
|
-
});
|
|
64
|
-
} catch (err) {
|
|
65
|
-
errors.push({ file, error: String(err?.message || err) });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return { plugins, errors };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function loadPlugins({ dir, dirs } = {}) {
|
|
72
|
-
// 兼容旧签名 loadPlugins({ dir });新签名 dirs 数组按优先级从低到高(后者覆盖前者同名文件)
|
|
73
|
-
const list = dirs || (dir ? [dir] : [pluginsDir()]);
|
|
74
|
-
let plugins = [];
|
|
75
|
-
const errors = [];
|
|
76
|
-
const seen = new Set(); // 已加载的 basename,先扫的目录跳过它们 → 后目录(高优先)覆盖
|
|
77
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
78
|
-
const r = await loadFromDir(list[i], seen);
|
|
79
|
-
for (const p of r.plugins) seen.add(basename(p.file));
|
|
80
|
-
plugins = [...r.plugins, ...plugins]; // 低优先目录在前,目录内保持文件名序
|
|
81
|
-
errors.push(...r.errors);
|
|
82
|
-
}
|
|
83
|
-
return { plugins, errors };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 串行执行某 hook。返回 { value, changed, errors }:
|
|
87
|
-
// - value:最后一个返回非 undefined 值的插件的返回值(链式传递;数组、对象、布尔均可)
|
|
88
|
-
// - changed:是否有插件返回了非 undefined 值(调用方可据此采用 value)
|
|
89
|
-
export async function runHook(plugins, name, ctx) {
|
|
90
|
-
let value;
|
|
91
|
-
let changed = false;
|
|
92
|
-
const errors = [];
|
|
93
|
-
for (const p of plugins || []) {
|
|
94
|
-
const fn = p.hooks?.[name];
|
|
95
|
-
if (typeof fn !== "function") continue;
|
|
96
|
-
try {
|
|
97
|
-
const out = await fn(ctx);
|
|
98
|
-
if (out !== undefined) {
|
|
99
|
-
value = out;
|
|
100
|
-
changed = true;
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
errors.push({ plugin: p.name, error: String(err?.message || err) });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { value, changed, errors };
|
|
107
|
-
}
|
|
1
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, basename, extname } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
// mslxdff 插件系统:约定目录下的 *.mjs / *.js 模块,default export:
|
|
7
|
+
// { name?, version?, description?, hooks?: { "<hook-name>": async (ctx) => nextValue? } }
|
|
8
|
+
// hook 返回数组时可替换流程值(如 model:select 的候选顺序);返回 undefined 表示只观察。
|
|
9
|
+
// 插件错误一律隔离:加载失败进 errors,hook 抛错不影响主链路。
|
|
10
|
+
|
|
11
|
+
export function pluginsDir() {
|
|
12
|
+
return process.env.MSLXDFF_PLUGINS_DIR ||
|
|
13
|
+
join(os.homedir(), ".config", "mslxdff", "plugins");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 安装目录下的 plugins/(随仓库/内置分发;auto-update 重装会丢,仅作开发/内置用)
|
|
17
|
+
export function bundledPluginsDir(pkgRoot) {
|
|
18
|
+
if (!pkgRoot) return null;
|
|
19
|
+
return join(pkgRoot, "plugins");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 解析扫描目录列表:env 完全接管;否则安装目录 plugins/ + 用户目录(同名文件用户目录优先)
|
|
23
|
+
export function resolvePluginDirs({ pkgRoot } = {}) {
|
|
24
|
+
const env = process.env.MSLXDFF_PLUGINS_DIR;
|
|
25
|
+
if (env) return [env];
|
|
26
|
+
const dirs = [];
|
|
27
|
+
const bundled = bundledPluginsDir(pkgRoot);
|
|
28
|
+
if (bundled) dirs.push(bundled);
|
|
29
|
+
dirs.push(pluginsDir());
|
|
30
|
+
return dirs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PLUGIN_EXTS = new Set([".mjs", ".js"]);
|
|
34
|
+
|
|
35
|
+
async function loadFromDir(dir, skipFiles) {
|
|
36
|
+
const plugins = [];
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (!dir || !existsSync(dir)) return { plugins, errors };
|
|
39
|
+
let files = [];
|
|
40
|
+
try {
|
|
41
|
+
files = readdirSync(dir).filter((f) => PLUGIN_EXTS.has(extname(f).toLowerCase()));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
errors.push({ file: dir, error: String(err?.message || err) });
|
|
44
|
+
return { plugins, errors };
|
|
45
|
+
}
|
|
46
|
+
files.sort();
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const file = join(dir, f);
|
|
49
|
+
if (skipFiles?.has(f)) continue; // 用户目录同名文件优先
|
|
50
|
+
try {
|
|
51
|
+
const mod = await import(pathToFileURL(file).href);
|
|
52
|
+
const plugin = mod?.default;
|
|
53
|
+
if (!plugin || typeof plugin !== "object") {
|
|
54
|
+
errors.push({ file, error: "no default export object" });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
plugins.push({
|
|
58
|
+
name: typeof plugin.name === "string" && plugin.name ? plugin.name : basename(f, extname(f)),
|
|
59
|
+
version: typeof plugin.version === "string" ? plugin.version : "",
|
|
60
|
+
description: typeof plugin.description === "string" ? plugin.description : "",
|
|
61
|
+
hooks: plugin.hooks && typeof plugin.hooks === "object" ? plugin.hooks : {},
|
|
62
|
+
file,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
errors.push({ file, error: String(err?.message || err) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { plugins, errors };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function loadPlugins({ dir, dirs } = {}) {
|
|
72
|
+
// 兼容旧签名 loadPlugins({ dir });新签名 dirs 数组按优先级从低到高(后者覆盖前者同名文件)
|
|
73
|
+
const list = dirs || (dir ? [dir] : [pluginsDir()]);
|
|
74
|
+
let plugins = [];
|
|
75
|
+
const errors = [];
|
|
76
|
+
const seen = new Set(); // 已加载的 basename,先扫的目录跳过它们 → 后目录(高优先)覆盖
|
|
77
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
78
|
+
const r = await loadFromDir(list[i], seen);
|
|
79
|
+
for (const p of r.plugins) seen.add(basename(p.file));
|
|
80
|
+
plugins = [...r.plugins, ...plugins]; // 低优先目录在前,目录内保持文件名序
|
|
81
|
+
errors.push(...r.errors);
|
|
82
|
+
}
|
|
83
|
+
return { plugins, errors };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 串行执行某 hook。返回 { value, changed, errors }:
|
|
87
|
+
// - value:最后一个返回非 undefined 值的插件的返回值(链式传递;数组、对象、布尔均可)
|
|
88
|
+
// - changed:是否有插件返回了非 undefined 值(调用方可据此采用 value)
|
|
89
|
+
export async function runHook(plugins, name, ctx) {
|
|
90
|
+
let value;
|
|
91
|
+
let changed = false;
|
|
92
|
+
const errors = [];
|
|
93
|
+
for (const p of plugins || []) {
|
|
94
|
+
const fn = p.hooks?.[name];
|
|
95
|
+
if (typeof fn !== "function") continue;
|
|
96
|
+
try {
|
|
97
|
+
const out = await fn(ctx);
|
|
98
|
+
if (out !== undefined) {
|
|
99
|
+
value = out;
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
errors.push({ plugin: p.name, error: String(err?.message || err) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { value, changed, errors };
|
|
107
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# src/providers — 供应商契约(改任何 provider 前必读)
|
|
2
|
+
|
|
3
|
+
> 从 `AGENTS.md` 下沉(2026-09-29 分层,就近生效)。改完跑:`npm run docs:check` + 相关 provider 测试 + 裸调验证 `-provider <id> models`。
|
|
4
|
+
> 相关 ADR:0007(前缀路由)/0010(allowlist)/0019(share-keys)/0023(key 直连)/0026(cline id)/0029(qoder)/0033(cline 自动同步)/0037(qwenwork)/0038(zcode)/0041(globalqwenwork)。
|
|
5
|
+
|
|
6
|
+
## 通用机制(dispatcher / keyring / 门禁)
|
|
7
|
+
|
|
8
|
+
- `src/providers/` 每供应商一文件,前缀路由 `<provider>/raw`(裸 id 归 opencode),dispatcher 剥前缀转发;`providerKeys: {id:[k1,k2]}` round-robin 30s 冷却;`shareKeys` via `x-mslxdff-share-keys` 瞬时借用且 opencode 恒排除。
|
|
9
|
+
- `providerConfigs.allowAnyModels` 默认 `false`(`opencode:true` 例外)+ `allowlist` 空即 `403`;`isModelAllowed` 对未配置 provider 放行(兼容测试),`listModels` 同步过滤;`providerConfigs.{modelsPath,chatPath}` 可配异形路径(`workbuddy=/v3/config` & `/v2/chat/completions`,其余默认 `/models` & `/chat/completions`,`b.ai=/v1/models`、`cline=/api/v1/models` 已验证)。
|
|
10
|
+
- `src/models.js` 的 `aggLoad`/`get` 二次过滤:`!allowed.length && !allowAny → 跳过整供应商`;`src/routes/chat/index.js` 的 `isAllowlistBlock` 在 `useAuto` 时软跳 `continue`(显式请求才硬 `403`)。
|
|
11
|
+
- 定制 provider 启用门禁下沉为纯函数 `src/runtime/provider-gate.js` —— auth 号型(workbuddy/traework/qoder)**不要求 baseUrl**,旧门禁 `baseUrl && keys` 曾把 qoder 整家静默跳过(表现:`/v1/models` 零条目 + `Model qoder/x is not supported`,无日志)。
|
|
12
|
+
- CLI:`provider <id> models` 与 `model list --provider` 为直查一级答案(`--json` 供脚本),HTTP `GET /v1/providers/:id/models`;价格列 `x0.00/浮动/—` 对齐展示(workbuddy 按 credits 升序)。
|
|
13
|
+
- 新增 provider 三步走:registry 注册 + classify(local-only 判据见 ADR-0023)+ allowlist 只增不减补齐;必须裸调验证 `free` 是否免 Key;`free` 4 个仅目录免 Key ≠ 对话免 Key(cline 教训)。
|
|
14
|
+
|
|
15
|
+
## qoder(原生 COSY 栈,ADR-0029/0036)
|
|
16
|
+
|
|
17
|
+
- 凭证单一源 `auths/qoder-<uid>.json`(0600,含每号 `region` —— `state.normalizeAuths` 会剥该字段,故 region 只能存 auths 文件);签到按每号 region 选域名(cn=`openapi.qoder.com.cn` 走 `daily-check-in/status→claim`,409=今日已领;global=`openapi.qoder.sh` 该端点 404 → 回落 `/sash/api/v1/me/campaigns→/{id}/claim`);认证只需 `Bearer <device_token>` + `cosy-clienttype: 10`(**无 COSY 签名、claim 空 body**);默认只领 `CLAIM_BENEFIT`,促销条目 `VIEW_DETAILS` 需 `-provider qoder checkin --any`(实测能 claim,但奖励是订阅折扣、不进 quota);余额 `GET /api/v2/quota/usage`;daemon 每日 09:00 自动(`MSLXDFF_QODER_CHECKIN=0` 关,`_CHECKIN_HOUR` 改点)。
|
|
18
|
+
- **流式坏号判决面有两条**:①首包 HTTP 非 200;②**HTTP 200 但 SSE 信封内 `statusCodeValue` 非 200**(限流裹成 `200` + `{"code":"403","message":{"isQueued":true,"retryAfterSeconds":30}}`,只盖①会漏掉②——实测后果:94 次 empty turn 502、0 次恢复)。②的取法:返回 Response **前预读上游首帧**取判决(`PEEK_MAX_FRAMES=8`,遇判决或首个有效载荷即停),预读字节原样回灌给 `reshapeQoderStream`(`prefetched` 参数),对外仍恒 200、状态码只走内部头 `x-mslxdff-qoder-upstream-status`(fetch 异常折 502)。`test/qoder-envelope-status.test.js` 4 例。
|
|
19
|
+
- **同请求粘号**(`scope=reqId`,`MSLXDFF_QODER_STICKY_MS` 默认 600s,`0`=关):一次客户端请求内复用同一号,仅 401/403/429/5xx 冷却才换;`pick=new|sticky|switch|forced` 经 `x-mslxdff-qoder-account` 回显、冷却经 `x-mslxdff-qoder-cooldown` 回显。`test/qoder-cooldown-stream.test.js` 3 例。
|
|
20
|
+
- **wire 四条硬规矩**(openspec qoder-p0-fixes,`test/qoder-p0-fixes.test.js` 19 例):① 推理模型 `model_config.is_reasoning` 与 `chat_context.extra.modelConfig` 必须**双写**(只写前者上游不吐 reasoning_content);数据源=`models.js peekModels` 快照(不吃 TTL、chat 路径恒 0 额外上游调用、冷目录降级 false 不阻断,preheat 灌缓存);② 客户端无 tools 发 `tools:[]`——baseprompt 模板内嵌 14 工具永不出门;③ 上游 `finish_reason` 如实透传(length 截断从此可见),仅全程未给才兜底 stop/tool_calls,stream 与 aggregate 收尾判定同构;④ SSE 未完结行超 2MiB 即断(完整行消费后判、不连坐合法帧;流式流内 error 收尾、聚合 502 走短冷却)。
|
|
21
|
+
- **判决三分类与冷却三档(ADR-0044)**:同一个 `10605` 容器既裹额度也裹排队,`sse.js` 判序固定「先 `findQuotaSignal`(内层 code 110 / Billing 措辞)→ 后 `findQueueSignal`(code 10605 / `isQueued:true` / `serviceAvailable:false`)」。排队**不新增 kind**(仍 `upstream`,只挂 `err.queued`),经内部头 `x-mslxdff-qoder-queued` 交出门面,对外仍恒 200。门面冷却分三档:额度 `quotaCooldownMs`(1h) > **排队 `queueCooldownMs`(300s)** > 其它 `cooldownMs`(30s),且额度与排队**都在同一请求内当场换号**(`isQuota || isQueued` 且 `ring.available()>0` → `continue`);空转(模型零输出)仍不换号不冷却(ADR-0036 否决方案 D 的口径不变,别把两者混为一谈)。全号都在冷却时 `pickKey` 的 `ring.next() || keys[0]` 仍会 `forced` 补一发(冷却是轮转偏好不是硬拒)。`test/qoder-queue-switch.test.js` 8 例;`test/qoder-envelope-status.test.js` 现为 5 例(前者首已按新行为改写)。
|
|
22
|
+
- `GET /v1/models` 在 `modelPicks` 非空时按勾选集裁剪(`filterByPicks`,空勾选=不裁剪,`?all=1` 绕过),`-models` 交互候选取数恒走 `?all=1`;交互选择器按终端行高开窗分页(`src/chooser.js`,多选行形状 `❯ [✓] <id>`)。
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## qwenwork(cn)与 globalqwenwork(国际站)— 同协议两租户(ADR-0037/0041)
|
|
26
|
+
|
|
27
|
+
- 千问办公两站 `gateway.qwenwork.cn` / `gateway.qwenwork.ai`:COSY 签名(**同一枚 RSA 模数**)、SSE 端点 `/algo/api/v2/service/pro/sse/agent_chat_generation`、chat body、refresh `{refresh_token}` **逐字同构**(现网双站实测 200),故 globalqwenwork 的协议模块从 qwenwork 逐字 vendor(仓库约定:provider 目录自包含、不跨目录 import)。**差异只在三处租户参数**:auth 域名、OAuth `client_id`(cn `e883ade2…` / intl `cc65e5fc…`)、深链 `redirect_uri`(`qwenwork-cn://` / `qwenwork://`);**凭证与积分池两站不互通**(cn token 打 .ai → 401 INVALID_TOKEN)。
|
|
28
|
+
- ⚠ **registry.js 子串陷阱**:qwenwork 的 matcher 是 `includes("qwenwork")`,而 `globalqwenwork` / `globalqwenwork://native` 都含该子串且 `getCustomProviderFactory` first-match-wins —— **globalqwenwork 条目必须排在 qwenwork 之前**,否则国际站 token 被 cn 工厂接管(乱 401 + refresh 污染 cn 号)。`test/globalqwenwork-provider.test.js` 第 2 例锁死该顺序;动 registry 顺序前先跑它。
|
|
29
|
+
- 模型池零交集:cn `flash`/`pro`/`qwen3.8-max-preview`(pf 0.1/1/1.8,reasoning)↔ intl 仅 `qwork-auto`(→qwen3.8-flash)/`qwork-advanced`(→gpt-5.6-sol)(pf 0,非 reasoning,is_vl,max_input 180000)。登录各自种 allowlist、`allowAnyModels=false`(额度池防 auto 烧分)。国际站官方客户端自身走 gRPC `model.chat.ChatService/ChatCompletionStream`(其 `region/endpoints` 在 .ai 404),本仓不走 —— 见 ADR-0041 决策 1。
|
|
30
|
+
## workbuddy
|
|
31
|
+
|
|
32
|
+
- 专用 `POST /v2/chat/completions` **强制 `stream:true`** + `GET /v3/config` 按 credits 升序前缀 `workbuddy/`(**2026-10-10 换端点**:旧 `GET /console/enterprises/personal/models` 对多数 token 恒 401/500 → 目录 0 条 → `aggLoad` 判 `!allowed.length` 把整家跳过;`/v3/config` 返回**随 UA 变**,本仓 UA 与 chat 同身份故目录即"自己能调的那套";`listModels` 已改逐 key 轮换,单个坏 token 不再清空目录);401/403 自动 `POST /v2/plugin/auth/token/refresh` 回写 `state.json`+`auths/workbuddy-<uid>.json`(0600 tmp+rename)。
|
|
33
|
+
- 凭据目录 `authDirFor()` 跟账本走 + `authDirCandidates()` 只读兜底旧位置 + `listAccountDocs()` 单一读取出口(provider/CLI/token-auto 四处复制品合一);`-workbuddy remove` 扫全部候选目录防旧副本「复活」(ADR-0025,`test/workbuddy-authdir.test.js` 6 例)。
|
|
34
|
+
- 接入禁手填 `<key>`,必须 `node workbuddy-token-auto.js` 自动化(whistle MITM :8899;whistle/MITM 取 codex token 路径已证伪);桌面登录态跨平台发现 `src/providers/workbuddy/desktop-info.js`(显式 `--file`/env > 后缀快查 > 文件名 BFS depth≤6;Win `%LOCALAPPDATA%`/`%APPDATA%`、mac `~/Library/Application Support`、Linux `~/.config`);设备授权加号(0.1.93):`POST /v2/plugin/auth/state?platform=CLI` → 浏览器登录 → 轮询 `token?state=`,UA `CLI/2.63.2 CodeBuddy/2.63.2` 可 `MSLXDFF_WORKBUDDY_UA` 覆盖;每日 `workbuddy-checkin.js` 双域幂等 `code 10001` 视为成功。
|
|
35
|
+
- `share` 默认 `off` 需 `MSLXDFF_WORKBUDDY_SHARE_KEYS=1`;`-setto workbuddy [modelId]` 原子写 `~/.workbuddy/models.json`(仅 127.0.0.1/v1)。
|
|
36
|
+
- 出网 11128 指纹剥离(`payload.js` CC_SENTENCES 清单式):上游扫 system/assistant,命中行首 `x-anthropic-billing-header:` 整行或官方固定整句("You are Claude Code…" / "To give feedback…anthropics/claude-code/issues")即 400 "unapproved channel"(实测整句纯子串匹配、user/tool 不扫、换说法放行)——只剥固定形态,新拦句加清单。
|
|
37
|
+
|
|
38
|
+
## cline
|
|
39
|
+
|
|
40
|
+
- 指纹头:UA `Cline/3.0.47` + `X-CLIENT-TYPE: cline-sdk` + `X-CLIENT-VERSION` 同 UA;对话链 = `POST /api/v1/auth/refresh`(**每次 refresh 轮换 refreshToken 必须回写**)→ `Bearer workos:<accessToken>` + 固定 `<base>/api/v1/chat/completions`(**baseUrl 已含 /api/v1 时只拼 `/chat/completions`,否则双 /api/v1 会 401/empty response**);deepseek/z-ai 免费通道必须 `stream:true` + SSE 聚合否则 `500 empty response content`;`X-Task-ID`/`session_id` 每次唯一。
|
|
41
|
+
- 发上游剥 `provider/` 前缀(`cline/deepseek/X→deepseek/X`)恢复 forceStream 聚合 + 非 JSON 响应体 TypeError 加固(`test/cline-chat-id.test.js` 4 用例,0.1.91)。
|
|
42
|
+
- 白名单自动同步(ADR-0033):`allowlist-sync.js` 纯函数 `planMerge`(只增不减)+ `createAllowlistSync`(无新增不写盘、异常吞掉留痕),由 `cline/index.js` 注入;`cline/models.js` 仅在**上游成功取数**路径调 `notifyRefresh`(内置兜底常量不写盘,防污染);`MSLXDFF_CLINE_AUTOSYNC=0` 关;**教训:`saveProviderAllowedModels`/`saveProviderConfig` 重建 cfg 时起手 `{baseUrl,keys}` 未带 `allowAnyModels` 会静默抹成 false**(已修,勿回退)。
|
|
43
|
+
- 登录走 WorkOS 设备授权流(client_id `client_01K3A541FN8TA3EPPHTD2325AR`),refreshToken 落 `providerConfigs.{cline}.keys`;bench 检测 keys 含 refreshToken 即走 refresh→指纹头→stream 测速专用分支。
|
|
44
|
+
|
|
45
|
+
## bench 与额度保护(`src/bench/`)
|
|
46
|
+
|
|
47
|
+
- `bench/bench --via` 仅测(allowlist ∩ 全局 picks)交集(`filterBenchModels`,picks 空回退 allowlist);计费模型(无 `-free` 后缀)需显式点名才测(`max_tokens=5` 也计费);`--via` 额度保护默认跳过 `opencode` 需 `--include-opencode` 且 TTY 二次确认 `y/N`;0.1.84 起完全串行 `直连→逐 peer`(`MSLXDFF_BENCH_DELAY_MS=120`)逐模型实时打印(`--json` 进度走 `stderr`)。
|
|
48
|
+
- `aihubmix gemini 400 no_available_channel` 为上游无通道非 bench bug;peer name 统一短化 `host:port`。
|
|
49
|
+
- **烧额度的实测必须先经用户明确同意**:付费/订阅/按量计费上游的裸调、A/B 长文生成一律先问(2026-09-23 教训);免费池探针(public 免费模型、短 prompt 微量)可直接跑。
|
package/src/providers/base.js
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import { createKeyRing } from "./keyring.js";
|
|
2
|
-
import { joinModelId } from "./model-id.js";
|
|
3
|
-
import { getUndici as compatGetUndici, compatFetch } from "../compat.js";
|
|
4
|
-
import { createSdkDispatch } from "../upstream-engine/sdk/dispatch.js";
|
|
5
|
-
|
|
6
|
-
const { fetch: UndiciFetch, Agent: UndiciAgent } = compatGetUndici();
|
|
7
|
-
|
|
8
|
-
export function envInt(name, fallback) {
|
|
9
|
-
const v = Number(process.env[name]);
|
|
10
|
-
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function joinUrl(base, path) {
|
|
14
|
-
const b = String(base || "").trim().replace(/\/+$/, "");
|
|
15
|
-
const p = String(path || "").trim();
|
|
16
|
-
if (!p) return b;
|
|
17
|
-
const pp = p.startsWith("/") ? p : `/${p}`;
|
|
18
|
-
return `${b}${pp}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function sleep(ms) {
|
|
22
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getUndici() {
|
|
26
|
-
return { UndiciAgent, UndiciFetch, fetch: UndiciFetch || compatFetch, Agent: UndiciAgent };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createAgent({ keepAliveTimeout = 30_000, keepAliveMaxTimeout = 60_000, connections = 20 } = {}) {
|
|
30
|
-
if (!UndiciAgent) return { agent: null, dispatcher: null };
|
|
31
|
-
try {
|
|
32
|
-
const agent = new UndiciAgent({ keepAliveTimeout, keepAliveMaxTimeout, connections, pipelining: 1 });
|
|
33
|
-
return { agent, dispatcher: agent };
|
|
34
|
-
} catch {
|
|
35
|
-
return { agent: null, dispatcher: null };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function collectApiKeysGeneric(id, apiKeys, apiKey, loadKeys) {
|
|
40
|
-
const list = [
|
|
41
|
-
...(Array.isArray(apiKeys) ? apiKeys : [apiKeys].filter(Boolean)),
|
|
42
|
-
apiKey,
|
|
43
|
-
...(apiKeys === undefined && apiKey === undefined ? loadKeys(id) : []),
|
|
44
|
-
].filter((k) => typeof k === "string" && k.trim().length);
|
|
45
|
-
return [...new Set(list.map((k) => k.trim()))];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function createChatRunner({ id, ring, cooldownMs, retry, fetchImpl, dispatcher, buildHeaders, getUrl, connectTimeoutMs = 30_000, sdkDispatch } = {}) {
|
|
49
|
-
// SDK 通道(缺省):仅流式;非流式委派原生,避免把 JSON 客户端 SSE 化。
|
|
50
|
-
const sdk = sdkDispatch === undefined ? createSdkDispatch({ id, providerName: id }) : sdkDispatch;
|
|
51
|
-
|
|
52
|
-
async function attemptOnce(url, body, key, activeRing) {
|
|
53
|
-
if (sdk?.enabled && body?.stream !== false) {
|
|
54
|
-
let r;
|
|
55
|
-
try { r = await sdk.trySdk({ url, body, headers: buildHeaders(body, key), fetchImpl }); }
|
|
56
|
-
catch (err) { return err; } // 交 runChat 的 network 重试/冷却
|
|
57
|
-
if (r) return r.status === 401 && !activeRing.size ? { __needKey: true } : r;
|
|
58
|
-
}
|
|
59
|
-
const controller = new AbortController();
|
|
60
|
-
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
61
|
-
try {
|
|
62
|
-
const opts = { method: "POST", headers: buildHeaders(body, key), body: JSON.stringify(body), signal: controller.signal };
|
|
63
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
64
|
-
const res = await fetchImpl(url, opts);
|
|
65
|
-
if (res.status === 401 && !activeRing.size) return { __needKey: true };
|
|
66
|
-
return res;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
return err;
|
|
69
|
-
} finally {
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function runChat(body, activeRing, sourceKey) {
|
|
75
|
-
const url = getUrl();
|
|
76
|
-
const t0 = performance.now();
|
|
77
|
-
const attempts = [];
|
|
78
|
-
let waitMs = 0;
|
|
79
|
-
const key = activeRing.next();
|
|
80
|
-
if (!key && activeRing.size > 0) {
|
|
81
|
-
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
82
|
-
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
for (let attempt = 0; ; attempt++) {
|
|
86
|
-
const t = performance.now();
|
|
87
|
-
const result = await attemptOnce(url, body, key, activeRing);
|
|
88
|
-
const type = result instanceof Error ? "network" : `http${result?.status}`;
|
|
89
|
-
attempts.push({ attempt, type, ms: Math.round(performance.now() - t) });
|
|
90
|
-
if (result?.__needKey) {
|
|
91
|
-
const err = new Error(`${id}: missing ${sourceKey} (chat requires a real key)`);
|
|
92
|
-
err._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
93
|
-
throw err;
|
|
94
|
-
}
|
|
95
|
-
if (result instanceof Error) {
|
|
96
|
-
const entry = retry?.network;
|
|
97
|
-
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
98
|
-
activeRing.onError(key);
|
|
99
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
100
|
-
throw result;
|
|
101
|
-
}
|
|
102
|
-
const entry = retry?.[result.status];
|
|
103
|
-
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
104
|
-
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
105
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { attemptOnce, runChat };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function createListModelsRunner({ id, ring, dispatcher, fetchImpl, getUrl, mapModel }) {
|
|
114
|
-
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
115
|
-
let cache = null;
|
|
116
|
-
let fetchedAt = 0;
|
|
117
|
-
async function listModels() {
|
|
118
|
-
const now = Date.now();
|
|
119
|
-
if (cache && now - fetchedAt < CACHE_TTL_MS) return cache;
|
|
120
|
-
const url = getUrl();
|
|
121
|
-
const controller = new AbortController();
|
|
122
|
-
const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
|
|
123
|
-
try {
|
|
124
|
-
const headers = { Accept: "application/json" };
|
|
125
|
-
const key = ring.next();
|
|
126
|
-
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
127
|
-
const opts = { headers, signal: controller.signal };
|
|
128
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
129
|
-
const res = await fetchImpl(url, opts);
|
|
130
|
-
if (!res.ok) return [];
|
|
131
|
-
const json = await res.json().catch(() => ({}));
|
|
132
|
-
const raw = Array.isArray(json.data) ? json.data : Array.isArray(json) ? json : [];
|
|
133
|
-
const mapped = mapModel ? mapModel(raw) : raw;
|
|
134
|
-
cache = mapped.filter((m) => m && typeof m.id === "string").map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
135
|
-
fetchedAt = now;
|
|
136
|
-
return cache;
|
|
137
|
-
} catch {
|
|
138
|
-
return [];
|
|
139
|
-
} finally {
|
|
140
|
-
clearTimeout(timer);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { listModels, _cache: () => cache };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function createPreheatRunner({ dispatcher, fetchImpl, getUrl, id, ring, loadKeys }) {
|
|
147
|
-
async function preheat() {
|
|
148
|
-
const url = getUrl();
|
|
149
|
-
const t0 = performance.now();
|
|
150
|
-
try {
|
|
151
|
-
const headers = { Accept: "application/json" };
|
|
152
|
-
const key = (loadKeys ? loadKeys(id)[0] : ring.next()) || ring.next();
|
|
153
|
-
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
154
|
-
const opts = { headers };
|
|
155
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
156
|
-
const res = await fetchImpl(url, opts);
|
|
157
|
-
try { if (res.body) await res.text().catch(() => {}); } catch {}
|
|
158
|
-
return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
|
|
159
|
-
} catch (err) {
|
|
160
|
-
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { preheat };
|
|
164
|
-
}
|
|
1
|
+
import { createKeyRing } from "./keyring.js";
|
|
2
|
+
import { joinModelId } from "./model-id.js";
|
|
3
|
+
import { getUndici as compatGetUndici, compatFetch } from "../compat.js";
|
|
4
|
+
import { createSdkDispatch } from "../upstream-engine/sdk/dispatch.js";
|
|
5
|
+
|
|
6
|
+
const { fetch: UndiciFetch, Agent: UndiciAgent } = compatGetUndici();
|
|
7
|
+
|
|
8
|
+
export function envInt(name, fallback) {
|
|
9
|
+
const v = Number(process.env[name]);
|
|
10
|
+
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function joinUrl(base, path) {
|
|
14
|
+
const b = String(base || "").trim().replace(/\/+$/, "");
|
|
15
|
+
const p = String(path || "").trim();
|
|
16
|
+
if (!p) return b;
|
|
17
|
+
const pp = p.startsWith("/") ? p : `/${p}`;
|
|
18
|
+
return `${b}${pp}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sleep(ms) {
|
|
22
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getUndici() {
|
|
26
|
+
return { UndiciAgent, UndiciFetch, fetch: UndiciFetch || compatFetch, Agent: UndiciAgent };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createAgent({ keepAliveTimeout = 30_000, keepAliveMaxTimeout = 60_000, connections = 20 } = {}) {
|
|
30
|
+
if (!UndiciAgent) return { agent: null, dispatcher: null };
|
|
31
|
+
try {
|
|
32
|
+
const agent = new UndiciAgent({ keepAliveTimeout, keepAliveMaxTimeout, connections, pipelining: 1 });
|
|
33
|
+
return { agent, dispatcher: agent };
|
|
34
|
+
} catch {
|
|
35
|
+
return { agent: null, dispatcher: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collectApiKeysGeneric(id, apiKeys, apiKey, loadKeys) {
|
|
40
|
+
const list = [
|
|
41
|
+
...(Array.isArray(apiKeys) ? apiKeys : [apiKeys].filter(Boolean)),
|
|
42
|
+
apiKey,
|
|
43
|
+
...(apiKeys === undefined && apiKey === undefined ? loadKeys(id) : []),
|
|
44
|
+
].filter((k) => typeof k === "string" && k.trim().length);
|
|
45
|
+
return [...new Set(list.map((k) => k.trim()))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createChatRunner({ id, ring, cooldownMs, retry, fetchImpl, dispatcher, buildHeaders, getUrl, connectTimeoutMs = 30_000, sdkDispatch } = {}) {
|
|
49
|
+
// SDK 通道(缺省):仅流式;非流式委派原生,避免把 JSON 客户端 SSE 化。
|
|
50
|
+
const sdk = sdkDispatch === undefined ? createSdkDispatch({ id, providerName: id }) : sdkDispatch;
|
|
51
|
+
|
|
52
|
+
async function attemptOnce(url, body, key, activeRing) {
|
|
53
|
+
if (sdk?.enabled && body?.stream !== false) {
|
|
54
|
+
let r;
|
|
55
|
+
try { r = await sdk.trySdk({ url, body, headers: buildHeaders(body, key), fetchImpl }); }
|
|
56
|
+
catch (err) { return err; } // 交 runChat 的 network 重试/冷却
|
|
57
|
+
if (r) return r.status === 401 && !activeRing.size ? { __needKey: true } : r;
|
|
58
|
+
}
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
61
|
+
try {
|
|
62
|
+
const opts = { method: "POST", headers: buildHeaders(body, key), body: JSON.stringify(body), signal: controller.signal };
|
|
63
|
+
if (dispatcher) opts.dispatcher = dispatcher;
|
|
64
|
+
const res = await fetchImpl(url, opts);
|
|
65
|
+
if (res.status === 401 && !activeRing.size) return { __needKey: true };
|
|
66
|
+
return res;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return err;
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runChat(body, activeRing, sourceKey) {
|
|
75
|
+
const url = getUrl();
|
|
76
|
+
const t0 = performance.now();
|
|
77
|
+
const attempts = [];
|
|
78
|
+
let waitMs = 0;
|
|
79
|
+
const key = activeRing.next();
|
|
80
|
+
if (!key && activeRing.size > 0) {
|
|
81
|
+
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
82
|
+
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
for (let attempt = 0; ; attempt++) {
|
|
86
|
+
const t = performance.now();
|
|
87
|
+
const result = await attemptOnce(url, body, key, activeRing);
|
|
88
|
+
const type = result instanceof Error ? "network" : `http${result?.status}`;
|
|
89
|
+
attempts.push({ attempt, type, ms: Math.round(performance.now() - t) });
|
|
90
|
+
if (result?.__needKey) {
|
|
91
|
+
const err = new Error(`${id}: missing ${sourceKey} (chat requires a real key)`);
|
|
92
|
+
err._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
if (result instanceof Error) {
|
|
96
|
+
const entry = retry?.network;
|
|
97
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
98
|
+
activeRing.onError(key);
|
|
99
|
+
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
100
|
+
throw result;
|
|
101
|
+
}
|
|
102
|
+
const entry = retry?.[result.status];
|
|
103
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
104
|
+
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
105
|
+
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { attemptOnce, runChat };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createListModelsRunner({ id, ring, dispatcher, fetchImpl, getUrl, mapModel }) {
|
|
114
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
115
|
+
let cache = null;
|
|
116
|
+
let fetchedAt = 0;
|
|
117
|
+
async function listModels() {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
if (cache && now - fetchedAt < CACHE_TTL_MS) return cache;
|
|
120
|
+
const url = getUrl();
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
|
|
123
|
+
try {
|
|
124
|
+
const headers = { Accept: "application/json" };
|
|
125
|
+
const key = ring.next();
|
|
126
|
+
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
127
|
+
const opts = { headers, signal: controller.signal };
|
|
128
|
+
if (dispatcher) opts.dispatcher = dispatcher;
|
|
129
|
+
const res = await fetchImpl(url, opts);
|
|
130
|
+
if (!res.ok) return [];
|
|
131
|
+
const json = await res.json().catch(() => ({}));
|
|
132
|
+
const raw = Array.isArray(json.data) ? json.data : Array.isArray(json) ? json : [];
|
|
133
|
+
const mapped = mapModel ? mapModel(raw) : raw;
|
|
134
|
+
cache = mapped.filter((m) => m && typeof m.id === "string").map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
135
|
+
fetchedAt = now;
|
|
136
|
+
return cache;
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { listModels, _cache: () => cache };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createPreheatRunner({ dispatcher, fetchImpl, getUrl, id, ring, loadKeys }) {
|
|
147
|
+
async function preheat() {
|
|
148
|
+
const url = getUrl();
|
|
149
|
+
const t0 = performance.now();
|
|
150
|
+
try {
|
|
151
|
+
const headers = { Accept: "application/json" };
|
|
152
|
+
const key = (loadKeys ? loadKeys(id)[0] : ring.next()) || ring.next();
|
|
153
|
+
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
154
|
+
const opts = { headers };
|
|
155
|
+
if (dispatcher) opts.dispatcher = dispatcher;
|
|
156
|
+
const res = await fetchImpl(url, opts);
|
|
157
|
+
try { if (res.body) await res.text().catch(() => {}); } catch {}
|
|
158
|
+
return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { preheat };
|
|
164
|
+
}
|