feat: add windows profile model

This commit is contained in:
2026-05-21 20:18:28 +03:00
parent 68158f3907
commit 39eca49f62
2 changed files with 339 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
import fs from "node:fs";
import path from "node:path";
const ITEM_TYPES = new Set(["process", "folder", "exe"]);
const PROTOCOLS = new Set(["TCP", "UDP"]);
function slug(value, fallback) {
const cleaned = String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return cleaned || fallback;
}
function cleanString(value) {
return String(value || "").trim();
}
function processName(value) {
const base = cleanString(value).split(/[\\/]/).pop() || "";
return base.replace(/\.exe$/i, "").trim();
}
function unique(values) {
return Array.from(new Set(values.filter(Boolean)));
}
export function normalizeWindowsProfiles(input) {
return (Array.isArray(input) ? input : [])
.map((profile, index) => {
const name = cleanString(profile.name) || `Profile ${index + 1}`;
const items = (Array.isArray(profile.items) ? profile.items : [])
.filter((item) => ITEM_TYPES.has(item?.type))
.map((item) => ({
type: item.type,
value:
item.type === "process"
? processName(item.value)
: cleanString(item.value),
recursive: item.type === "folder" ? item.recursive !== false : false,
}))
.filter((item) => item.value);
return {
id: slug(profile.id || name, `profile-${index + 1}`),
name,
enabled: profile.enabled !== false,
proxyTargetId: cleanString(profile.proxyTargetId) || "local-singbox",
protocols: unique(
(Array.isArray(profile.protocols)
? profile.protocols
: ["TCP", "UDP"])
.map((protocol) => cleanString(protocol).toUpperCase())
.filter((protocol) => PROTOCOLS.has(protocol)),
),
items,
};
})
.map((profile) => ({
...profile,
protocols: profile.protocols.length ? profile.protocols : ["TCP", "UDP"],
}));
}
export function normalizeProxyTargets(input) {
const local = {
id: "local-singbox",
name: "Local sing-box",
protocol: "socks5",
host: "127.0.0.1",
port: 1080,
managed: true,
};
const seen = new Set([local.id]);
const custom = (Array.isArray(input) ? input : [])
.map((target, index) => ({
id: slug(target.id || target.name, `target-${index + 1}`),
name: cleanString(target.name) || `Proxy target ${index + 1}`,
protocol:
cleanString(target.protocol || "socks5").toLowerCase() === "http"
? "http"
: "socks5",
host: cleanString(target.host),
port: Number.parseInt(target.port, 10),
managed: false,
}))
.filter((target) => {
if (!target.host || !Number.isInteger(target.port)) return false;
if (target.port <= 0 || target.port > 65535) return false;
if (seen.has(target.id)) return false;
seen.add(target.id);
return true;
});
return [local, ...custom];
}
function joinPath(base, name, pathSep) {
return base.endsWith(pathSep) ? `${base}${name}` : `${base}${pathSep}${name}`;
}
function walkExeFiles(dir, { fsAdapter, recursive, pathSep }) {
const entries = fsAdapter.readdirSync(dir, { withFileTypes: true });
const results = [];
for (const entry of entries) {
const fullPath = joinPath(dir, entry.name, pathSep);
if (entry.isFile() && /\.exe$/i.test(entry.name)) results.push(fullPath);
if (recursive && entry.isDirectory()) {
results.push(...walkExeFiles(fullPath, { fsAdapter, recursive, pathSep }));
}
}
return results;
}
export function resolveProfileItems(items, options = {}) {
const fsAdapter = options.fsAdapter || fs;
const pathSep = options.pathSep || path.sep;
const resolved = [];
for (const item of Array.isArray(items) ? items : []) {
if (item.type === "process") {
const appName = processName(item.value);
if (appName) resolved.push({ ...item, appName, source: item.value });
}
if (item.type === "exe") {
const appName = processName(item.value);
if (appName) resolved.push({ ...item, appName, source: item.value });
}
if (item.type === "folder" && fsAdapter.existsSync(item.value)) {
const stat = fsAdapter.statSync(item.value);
if (stat.isDirectory()) {
for (const filePath of walkExeFiles(item.value, {
fsAdapter,
recursive: item.recursive !== false,
pathSep,
})) {
resolved.push({
...item,
appName: processName(filePath),
source: filePath,
});
}
}
}
}
const byName = new Map();
for (const item of resolved) {
if (!byName.has(item.appName)) byName.set(item.appName, item);
}
return Array.from(byName.values());
}
export function buildProxiFyreConfig(profiles, targets, options = {}) {
const normalizedTargets = normalizeProxyTargets(targets);
const targetById = new Map(
normalizedTargets.map((target) => [target.id, target]),
);
const groups = new Map();
for (const profile of normalizeWindowsProfiles(profiles).filter(
(item) => item.enabled,
)) {
const target =
targetById.get(profile.proxyTargetId) || targetById.get("local-singbox");
const resolved = resolveProfileItems(profile.items, options);
if (!target || resolved.length === 0) continue;
const key = `${target.id}|${profile.protocols.join(",")}`;
const existing = groups.get(key) || {
appNames: [],
socks5ProxyEndpoint: `${target.host}:${target.port}`,
supportedProtocols: profile.protocols,
};
existing.appNames.push(...resolved.map((item) => item.appName));
existing.appNames = unique(existing.appNames).sort((a, b) =>
a.localeCompare(b),
);
groups.set(key, existing);
}
return {
logLevel: "Info",
proxies: Array.from(groups.values()),
excludes: [],
};
}