mslxdff 0.1.94 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/bench/probe.js +2 -1
- package/src/bench/runner.js +2 -1
- package/src/bench/via-probe.js +2 -1
- package/src/bench/workbuddy-bench.js +2 -1
- package/src/chat/direct.js +2 -1
- package/src/chat/gateway.js +2 -1
- package/src/chat/repl.js +2 -0
- package/src/chat/stats.js +2 -1
- package/src/chat/terminal.js +2 -2
- package/src/chat/tools.js +3 -6
- package/src/chat/upstream.js +3 -2
- package/src/cli/commands/daemon.js +2 -1
- package/src/cli/commands/group.js +4 -3
- package/src/cli/commands/provider/bench.js +3 -2
- package/src/cli/commands/provider/cline-login.js +4 -3
- package/src/cli/commands/provider/keys.js +2 -2
- package/src/cli/commands/provider/workbuddy-login.js +8 -7
- package/src/cli/group-helpers.js +2 -1
- package/src/cli/policy.js +2 -1
- package/src/cli/status.js +3 -2
- package/src/compat.js +35 -0
- package/src/free-watcher.js +2 -1
- package/src/models.js +2 -1
- package/src/providers/base.js +3 -8
- package/src/providers/cline/auth.js +2 -1
- package/src/providers/workbuddy/checkin.js +4 -3
- package/src/readline-compat.js +26 -0
- package/src/routes/chat/via-route-handler.js +2 -1
- package/src/routes/groups-relay.js +2 -1
- package/src/routes/peers.js +3 -2
- package/src/routes/relay-queue.js +3 -2
- package/src/routes/relay.js +2 -1
- package/src/runtime/broadband-stream.js +2 -1
- package/src/runtime/broadband.js +7 -6
- package/src/runtime/workbuddy-checkin.js +2 -1
- package/src/state/memory.js +10 -9
- package/src/transport/index.js +3 -6
- package/src/transport/pool.js +3 -7
- package/src/upstream.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mslxdff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.96",
|
|
4
4
|
"description": "测试项目,请勿使用。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"docs:check": "node scripts/docs-check.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=16"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
],
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"undici": "^
|
|
31
|
+
"undici": "^5.28.4"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/bench/probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { getCustomNormalizer } from "../providers/registry.js";
|
|
3
3
|
import { createTransport } from "../transport/index.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
function normalizeModelsPayload(json, baseUrl = "") {
|
|
6
7
|
if (!json) return [];
|
|
@@ -32,7 +33,7 @@ export async function probeModels({
|
|
|
32
33
|
modelsPath,
|
|
33
34
|
chatPath,
|
|
34
35
|
headers = {},
|
|
35
|
-
fetchImpl =
|
|
36
|
+
fetchImpl = compatFetch,
|
|
36
37
|
timeoutMs = 8000,
|
|
37
38
|
} = {}) {
|
|
38
39
|
const base = String(baseUrl || "").replace(/\/+$/, "");
|
package/src/bench/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
3
|
import { createTransport } from "../transport/index.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
function extractInnerMessage(bodyText) {
|
|
6
7
|
const t = String(bodyText || "");
|
|
@@ -37,7 +38,7 @@ export async function runOne({
|
|
|
37
38
|
prompt = "hi",
|
|
38
39
|
maxTokens = 32,
|
|
39
40
|
timeoutMs = 30000,
|
|
40
|
-
fetchImpl =
|
|
41
|
+
fetchImpl = compatFetch,
|
|
41
42
|
} = {}) {
|
|
42
43
|
if (!baseUrl) return { id: model, ok: false, error: "missing baseUrl", label: "配置错误", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
43
44
|
if (!model) return { id: model, ok: false, error: "missing model", label: "配置错误", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
package/src/bench/via-probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
3
|
import { createTransport } from "../transport/index.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
function extractInnerMessage(bodyText) {
|
|
6
7
|
const t = String(bodyText || "");
|
|
@@ -35,7 +36,7 @@ export async function viaProbe({
|
|
|
35
36
|
prompt = "hi",
|
|
36
37
|
maxTokens = 5,
|
|
37
38
|
timeoutMs = 30000,
|
|
38
|
-
fetchImpl =
|
|
39
|
+
fetchImpl = compatFetch,
|
|
39
40
|
clock = Date.now,
|
|
40
41
|
shareKeys,
|
|
41
42
|
shareKeysHeader,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computeMetrics } from "../metrics.js";
|
|
2
2
|
import { createTransport } from "../transport/index.js";
|
|
3
|
+
import { compatFetch } from "../compat.js";
|
|
3
4
|
|
|
4
5
|
function buildWorkbuddyHeaders(apiKey, auth) {
|
|
5
6
|
const h = {
|
|
@@ -22,7 +23,7 @@ function sseContent(obj) {
|
|
|
22
23
|
return typeof c === "string" ? c : "";
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl =
|
|
26
|
+
export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl = compatFetch }) {
|
|
26
27
|
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
27
28
|
let ttfbMs = null;
|
|
28
29
|
let totalMs = null;
|
package/src/chat/direct.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { compatFetch } from "../compat.js";
|
|
2
3
|
|
|
3
4
|
function isInput400(status, msg, hasTools) {
|
|
4
5
|
return status === 400 && /prompt|messages/i.test(String(msg || "")) && hasTools;
|
|
@@ -8,7 +9,7 @@ function isInput400(status, msg, hasTools) {
|
|
|
8
9
|
* 直连深模块:mimo/pickle 经 createUpstreamClient 的 stream:false 调用
|
|
9
10
|
* 注入化:便于用 fake client 触发 400→去 tools 重试
|
|
10
11
|
*/
|
|
11
|
-
export function createDirectClient({ createUpstreamClient, chatTimeoutMs = 15000, env = process.env, fetchImpl =
|
|
12
|
+
export function createDirectClient({ createUpstreamClient, chatTimeoutMs = 15000, env = process.env, fetchImpl = compatFetch } = {}) {
|
|
12
13
|
const _create = createUpstreamClient || (() => { throw new Error("createUpstreamClient not injected"); });
|
|
13
14
|
|
|
14
15
|
async function doChat({ messages, tools, model }, withoutTools) {
|
package/src/chat/gateway.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { parseSse } from "./sse.js";
|
|
3
3
|
import { DEFAULT_PORT } from "../state.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 网关深模块:POST 127.0.0.1:port/v1/chat/completions model:auto
|
|
7
8
|
* 注入化:fetch/loadToken/getPort/readModelsJson 均可伪,便于单测
|
|
8
9
|
*/
|
|
9
10
|
export function createGatewayClient({
|
|
10
|
-
fetchImpl =
|
|
11
|
+
fetchImpl = compatFetch,
|
|
11
12
|
loadToken,
|
|
12
13
|
getPort,
|
|
13
14
|
defaultPort = DEFAULT_PORT,
|
package/src/chat/repl.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadHistory, saveHistory, histPath } from "./store.js";
|
|
|
5
5
|
import { CHAT_PREFERRED } from "./config.js";
|
|
6
6
|
import { createEngine } from "./engine.js";
|
|
7
7
|
import { printBanner, printFooter, handleSlash, createReadline, createSpinner, estimateChars } from "./terminal.js";
|
|
8
|
+
import { assertChatNode } from "../readline-compat.js";
|
|
8
9
|
|
|
9
10
|
function trace(line) {
|
|
10
11
|
if (process.env.MSLXDFF_CHAT_TRACE === "0") return;
|
|
@@ -12,6 +13,7 @@ function trace(line) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export async function startRepl({ singleShot } = {}) {
|
|
16
|
+
if (!assertChatNode()) process.exit(1);
|
|
15
17
|
const models = getModelsForPrompt();
|
|
16
18
|
const system = buildSystemPrompt({ modelsOverride: models });
|
|
17
19
|
let messages = [{ role: "system", content: system }];
|
package/src/chat/stats.js
CHANGED
|
@@ -5,6 +5,7 @@ import { logDir, callsFile, errorsFile, recentCalls, lastError } from "../logs.j
|
|
|
5
5
|
import { fmtShanghai } from "../time.js";
|
|
6
6
|
import { CHAT_PREFERRED, CHAT_FALLBACK } from "./config.js";
|
|
7
7
|
import { normalizeFullId } from "../providers/model-id.js";
|
|
8
|
+
import { compatFetch } from "../compat.js";
|
|
8
9
|
|
|
9
10
|
function readLinesCount(file) {
|
|
10
11
|
try {
|
|
@@ -146,7 +147,7 @@ export async function probeGateway(port, timeoutMs = 800) {
|
|
|
146
147
|
const ctrl = new AbortController();
|
|
147
148
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
148
149
|
try {
|
|
149
|
-
const r = await
|
|
150
|
+
const r = await compatFetch(url, { signal: ctrl.signal });
|
|
150
151
|
clearTimeout(t);
|
|
151
152
|
return { alive: r.ok, status: r.status, ms: 0 };
|
|
152
153
|
} catch (e) {
|
package/src/chat/terminal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createInterface } from "../readline-compat.js";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import { formatBannerLines, formatStatsDetail, collectStats, probeGateway } from "./stats.js";
|
|
4
4
|
import { createSpinner } from "./spinner.js";
|
|
@@ -89,7 +89,7 @@ export function handleSlash(line, ctx) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export function createReadline(promptStr) {
|
|
92
|
-
return
|
|
92
|
+
return createInterface({ input: stdin, output: stdout, prompt: promptStr });
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export { createSpinner, loadHistory, saveHistory, histPath, estimateChars };
|
package/src/chat/tools.js
CHANGED
|
@@ -6,6 +6,7 @@ import { performance } from "node:perf_hooks";
|
|
|
6
6
|
import { FORBIDDEN } from "./config.js";
|
|
7
7
|
import { logDir } from "../logs.js";
|
|
8
8
|
import { defaultStateFile, loadProviderKeys, loadProviderConfigs } from "../state.js";
|
|
9
|
+
import { compatFetch } from "../compat.js";
|
|
9
10
|
|
|
10
11
|
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
11
12
|
const allowedRoots = [
|
|
@@ -254,12 +255,8 @@ export async function curlTool({ url, method, headers, body, timeoutMs }) {
|
|
|
254
255
|
}
|
|
255
256
|
} catch {}
|
|
256
257
|
}
|
|
257
|
-
// fetch
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
const mod = await import("undici");
|
|
261
|
-
if (mod?.fetch) fetchImpl = mod.fetch;
|
|
262
|
-
} catch {}
|
|
258
|
+
// fetch 走兼容层(undici 优先,老 Node 兜底)
|
|
259
|
+
const fetchImpl = compatFetch;
|
|
263
260
|
const controller = new AbortController();
|
|
264
261
|
const timer = setTimeout(() => controller.abort(new Error(`curl timed out after ${timeout}ms`)), timeout);
|
|
265
262
|
const t0 = performance.now();
|
package/src/chat/upstream.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createUpstreamClient } from "../upstream.js";
|
|
|
6
6
|
import { CHAT_TIMEOUT_MS, CHAT_GATEWAY_TIMEOUT_MS, CHAT_PREFERRED, CHAT_FALLBACK } from "./config.js";
|
|
7
7
|
import * as state from "../state.js";
|
|
8
8
|
import { performance } from "node:perf_hooks";
|
|
9
|
+
import { compatFetch } from "../compat.js";
|
|
9
10
|
|
|
10
11
|
// 冷却深模块对接真实 state
|
|
11
12
|
const cooling = createCooling({
|
|
@@ -25,11 +26,11 @@ const direct = createDirectClient({
|
|
|
25
26
|
createUpstreamClient,
|
|
26
27
|
chatTimeoutMs: CHAT_TIMEOUT_MS,
|
|
27
28
|
env: process.env,
|
|
28
|
-
fetchImpl:
|
|
29
|
+
fetchImpl: compatFetch,
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
const gateway = createGatewayClient({
|
|
32
|
-
fetchImpl:
|
|
33
|
+
fetchImpl: compatFetch,
|
|
33
34
|
loadToken: async () => {
|
|
34
35
|
try { const l = await state.loadToken(); return String(l?.token || "").trim(); } catch { return ""; }
|
|
35
36
|
},
|
|
@@ -4,6 +4,7 @@ import { setPort } from "../../state.js";
|
|
|
4
4
|
import { logDir, eventsFile, callsFile, errorsFile } from "../../logs.js";
|
|
5
5
|
import { effectivePort, waitForHealth, stopDaemonIfOutdated, compareSemver, argValue } from "../policy.js";
|
|
6
6
|
import { printStatus } from "../status.js";
|
|
7
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
7
8
|
|
|
8
9
|
export async function handleStop(args) {
|
|
9
10
|
if (!(args.includes("-stop") || args.includes("--stop"))) return false;
|
|
@@ -35,7 +36,7 @@ export async function handleRestart(args, VERSION) {
|
|
|
35
36
|
await waitForHealth(port, 4000);
|
|
36
37
|
let ok = false;
|
|
37
38
|
try {
|
|
38
|
-
const r = await
|
|
39
|
+
const r = await compatFetch(`http://127.0.0.1:${port}/health`, { signal: timeoutSignal(1200) });
|
|
39
40
|
ok = r.ok;
|
|
40
41
|
} catch {}
|
|
41
42
|
if (ok) console.log(`mslxdff v${VERSION} restarted as a background daemon (pid ${spawnedPid})`);
|
|
@@ -4,6 +4,7 @@ import { loadToken, loadGroupsJoined, saveGroupsJoined } from "../../state.js";
|
|
|
4
4
|
import { groupIs, markJoined, probeHealth, syncAllJoinedGroups } from "../group-helpers.js";
|
|
5
5
|
import { errMsg } from "../util.js";
|
|
6
6
|
import { argValue } from "../policy.js";
|
|
7
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
7
8
|
|
|
8
9
|
export async function handleGroupCreate(args) {
|
|
9
10
|
const createGroupArg = argValue(args, "-creategroup", "--creategroup") || groupIs("create", args);
|
|
@@ -50,7 +51,7 @@ export async function handleGroupCommand(args) {
|
|
|
50
51
|
process.exit(0);
|
|
51
52
|
}
|
|
52
53
|
const { token } = await loadToken();
|
|
53
|
-
const fetchImpl = (url, opts) =>
|
|
54
|
+
const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
|
|
54
55
|
for (const g of joinedList) {
|
|
55
56
|
const isLeader = !g.leaderUrl;
|
|
56
57
|
let members;
|
|
@@ -169,7 +170,7 @@ export async function handleAddToGroup(args) {
|
|
|
169
170
|
joinBody = { name, key: name, leaderUrl, myPort, token: myToken, kind: "static" };
|
|
170
171
|
}
|
|
171
172
|
try {
|
|
172
|
-
const res = await
|
|
173
|
+
const res = await compatFetch(`${leaderUrl}/v1/groups/join`, {
|
|
173
174
|
method: "POST",
|
|
174
175
|
headers: { "Content-Type": "application/json" },
|
|
175
176
|
body: JSON.stringify(joinBody),
|
|
@@ -219,7 +220,7 @@ export async function handleLeaveGroup(args) {
|
|
|
219
220
|
if (g.leaderUrl) {
|
|
220
221
|
const peersRemoved = peers.removeByGroup(g.name);
|
|
221
222
|
try {
|
|
222
|
-
const res = await
|
|
223
|
+
const res = await compatFetch(`${g.leaderUrl}/v1/groups/leave`, {
|
|
223
224
|
method: "POST",
|
|
224
225
|
headers: {
|
|
225
226
|
"Content-Type": "application/json",
|
|
@@ -7,6 +7,7 @@ import { refreshTokenForBase } from "../../../providers/cline/auth.js";
|
|
|
7
7
|
import { clineBenchOne } from "../../../bench/cline-bench.js";
|
|
8
8
|
import { workbuddyBenchOne } from "../../../bench/workbuddy-bench.js";
|
|
9
9
|
import { buildHeadersForProvider, filterBenchModels, handleVia } from "./bench-via.js";
|
|
10
|
+
import { compatFetch } from "../../../compat.js";
|
|
10
11
|
|
|
11
12
|
function parseBenchArgs(rest) {
|
|
12
13
|
const opts = { json: false, prompt: "hi", maxTokens: 32, timeoutMs: 30000, via: false, includeOpencode: false, samples: 1, apply: false };
|
|
@@ -43,10 +44,10 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
|
|
|
43
44
|
const loadBaseUrl = deps.loadProviderBaseUrl || stateMod.loadProviderBaseUrl;
|
|
44
45
|
const viaPid = _isBenchViaAll ? "bench" : String(id || "").trim();
|
|
45
46
|
if (!viaPid) { console.error("usage: mslxdff -provider <id> bench --via [--json] [--include-opencode]"); process.exit(1); }
|
|
46
|
-
await handleVia({ providerId: viaPid, opts, fetchImpl: deps.fetchImpl ||
|
|
47
|
+
await handleVia({ providerId: viaPid, opts, fetchImpl: deps.fetchImpl || compatFetch, loadConfigs, loadKeys, loadAllowed, loadBaseUrl, loadAllowAny: deps.loadProviderAllowAnyModels || stateMod.loadProviderAllowAnyModels, loadModelPicks: deps.loadModelPicks, getOnlinePeersFn: deps.getOnlinePeers });
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
49
|
-
const fetchImpl = deps.fetchImpl ||
|
|
50
|
+
const fetchImpl = deps.fetchImpl || compatFetch;
|
|
50
51
|
const loadConfigs = deps.loadProviderConfigs || (await import("../../../state.js")).loadProviderConfigs;
|
|
51
52
|
const loadKeys = deps.loadProviderKeys || (await import("../../../state.js")).loadProviderKeys;
|
|
52
53
|
const loadAllowed = deps.loadProviderAllowedModels || (await import("../../../state.js")).loadProviderAllowedModels;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compatFetch, getUndici, timeoutSignal } from "../../../compat.js";
|
|
1
2
|
export async function handleClineLogin(id, sub) {
|
|
2
3
|
if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
|
|
3
4
|
if (id !== "cline" && id !== "clinebot" && id !== "cline-bot") return false;
|
|
@@ -13,7 +14,7 @@ export async function handleClineLogin(id, sub) {
|
|
|
13
14
|
let dispatcher = null;
|
|
14
15
|
if (PROXY_URL) {
|
|
15
16
|
try {
|
|
16
|
-
const { ProxyAgent } =
|
|
17
|
+
const { ProxyAgent } = getUndici();
|
|
17
18
|
dispatcher = new ProxyAgent(PROXY_URL);
|
|
18
19
|
} catch (e) {
|
|
19
20
|
console.error(`⚠️ 代理变量 ${PROXY_URL} 不可用(${e.message}),回退直连`);
|
|
@@ -35,7 +36,7 @@ export async function handleClineLogin(id, sub) {
|
|
|
35
36
|
const body = new URLSearchParams(form).toString();
|
|
36
37
|
let res;
|
|
37
38
|
try {
|
|
38
|
-
res = await
|
|
39
|
+
res = await compatFetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, ...extraOpts, signal: timeoutSignal(20000) });
|
|
39
40
|
} catch (e) { throw new Error(`连不上 WorkOS:${netHint(e, url)}`); }
|
|
40
41
|
const txt = await res.text();
|
|
41
42
|
try { return JSON.parse(txt); } catch { throw new Error(`WorkOS 返回非 JSON: ${txt.slice(0, 200)}`); }
|
|
@@ -43,7 +44,7 @@ export async function handleClineLogin(id, sub) {
|
|
|
43
44
|
async function postJson(url, obj) {
|
|
44
45
|
let res;
|
|
45
46
|
try {
|
|
46
|
-
res = await
|
|
47
|
+
res = await compatFetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj), ...extraOpts, signal: timeoutSignal(20000) });
|
|
47
48
|
} catch (e) { throw new Error(`连不上 Cline:${netHint(e, url)}`); }
|
|
48
49
|
const txt = await res.text();
|
|
49
50
|
try { return JSON.parse(txt); } catch { throw new Error(`Cline 返回非 JSON: ${txt.slice(0, 200)}`); }
|
|
@@ -109,8 +109,8 @@ export async function handleProviderKeys(id, sub, rest, args) {
|
|
|
109
109
|
}
|
|
110
110
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
111
111
|
const { loadProviderKeys, addProviderKey } = await import("../../../state.js");
|
|
112
|
-
const
|
|
113
|
-
const rl =
|
|
112
|
+
const { createInterface } = await import("../../../readline-compat.js");
|
|
113
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
114
114
|
console.log(`Enter ${id} API keys, one per line (input hidden). Blank line to finish:`);
|
|
115
115
|
const existing = loadProviderKeys(id);
|
|
116
116
|
const collected = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compatFetch, timeoutSignal } from "../../../compat.js";
|
|
1
2
|
// mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 clinebot login)。
|
|
2
3
|
// 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
|
|
3
4
|
// 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
|
|
@@ -26,12 +27,12 @@ async function readEnvelope(res) {
|
|
|
26
27
|
return j;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export async function requestDeviceState(fetchImpl =
|
|
30
|
+
export async function requestDeviceState(fetchImpl = compatFetch) {
|
|
30
31
|
const res = await fetchImpl(`${BASE}/v2/plugin/auth/state?platform=CLI`, {
|
|
31
32
|
method: "POST",
|
|
32
33
|
headers: baseHeaders(),
|
|
33
34
|
body: "{}",
|
|
34
|
-
signal:
|
|
35
|
+
signal: timeoutSignal(20000),
|
|
35
36
|
});
|
|
36
37
|
const j = await readEnvelope(res);
|
|
37
38
|
if (j.code !== 0 || !j.data?.state || !j.data?.authUrl) {
|
|
@@ -41,12 +42,12 @@ export async function requestDeviceState(fetchImpl = globalThis.fetch) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// 单次轮询:pending(业务 code 非 0)返回 null;HTTP 5xx 抛错;成功返回 token bundle。
|
|
44
|
-
export async function pollDeviceToken(fetchImpl =
|
|
45
|
+
export async function pollDeviceToken(fetchImpl = compatFetch, state) {
|
|
45
46
|
let res;
|
|
46
47
|
try {
|
|
47
48
|
res = await fetchImpl(`${BASE}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, {
|
|
48
49
|
headers: baseHeaders(),
|
|
49
|
-
signal:
|
|
50
|
+
signal: timeoutSignal(20000),
|
|
50
51
|
});
|
|
51
52
|
} catch (e) { throw new Error(`轮询失败: ${e.message}`); }
|
|
52
53
|
if (res.status >= 500) throw new Error(`token 端点故障: HTTP ${res.status}`);
|
|
@@ -60,10 +61,10 @@ export async function pollDeviceToken(fetchImpl = globalThis.fetch, state) {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export async function fetchDeviceAccount(fetchImpl =
|
|
64
|
+
export async function fetchDeviceAccount(fetchImpl = compatFetch, state, accessToken) {
|
|
64
65
|
const res = await fetchImpl(`${BASE}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, {
|
|
65
66
|
headers: { ...baseHeaders(), Authorization: `Bearer ${accessToken}` },
|
|
66
|
-
signal:
|
|
67
|
+
signal: timeoutSignal(20000),
|
|
67
68
|
});
|
|
68
69
|
const j = await readEnvelope(res);
|
|
69
70
|
if (j.code !== 0 || !j.data?.uid) throw new Error(`获取账号失败: code=${j.code} ${j.msg || ""}`.trim());
|
|
@@ -97,7 +98,7 @@ export async function handleWorkbuddyLogin(id, sub, rest = [], deps = {}) {
|
|
|
97
98
|
process.exit(0);
|
|
98
99
|
}
|
|
99
100
|
if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
|
|
100
|
-
const fetchImpl = deps.fetchImpl ||
|
|
101
|
+
const fetchImpl = deps.fetchImpl || compatFetch;
|
|
101
102
|
|
|
102
103
|
console.log("🚀 启动 WorkBuddy 设备授权流程...\n");
|
|
103
104
|
let dev;
|
package/src/cli/group-helpers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadToken, loadGroupsJoined, saveGroupsJoined } from "../state.js";
|
|
2
2
|
import { refreshGroupMembers, syncPeersFromMembers } from "../groups.js";
|
|
3
3
|
import { errMsg } from "./util.js";
|
|
4
|
+
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
const HEALTH_TIMEOUT_MS = 4000;
|
|
6
7
|
|
|
@@ -28,7 +29,7 @@ export async function probeHealth({ id, url, kind, lastSeen, publicIp } = {}) {
|
|
|
28
29
|
const base = String(url).replace(/\/+$/, "");
|
|
29
30
|
const startedAt = Date.now();
|
|
30
31
|
try {
|
|
31
|
-
const res = await
|
|
32
|
+
const res = await compatFetch(`${base}/health`, { signal: timeoutSignal(HEALTH_TIMEOUT_MS) });
|
|
32
33
|
if (!res.ok) return { id, url: base, fail: `HTTP ${res.status}`, rank: 2 };
|
|
33
34
|
return { id, url: base, ms: Date.now() - startedAt, rank: 0 };
|
|
34
35
|
} catch (err) {
|
package/src/cli/policy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readPid, readPidVersion, isPidAlive, stopDaemon } from "../daemon.js";
|
|
2
2
|
import { resolvePort } from "../server.js";
|
|
3
3
|
import { getPort, loadGroupsJoined } from "../state.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
export function compareSemver(a, b) {
|
|
6
7
|
const pa = a.split(".").map((x) => parseInt(x, 10) || 0);
|
|
@@ -39,7 +40,7 @@ export async function waitForHealth(port, timeoutMs) {
|
|
|
39
40
|
const start = Date.now();
|
|
40
41
|
while (Date.now() - start < timeoutMs) {
|
|
41
42
|
try {
|
|
42
|
-
const res = await
|
|
43
|
+
const res = await compatFetch(`http://127.0.0.1:${port}/health`);
|
|
43
44
|
if (res.ok) return;
|
|
44
45
|
} catch {
|
|
45
46
|
// not up yet
|
package/src/cli/status.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fmtShanghaiYMDHM } from "../time.js";
|
|
|
14
14
|
import { fmtStatus, fmtUptime, fmtTs } from "./format.js";
|
|
15
15
|
import { compareSemver } from "./policy.js";
|
|
16
16
|
import { buildProviderRows, formatProviderRow, formatProviderSection } from "./provider-row.js";
|
|
17
|
+
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
17
18
|
|
|
18
19
|
export async function printStatus(VERSION) {
|
|
19
20
|
const daemon = readPid();
|
|
@@ -37,7 +38,7 @@ export async function printStatus(VERSION) {
|
|
|
37
38
|
let healthLine = "";
|
|
38
39
|
try {
|
|
39
40
|
const t0 = Date.now();
|
|
40
|
-
const r = await
|
|
41
|
+
const r = await compatFetch(`http://127.0.0.1:${port}/health`, { signal: timeoutSignal(1200) });
|
|
41
42
|
const ms = Date.now() - t0;
|
|
42
43
|
healthLine = r.ok ? `health ok ${ms}ms` : `health HTTP ${r.status} ${ms}ms`;
|
|
43
44
|
} catch (e) {
|
|
@@ -175,7 +176,7 @@ export async function printStatus(VERSION) {
|
|
|
175
176
|
if (isLeader) {
|
|
176
177
|
members = groups.list()[g.name]?.members ?? {};
|
|
177
178
|
} else {
|
|
178
|
-
const fetchImpl = (url, opts) =>
|
|
179
|
+
const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
|
|
179
180
|
try {
|
|
180
181
|
members = await refreshGroupMembers(g.name, {
|
|
181
182
|
leaderUrl: g.leaderUrl,
|
package/src/compat.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// 老 Node 兼容层(engines >=16):Node 16 缺 globalThis.fetch(18+)、
|
|
2
|
+
// AbortSignal.timeout(17.3+)、structuredClone(17+);裸全局 crypto 是
|
|
3
|
+
// WebCrypto,其 randomUUID 要 19+,只有 node:crypto 的 14.17+ 可用。
|
|
4
|
+
// undici 8 需 Node 22+,依赖已锁 5.x(老 Node 可跑,API 兼容我们用到的面)。
|
|
5
|
+
// 统一从这里取,源码里勿直接用这些全局。
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
let _undici = null;
|
|
9
|
+
try { _undici = await import("undici"); } catch {}
|
|
10
|
+
|
|
11
|
+
export function getUndici() {
|
|
12
|
+
return _undici || {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _nativeFetch = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null;
|
|
16
|
+
|
|
17
|
+
export const compatFetch = _undici?.fetch || _nativeFetch || function noFetch() {
|
|
18
|
+
throw new Error("当前环境没有可用的 fetch:Node <18 且 undici 未安装。请 npm i undici@^5 或升级 Node >=18。");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function timeoutSignal(ms) {
|
|
22
|
+
if (typeof AbortSignal?.timeout === "function") return AbortSignal.timeout(ms);
|
|
23
|
+
const ctl = new AbortController();
|
|
24
|
+
const t = setTimeout(() => ctl.abort(new Error(`aborted (timeout ${ms}ms)`)), ms);
|
|
25
|
+
if (typeof t.unref === "function") t.unref();
|
|
26
|
+
return ctl.signal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clone(v) {
|
|
30
|
+
if (typeof structuredClone === "function") return structuredClone(v);
|
|
31
|
+
if (v === null || typeof v !== "object") return v;
|
|
32
|
+
return JSON.parse(JSON.stringify(v));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const uuid = () => randomUUID();
|
package/src/free-watcher.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compatFetch } from "./compat.js";
|
|
1
2
|
const V2EX_LATEST = "https://www.v2ex.com/api/topics/latest.json";
|
|
2
3
|
const V2EX_HOT = "https://www.v2ex.com/api/topics/hot.json";
|
|
3
4
|
|
|
@@ -15,7 +16,7 @@ async function fetchJson(url, timeoutMs = 6000) {
|
|
|
15
16
|
const ctrl = new AbortController();
|
|
16
17
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
17
18
|
try {
|
|
18
|
-
const res = await
|
|
19
|
+
const res = await compatFetch(url, {
|
|
19
20
|
headers: { "User-Agent": "mslxdff/free-watcher", Accept: "application/json" },
|
|
20
21
|
signal: ctrl.signal,
|
|
21
22
|
});
|
package/src/models.js
CHANGED
|
@@ -3,6 +3,7 @@ const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
|
3
3
|
const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
|
|
4
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
+
import { compatFetch } from "./compat.js";
|
|
6
7
|
|
|
7
8
|
export function isFreeModel(id) {
|
|
8
9
|
return (typeof id === "string" && id.endsWith("-free")) ||
|
|
@@ -197,7 +198,7 @@ async function attemptFetch(url, headers, connectTimeoutMs) {
|
|
|
197
198
|
const controller = new AbortController();
|
|
198
199
|
const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
199
200
|
try {
|
|
200
|
-
return await
|
|
201
|
+
return await compatFetch(url, { headers, signal: controller.signal });
|
|
201
202
|
} catch (err) {
|
|
202
203
|
return err;
|
|
203
204
|
} finally {
|
package/src/providers/base.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { createKeyRing } from "./keyring.js";
|
|
2
2
|
import { joinModelId } from "./model-id.js";
|
|
3
|
+
import { getUndici as compatGetUndici, compatFetch } from "../compat.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
let UndiciFetch = null;
|
|
6
|
-
try {
|
|
7
|
-
const mod = await import("undici");
|
|
8
|
-
UndiciAgent = mod.Agent;
|
|
9
|
-
UndiciFetch = mod.fetch;
|
|
10
|
-
} catch {}
|
|
5
|
+
const { fetch: UndiciFetch, Agent: UndiciAgent } = compatGetUndici();
|
|
11
6
|
|
|
12
7
|
export function envInt(name, fallback) {
|
|
13
8
|
const v = Number(process.env[name]);
|
|
@@ -27,7 +22,7 @@ export function sleep(ms) {
|
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
export function getUndici() {
|
|
30
|
-
return { UndiciAgent, UndiciFetch };
|
|
25
|
+
return { UndiciAgent, UndiciFetch, fetch: UndiciFetch || compatFetch, Agent: UndiciAgent };
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
export function createAgent({ keepAliveTimeout = 30_000, keepAliveMaxTimeout = 60_000, connections = 20 } = {}) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { joinUrl, sleep } from "../base.js";
|
|
2
|
+
import { compatFetch } from "../../compat.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Cline Token 池:多账号 round-robin + refresh 换 accessToken + 冷却 + 队列
|
|
@@ -22,7 +23,7 @@ export function parseCooldown(body, status) {
|
|
|
22
23
|
* 一次性 refresh:bench/诊断用,不落盘、不建池。
|
|
23
24
|
* 返回 accessToken 或 null。
|
|
24
25
|
*/
|
|
25
|
-
export async function refreshTokenForBase({ refreshToken, baseUrl = "https://api.cline.bot", fetchImpl =
|
|
26
|
+
export async function refreshTokenForBase({ refreshToken, baseUrl = "https://api.cline.bot", fetchImpl = compatFetch, dispatcher } = {}) {
|
|
26
27
|
const rt = String(refreshToken || "").trim();
|
|
27
28
|
if (!rt) return null;
|
|
28
29
|
const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "") || "https://api.cline.bot";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
1
2
|
// WorkBuddy 每日签到 core:纯逻辑 + fetch 注入,被 daemon scheduler 与 CLI 共用。
|
|
2
3
|
// 接口:双域 POST /v2/billing/meter/daily-checkin,code 0=新签成功,10001=已签(幂等成功)。
|
|
3
4
|
|
|
@@ -21,7 +22,7 @@ function checkinHeaders({ at, uid, domain, enterpriseId }) {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export async function checkinOne({ uid, at, domain, enterpriseId, fetchImpl =
|
|
25
|
+
export async function checkinOne({ uid, at, domain, enterpriseId, fetchImpl = compatFetch, timeoutMs = 15000 } = {}) {
|
|
25
26
|
const headers = checkinHeaders({ at, uid, domain, enterpriseId });
|
|
26
27
|
let last = null;
|
|
27
28
|
for (const url of CHECKIN_ENDPOINTS) {
|
|
@@ -30,7 +31,7 @@ export async function checkinOne({ uid, at, domain, enterpriseId, fetchImpl = gl
|
|
|
30
31
|
method: "POST",
|
|
31
32
|
headers,
|
|
32
33
|
body: "{}",
|
|
33
|
-
signal:
|
|
34
|
+
signal: timeoutSignal(timeoutMs),
|
|
34
35
|
});
|
|
35
36
|
const text = await res.text();
|
|
36
37
|
let j;
|
|
@@ -49,7 +50,7 @@ export async function checkinOne({ uid, at, domain, enterpriseId, fetchImpl = gl
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// 多账号签到:逐个独立,失败不挡别人;返回按 uid 排序的 rows。
|
|
52
|
-
export async function checkinAll({ accounts = [], fetchImpl =
|
|
53
|
+
export async function checkinAll({ accounts = [], fetchImpl = compatFetch, concurrency = 3, onAccount } = {}) {
|
|
53
54
|
const list = (Array.isArray(accounts) ? accounts : []).filter((a) => a && a.uid && a.at);
|
|
54
55
|
const results = new Array(list.length);
|
|
55
56
|
let idx = 0;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// readline 兼容层:`node:readline/promises` 需要 Node 17+,老 Node(如 VPS 的 16)
|
|
2
|
+
// 会 `ERR_UNKNOWN_BUILTIN_MODULE` 直接崩在 import 期,连人话报错都来不及打。
|
|
3
|
+
// 这里只用回调版 `node:readline`(Node 12+ 全有),把 `question` 包成 Promise;
|
|
4
|
+
// `prompt()/close()/for await` 回调版原生即有,对我们用到的部分行为一致。
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
|
|
7
|
+
export function createInterface(opts) {
|
|
8
|
+
const rl = readline.createInterface(opts);
|
|
9
|
+
const ask = rl.question.bind(rl);
|
|
10
|
+
rl.question = (query) => new Promise((resolve) => ask(query, (ans) => resolve(ans)));
|
|
11
|
+
return rl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nodeMajor() {
|
|
15
|
+
return Number(String(process.versions?.node || "0").split(".")[0]) || 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// -chat 依赖 fetch(<18 由 src/compat.js 用 undici polyfill),低于 engines 下限
|
|
19
|
+
// 直接给一句人话。返回 true=通过,false=已打印升级指引。
|
|
20
|
+
export function assertChatNode({ min = 16 } = {}) {
|
|
21
|
+
const major = nodeMajor();
|
|
22
|
+
if (major >= min) return true;
|
|
23
|
+
console.error(`Node 版本过旧(当前 v${process.versions.node}),-chat 需要 Node ${min}+(推荐 20+)。`);
|
|
24
|
+
console.error("先升级 Node 再重试:nvm install 20 && nvm use 20,或到 https://nodejs.org/ 下 LTS。");
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
@@ -5,6 +5,7 @@ import { getViaRoute } from "../../bench/via-routes.js";
|
|
|
5
5
|
import { loadProviderKeys } from "../../state.js";
|
|
6
6
|
import { SHARE_KEYS_HEADER } from "../../providers/share-keys.js";
|
|
7
7
|
import { errMsg } from "../helpers.js";
|
|
8
|
+
import { compatFetch } from "../../compat.js";
|
|
8
9
|
|
|
9
10
|
function shortLabel(p) {
|
|
10
11
|
const raw = String(p?.name || p?.id || p?.url || "").trim();
|
|
@@ -81,7 +82,7 @@ export async function handleViaRoute({
|
|
|
81
82
|
// workbuddyUid 透传
|
|
82
83
|
if (handlerCtx.workbuddyUid) headers["x-mslxdff-workbuddy-uid"] = handlerCtx.workbuddyUid;
|
|
83
84
|
evt("via-route-request", { reqId: handlerCtx.reqId, peer: peer.url, model, hops: hops + 1, hasShare: Boolean(shareHeader) });
|
|
84
|
-
upRes = await
|
|
85
|
+
upRes = await compatFetch(`${String(peer.url).replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
85
86
|
method: "POST",
|
|
86
87
|
headers,
|
|
87
88
|
body: JSON.stringify({ ...body, model }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clientIp, json, readBody, parseHops, errMsg } from "./helpers.js";
|
|
2
2
|
import { DEFAULT_MAX_HOPS } from "../peers.js";
|
|
3
3
|
import { enqueueRelay, dequeueRelayForPoll, resolveRelay, subscribeStream, unsubscribeStream } from "./relay-queue.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
export async function heartbeatHandler({ req, res, groups, bus, logs }) {
|
|
6
7
|
const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
|
|
@@ -162,7 +163,7 @@ export async function forwardHandler({ req, res, groups, bus, logs }) {
|
|
|
162
163
|
if (!isBb) {
|
|
163
164
|
try {
|
|
164
165
|
const fwdBody = body.body || body;
|
|
165
|
-
const r = await
|
|
166
|
+
const r = await compatFetch(`${targetMember.url}/v1/chat/completions`, {
|
|
166
167
|
method: "POST",
|
|
167
168
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${targetMember.token || ""}`, "x-mslxdff-hops": String(hops + 1), "x-mslxdff-model-lock": fwdBody.model || "", "Accept": "text/event-stream" },
|
|
168
169
|
body: JSON.stringify(fwdBody),
|
package/src/routes/peers.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isAutoModel } from "../auto.js";
|
|
|
3
3
|
import { errMsg } from "./helpers.js";
|
|
4
4
|
import { runHook } from "../plugins.js";
|
|
5
5
|
import { buildShareKeysHeader, SHARE_KEYS_HEADER } from "../providers/share-keys.js";
|
|
6
|
+
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
6
7
|
|
|
7
8
|
const PEER_TIMEOUT_MS = 30_000;
|
|
8
9
|
const PEER_STATUS_TIMEOUT_MS = 2_000;
|
|
@@ -36,7 +37,7 @@ export async function peerHealthyModels(peer, { timeoutMs = PEER_STATUS_TIMEOUT_
|
|
|
36
37
|
"Authorization": `Bearer ${peer.token || ""}`,
|
|
37
38
|
"Accept": "application/json",
|
|
38
39
|
},
|
|
39
|
-
signal:
|
|
40
|
+
signal: timeoutSignal(timeoutMs),
|
|
40
41
|
});
|
|
41
42
|
if (!res.ok) return [];
|
|
42
43
|
const j = await res.json().catch(() => ({}));
|
|
@@ -74,7 +75,7 @@ async function forwardToPeer(peer, body, model, hops) {
|
|
|
74
75
|
// ADR-0008:该模型命中的供应商若开启 share → 附带瞬时 key 给组员借用(opencode 恒排除)
|
|
75
76
|
const shareHeader = buildShareKeysHeader(model);
|
|
76
77
|
if (shareHeader) headers[SHARE_KEYS_HEADER] = shareHeader;
|
|
77
|
-
return await
|
|
78
|
+
return await compatFetch(`${peer.url}/v1/chat/completions`, {
|
|
78
79
|
method: "POST",
|
|
79
80
|
headers,
|
|
80
81
|
body: JSON.stringify({ ...body, model }),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadGroupsJoined } from "../state.js";
|
|
2
|
+
import { compatFetch } from "../compat.js";
|
|
2
3
|
|
|
3
4
|
const relayPending = new Map();
|
|
4
5
|
const relayPendingByReqId = new Map();
|
|
@@ -118,7 +119,7 @@ export async function tryBroadbandRelay({ groups, token: myToken, model, body, h
|
|
|
118
119
|
try {
|
|
119
120
|
const controller = new AbortController();
|
|
120
121
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
121
|
-
const r = await
|
|
122
|
+
const r = await compatFetch(`${g.leaderUrl}/v1/groups/join`, {
|
|
122
123
|
method: "POST",
|
|
123
124
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${myToken}` },
|
|
124
125
|
body: JSON.stringify({ name: g.name, memberName: g.memberName, url: g.myUrl, token: myToken }),
|
|
@@ -154,7 +155,7 @@ export async function tryBroadbandRelay({ groups, token: myToken, model, body, h
|
|
|
154
155
|
} else {
|
|
155
156
|
const ctrl = new AbortController();
|
|
156
157
|
const t = setTimeout(() => ctrl.abort(), 35_000);
|
|
157
|
-
const r = await
|
|
158
|
+
const r = await compatFetch(`${cand.leaderUrl}/v1/groups/relay/forward`, {
|
|
158
159
|
method: "POST",
|
|
159
160
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${myToken}`, "x-mslxdff-hops": String(hops + 1) },
|
|
160
161
|
body: JSON.stringify({ group: cand.group, target: cand.target, body: { ...body, model }, hops: hops + 1, reqId }),
|
package/src/routes/relay.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readBody, json, authorized } from "./helpers.js";
|
|
2
|
+
import { compatFetch } from "../compat.js";
|
|
2
3
|
|
|
3
4
|
// POST /v1/relay 纯网络中继:A 把 targetUrl+headers+body 发给 B,B 原样 fetch 到上游再回给 A
|
|
4
5
|
// B 侧不查本地 providerConfigs、不做 model 前缀路由、不验 allowlist,仅当 TCP 出口
|
|
@@ -42,7 +43,7 @@ export async function relayHandler({ req, res, token }) {
|
|
|
42
43
|
const timer = setTimeout(() => controller.abort(new Error("relay timeout 30000ms")), 30000);
|
|
43
44
|
try {
|
|
44
45
|
const fetchBody = rawBody == null ? undefined : (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
|
|
45
|
-
const r = await
|
|
46
|
+
const r = await compatFetch(targetUrl, { method, headers: fwdHeaders, body: fetchBody, signal: controller.signal });
|
|
46
47
|
const txt = await r.text();
|
|
47
48
|
// 原样回透:状态码 + 头(仅透 content-type) + body
|
|
48
49
|
res.statusCode = r.status;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errMsg } from "../cli/util.js";
|
|
2
|
+
import { compatFetch } from "../compat.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 宽带 SSE 长连 — 单组成员订阅 + 指数退避重连 + ensure 定时器。
|
|
@@ -17,7 +18,7 @@ export function startBroadbandStream({ token, upstream, execAndPost, broadbandGr
|
|
|
17
18
|
const controller = new AbortController();
|
|
18
19
|
abort = controller;
|
|
19
20
|
try {
|
|
20
|
-
const res = await
|
|
21
|
+
const res = await compatFetch(url, {
|
|
21
22
|
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
22
23
|
signal: controller.signal,
|
|
23
24
|
});
|
package/src/runtime/broadband.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadGroupsJoined } from "../state.js";
|
|
2
2
|
import { errMsg } from "../cli/util.js";
|
|
3
3
|
import { startBroadbandStream } from "./broadband-stream.js";
|
|
4
|
+
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 宽带中继 — poll 模式(heartbeat + poll)与 stream 模式分发。
|
|
@@ -39,22 +40,22 @@ export function startBroadband({ token, upstream }) {
|
|
|
39
40
|
result = { status: 502, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: errMsg(err) }) };
|
|
40
41
|
}
|
|
41
42
|
try {
|
|
42
|
-
await
|
|
43
|
+
await compatFetch(`${g.leaderUrl}/v1/groups/relay/result`, {
|
|
43
44
|
method: "POST",
|
|
44
45
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
45
46
|
body: JSON.stringify({ name: g.name, group: g.name, reqId, result }),
|
|
46
|
-
signal:
|
|
47
|
+
signal: timeoutSignal(5000),
|
|
47
48
|
});
|
|
48
49
|
} catch {}
|
|
49
50
|
};
|
|
50
51
|
const doHeartbeat = async () => {
|
|
51
52
|
for (const g of broadbandGroups()) {
|
|
52
53
|
try {
|
|
53
|
-
const res = await
|
|
54
|
+
const res = await compatFetch(`${g.leaderUrl}/v1/groups/relay/heartbeat`, {
|
|
54
55
|
method: "POST",
|
|
55
56
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
56
57
|
body: JSON.stringify({ name: g.name, group: g.name }),
|
|
57
|
-
signal:
|
|
58
|
+
signal: timeoutSignal(5000),
|
|
58
59
|
});
|
|
59
60
|
if (!res.ok) {
|
|
60
61
|
const txt = await res.text().catch(() => "");
|
|
@@ -68,11 +69,11 @@ export function startBroadband({ token, upstream }) {
|
|
|
68
69
|
const doPoll = async () => {
|
|
69
70
|
for (const g of broadbandGroups()) {
|
|
70
71
|
try {
|
|
71
|
-
const pollRes = await
|
|
72
|
+
const pollRes = await compatFetch(`${g.leaderUrl}/v1/groups/relay/poll`, {
|
|
72
73
|
method: "POST",
|
|
73
74
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
74
75
|
body: JSON.stringify({ name: g.name, group: g.name }),
|
|
75
|
-
signal:
|
|
76
|
+
signal: timeoutSignal(8000),
|
|
76
77
|
});
|
|
77
78
|
if (!pollRes.ok) continue;
|
|
78
79
|
const data = await pollRes.json().catch(() => ({}));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compatFetch } from "../compat.js";
|
|
1
2
|
// daemon 内置 WorkBuddy 每日自动签到(A 方案):多账号全签 + 启动补签 + 每日定时。
|
|
2
3
|
// 开关:MSLXDFF_WORKBUDDY_CHECKIN=0 关闭(默认开);时间:MSLXDFF_WORKBUDDY_CHECKIN_HOUR(默认 9 点本地时)。
|
|
3
4
|
// 幂等:上游 code 10001(今天已签)视为成功;落盘 workbuddyCheckin {date} 防重复。
|
|
@@ -68,7 +69,7 @@ export async function setupWorkbuddyCheckin({ bus, logs } = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
// 先给过期 token 续期(复用 chat 同款 refresh,落盘回 state)
|
|
70
71
|
const authService = createAuthService({
|
|
71
|
-
fetchImpl:
|
|
72
|
+
fetchImpl: compatFetch,
|
|
72
73
|
store: { keys, authList },
|
|
73
74
|
});
|
|
74
75
|
await Promise.all(authList.map(async (auth, i) => {
|
package/src/state/memory.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 用于 `—test-concurrency>1` 并行与单测快速路径。
|
|
6
6
|
*/
|
|
7
7
|
import { mergeState, COLD_WINS } from "./merge.js";
|
|
8
|
+
import { clone } from "../compat.js";
|
|
8
9
|
|
|
9
10
|
const memCache = new Map(); // file -> { data, dirty, timer, mtimeMs }
|
|
10
11
|
const memDisk = new Map(); // file -> { obj, mtimeMs }
|
|
@@ -20,7 +21,7 @@ function getEntry(file) {
|
|
|
20
21
|
|
|
21
22
|
function loadFromDiskMem(file) {
|
|
22
23
|
const v = memDisk.get(file);
|
|
23
|
-
return v ?
|
|
24
|
+
return v ? clone(v.obj) : {};
|
|
24
25
|
}
|
|
25
26
|
function getMtimeMem(file) {
|
|
26
27
|
const v = memDisk.get(file);
|
|
@@ -31,7 +32,7 @@ function atomicWriteSyncMem(file, data) {
|
|
|
31
32
|
// ensure monotonic mtime
|
|
32
33
|
const prev = memDisk.get(file)?.mtimeMs || 0;
|
|
33
34
|
const mtime = now <= prev ? prev + 1 : now;
|
|
34
|
-
memDisk.set(file, { obj:
|
|
35
|
+
memDisk.set(file, { obj: clone(data), mtimeMs: mtime });
|
|
35
36
|
return mtime;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -49,7 +50,7 @@ function readState(file) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
const disk = loadFromDiskMem(file);
|
|
52
|
-
e.data = disk && typeof disk === "object" ?
|
|
53
|
+
e.data = disk && typeof disk === "object" ? clone(disk) : {};
|
|
53
54
|
e.mtimeMs = getMtimeMem(file) || Date.now();
|
|
54
55
|
e.dirty = false;
|
|
55
56
|
return e.data;
|
|
@@ -108,14 +109,14 @@ export function createMemoryState(file = "mem://default") {
|
|
|
108
109
|
return {
|
|
109
110
|
loadModelErrors: () => {
|
|
110
111
|
const v = readState(f).modelErrors;
|
|
111
|
-
return v && typeof v === "object" && !Array.isArray(v) ?
|
|
112
|
+
return v && typeof v === "object" && !Array.isArray(v) ? clone(v) : {};
|
|
112
113
|
},
|
|
113
|
-
saveModelErrors: (errors) => { writeStateDeferred(f, { modelErrors:
|
|
114
|
+
saveModelErrors: (errors) => { writeStateDeferred(f, { modelErrors: clone(errors) }); return errors; },
|
|
114
115
|
loadModelLatencies: () => {
|
|
115
116
|
const v = readState(f).modelLatencies;
|
|
116
|
-
return v && typeof v === "object" && !Array.isArray(v) ?
|
|
117
|
+
return v && typeof v === "object" && !Array.isArray(v) ? clone(v) : {};
|
|
117
118
|
},
|
|
118
|
-
saveModelLatencies: (v) => { writeStateDeferred(f, { modelLatencies:
|
|
119
|
+
saveModelLatencies: (v) => { writeStateDeferred(f, { modelLatencies: clone(v) }); return v; },
|
|
119
120
|
loadProviderKeys: (id) => {
|
|
120
121
|
const keys = readState(f).providerKeys;
|
|
121
122
|
const v = keys && typeof keys === "object" ? keys[id] : undefined;
|
|
@@ -147,9 +148,9 @@ export function createMemoryState(file = "mem://default") {
|
|
|
147
148
|
},
|
|
148
149
|
loadPeers: () => {
|
|
149
150
|
const v = readState(f).peers;
|
|
150
|
-
return Array.isArray(v) ?
|
|
151
|
+
return Array.isArray(v) ? clone(v) : [];
|
|
151
152
|
},
|
|
152
|
-
savePeers: (peers) => { writeStateImmediate(f, { peers:
|
|
153
|
+
savePeers: (peers) => { writeStateImmediate(f, { peers: clone(peers) }); return peers; },
|
|
153
154
|
flushSync: () => flushStateSync(f),
|
|
154
155
|
clear: () => clearStateCache(f),
|
|
155
156
|
_memDisk: memDisk,
|
package/src/transport/index.js
CHANGED
|
@@ -2,12 +2,9 @@ import { performance } from "node:perf_hooks";
|
|
|
2
2
|
import { resolveRetry, sleep, backoffDelay } from "./retry.js";
|
|
3
3
|
import { createSseParser } from "./sse.js";
|
|
4
4
|
import { createPool } from "./pool.js";
|
|
5
|
+
import { compatFetch, getUndici } from "../compat.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const mod = await import("undici");
|
|
9
|
-
UndiciFetch = mod.fetch;
|
|
10
|
-
} catch {}
|
|
7
|
+
const UndiciFetch = getUndici().fetch;
|
|
11
8
|
|
|
12
9
|
const DEFAULT_RETRY = {
|
|
13
10
|
network: { attempts: 2, delayMs: 300 },
|
|
@@ -27,7 +24,7 @@ export function createTransport({
|
|
|
27
24
|
retry: defaultRetry = DEFAULT_RETRY,
|
|
28
25
|
hooks,
|
|
29
26
|
} = {}) {
|
|
30
|
-
if (!fetchImpl) fetchImpl = UndiciFetch ||
|
|
27
|
+
if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
|
|
31
28
|
const pool = keepAlive && !extDispatcher ? createPool({ keepAlive }) : null;
|
|
32
29
|
const getDispatcher = () => extDispatcher || pool?.dispatcher || null;
|
|
33
30
|
|
package/src/transport/pool.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
UndiciAgent = mod.Agent;
|
|
5
|
-
} catch {
|
|
6
|
-
UndiciAgent = null;
|
|
7
|
-
}
|
|
1
|
+
import { getUndici } from "../compat.js";
|
|
2
|
+
|
|
3
|
+
const UndiciAgent = getUndici().Agent;
|
|
8
4
|
|
|
9
5
|
function envInt(name, fallback) {
|
|
10
6
|
const v = Number(process.env[name]);
|
package/src/upstream.js
CHANGED
|
@@ -7,9 +7,10 @@ import { isFreeModel } from "./models.js";
|
|
|
7
7
|
import { fmtShanghaiYMDHMS } from "./time.js";
|
|
8
8
|
import { createTransport } from "./transport/index.js";
|
|
9
9
|
import { isResponsesModel, chatToResponsesBody, toChatResponse } from "./upstream-responses.js";
|
|
10
|
+
import { uuid } from "./compat.js";
|
|
10
11
|
|
|
11
12
|
function genId(prefix) {
|
|
12
|
-
return `${prefix}${
|
|
13
|
+
return `${prefix}${uuid().replace(/-/g, "")}`;
|
|
13
14
|
}
|
|
14
15
|
function envInt(name, fallback) {
|
|
15
16
|
const v = Number(process.env[name]);
|