feat: add windows API model
This commit is contained in:
@@ -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) });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
26
test/server/windows-api-model.test.js
Normal file
26
test/server/windows-api-model.test.js
Normal 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"]);
|
||||
});
|
||||
Reference in New Issue
Block a user