feat: add windows API model

This commit is contained in:
2026-05-21 20:21:42 +03:00
parent f7e8138ab1
commit 71e628fbde
3 changed files with 224 additions and 0 deletions

View File

@@ -27,6 +27,14 @@ import {
} from "./sharedProxy.js";
import { matchRoute, detectRuleConflicts } from "./routeMatcher.js";
import { tcpPing, resolveHost } from "./ping.js";
import {
buildProxiFyreConfig,
createActivityEntry,
normalizeProxyTargets,
normalizeWindowsProfiles,
summarizeProfiles,
} from "./windowsProfiles.js";
import { windowsHelper } from "./windowsHelper.js";
const APPLY_HISTORY_LIMIT = 10;
const RULE_SET_TAG_RE = /^[a-z0-9][a-z0-9_.@!-]*$/i;
@@ -603,6 +611,8 @@ function publicState() {
const customRules = readJson(settings.customRulesPath, []);
const deviceProfiles = readDeviceProfiles();
const clientSettings = readClientSettings();
const windowsTargets =
settings.appMode === "windows" ? readProxyTargets() : [];
const { subscriptionUrl, ...rest } = state;
return {
mode: settings.appMode,
@@ -634,6 +644,17 @@ function publicState() {
directBypassCount,
directBypassEnabled: DIRECT_BYPASS_CACHE,
directBypassAvailable: IPSET_AVAILABLE,
windows:
settings.appMode === "windows"
? {
profiles: summarizeProfiles(
readWindowsProfiles(),
windowsTargets,
),
targets: windowsTargets,
activity: readWindowsActivity().slice(-20).reverse(),
}
: null,
...rest,
};
}
@@ -686,6 +707,62 @@ function normalizeDeviceRules(input) {
}));
}
function readWindowsProfiles() {
return normalizeWindowsProfiles(readJson(settings.windowsProfilesPath, []));
}
function writeWindowsProfiles(profiles) {
const normalized = normalizeWindowsProfiles(profiles);
writeJson(settings.windowsProfilesPath, normalized);
return normalized;
}
function readProxyTargets() {
return normalizeProxyTargets(readJson(settings.windowsTargetsPath, []));
}
function writeProxyTargets(targets) {
const normalized = normalizeProxyTargets(targets);
writeJson(
settings.windowsTargetsPath,
normalized.filter((target) => !target.managed),
);
return normalized;
}
function readWindowsActivity() {
return readJson(settings.windowsActivityPath, []).slice(-100);
}
function pushWindowsActivity(type, message, details = {}) {
const activity = readWindowsActivity();
const entry = createActivityEntry(type, message, details);
writeJson(settings.windowsActivityPath, [...activity, entry].slice(-100));
return entry;
}
async function getWindowsStatus() {
let helperStatus = null;
if (settings.appMode === "windows") {
try {
helperStatus = await windowsHelper.run("status.get", {});
} catch (error) {
helperStatus = { success: false, error: error.message };
}
}
const profiles = readWindowsProfiles();
const targets = readProxyTargets();
return {
mode: settings.appMode,
installMode:
readJson(settings.windowsStatePath, {}).installMode || "not-configured",
profiles: summarizeProfiles(profiles, targets),
targets,
activity: readWindowsActivity().slice(-20).reverse(),
helperStatus,
};
}
async function applySelectedServer(selectedTag) {
const cached = readJson(settings.subscriptionCachePath, null);
if (!cached?.config) {
@@ -809,6 +886,99 @@ async function handleApi(req, res) {
return sendJson(res, 200, { success: true, config });
}
if (req.method === "GET" && req.url === "/api/windows/status") {
return sendJson(res, 200, { success: true, ...(await getWindowsStatus()) });
}
if (req.method === "GET" && req.url === "/api/windows/profiles") {
const profiles = readWindowsProfiles();
const targets = readProxyTargets();
return sendJson(res, 200, {
success: true,
profiles,
summaries: summarizeProfiles(profiles, targets),
});
}
if (req.method === "PUT" && req.url === "/api/windows/profiles") {
const body = await readBody(req);
const profiles = writeWindowsProfiles(body.profiles || []);
pushWindowsActivity("profiles.saved", "Profiles saved", {
count: profiles.length,
});
return sendJson(res, 200, {
success: true,
profiles,
summaries: summarizeProfiles(profiles, readProxyTargets()),
});
}
if (req.method === "POST" && req.url === "/api/windows/profiles/scan") {
const body = await readBody(req);
const profiles = normalizeWindowsProfiles(body.profiles || []);
return sendJson(res, 200, {
success: true,
summaries: summarizeProfiles(profiles, readProxyTargets()),
});
}
if (req.method === "POST" && req.url === "/api/windows/profiles/apply") {
const profiles = readWindowsProfiles();
const targets = readProxyTargets();
const proxifyreConfig = buildProxiFyreConfig(profiles, targets);
const helperResult = await windowsHelper.run("proxifyre.apply", {
configPath: settings.proxifyreConfigPath,
config: proxifyreConfig,
});
pushWindowsActivity("profiles.applied", "ProxiFyre config applied", {
proxyGroups: proxifyreConfig.proxies.length,
});
return sendJson(res, 200, {
success: true,
config: proxifyreConfig,
helperResult,
});
}
if (req.method === "GET" && req.url === "/api/windows/targets") {
return sendJson(res, 200, { success: true, targets: readProxyTargets() });
}
if (req.method === "PUT" && req.url === "/api/windows/targets") {
const body = await readBody(req);
const targets = writeProxyTargets(body.targets || []);
pushWindowsActivity("targets.saved", "Proxy targets saved", {
count: targets.length,
});
return sendJson(res, 200, { success: true, targets });
}
if (req.method === "POST" && req.url === "/api/windows/service") {
const body = await readBody(req);
const service = String(body.service || "");
const action = String(body.action || "");
if (!["sing-box", "proxifyre", "ui"].includes(service)) {
return sendJson(res, 400, { success: false, error: "Unknown service" });
}
if (!["start", "stop", "restart"].includes(action)) {
return sendJson(res, 400, { success: false, error: "Unknown action" });
}
const helperResult = await windowsHelper.run("service.control", {
service,
action,
});
pushWindowsActivity("service.control", `${service} ${action}`, {
service,
action,
});
return sendJson(res, 200, { success: true, helperResult });
}
if (req.method === "GET" && req.url === "/api/windows/logs") {
const helperResult = await windowsHelper.run("logs.get", {});
return sendJson(res, 200, { success: true, helperResult });
}
if (req.method === "GET" && req.url === "/api/logs") {
return sendJson(res, 200, { success: true, logs: logBuffer.slice(-200) });
}

View File

@@ -180,3 +180,31 @@ export function buildProxiFyreConfig(profiles, targets, options = {}) {
excludes: [],
};
}
export function summarizeProfiles(profiles, targets, options = {}) {
const normalizedTargets = normalizeProxyTargets(targets);
const targetById = new Map(
normalizedTargets.map((target) => [target.id, target]),
);
return normalizeWindowsProfiles(profiles).map((profile) => {
const resolvedItems = resolveProfileItems(profile.items, options);
const target =
targetById.get(profile.proxyTargetId) || targetById.get("local-singbox");
return {
...profile,
target,
resolvedCount: resolvedItems.length,
resolvedItems,
};
});
}
export function createActivityEntry(type, message, details = {}) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
ts: new Date().toISOString(),
type,
message,
details,
};
}

View File

@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildProxiFyreConfig,
normalizeProxyTargets,
normalizeWindowsProfiles,
summarizeProfiles,
} from "../../src/server/windowsProfiles.js";
test("windows API model returns summaries and generated config", () => {
const profiles = normalizeWindowsProfiles([
{
name: "Discord",
proxyTargetId: "local-singbox",
items: [{ type: "process", value: "Discord" }],
},
]);
const targets = normalizeProxyTargets([]);
const summaries = summarizeProfiles(profiles, targets);
const config = buildProxiFyreConfig(profiles, targets);
assert.equal(summaries[0].resolvedCount, 1);
assert.equal(summaries[0].target.id, "local-singbox");
assert.deepEqual(config.proxies[0].appNames, ["Discord"]);
});