From 758224749d023c7d8c481cc7c086f598ff25f7b0 Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 7 Mar 2026 10:08:55 +0700 Subject: [PATCH] Feat : Add support for the new "alicode-intl" provider --- open-sse/config/constants.js | 5 + open-sse/config/providerModels.js | 12 ++ package.json | 2 +- public/providers/alicode-intl.png | Bin 0 -> 4859 bytes public/providers/alicode.png | Bin 0 -> 4859 bytes .../dashboard/providers/[id]/page.js | 61 ++++++++- src/app/api/providers/[id]/models/route.js | 8 ++ src/app/api/providers/[id]/test/testUtils.js | 10 +- src/app/api/providers/validate/route.js | 6 +- src/mitm/dns/dnsConfig.js | 125 ++++++++++++++---- src/mitm/manager.js | 115 +++++----------- src/shared/constants/config.js | 1 + src/shared/constants/providers.js | 1 + 13 files changed, 227 insertions(+), 119 deletions(-) create mode 100644 public/providers/alicode-intl.png create mode 100644 public/providers/alicode.png diff --git a/open-sse/config/constants.js b/open-sse/config/constants.js index c9bd203a..801c2db0 100644 --- a/open-sse/config/constants.js +++ b/open-sse/config/constants.js @@ -262,6 +262,11 @@ export const PROVIDERS = { format: "openai", headers: {} }, + "alicode-intl": { + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", + format: "openai", + headers: {} + }, github: { baseUrl: "https://api.githubcopilot.com/chat/completions", // GitHub Copilot API endpoint for chat responsesUrl: "https://api.githubcopilot.com/responses", diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index b1676859..85b1099a 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -12,6 +12,7 @@ export const PROVIDER_MODELS = { { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, ], cx: [ // OpenAI Codex + { id: "gpt-5.4", name: "GPT 5.4" }, // GPT 5.3 Codex - all thinking levels { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, { id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" }, @@ -80,6 +81,7 @@ export const PROVIDER_MODELS = { { id: "gpt-5.2", name: "GPT-5.2" }, { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, + { id: "gpt-5.4", name: "GPT-5.4" }, // GitHub Copilot - Anthropic models { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, { id: "claude-opus-4.1", name: "Claude Opus 4.1" }, @@ -202,6 +204,15 @@ export const PROVIDER_MODELS = { { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, { id: "glm-4.7", name: "GLM 4.7" }, ], + "alicode-intl": [ + { id: "qwen3.5-plus", name: "Qwen3.5 Plus" }, + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "glm-5", name: "GLM 5" }, + { id: "MiniMax-M2.5", name: "MiniMax M2.5" }, + { id: "qwen3-coder-next", name: "Qwen3 Coder Next" }, + { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, + { id: "glm-4.7", name: "GLM 4.7" }, + ], deepseek: [ { id: "deepseek-chat", name: "DeepSeek V3.2 Chat" }, { id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" }, @@ -341,6 +352,7 @@ export const PROVIDER_ID_TO_ALIAS = { minimax: "minimax", "minimax-cn": "minimax-cn", alicode: "alicode", + "alicode-intl": "alicode-intl", deepseek: "deepseek", groq: "groq", xai: "xai", diff --git a/package.json b/package.json index f0fc941a..9288d359 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.33", + "version": "0.3.35", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/public/providers/alicode-intl.png b/public/providers/alicode-intl.png new file mode 100644 index 0000000000000000000000000000000000000000..21c3ef3df575a3dd0a19ef8d384dee023f8421d7 GIT binary patch literal 4859 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 - smart_toy +
+ + {testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"} +

{modelId}

@@ -855,6 +872,18 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias {copied === `model-${modelId}` ? "check" : "content_copy"} + {onTest && ( + + )}
@@ -876,12 +905,35 @@ PassthroughModelRow.propTypes = { copied: PropTypes.string, onCopy: PropTypes.func.isRequired, onDeleteAlias: PropTypes.func.isRequired, + onTest: PropTypes.func, + testStatus: PropTypes.oneOf(["ok", "error"]), + isTesting: PropTypes.bool, }; function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) { const [newModel, setNewModel] = useState(""); const [adding, setAdding] = useState(false); const [importing, setImporting] = useState(false); + const [testingModelId, setTestingModelId] = useState(null); + const [modelTestResults, setModelTestResults] = useState({}); + + const handleTestModel = async (modelId) => { + if (testingModelId) return; + setTestingModelId(modelId); + try { + const res = await fetch("/api/models/test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: `${providerStorageAlias}/${modelId}` }), + }); + const data = await res.json(); + setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" })); + } catch { + setModelTestResults((prev) => ({ ...prev, [modelId]: "error" })); + } finally { + setTestingModelId(null); + } + }; const providerAliases = Object.entries(modelAliases).filter( ([, model]) => model.startsWith(`${providerStorageAlias}/`) @@ -1008,6 +1060,9 @@ function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, m copied={copied} onCopy={onCopy} onDeleteAlias={() => onDeleteAlias(alias)} + onTest={connections.length > 0 ? () => handleTestModel(modelId) : undefined} + testStatus={modelTestResults[modelId]} + isTesting={testingModelId === modelId} /> ))} diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js index dae6b898..e5fbda6e 100644 --- a/src/app/api/providers/[id]/models/route.js +++ b/src/app/api/providers/[id]/models/route.js @@ -140,6 +140,14 @@ const PROVIDER_MODELS_CONFIG = { authPrefix: "Bearer ", parseResponse: (data) => data.data || [] }, + "alicode-intl": { + url: "https://coding-intl.dashscope.aliyuncs.com/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || [] + }, // OpenAI-compatible API key providers deepseek: createOpenAIModelsConfig("https://api.deepseek.com/models"), diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index ce934ae9..2ff004f9 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -309,12 +309,16 @@ async function testApiKeyConnection(connection) { const valid = res.status !== 401 && res.status !== 403; return { valid, error: valid ? null : "Invalid API key" }; } - case "alicode": { + case "alicode": + case "alicode-intl": { // Aliyun Coding Plan uses OpenAI-compatible API - const res = await fetch("https://coding.dashscope.aliyuncs.com/v1/chat/completions", { + const aliBaseUrl = connection.provider === "alicode-intl" + ? "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions" + : "https://coding.dashscope.aliyuncs.com/v1/chat/completions"; + const res = await fetch(aliBaseUrl, { method: "POST", headers: { "Authorization": `Bearer ${connection.apiKey}`, "content-type": "application/json" }, - body: JSON.stringify({ model: getDefaultModel("alicode"), max_tokens: 1, messages: [{ role: "user", content: "test" }] }), + body: JSON.stringify({ model: getDefaultModel(connection.provider), max_tokens: 1, messages: [{ role: "user", content: "test" }] }), }); const valid = res.status !== 401 && res.status !== 403; return { valid, error: valid ? null : "Invalid API key" }; diff --git a/src/app/api/providers/validate/route.js b/src/app/api/providers/validate/route.js index 2934953c..e4f16c94 100644 --- a/src/app/api/providers/validate/route.js +++ b/src/app/api/providers/validate/route.js @@ -104,6 +104,7 @@ export async function POST(request) { case "kimi": case "minimax": case "minimax-cn": + case "alicode-intl": case "alicode": { const claudeBaseUrls = { glm: "https://api.z.ai/api/anthropic/v1/messages", @@ -112,10 +113,11 @@ export async function POST(request) { minimax: "https://api.minimax.io/anthropic/v1/messages", "minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages", alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions", + "alicode-intl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", }; - // glm-cn and alicode use OpenAI format - if (provider === "glm-cn" || provider === "alicode") { + // glm-cn, alicode and alicode-intl use OpenAI format + if (provider === "glm-cn" || provider === "alicode" || provider === "alicode-intl") { const testModel = getDefaultModel(provider); const glmCnRes = await fetch(claudeBaseUrls[provider], { method: "POST", diff --git a/src/mitm/dns/dnsConfig.js b/src/mitm/dns/dnsConfig.js index 42ff3e0e..fca22630 100644 --- a/src/mitm/dns/dnsConfig.js +++ b/src/mitm/dns/dnsConfig.js @@ -15,6 +15,47 @@ const HOSTS_FILE = IS_WIN ? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts"; +/** + * Execute elevated PowerShell script on Windows via Start-Process -Verb RunAs. + * Only UAC consent dialog appears, no CMD/PS window popup. + */ +function executeElevatedPowerShell(psScriptPath, timeoutMs = 30000) { + const flagFile = path.join(os.tmpdir(), `ps_done_${Date.now()}.flag`); + const psSQ = (s) => s.replace(/'/g, "''"); + + let psContent = fs.readFileSync(psScriptPath, "utf8"); + psContent += `\nSet-Content -Path '${psSQ(flagFile)}' -Value 'done' -Encoding UTF8\n`; + fs.writeFileSync(psScriptPath, psContent, "utf8"); + + const outerCmd = `Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File','${psSQ(psScriptPath)}' -Verb RunAs -WindowStyle Hidden`; + + return new Promise((resolve, reject) => { + let settled = false; + const settle = (fn, arg) => { if (!settled) { settled = true; fn(arg); } }; + + exec( + `powershell -NoProfile -NonInteractive -WindowStyle Hidden -Command "${outerCmd}"`, + { windowsHide: true }, + () => {} + ); + + const deadline = Date.now() + timeoutMs; + const poll = () => { + if (settled) return; + if (fs.existsSync(flagFile)) { + try { fs.unlinkSync(flagFile); fs.unlinkSync(psScriptPath); } catch { /* ignore */ } + return settle(resolve); + } + if (Date.now() > deadline) { + try { fs.unlinkSync(psScriptPath); } catch { /* ignore */ } + return settle(reject, new Error("Timed out waiting for UAC confirmation")); + } + setTimeout(poll, 500); + }; + setTimeout(poll, 300); + }); +} + /** * Execute command with sudo password via stdin (macOS/Linux only) */ @@ -99,18 +140,37 @@ async function addDNSEntry(tool, sudoPassword) { try { if (IS_WIN) { const hostsPath = HOSTS_FILE.replace(/'/g, "''"); - const addLines = entriesToAdd.map(h => - `$hc = Get-Content -Path '${hostsPath}' -Raw -ErrorAction SilentlyContinue; if ($hc -notmatch '${h}') { Add-Content -Path '${hostsPath}' -Value '127.0.0.1 ${h}' -Encoding UTF8 }` - ).join("; "); - const psScript = `${addLines}; ipconfig /flushdns | Out-Null`; - await new Promise((resolve, reject) => { - const escaped = psScript.replace(/"/g, '\\"'); - exec( - `powershell -NonInteractive -WindowStyle Hidden -Command "Start-Process powershell -ArgumentList '-NonInteractive -WindowStyle Hidden -Command \\"${escaped}\\"' -Verb RunAs -Wait"`, - { windowsHide: true }, - (error) => { if (error) reject(new Error(`Failed to add DNS: ${error.message}`)); else resolve(); } - ); - }); + + // Build PowerShell script with proper error handling + const scriptLines = []; + scriptLines.push(`$ErrorActionPreference = 'Stop'`); + scriptLines.push(`$hostsPath = '${hostsPath}'`); + scriptLines.push(`try {`); + scriptLines.push(` $hostsContent = Get-Content -Path $hostsPath -Raw -ErrorAction SilentlyContinue`); + scriptLines.push(` if (-not $hostsContent) { $hostsContent = '' }`); + + for (const host of entriesToAdd) { + // Escape special regex chars in hostname + const escapedHost = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + scriptLines.push(` if ($hostsContent -notmatch '${escapedHost}') {`); + scriptLines.push(` Add-Content -Path $hostsPath -Value '127.0.0.1 ${host}' -Encoding UTF8 -ErrorAction Stop`); + scriptLines.push(` Write-Host "Added DNS entry: ${host}"`); + scriptLines.push(` } else {`); + scriptLines.push(` Write-Host "DNS entry already exists: ${host}"`); + scriptLines.push(` }`); + } + + scriptLines.push(` ipconfig /flushdns | Out-Null`); + scriptLines.push(`} catch {`); + scriptLines.push(` Write-Error "Failed to add DNS: $_"`); + scriptLines.push(` exit 1`); + scriptLines.push(`}`); + + const psScript = scriptLines.join("\n"); + const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_add_${Date.now()}.ps1`); + fs.writeFileSync(tmpPs1, psScript, "utf8"); + + await executeElevatedPowerShell(tmpPs1, 30000); } else { await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword); await flushDNS(sudoPassword); @@ -139,23 +199,35 @@ async function removeDNSEntry(tool, sudoPassword) { if (IS_WIN) { const content = fs.readFileSync(HOSTS_FILE, "utf8"); const filtered = content.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n"); - const tmpFile = path.join(os.tmpdir(), "hosts_filtered.tmp"); + const tmpFile = path.join(os.tmpdir(), `hosts_filtered_${Date.now()}.tmp`); fs.writeFileSync(tmpFile, filtered, "utf8"); + const tmpEsc = tmpFile.replace(/'/g, "''"); const hostsEsc = HOSTS_FILE.replace(/'/g, "''"); - const psScript = `Copy-Item -Path '${tmpEsc}' -Destination '${hostsEsc}' -Force; ipconfig /flushdns | Out-Null; Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`; - await new Promise((resolve, reject) => { - const escaped = psScript.replace(/"/g, '\\"'); - exec( - `powershell -NonInteractive -WindowStyle Hidden -Command "Start-Process powershell -ArgumentList '-NonInteractive -WindowStyle Hidden -Command \\"${escaped}\\"' -Verb RunAs -Wait"`, - { windowsHide: true }, - (error) => { - try { fs.unlinkSync(tmpFile); } catch { /* ignore */ } - if (error) reject(new Error(`Failed to remove DNS: ${error.message}`)); - else resolve(); - } - ); - }); + + // Build PowerShell script with proper error handling + const scriptLines = []; + scriptLines.push(`$ErrorActionPreference = 'Stop'`); + scriptLines.push(`try {`); + scriptLines.push(` Copy-Item -Path '${tmpEsc}' -Destination '${hostsEsc}' -Force -ErrorAction Stop`); + scriptLines.push(` Write-Host "Hosts file updated successfully"`); + scriptLines.push(` ipconfig /flushdns | Out-Null`); + scriptLines.push(` Write-Host "DNS cache flushed"`); + scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`); + scriptLines.push(`} catch {`); + scriptLines.push(` Write-Error "Failed to remove DNS: $_"`); + scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`); + scriptLines.push(` exit 1`); + scriptLines.push(`}`); + + const psScript = scriptLines.join("\n"); + const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_remove_${Date.now()}.ps1`); + fs.writeFileSync(tmpPs1, psScript, "utf8"); + + await executeElevatedPowerShell(tmpPs1, 30000); + + // Cleanup temp file if still exists + try { fs.unlinkSync(tmpFile); } catch { /* ignore */ } } else { for (const host of entriesToRemove) { const sedCmd = IS_MAC @@ -191,6 +263,7 @@ module.exports = { removeDNSEntry, removeAllDNSEntries, execWithPassword, + executeElevatedPowerShell, checkDNSEntry, checkAllDNSStatus, }; diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 5a68d5d5..a265f46f 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -5,7 +5,7 @@ const os = require("os"); const net = require("net"); const https = require("https"); const crypto = require("crypto"); -const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus } = require("./dns/dnsConfig"); +const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, executeElevatedPowerShell, TOOL_HOSTS } = require("./dns/dnsConfig"); const IS_WIN = process.platform === "win32"; const { generateCert } = require("./cert/generate"); @@ -345,49 +345,23 @@ async function startServer(apiKey, sudoPassword) { // Step 2: Spawn server (Root CA already installed in Step 1.5) if (IS_WIN) { - const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts"); - const flagFile = path.join(os.tmpdir(), `mitm_ready_${Date.now()}.flag`); const psSQ = (s) => s.replace(/'/g, "''"); const nodePs = psSQ(process.execPath); const serverPs = psSQ(SERVER_PATH); - const flagPs = psSQ(flagFile); const psScript = [ + `$ErrorActionPreference = 'Stop'`, `$conn = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`, `if ($conn -and $conn.OwningProcess -gt 4) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }`, `Start-Sleep -Milliseconds 500`, `$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`, `Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`, `Start-Sleep -Milliseconds 500`, - `Set-Content -Path '${flagPs}' -Value 'ready' -Encoding UTF8`, ].join("\n"); const tmpPs1 = path.join(os.tmpdir(), `mitm_start_${Date.now()}.ps1`); fs.writeFileSync(tmpPs1, psScript, "utf8"); - const vbs = [ - `Set oShell = CreateObject("Shell.Application")`, - `Dim ps`, - `ps = Chr(34) & "powershell.exe" & Chr(34)`, - `Dim args`, - `args = "-NoProfile -ExecutionPolicy Bypass -File " & Chr(34) & "${tmpPs1}" & Chr(34)`, - `oShell.ShellExecute ps, args, "", "runas", 1`, - ].join("\r\n"); - const tmpVbs = path.join(os.tmpdir(), `mitm_uac_${Date.now()}.vbs`); - fs.writeFileSync(tmpVbs, vbs, "utf8"); - spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref(); - - await new Promise((resolve, reject) => { - const deadline = Date.now() + 90000; - const poll = () => { - if (fs.existsSync(flagFile)) { - try { fs.unlinkSync(flagFile); fs.unlinkSync(tmpPs1); fs.unlinkSync(tmpVbs); } catch { /* ignore */ } - return resolve(); - } - if (Date.now() > deadline) return reject(new Error("Timed out waiting for UAC confirmation.")); - setTimeout(poll, 500); - }; - poll(); - }); + await executeElevatedPowerShell(tmpPs1, 90000); if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { }); } else { @@ -451,38 +425,27 @@ async function startServer(apiKey, sudoPassword) { * Stop MITM server — removes ALL tool DNS entries first, then kills server */ async function stopServer(sudoPassword) { - // Remove all DNS entries first (before killing server) - console.log("[MITM] Removing all DNS entries before stopping server..."); - await removeAllDNSEntries(sudoPassword); + console.log("[MITM] Stopping server..."); + // Kill server process const proc = serverProcess; - if (proc && !proc.killed) { - console.log("Stopping MITM server..."); - killProcess(proc.pid, false, sudoPassword); - await new Promise(resolve => setTimeout(resolve, 1000)); - if (isProcessAlive(proc.pid)) killProcess(proc.pid, true, sudoPassword); - serverProcess = null; - serverPid = null; - } else { - try { - if (fs.existsSync(PID_FILE)) { - const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); - if (savedPid && isProcessAlive(savedPid)) { - console.log(`Killing MITM server (PID: ${savedPid})...`); - killProcess(savedPid, false, sudoPassword); - await new Promise(resolve => setTimeout(resolve, 1000)); - if (isProcessAlive(savedPid)) killProcess(savedPid, true, sudoPassword); - } - } - } catch { /* ignore */ } - serverProcess = null; - serverPid = null; + const pidToKill = proc && !proc.killed + ? proc.pid + : (() => { try { return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); } catch { return null; } })(); + + if (pidToKill && isProcessAlive(pidToKill)) { + console.log(`Killing MITM server (PID: ${pidToKill})...`); + killProcess(pidToKill, false, sudoPassword); + await new Promise(r => setTimeout(r, 1000)); + if (isProcessAlive(pidToKill)) killProcess(pidToKill, true, sudoPassword); } + serverProcess = null; + serverPid = null; if (IS_WIN) { + // Single elevated script: clean DNS + flush — 1 UAC prompt only const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts"); const psSQ = (s) => s.replace(/'/g, "''"); - const { TOOL_HOSTS } = require("./dns/dnsConfig"); const allHosts = Object.values(TOOL_HOSTS).flat(); let hostsContent = ""; @@ -490,41 +453,25 @@ async function stopServer(sudoPassword) { const filtered = hostsContent.split(/\r?\n/) .filter(l => !allHosts.some(h => l.includes(h))) .join("\r\n"); - const tmpHosts = path.join(os.tmpdir(), "mitm_hosts_clean.tmp"); + const tmpHosts = path.join(os.tmpdir(), `mitm_hosts_clean_${Date.now()}.tmp`); fs.writeFileSync(tmpHosts, filtered, "utf8"); - const flagFile = path.join(os.tmpdir(), "mitm_stop_done.flag"); const psScript = [ - `Copy-Item -Path '${psSQ(tmpHosts)}' -Destination '${psSQ(hostsFile)}' -Force`, - `& ipconfig /flushdns | Out-Null`, - `Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`, - `Set-Content -Path '${psSQ(flagFile)}' -Value 'done' -Encoding UTF8`, + `$ErrorActionPreference = 'Stop'`, + `try {`, + ` Copy-Item -Path '${psSQ(tmpHosts)}' -Destination '${psSQ(hostsFile)}' -Force -ErrorAction Stop`, + ` ipconfig /flushdns | Out-Null`, + ` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`, + `} catch {`, + ` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`, + `}`, ].join("\n"); - const tmpPs1 = path.join(os.tmpdir(), "mitm_stop.ps1"); + + const tmpPs1 = path.join(os.tmpdir(), `mitm_stop_${Date.now()}.ps1`); fs.writeFileSync(tmpPs1, psScript, "utf8"); - - const vbs = [ - `Set oShell = CreateObject("Shell.Application")`, - `Dim args`, - `args = "-NoProfile -ExecutionPolicy Bypass -File " & Chr(34) & "${tmpPs1}" & Chr(34)`, - `oShell.ShellExecute "powershell.exe", args, "", "runas", 1`, - ].join("\r\n"); - const tmpVbs = path.join(os.tmpdir(), "mitm_stop_uac.vbs"); - fs.writeFileSync(tmpVbs, vbs, "utf8"); - spawn("wscript.exe", [tmpVbs], { stdio: "ignore", windowsHide: true, detached: true }).unref(); - - await new Promise((resolve) => { - const deadline = Date.now() + 30000; - const poll = () => { - if (fs.existsSync(flagFile)) { - try { fs.unlinkSync(flagFile); fs.unlinkSync(tmpPs1); fs.unlinkSync(tmpVbs); } catch { /* ignore */ } - return resolve(); - } - if (Date.now() > deadline) return resolve(); - setTimeout(poll, 500); - }; - poll(); - }); + await executeElevatedPowerShell(tmpPs1, 30000); + } else { + await removeAllDNSEntries(sudoPassword); } try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ } diff --git a/src/shared/constants/config.js b/src/shared/constants/config.js index 6c2afb55..a680cf8b 100644 --- a/src/shared/constants/config.js +++ b/src/shared/constants/config.js @@ -43,6 +43,7 @@ export const PROVIDER_ENDPOINTS = { minimax: "https://api.minimax.io/anthropic/v1/messages", "minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages", alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions", + "alicode-intl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", openai: "https://api.openai.com/v1/chat/completions", anthropic: "https://api.anthropic.com/v1/messages", gemini: "https://generativelanguage.googleapis.com/v1beta/models", diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js index 507df145..b5ba86d1 100644 --- a/src/shared/constants/providers.js +++ b/src/shared/constants/providers.js @@ -27,6 +27,7 @@ export const APIKEY_PROVIDERS = { minimax: { id: "minimax", alias: "minimax", name: "Minimax Coding", icon: "memory", color: "#7C3AED", textIcon: "MM", website: "https://www.minimaxi.com" }, "minimax-cn": { id: "minimax-cn", alias: "minimax-cn", name: "Minimax (China)", icon: "memory", color: "#DC2626", textIcon: "MC", website: "https://www.minimaxi.com" }, alicode: { id: "alicode", alias: "alicode", name: "Alibaba", icon: "cloud", color: "#FF6A00", textIcon: "ALi" }, + "alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi" }, openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com" }, anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com" }, gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev" },