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: [], }; } 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, }; }