mslxdff 0.1.236 → 0.1.237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mslxdff.js +3 -3
- package/package.json +1 -1
- package/src/auto.js +264 -264
- package/src/autostart.js +62 -15
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +155 -155
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +282 -282
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +293 -293
- package/src/peers.js +389 -389
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +292 -292
- package/src/providers/qoder/index.js +311 -311
- package/src/providers/qoder/sse.js +233 -233
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/const.js +219 -219
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/chat.js +298 -298
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +158 -158
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -364
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/runtime/server-lifecycle.js +34 -10
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +355 -355
- package/docs/cli_help_mini.md +0 -149
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/runtime/bootstrap.js
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import { appendCall, appendError, appendEvent, appendTimeline } from "../logs.js";
|
|
2
|
-
import { setupProviders } from "./providers-setup.js";
|
|
3
|
-
import { startServerLifecycle } from "./server-lifecycle.js";
|
|
4
|
-
import { appendModelTrace } from "../model-trace.js";
|
|
5
|
-
import { startGroupSync } from "./group-sync.js";
|
|
6
|
-
import { startBroadband } from "./broadband.js";
|
|
7
|
-
import { installLogStamp } from "./log-stamp.js";
|
|
8
|
-
|
|
9
|
-
const logs = { appendCall, appendError, appendEvent, appendTimeline, appendModelTrace };
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* daemon 启动门面 — 仅编排:组装世界 → 服务生命周期 → 群组同步 → 宽带中继 → 自更新。
|
|
13
|
-
* 重活下沉 providers-setup / server-lifecycle / group-sync / broadband(-stream)。
|
|
14
|
-
*/
|
|
15
|
-
export async function startDaemonMain(VERSION) {
|
|
16
|
-
// 每行带 [时间 pid]:一份 daemon.log 被多进程/多版本写过时,这是唯一能回答「谁写的、哪一轮」的东西
|
|
17
|
-
if (process.env.MSLXDFF_DAEMON === "1") installLogStamp();
|
|
18
|
-
const { installLifecycleLog } = await import("./lifecycle-log.js");
|
|
19
|
-
installLifecycleLog({ version: VERSION, logs });
|
|
20
|
-
// 接管:内存里若还有旧的 mslxdff daemon,先请它退出。它不在 daemon.pid 里(-restart 结构性碰不到它),
|
|
21
|
-
// 两个 daemon 会互抢 warp-pool 的端口(真机 2026-10-17:旧 daemon 活了一整天,日志只显示「起一条死一条」)。
|
|
22
|
-
// 必须早于 setupProviders —— 那一步才建池抢端口。debug 前台有自己的收口逻辑,不在这里抢。
|
|
23
|
-
if (process.env.MSLXDFF_DAEMON === "1" && process.env.MSLXDFF_DEBUG !== "1") {
|
|
24
|
-
try {
|
|
25
|
-
const { takeoverOlderDaemons } = await import("../daemon.js");
|
|
26
|
-
const r = await takeoverOlderDaemons({ log: (m) => console.log(m) });
|
|
27
|
-
if (!r.killed.length && !r.skipped.length && !r.remaining.length) {
|
|
28
|
-
console.log("[daemon] 内存里没有别的 mslxdff daemon(扫过一遍 /proc)—— 本进程独占出口池端口");
|
|
29
|
-
}
|
|
30
|
-
} catch (e) {
|
|
31
|
-
console.log(`[daemon] 接管旧 daemon 失败(不阻断启动):${String(e?.message || e)}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const ctx = await setupProviders();
|
|
35
|
-
const { srv, bus } = await startServerLifecycle({ VERSION, ...ctx, logs });
|
|
36
|
-
void srv;
|
|
37
|
-
startGroupSync({ peers: ctx.peers, groups: ctx.groups, logs });
|
|
38
|
-
startBroadband({ token: ctx.token, upstream: ctx.upstream, logs });
|
|
39
|
-
|
|
40
|
-
// 保热心跳:每 ≤120s 经 **chat 同池**访问每个组员(/health) + 每个 picks 供应商(preheat),
|
|
41
|
-
// 防「久未使用 → 首发 fetch failed / 上游冷启动」(现网 2026-10-18 16:16 三组员同时 fetch failed)。
|
|
42
|
-
{
|
|
43
|
-
const { startKeepWarm } = await import("./keep-warm.js");
|
|
44
|
-
const { loadModelPicks } = await import("../state.js");
|
|
45
|
-
startKeepWarm({ peers: ctx.peers, providers: ctx.providers, upstream: ctx.upstream, logs, loadModelPicks: () => loadModelPicks({}) });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const { setupAutoUpdate } = await import("./auto-update.js");
|
|
49
|
-
setupAutoUpdate({ VERSION, bus, logs });
|
|
50
|
-
|
|
51
|
-
const { setupWorkbuddyCheckin } = await import("./workbuddy-checkin.js");
|
|
52
|
-
void setupWorkbuddyCheckin({ bus, logs }).catch((e) => {
|
|
53
|
-
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const { setupTraeworkCheckin } = await import("./traework-checkin.js");
|
|
57
|
-
void setupTraeworkCheckin({ bus, logs }).catch((e) => {
|
|
58
|
-
try { logs.appendEvent({ ts: Date.now(), type: "traework-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const { setupQoderCheckin } = await import("./qoder-checkin.js");
|
|
62
|
-
void setupQoderCheckin({ bus, logs }).catch((e) => {
|
|
63
|
-
try { logs.appendEvent({ ts: Date.now(), type: "qoder-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const { setupRaccoonCheckin } = await import("./raccoon-checkin.js");
|
|
67
|
-
void setupRaccoonCheckin({ bus, logs }).catch((e) => {
|
|
68
|
-
try { logs.appendEvent({ ts: Date.now(), type: "raccoon-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
|
|
72
|
-
void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
|
|
73
|
-
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
1
|
+
import { appendCall, appendError, appendEvent, appendTimeline } from "../logs.js";
|
|
2
|
+
import { setupProviders } from "./providers-setup.js";
|
|
3
|
+
import { startServerLifecycle } from "./server-lifecycle.js";
|
|
4
|
+
import { appendModelTrace } from "../model-trace.js";
|
|
5
|
+
import { startGroupSync } from "./group-sync.js";
|
|
6
|
+
import { startBroadband } from "./broadband.js";
|
|
7
|
+
import { installLogStamp } from "./log-stamp.js";
|
|
8
|
+
|
|
9
|
+
const logs = { appendCall, appendError, appendEvent, appendTimeline, appendModelTrace };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* daemon 启动门面 — 仅编排:组装世界 → 服务生命周期 → 群组同步 → 宽带中继 → 自更新。
|
|
13
|
+
* 重活下沉 providers-setup / server-lifecycle / group-sync / broadband(-stream)。
|
|
14
|
+
*/
|
|
15
|
+
export async function startDaemonMain(VERSION) {
|
|
16
|
+
// 每行带 [时间 pid]:一份 daemon.log 被多进程/多版本写过时,这是唯一能回答「谁写的、哪一轮」的东西
|
|
17
|
+
if (process.env.MSLXDFF_DAEMON === "1") installLogStamp();
|
|
18
|
+
const { installLifecycleLog } = await import("./lifecycle-log.js");
|
|
19
|
+
installLifecycleLog({ version: VERSION, logs });
|
|
20
|
+
// 接管:内存里若还有旧的 mslxdff daemon,先请它退出。它不在 daemon.pid 里(-restart 结构性碰不到它),
|
|
21
|
+
// 两个 daemon 会互抢 warp-pool 的端口(真机 2026-10-17:旧 daemon 活了一整天,日志只显示「起一条死一条」)。
|
|
22
|
+
// 必须早于 setupProviders —— 那一步才建池抢端口。debug 前台有自己的收口逻辑,不在这里抢。
|
|
23
|
+
if (process.env.MSLXDFF_DAEMON === "1" && process.env.MSLXDFF_DEBUG !== "1") {
|
|
24
|
+
try {
|
|
25
|
+
const { takeoverOlderDaemons } = await import("../daemon.js");
|
|
26
|
+
const r = await takeoverOlderDaemons({ log: (m) => console.log(m) });
|
|
27
|
+
if (!r.killed.length && !r.skipped.length && !r.remaining.length) {
|
|
28
|
+
console.log("[daemon] 内存里没有别的 mslxdff daemon(扫过一遍 /proc)—— 本进程独占出口池端口");
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.log(`[daemon] 接管旧 daemon 失败(不阻断启动):${String(e?.message || e)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const ctx = await setupProviders();
|
|
35
|
+
const { srv, bus } = await startServerLifecycle({ VERSION, ...ctx, logs });
|
|
36
|
+
void srv;
|
|
37
|
+
startGroupSync({ peers: ctx.peers, groups: ctx.groups, logs });
|
|
38
|
+
startBroadband({ token: ctx.token, upstream: ctx.upstream, logs });
|
|
39
|
+
|
|
40
|
+
// 保热心跳:每 ≤120s 经 **chat 同池**访问每个组员(/health) + 每个 picks 供应商(preheat),
|
|
41
|
+
// 防「久未使用 → 首发 fetch failed / 上游冷启动」(现网 2026-10-18 16:16 三组员同时 fetch failed)。
|
|
42
|
+
{
|
|
43
|
+
const { startKeepWarm } = await import("./keep-warm.js");
|
|
44
|
+
const { loadModelPicks } = await import("../state.js");
|
|
45
|
+
startKeepWarm({ peers: ctx.peers, providers: ctx.providers, upstream: ctx.upstream, logs, loadModelPicks: () => loadModelPicks({}) });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { setupAutoUpdate } = await import("./auto-update.js");
|
|
49
|
+
setupAutoUpdate({ VERSION, bus, logs });
|
|
50
|
+
|
|
51
|
+
const { setupWorkbuddyCheckin } = await import("./workbuddy-checkin.js");
|
|
52
|
+
void setupWorkbuddyCheckin({ bus, logs }).catch((e) => {
|
|
53
|
+
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { setupTraeworkCheckin } = await import("./traework-checkin.js");
|
|
57
|
+
void setupTraeworkCheckin({ bus, logs }).catch((e) => {
|
|
58
|
+
try { logs.appendEvent({ ts: Date.now(), type: "traework-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { setupQoderCheckin } = await import("./qoder-checkin.js");
|
|
62
|
+
void setupQoderCheckin({ bus, logs }).catch((e) => {
|
|
63
|
+
try { logs.appendEvent({ ts: Date.now(), type: "qoder-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const { setupRaccoonCheckin } = await import("./raccoon-checkin.js");
|
|
67
|
+
void setupRaccoonCheckin({ bus, logs }).catch((e) => {
|
|
68
|
+
try { logs.appendEvent({ ts: Date.now(), type: "raccoon-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
|
|
72
|
+
void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
|
|
73
|
+
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/src/runtime/keep-warm.js
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
// 保热心跳(用户 2026-10-18 拍板):每 ≤120s 主动访问一次「每个组员 + 每个在 picks 的上游供应商」,
|
|
2
|
-
// 保持连接热度,防「久未使用 → 首发 fetch failed / 上游冷启动 26s」。
|
|
3
|
-
//
|
|
4
|
-
// ⚠ 必须用**与 chat 完全相同的那条连接池**,否则白热(用户 2026-10-18 纠正):
|
|
5
|
-
// - 组员 → warmPeerHealth()(getPeerDispatcher,正是 peer 转发复用的池);
|
|
6
|
-
// - 供应商 → provider.preheat()(各家 preheat 经各自 agent/extDispatcher 打自己的 models 端点,
|
|
7
|
-
// 与 chat 同池:opencode 走 v6/warp 池、generic/custom 走各自 agent)。
|
|
8
|
-
// 绝不能裸 compatFetch / directProbe——那热的是全局池的 socket,真流量复用的是另一条,等于没热。
|
|
9
|
-
//
|
|
10
|
-
// 为什么需要它(现网取证 2026-10-18 16:16,req mvdjwol5-aysc):三个组员同时 `fetch failed`(TCP 都没建立),
|
|
11
|
-
// 根因是两端 keep-alive 打架——服务端 Node 默认 5s 掐闲置连接,而组长 undici 池账面 30s 内以为 socket 还活着,
|
|
12
|
-
// 复用死 socket 即失败。已把服务端 keepAliveTimeout 提到 65s(server.js)治本;本心跳是**补强**:
|
|
13
|
-
// ① 间隔 120s < 家里 NAT/防火墙更短的空闲回收窗(很多路由 60~120s 就丢映射,单靠 65s keep-alive 挡不住);
|
|
14
|
-
// ② 顺带让上游对我们出口保持"温热"(opencode 实测固定 session 1.2s、冷机器 26s+)。
|
|
15
|
-
//
|
|
16
|
-
// 零额度消耗(全部经各家 preheat:GET models / GET /health,不打 chat)。永不抛,只记事件。
|
|
17
|
-
import { warmPeerHealth } from "../routes/peers.js";
|
|
18
|
-
|
|
19
|
-
export function keepWarmIntervalMs(env = process.env) {
|
|
20
|
-
const n = Number(env.MSLXDFF_KEEP_WARM_MS);
|
|
21
|
-
return Number.isInteger(n) && n >= 0 ? n : 120_000; // 0 = 关闭
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 从 picks 里算出要保热的「供应商头」集合:裸 id(无 `/`)归 opencode(免费池);带前缀取首段。
|
|
26
|
-
* 只保留出现过的供应商,天然覆盖"每个在 picks 的接口"。opencode 恒排第一(免费池是主力、最该保热)。
|
|
27
|
-
* @param {string[]} picks
|
|
28
|
-
* @returns {string[]} 供应商 id 列表(含 "opencode"),已去重、opencode 置顶
|
|
29
|
-
*/
|
|
30
|
-
export function providerHeadsFromPicks(picks = []) {
|
|
31
|
-
const heads = new Set();
|
|
32
|
-
for (const id of picks) {
|
|
33
|
-
const s = String(id || "").trim();
|
|
34
|
-
if (!s) continue;
|
|
35
|
-
heads.add(s.includes("/") ? s.slice(0, s.indexOf("/")) : "opencode");
|
|
36
|
-
}
|
|
37
|
-
const out = [...heads];
|
|
38
|
-
if (out.includes("opencode")) return ["opencode", ...out.filter((h) => h !== "opencode")];
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 单轮保热:组员各 warmPeerHealth、picks 供应商各 preheat。返回汇总(供事件取证)。绝不抛。
|
|
44
|
-
* @param {{peers:{url:string}[], picks:string[], byId?:Map<string,{preheat?:Function}>, upstream?:{preheat?:Function},
|
|
45
|
-
* warmPeer?:Function, evt?:Function}} deps
|
|
46
|
-
*/
|
|
47
|
-
export async function warmOnce({
|
|
48
|
-
peers = [],
|
|
49
|
-
picks = [],
|
|
50
|
-
byId = null,
|
|
51
|
-
upstream = null,
|
|
52
|
-
warmPeer = warmPeerHealth,
|
|
53
|
-
markProbe = null, // (url, ok) => void:探测结果写进 peer 冷却账(health 探不通 ⇒ 10min 不当候选)
|
|
54
|
-
evt = () => {},
|
|
55
|
-
} = {}) {
|
|
56
|
-
const staticPeers = peers.filter((p) => p?.url && !String(p.url).startsWith("relay://")); // broadband 走 relay 队列,不直连
|
|
57
|
-
const safeMark = (url, ok) => {
|
|
58
|
-
try {
|
|
59
|
-
const p = markProbe?.(url, ok);
|
|
60
|
-
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
61
|
-
} catch { /* 记账失败不影响保热 */ }
|
|
62
|
-
};
|
|
63
|
-
let peerOk = 0, peerFail = 0;
|
|
64
|
-
for (const p of staticPeers) {
|
|
65
|
-
try {
|
|
66
|
-
const r = await warmPeer(String(p.url));
|
|
67
|
-
if (r?.ok) { peerOk += 1; safeMark(String(p.url), true); }
|
|
68
|
-
else { peerFail += 1; safeMark(String(p.url), false); }
|
|
69
|
-
} catch { peerFail += 1; safeMark(String(p.url), false); }
|
|
70
|
-
}
|
|
71
|
-
const heads = providerHeadsFromPicks(picks);
|
|
72
|
-
let provOk = 0, provFail = 0, provSkip = 0;
|
|
73
|
-
for (const head of heads) {
|
|
74
|
-
try {
|
|
75
|
-
// 供应商预热一律走它自己的 preheat(chat 同池)。opencode 特判:**多 provider 装配**下
|
|
76
|
-
// providers 数组里含 opencode,直接 byId 取;单 provider 模式下 byId 为空,退回 upstream.preheat。
|
|
77
|
-
const inst = byId?.get?.(head);
|
|
78
|
-
const preheat = inst?.preheat || (head === "opencode" ? upstream?.preheat : null);
|
|
79
|
-
if (typeof preheat !== "function") { provSkip += 1; continue; } // 本地号型/无域名供应商:本就没有可保热的域
|
|
80
|
-
const r = await preheat.call(inst || upstream);
|
|
81
|
-
if (r?.ok) provOk += 1; else provFail += 1;
|
|
82
|
-
} catch { provFail += 1; }
|
|
83
|
-
}
|
|
84
|
-
evt("keep-warm", { peers: staticPeers.length, peerOk, peerFail, providers: heads.length, provOk, provFail, provSkip });
|
|
85
|
-
return { peers: staticPeers.length, peerOk, peerFail, providers: heads.length, provOk, provFail, provSkip };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* daemon 装配:起一个 unref 的定时器,每 keepWarmIntervalMs 跑一轮 warmOnce。
|
|
90
|
-
* 关(=0)返回 null。依赖注入以便测试;生产用 setupProviders 的 ctx(providers/upstream/peers)。
|
|
91
|
-
*/
|
|
92
|
-
export function startKeepWarm({
|
|
93
|
-
peers,
|
|
94
|
-
providers = [],
|
|
95
|
-
upstream,
|
|
96
|
-
env = process.env,
|
|
97
|
-
evt = () => {},
|
|
98
|
-
logs,
|
|
99
|
-
loadModelPicks,
|
|
100
|
-
warmPeer = warmPeerHealth,
|
|
101
|
-
} = {}) {
|
|
102
|
-
const interval = keepWarmIntervalMs(env);
|
|
103
|
-
if (interval === 0) return null;
|
|
104
|
-
const byId = new Map((providers || []).map((p) => [p.id, p]));
|
|
105
|
-
const emit = (type, data) => {
|
|
106
|
-
const entry = { ts: Date.now(), type, ...data };
|
|
107
|
-
try { evt(type, data); } catch {}
|
|
108
|
-
try { logs?.appendEvent?.(entry); } catch {}
|
|
109
|
-
};
|
|
110
|
-
const runTick = async () => {
|
|
111
|
-
try {
|
|
112
|
-
const picks = loadModelPicks ? loadModelPicks() : [];
|
|
113
|
-
await warmOnce({
|
|
114
|
-
peers: peers?.all?.() || [],
|
|
115
|
-
picks: Array.isArray(picks) ? picks : [],
|
|
116
|
-
byId,
|
|
117
|
-
upstream,
|
|
118
|
-
warmPeer,
|
|
119
|
-
// 探测结果写进冷却账:health 连不上 ⇒ 10min 不当候选(防"探到了但选路不知道"——现网 2026-10-20 13:27)
|
|
120
|
-
markProbe: async (url, ok) => {
|
|
121
|
-
try {
|
|
122
|
-
if (ok) await peers?.recordProbeOk?.(url);
|
|
123
|
-
else await peers?.recordProbeDead?.(url);
|
|
124
|
-
} catch { /* 记账失败不影响保热 */ }
|
|
125
|
-
},
|
|
126
|
-
evt: emit,
|
|
127
|
-
});
|
|
128
|
-
} catch { /* 保热永不影响主链路 */ }
|
|
129
|
-
};
|
|
130
|
-
console.log(`[keep-warm] enabled: every ${Math.round(interval / 1000)}s 经 chat 同池访问每个组员(/health) + 每个 picks 供应商(preheat) 保热,设 MSLXDFF_KEEP_WARM_MS=0 关`);
|
|
131
|
-
void runTick(); // 启动即热一次,别等第一个间隔
|
|
132
|
-
const timer = setInterval(() => { void runTick(); }, interval);
|
|
133
|
-
timer.unref?.();
|
|
134
|
-
return { stop: () => clearInterval(timer), interval, tick: runTick };
|
|
135
|
-
}
|
|
1
|
+
// 保热心跳(用户 2026-10-18 拍板):每 ≤120s 主动访问一次「每个组员 + 每个在 picks 的上游供应商」,
|
|
2
|
+
// 保持连接热度,防「久未使用 → 首发 fetch failed / 上游冷启动 26s」。
|
|
3
|
+
//
|
|
4
|
+
// ⚠ 必须用**与 chat 完全相同的那条连接池**,否则白热(用户 2026-10-18 纠正):
|
|
5
|
+
// - 组员 → warmPeerHealth()(getPeerDispatcher,正是 peer 转发复用的池);
|
|
6
|
+
// - 供应商 → provider.preheat()(各家 preheat 经各自 agent/extDispatcher 打自己的 models 端点,
|
|
7
|
+
// 与 chat 同池:opencode 走 v6/warp 池、generic/custom 走各自 agent)。
|
|
8
|
+
// 绝不能裸 compatFetch / directProbe——那热的是全局池的 socket,真流量复用的是另一条,等于没热。
|
|
9
|
+
//
|
|
10
|
+
// 为什么需要它(现网取证 2026-10-18 16:16,req mvdjwol5-aysc):三个组员同时 `fetch failed`(TCP 都没建立),
|
|
11
|
+
// 根因是两端 keep-alive 打架——服务端 Node 默认 5s 掐闲置连接,而组长 undici 池账面 30s 内以为 socket 还活着,
|
|
12
|
+
// 复用死 socket 即失败。已把服务端 keepAliveTimeout 提到 65s(server.js)治本;本心跳是**补强**:
|
|
13
|
+
// ① 间隔 120s < 家里 NAT/防火墙更短的空闲回收窗(很多路由 60~120s 就丢映射,单靠 65s keep-alive 挡不住);
|
|
14
|
+
// ② 顺带让上游对我们出口保持"温热"(opencode 实测固定 session 1.2s、冷机器 26s+)。
|
|
15
|
+
//
|
|
16
|
+
// 零额度消耗(全部经各家 preheat:GET models / GET /health,不打 chat)。永不抛,只记事件。
|
|
17
|
+
import { warmPeerHealth } from "../routes/peers.js";
|
|
18
|
+
|
|
19
|
+
export function keepWarmIntervalMs(env = process.env) {
|
|
20
|
+
const n = Number(env.MSLXDFF_KEEP_WARM_MS);
|
|
21
|
+
return Number.isInteger(n) && n >= 0 ? n : 120_000; // 0 = 关闭
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从 picks 里算出要保热的「供应商头」集合:裸 id(无 `/`)归 opencode(免费池);带前缀取首段。
|
|
26
|
+
* 只保留出现过的供应商,天然覆盖"每个在 picks 的接口"。opencode 恒排第一(免费池是主力、最该保热)。
|
|
27
|
+
* @param {string[]} picks
|
|
28
|
+
* @returns {string[]} 供应商 id 列表(含 "opencode"),已去重、opencode 置顶
|
|
29
|
+
*/
|
|
30
|
+
export function providerHeadsFromPicks(picks = []) {
|
|
31
|
+
const heads = new Set();
|
|
32
|
+
for (const id of picks) {
|
|
33
|
+
const s = String(id || "").trim();
|
|
34
|
+
if (!s) continue;
|
|
35
|
+
heads.add(s.includes("/") ? s.slice(0, s.indexOf("/")) : "opencode");
|
|
36
|
+
}
|
|
37
|
+
const out = [...heads];
|
|
38
|
+
if (out.includes("opencode")) return ["opencode", ...out.filter((h) => h !== "opencode")];
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 单轮保热:组员各 warmPeerHealth、picks 供应商各 preheat。返回汇总(供事件取证)。绝不抛。
|
|
44
|
+
* @param {{peers:{url:string}[], picks:string[], byId?:Map<string,{preheat?:Function}>, upstream?:{preheat?:Function},
|
|
45
|
+
* warmPeer?:Function, evt?:Function}} deps
|
|
46
|
+
*/
|
|
47
|
+
export async function warmOnce({
|
|
48
|
+
peers = [],
|
|
49
|
+
picks = [],
|
|
50
|
+
byId = null,
|
|
51
|
+
upstream = null,
|
|
52
|
+
warmPeer = warmPeerHealth,
|
|
53
|
+
markProbe = null, // (url, ok) => void:探测结果写进 peer 冷却账(health 探不通 ⇒ 10min 不当候选)
|
|
54
|
+
evt = () => {},
|
|
55
|
+
} = {}) {
|
|
56
|
+
const staticPeers = peers.filter((p) => p?.url && !String(p.url).startsWith("relay://")); // broadband 走 relay 队列,不直连
|
|
57
|
+
const safeMark = (url, ok) => {
|
|
58
|
+
try {
|
|
59
|
+
const p = markProbe?.(url, ok);
|
|
60
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
61
|
+
} catch { /* 记账失败不影响保热 */ }
|
|
62
|
+
};
|
|
63
|
+
let peerOk = 0, peerFail = 0;
|
|
64
|
+
for (const p of staticPeers) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await warmPeer(String(p.url));
|
|
67
|
+
if (r?.ok) { peerOk += 1; safeMark(String(p.url), true); }
|
|
68
|
+
else { peerFail += 1; safeMark(String(p.url), false); }
|
|
69
|
+
} catch { peerFail += 1; safeMark(String(p.url), false); }
|
|
70
|
+
}
|
|
71
|
+
const heads = providerHeadsFromPicks(picks);
|
|
72
|
+
let provOk = 0, provFail = 0, provSkip = 0;
|
|
73
|
+
for (const head of heads) {
|
|
74
|
+
try {
|
|
75
|
+
// 供应商预热一律走它自己的 preheat(chat 同池)。opencode 特判:**多 provider 装配**下
|
|
76
|
+
// providers 数组里含 opencode,直接 byId 取;单 provider 模式下 byId 为空,退回 upstream.preheat。
|
|
77
|
+
const inst = byId?.get?.(head);
|
|
78
|
+
const preheat = inst?.preheat || (head === "opencode" ? upstream?.preheat : null);
|
|
79
|
+
if (typeof preheat !== "function") { provSkip += 1; continue; } // 本地号型/无域名供应商:本就没有可保热的域
|
|
80
|
+
const r = await preheat.call(inst || upstream);
|
|
81
|
+
if (r?.ok) provOk += 1; else provFail += 1;
|
|
82
|
+
} catch { provFail += 1; }
|
|
83
|
+
}
|
|
84
|
+
evt("keep-warm", { peers: staticPeers.length, peerOk, peerFail, providers: heads.length, provOk, provFail, provSkip });
|
|
85
|
+
return { peers: staticPeers.length, peerOk, peerFail, providers: heads.length, provOk, provFail, provSkip };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* daemon 装配:起一个 unref 的定时器,每 keepWarmIntervalMs 跑一轮 warmOnce。
|
|
90
|
+
* 关(=0)返回 null。依赖注入以便测试;生产用 setupProviders 的 ctx(providers/upstream/peers)。
|
|
91
|
+
*/
|
|
92
|
+
export function startKeepWarm({
|
|
93
|
+
peers,
|
|
94
|
+
providers = [],
|
|
95
|
+
upstream,
|
|
96
|
+
env = process.env,
|
|
97
|
+
evt = () => {},
|
|
98
|
+
logs,
|
|
99
|
+
loadModelPicks,
|
|
100
|
+
warmPeer = warmPeerHealth,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const interval = keepWarmIntervalMs(env);
|
|
103
|
+
if (interval === 0) return null;
|
|
104
|
+
const byId = new Map((providers || []).map((p) => [p.id, p]));
|
|
105
|
+
const emit = (type, data) => {
|
|
106
|
+
const entry = { ts: Date.now(), type, ...data };
|
|
107
|
+
try { evt(type, data); } catch {}
|
|
108
|
+
try { logs?.appendEvent?.(entry); } catch {}
|
|
109
|
+
};
|
|
110
|
+
const runTick = async () => {
|
|
111
|
+
try {
|
|
112
|
+
const picks = loadModelPicks ? loadModelPicks() : [];
|
|
113
|
+
await warmOnce({
|
|
114
|
+
peers: peers?.all?.() || [],
|
|
115
|
+
picks: Array.isArray(picks) ? picks : [],
|
|
116
|
+
byId,
|
|
117
|
+
upstream,
|
|
118
|
+
warmPeer,
|
|
119
|
+
// 探测结果写进冷却账:health 连不上 ⇒ 10min 不当候选(防"探到了但选路不知道"——现网 2026-10-20 13:27)
|
|
120
|
+
markProbe: async (url, ok) => {
|
|
121
|
+
try {
|
|
122
|
+
if (ok) await peers?.recordProbeOk?.(url);
|
|
123
|
+
else await peers?.recordProbeDead?.(url);
|
|
124
|
+
} catch { /* 记账失败不影响保热 */ }
|
|
125
|
+
},
|
|
126
|
+
evt: emit,
|
|
127
|
+
});
|
|
128
|
+
} catch { /* 保热永不影响主链路 */ }
|
|
129
|
+
};
|
|
130
|
+
console.log(`[keep-warm] enabled: every ${Math.round(interval / 1000)}s 经 chat 同池访问每个组员(/health) + 每个 picks 供应商(preheat) 保热,设 MSLXDFF_KEEP_WARM_MS=0 关`);
|
|
131
|
+
void runTick(); // 启动即热一次,别等第一个间隔
|
|
132
|
+
const timer = setInterval(() => { void runTick(); }, interval);
|
|
133
|
+
timer.unref?.();
|
|
134
|
+
return { stop: () => clearInterval(timer), interval, tick: runTick };
|
|
135
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
// 定制 provider 启用门禁(纯函数 + auth 号型名单),从 providers-setup.js 拆出以守住 ≤10KB 体积门。
|
|
2
|
-
// 背景:门禁曾要求 `baseUrl && keys` 两者皆有,导致 qoder(keys 有、baseUrl 空)被静默跳过 ——
|
|
3
|
-
// 表现为 /v1/models 无该供应商模型、`-models` 挑不到、网关报 `Model qoder/x is not supported`。
|
|
4
|
-
// Note: auth 号型不要求 baseUrl 是硬约束(旧门禁 baseUrl&&keys 曾静默跳过整家 qoder)— 见 .agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md
|
|
5
|
-
|
|
6
|
-
/** 凭证在 auth 目录、无需 baseUrl 的「auth 号型」定制供应商(端点由各自 constants 决定)。 */
|
|
7
|
-
export const AUTH_DOC_PROVIDER_IDS = ["workbuddy", "traework", "qoder", "qwenwork", "globalqwenwork", "raccoon"];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 免凭证供应商:上游就是 opencode zen 的免费层代理,没有 key 也能用(按出口 IP 计额度)。
|
|
11
|
-
* 与 auth 号型不同:它连账号都不需要,只要在 state.providerConfigs 里有一条记录就启用。
|
|
12
|
-
*/
|
|
13
|
-
export const NO_CREDENTIAL_PROVIDER_IDS = ["railway"];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* - 免凭证型(railway):有配置记录即启用(无需 keys/baseUrl)
|
|
17
|
-
* - auth 号型:keys 或 auths 或 auth 目录有号 → 启用(baseUrl 可空)
|
|
18
|
-
* - 其他定制(cline/codearts/插件注册):有 keys → 启用(baseUrl 可空,端点由工厂决定)
|
|
19
|
-
*/
|
|
20
|
-
export function shouldEnableCustomProvider(gid, { keys = [], auths = [], hasAuthDocs = false } = {}) {
|
|
21
|
-
if (NO_CREDENTIAL_PROVIDER_IDS.includes(gid)) return true;
|
|
22
|
-
if (AUTH_DOC_PROVIDER_IDS.includes(gid)) return keys.length > 0 || auths.length > 0 || hasAuthDocs;
|
|
23
|
-
return keys.length > 0;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** 按 gid 载入各自 account-store 并列出落盘账号(失败/无目录 → 空数组,不抛)。 */
|
|
27
|
-
export async function loadAuthDocs(gid) { try {
|
|
28
|
-
const store = gid === "workbuddy"
|
|
29
|
-
? await import("../providers/workbuddy/account-store.js")
|
|
30
|
-
: gid === "traework"
|
|
31
|
-
? await import("../providers/traework/account-store.js")
|
|
32
|
-
: gid === "qwenwork"
|
|
33
|
-
? await import("../providers/qwenwork/account-store.js")
|
|
34
|
-
: gid === "globalqwenwork"
|
|
35
|
-
? await import("../providers/globalqwenwork/account-store.js")
|
|
36
|
-
: gid === "raccoon"
|
|
37
|
-
? await import("../providers/raccoon/account-store.js")
|
|
38
|
-
: await import("../providers/qoder/account-store.js");
|
|
39
|
-
return store.listAccountDocs();
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 免凭证供应商的配置种子:providers-setup 只遍历 state.providerConfigs 里已有的条目,
|
|
47
|
-
* 没有记录就永远不会被装配。这里幂等补一条(已有则不动,保留用户改过的 baseUrl)。
|
|
48
|
-
* 关闭:设 MSLXDFF_RAILWAY=0。
|
|
49
|
-
*
|
|
50
|
-
* saveConfig(gid, cfg) 由调用方注入,需自行处理两件事:
|
|
51
|
-
* ① 非空 baseUrl —— 否则 saveProviderConfig 见「无 url/key/白名单」会整条 delete;
|
|
52
|
-
* ② allowAnyModels —— 否则 /v1/models 按「空白名单=不暴露」的安全默认,一个模型都列不出。
|
|
53
|
-
*/
|
|
54
|
-
export async function seedNoCredentialProviders({ configs, saveConfig } = {}) {
|
|
55
|
-
if (!configs || typeof saveConfig !== "function") return [];
|
|
56
|
-
const seeded = [];
|
|
57
|
-
for (const gid of NO_CREDENTIAL_PROVIDER_IDS) {
|
|
58
|
-
if (configs[gid]) continue; // 已有配置:尊重用户设置
|
|
59
|
-
if (gid === "railway") {
|
|
60
|
-
if (isDisabled("MSLXDFF_RAILWAY")) continue;
|
|
61
|
-
await saveConfig(gid, { baseUrl: RAILWAY_DEFAULT_BASE, keys: [], allowAnyModels: true });
|
|
62
|
-
seeded.push(gid);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return seeded;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const RAILWAY_DEFAULT_BASE = "https://mytestip-production.up.railway.app/ai-api";
|
|
69
|
-
|
|
70
|
-
function isDisabled(envName) {
|
|
71
|
-
const raw = process.env[envName];
|
|
72
|
-
if (raw === undefined || raw === null || raw === "") return false;
|
|
73
|
-
const s = String(raw).trim().toLowerCase();
|
|
74
|
-
return s === "0" || s === "false" || s === "off" || s === "no";
|
|
75
|
-
}
|
|
1
|
+
// 定制 provider 启用门禁(纯函数 + auth 号型名单),从 providers-setup.js 拆出以守住 ≤10KB 体积门。
|
|
2
|
+
// 背景:门禁曾要求 `baseUrl && keys` 两者皆有,导致 qoder(keys 有、baseUrl 空)被静默跳过 ——
|
|
3
|
+
// 表现为 /v1/models 无该供应商模型、`-models` 挑不到、网关报 `Model qoder/x is not supported`。
|
|
4
|
+
// Note: auth 号型不要求 baseUrl 是硬约束(旧门禁 baseUrl&&keys 曾静默跳过整家 qoder)— 见 .agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md
|
|
5
|
+
|
|
6
|
+
/** 凭证在 auth 目录、无需 baseUrl 的「auth 号型」定制供应商(端点由各自 constants 决定)。 */
|
|
7
|
+
export const AUTH_DOC_PROVIDER_IDS = ["workbuddy", "traework", "qoder", "qwenwork", "globalqwenwork", "raccoon"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 免凭证供应商:上游就是 opencode zen 的免费层代理,没有 key 也能用(按出口 IP 计额度)。
|
|
11
|
+
* 与 auth 号型不同:它连账号都不需要,只要在 state.providerConfigs 里有一条记录就启用。
|
|
12
|
+
*/
|
|
13
|
+
export const NO_CREDENTIAL_PROVIDER_IDS = ["railway"];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* - 免凭证型(railway):有配置记录即启用(无需 keys/baseUrl)
|
|
17
|
+
* - auth 号型:keys 或 auths 或 auth 目录有号 → 启用(baseUrl 可空)
|
|
18
|
+
* - 其他定制(cline/codearts/插件注册):有 keys → 启用(baseUrl 可空,端点由工厂决定)
|
|
19
|
+
*/
|
|
20
|
+
export function shouldEnableCustomProvider(gid, { keys = [], auths = [], hasAuthDocs = false } = {}) {
|
|
21
|
+
if (NO_CREDENTIAL_PROVIDER_IDS.includes(gid)) return true;
|
|
22
|
+
if (AUTH_DOC_PROVIDER_IDS.includes(gid)) return keys.length > 0 || auths.length > 0 || hasAuthDocs;
|
|
23
|
+
return keys.length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 按 gid 载入各自 account-store 并列出落盘账号(失败/无目录 → 空数组,不抛)。 */
|
|
27
|
+
export async function loadAuthDocs(gid) { try {
|
|
28
|
+
const store = gid === "workbuddy"
|
|
29
|
+
? await import("../providers/workbuddy/account-store.js")
|
|
30
|
+
: gid === "traework"
|
|
31
|
+
? await import("../providers/traework/account-store.js")
|
|
32
|
+
: gid === "qwenwork"
|
|
33
|
+
? await import("../providers/qwenwork/account-store.js")
|
|
34
|
+
: gid === "globalqwenwork"
|
|
35
|
+
? await import("../providers/globalqwenwork/account-store.js")
|
|
36
|
+
: gid === "raccoon"
|
|
37
|
+
? await import("../providers/raccoon/account-store.js")
|
|
38
|
+
: await import("../providers/qoder/account-store.js");
|
|
39
|
+
return store.listAccountDocs();
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 免凭证供应商的配置种子:providers-setup 只遍历 state.providerConfigs 里已有的条目,
|
|
47
|
+
* 没有记录就永远不会被装配。这里幂等补一条(已有则不动,保留用户改过的 baseUrl)。
|
|
48
|
+
* 关闭:设 MSLXDFF_RAILWAY=0。
|
|
49
|
+
*
|
|
50
|
+
* saveConfig(gid, cfg) 由调用方注入,需自行处理两件事:
|
|
51
|
+
* ① 非空 baseUrl —— 否则 saveProviderConfig 见「无 url/key/白名单」会整条 delete;
|
|
52
|
+
* ② allowAnyModels —— 否则 /v1/models 按「空白名单=不暴露」的安全默认,一个模型都列不出。
|
|
53
|
+
*/
|
|
54
|
+
export async function seedNoCredentialProviders({ configs, saveConfig } = {}) {
|
|
55
|
+
if (!configs || typeof saveConfig !== "function") return [];
|
|
56
|
+
const seeded = [];
|
|
57
|
+
for (const gid of NO_CREDENTIAL_PROVIDER_IDS) {
|
|
58
|
+
if (configs[gid]) continue; // 已有配置:尊重用户设置
|
|
59
|
+
if (gid === "railway") {
|
|
60
|
+
if (isDisabled("MSLXDFF_RAILWAY")) continue;
|
|
61
|
+
await saveConfig(gid, { baseUrl: RAILWAY_DEFAULT_BASE, keys: [], allowAnyModels: true });
|
|
62
|
+
seeded.push(gid);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return seeded;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RAILWAY_DEFAULT_BASE = "https://mytestip-production.up.railway.app/ai-api";
|
|
69
|
+
|
|
70
|
+
function isDisabled(envName) {
|
|
71
|
+
const raw = process.env[envName];
|
|
72
|
+
if (raw === undefined || raw === null || raw === "") return false;
|
|
73
|
+
const s = String(raw).trim().toLowerCase();
|
|
74
|
+
return s === "0" || s === "false" || s === "off" || s === "no";
|
|
75
|
+
}
|