mslxdff 0.1.72 → 0.1.74
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 +1 -1
- package/src/autostart.js +194 -0
- package/src/routes/chat/gateway.js +7 -23
package/package.json
CHANGED
package/src/autostart.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TASK_NAME = "mslxdff";
|
|
8
|
+
const SERVICE_NAME = "mslxdff";
|
|
9
|
+
|
|
10
|
+
function isWin() { return process.platform === "win32"; }
|
|
11
|
+
|
|
12
|
+
function nodePath() { return process.execPath; }
|
|
13
|
+
|
|
14
|
+
function scriptPath() {
|
|
15
|
+
// src/autostart.js -> ../bin/mslxdff.js
|
|
16
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "mslxdff.js");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function execAsync(file, args, opts = {}) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile(file, args, { windowsHide: true, timeout: 8000, ...opts }, (err, stdout, stderr) => {
|
|
22
|
+
resolve({ err, stdout: String(stdout || ""), stderr: String(stderr || ""), code: err?.code ?? 0 });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------- Windows: schtasks ----------
|
|
28
|
+
async function winRegQuery() {
|
|
29
|
+
const r = await execAsync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", TASK_NAME]);
|
|
30
|
+
if (!r.err && r.stdout.includes(TASK_NAME)) return { exists: true, raw: r.stdout, method: "registry" };
|
|
31
|
+
return { exists: false, raw: r.stdout || r.stderr };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function winQuery() {
|
|
35
|
+
const r = await execAsync("schtasks", ["/query", "/tn", TASK_NAME, "/fo", "LIST", "/v"]);
|
|
36
|
+
if (!r.err) {
|
|
37
|
+
const out = r.stdout || "";
|
|
38
|
+
if (out.includes(TASK_NAME)) return { exists: true, raw: out, method: "schtasks" };
|
|
39
|
+
}
|
|
40
|
+
// 回退查注册表
|
|
41
|
+
const reg = await winRegQuery();
|
|
42
|
+
if (reg.exists) return { exists: true, raw: reg.raw, method: "registry" };
|
|
43
|
+
return { exists: false, raw: r.stderr || r.stdout };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function winRegEnable() {
|
|
47
|
+
const node = nodePath();
|
|
48
|
+
const script = scriptPath();
|
|
49
|
+
const cmd = `"${node}" "${script}"`;
|
|
50
|
+
const r = await execAsync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", TASK_NAME, "/t", "REG_SZ", "/d", cmd, "/f"]);
|
|
51
|
+
if (!r.err) return { ok: true, method: "registry:HKCU\\Run", stdout: r.stdout };
|
|
52
|
+
return { ok: false, error: (r.stderr || r.stdout || r.err?.message || "").slice(0, 800) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function winEnable() {
|
|
56
|
+
const node = nodePath();
|
|
57
|
+
const script = scriptPath();
|
|
58
|
+
const tr = `"${node}" "${script}"`;
|
|
59
|
+
// 1) schtasks ONLOGON(需任务计划权限,部分受限环境会拒绝访问)
|
|
60
|
+
let r = await execAsync("schtasks", ["/create", "/tn", TASK_NAME, "/tr", tr, "/sc", "onlogon", "/f"]);
|
|
61
|
+
if (!r.err) return { ok: true, method: "schtasks:onlogon", stdout: r.stdout };
|
|
62
|
+
r = await execAsync("schtasks", ["/create", "/tn", TASK_NAME, "/tr", tr, "/sc", "onlogon", "/rl", "highest", "/f"]);
|
|
63
|
+
if (!r.err) return { ok: true, method: "schtasks:onlogon:highest", stdout: r.stdout };
|
|
64
|
+
// 2) 回退注册表 HKCU Run(无需管理员,当前用户登录即起,最兼容)
|
|
65
|
+
const reg = await winRegEnable();
|
|
66
|
+
if (reg.ok) return reg;
|
|
67
|
+
// 3) 最后尝试 ONSTART + SYSTEM
|
|
68
|
+
r = await execAsync("schtasks", ["/create", "/tn", TASK_NAME, "/tr", tr, "/sc", "onstart", "/ru", "SYSTEM", "/f"]);
|
|
69
|
+
if (!r.err) return { ok: true, method: "schtasks:onstart:SYSTEM", stdout: r.stdout };
|
|
70
|
+
const msg = r.stderr || r.stdout || reg.error || "";
|
|
71
|
+
// 若错误含拒绝访问,明确提示注册表已成功则不报错
|
|
72
|
+
if (reg.ok) return reg;
|
|
73
|
+
return { ok: false, error: (msg || "schtasks failed").slice(0, 800), stdout: r.stdout, stderr: r.stderr };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function winRegDisable() {
|
|
77
|
+
const r = await execAsync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", TASK_NAME, "/f"]);
|
|
78
|
+
if (!r.err) return { ok: true, stdout: r.stdout };
|
|
79
|
+
const msg = r.stderr || r.stdout || "";
|
|
80
|
+
if (/ERROR.*cannot find/i.test(msg) || /找不到/.test(msg) || /未找到/.test(msg)) return { ok: true, stdout: "already disabled" };
|
|
81
|
+
return { ok: false, error: msg.slice(0, 800) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function winDisable() {
|
|
85
|
+
let r = await execAsync("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]);
|
|
86
|
+
let okSchtasks = !r.err;
|
|
87
|
+
let regR = await winRegDisable();
|
|
88
|
+
if (okSchtasks || regR.ok) return { ok: true, stdout: (r.stdout || "") + (regR.stdout || "") };
|
|
89
|
+
const msg = r.stderr || r.stdout || "";
|
|
90
|
+
if (/ERROR.*cannot find/i.test(msg) || /找不到/.test(msg)) {
|
|
91
|
+
if (regR.ok) return { ok: true, stdout: "already disabled" };
|
|
92
|
+
}
|
|
93
|
+
// 若两者都失败但注册表已清,也算成功
|
|
94
|
+
if (regR.ok) return { ok: true };
|
|
95
|
+
return { ok: false, error: (msg || regR.error || "").slice(0, 800) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function winStatus() {
|
|
99
|
+
const q = await winQuery();
|
|
100
|
+
if (!q.exists) return { enabled: false, detail: "未启用(无任务计划也无注册表)" };
|
|
101
|
+
if (q.method === "registry") {
|
|
102
|
+
const m = q.raw.match(/mslxdff\s+REG_SZ\s+(.+)/);
|
|
103
|
+
return { enabled: true, detail: `已启用 · 注册表 HKCU\\Run(登录即起,无需管理员)`, taskToRun: m ? m[1].trim() : "", raw: q.raw };
|
|
104
|
+
}
|
|
105
|
+
const m = q.raw.match(/Status:\s*(.+)/i) || q.raw.match(/状态:\s*(.+)/);
|
|
106
|
+
const last = q.raw.match(/Last Run Result:\s*(.+)/i) || q.raw.match(/上次运行结果:\s*(.+)/);
|
|
107
|
+
const tr = q.raw.match(/Task To Run:\s*(.+)/i) || q.raw.match(/要运行的任务:\s*(.+)/);
|
|
108
|
+
return {
|
|
109
|
+
enabled: true,
|
|
110
|
+
detail: `已启用 · ${m ? m[1].trim() : "Ready"}${last ? ` · 上次结果 ${last[1].trim()}` : ""}`,
|
|
111
|
+
taskToRun: tr ? tr[1].trim() : "",
|
|
112
|
+
raw: q.raw,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------- Linux: systemd user ----------
|
|
117
|
+
function systemdUnitPath() {
|
|
118
|
+
return join(homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function systemdUnitContent() {
|
|
122
|
+
const node = nodePath();
|
|
123
|
+
const script = scriptPath();
|
|
124
|
+
return `[Unit]
|
|
125
|
+
Description=mslxdff daemon — OpenCode Free proxy
|
|
126
|
+
After=network.target
|
|
127
|
+
|
|
128
|
+
[Service]
|
|
129
|
+
Type=simple
|
|
130
|
+
ExecStart=${node} ${script}
|
|
131
|
+
Restart=always
|
|
132
|
+
RestartSec=3
|
|
133
|
+
|
|
134
|
+
[Install]
|
|
135
|
+
WantedBy=default.target
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function linuxEnable() {
|
|
140
|
+
const unit = systemdUnitPath();
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(dirname(unit), { recursive: true });
|
|
143
|
+
writeFileSync(unit, systemdUnitContent(), "utf8");
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { ok: false, error: `write unit failed: ${e.message}` };
|
|
146
|
+
}
|
|
147
|
+
let r = await execAsync("systemctl", ["--user", "daemon-reload"]);
|
|
148
|
+
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
149
|
+
r = await execAsync("systemctl", ["--user", "enable", SERVICE_NAME]);
|
|
150
|
+
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
151
|
+
// 可选立即拉起(不强求)
|
|
152
|
+
await execAsync("systemctl", ["--user", "start", SERVICE_NAME]);
|
|
153
|
+
return { ok: true, method: "systemd:user", unit };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function linuxDisable() {
|
|
157
|
+
await execAsync("systemctl", ["--user", "disable", SERVICE_NAME]);
|
|
158
|
+
await execAsync("systemctl", ["--user", "stop", SERVICE_NAME]);
|
|
159
|
+
try { rmSync(systemdUnitPath(), { force: true }); } catch {}
|
|
160
|
+
await execAsync("systemctl", ["--user", "daemon-reload"]);
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function linuxStatus() {
|
|
165
|
+
const unit = systemdUnitPath();
|
|
166
|
+
const exists = existsSync(unit);
|
|
167
|
+
if (!exists) return { enabled: false, detail: "未启用(无 systemd user unit)" };
|
|
168
|
+
const r = await execAsync("systemctl", ["--user", "is-enabled", SERVICE_NAME]);
|
|
169
|
+
const enabled = !r.err && /enabled/i.test(r.stdout);
|
|
170
|
+
const r2 = await execAsync("systemctl", ["--user", "is-active", SERVICE_NAME]);
|
|
171
|
+
const active = !r2.err && /active/i.test(r2.stdout);
|
|
172
|
+
return { enabled, detail: `${enabled ? "已启用" : "已禁用"} · ${active ? "运行中" : "未运行"} · ${unit}`, unit };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------- facade ----------
|
|
176
|
+
export async function enableAutostart() {
|
|
177
|
+
if (isWin()) return winEnable();
|
|
178
|
+
return linuxEnable();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function disableAutostart() {
|
|
182
|
+
if (isWin()) return winDisable();
|
|
183
|
+
return linuxDisable();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function getAutostartStatus() {
|
|
187
|
+
if (isWin()) return winStatus();
|
|
188
|
+
return linuxStatus();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function autostartHelp() {
|
|
192
|
+
if (isWin()) return `Windows: schtasks 任务 "mslxdff"(ONLOGON,最高权限,当前用户登录即自启)`;
|
|
193
|
+
return `Linux: systemd user unit ~/.config/systemd/user/mslxdff.service`;
|
|
194
|
+
}
|
|
@@ -124,28 +124,9 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
|
|
|
124
124
|
if (Number.isInteger(v) && v > 0) return Math.min(v, nonCoolingOrder.length);
|
|
125
125
|
return Math.min(nonCoolingOrder.length, 5);
|
|
126
126
|
})();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const raceStart
|
|
130
|
-
const attempts = raceModels.map(async (m) => {
|
|
131
|
-
const fwd = { ...injectReasoningContent(m, body), model: m };
|
|
132
|
-
let r = null;
|
|
133
|
-
try {
|
|
134
|
-
const chatOpts = {};
|
|
135
|
-
if (Object.keys(shareKeys).length) chatOpts.shareKeys = shareKeys;
|
|
136
|
-
if (workbuddyUid) chatOpts.workbuddyUid = workbuddyUid;
|
|
137
|
-
r = await upstream.chat(fwd, Object.keys(chatOpts).length ? chatOpts : undefined);
|
|
138
|
-
} catch (err) {
|
|
139
|
-
return { model: m, ok: false, error: errMsg(err), status: 502, timing: err?._t ?? null };
|
|
140
|
-
}
|
|
141
|
-
if (r && r.status >= 400) {
|
|
142
|
-
const isAllow = r.status === 403 && r.headers?.get?.("x-mslxdff-allowlist") === "1";
|
|
143
|
-
if (isAllow) return { model: m, ok: false, error: "allowlist", status: 403, allowlist: true };
|
|
144
|
-
return { model: m, ok: false, error: `upstream ${r.status}`, status: r.status, res: r, timing: r._t ?? null };
|
|
145
|
-
}
|
|
146
|
-
if (r instanceof Error) return { model: m, ok: false, error: errMsg(r), status: 502 };
|
|
147
|
-
return { model: m, ok: true, res: r, status: r.status, timing: r._t ?? null };
|
|
148
|
-
});
|
|
127
|
+
let raceModels=nonCoolingOrder.slice(0,concLimit);
|
|
128
|
+
if(plugins?.length){const k=[];for(const m of raceModels){const b=await runHook(plugins,"model:beforeTry",{reqId,requested,model:m,hops});if(b.value===false||b.value?.skip)continue;k.push(m);}raceModels=k;}
|
|
129
|
+
if(!raceModels.length){order=order.filter(m=>!new Set(nonCoolingOrder.slice(0,concLimit)).has(m));if(!order.length){await handleExhaustedAll({res,body,lastErr:{model:requested,status:502,message:"all concurrent candidates skipped by plugin"},order:nonCoolingOrder.slice(0,concLimit),requested,handlerCtx:{...handlerCtx,reqId,startedAt},evt,logCall,mark,perf0,stages});return;}}else{evt("auto-concurrent-race",{reqId,models:raceModels,skippedFaulty:order.length-nonCoolingOrder.length,limit:concLimit});const raceStart=performance.now();const attempts=raceModels.map(async m=>{let f={...injectReasoningContent(m,body),model:m};if(plugins?.length){const u=await runHook(plugins,"upstream:request",{reqId,requested,model:m,payload:f,stream:Boolean(body.stream)});if(u.changed&&u.value?.payload) f=u.value.payload;}let r=null;try{const o={};if(Object.keys(shareKeys).length)o.shareKeys=shareKeys;if(workbuddyUid)o.workbuddyUid=workbuddyUid;r=await upstream.chat(f,Object.keys(o).length?o:undefined);}catch(e){if(plugins?.length)runHook(plugins,"upstream:response",{reqId,requested,model:m,status:null,ok:false,error:errMsg(e),timing:e?._t??null}).catch(()=>{});return{model:m,ok:false,error:errMsg(e),status:502,timing:e?._t??null};}if(plugins?.length)runHook(plugins,"upstream:response",{reqId,requested,model:m,status:r instanceof Error?null:r?.status??null,ok:!(r instanceof Error)&&r?r.status<400:false,error:r instanceof Error?errMsg(r):null,timing:r?._t??null}).catch(()=>{});if(r&&r.status>=400){const a=r.status===403&&r.headers?.get?.("x-mslxdff-allowlist")==="1";if(a)return{model:m,ok:false,error:"allowlist",status:403,allowlist:true};return{model:m,ok:false,error:`upstream ${r.status}`,status:r.status,res:r,timing:r._t??null};}if(r instanceof Error)return{model:m,ok:false,error:errMsg(r),status:502};return{model:m,ok:true,res:r,status:r.status,timing:r._t??null};});
|
|
149
130
|
const results = await Promise.allSettled(attempts);
|
|
150
131
|
const okList = results.map((r, i) => ({ r, i, model: raceModels[i] }))
|
|
151
132
|
.filter(({ r }) => r.status === "fulfilled" && r.value?.ok)
|
|
@@ -185,10 +166,13 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
|
|
|
185
166
|
const triedSet = new Set(raceModels);
|
|
186
167
|
order = order.filter((m) => !triedSet.has(m));
|
|
187
168
|
if (!order.length) {
|
|
188
|
-
const
|
|
169
|
+
const failedStatuses = results.map((r) => (r.status === "fulfilled" ? r.value?.status : null)).filter((s) => Number.isInteger(s));
|
|
170
|
+
const lastStatus = failedStatuses[failedStatuses.length - 1] || failedStatuses[0] || 502;
|
|
171
|
+
const last = { model: raceModels[0] || requested, status: lastStatus, message: "all concurrent candidates failed" };
|
|
189
172
|
await handleExhaustedAll({ res, body, lastErr: last, order: raceModels, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
190
173
|
return;
|
|
191
174
|
}
|
|
175
|
+
} // close else (raceModels not empty)
|
|
192
176
|
}
|
|
193
177
|
}
|
|
194
178
|
|