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 00000000..21c3ef3d Binary files /dev/null and b/public/providers/alicode-intl.png differ diff --git a/public/providers/alicode.png b/public/providers/alicode.png new file mode 100644 index 00000000..21c3ef3d Binary files /dev/null and b/public/providers/alicode.png differ diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 5c8066d3..d8c265fb 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -836,10 +836,27 @@ PassthroughModelsSection.propTypes = { onDeleteAlias: PropTypes.func.isRequired, }; -function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) { +function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias, onTest, testStatus, isTesting }) { + const borderColor = testStatus === "ok" + ? "border-green-500/40" + : testStatus === "error" + ? "border-red-500/40" + : "border-border"; + + const iconColor = testStatus === "ok" + ? "#22c55e" + : testStatus === "error" + ? "#ef4444" + : undefined; + return ( -
- 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" },