Feat : Add support for the new "alicode-intl" provider
This commit is contained in:
parent
0cf78fd76a
commit
758224749d
13 changed files with 227 additions and 119 deletions
|
|
@ -262,6 +262,11 @@ export const PROVIDERS = {
|
||||||
format: "openai",
|
format: "openai",
|
||||||
headers: {}
|
headers: {}
|
||||||
},
|
},
|
||||||
|
"alicode-intl": {
|
||||||
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
|
||||||
|
format: "openai",
|
||||||
|
headers: {}
|
||||||
|
},
|
||||||
github: {
|
github: {
|
||||||
baseUrl: "https://api.githubcopilot.com/chat/completions", // GitHub Copilot API endpoint for chat
|
baseUrl: "https://api.githubcopilot.com/chat/completions", // GitHub Copilot API endpoint for chat
|
||||||
responsesUrl: "https://api.githubcopilot.com/responses",
|
responsesUrl: "https://api.githubcopilot.com/responses",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export const PROVIDER_MODELS = {
|
||||||
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
||||||
],
|
],
|
||||||
cx: [ // OpenAI Codex
|
cx: [ // OpenAI Codex
|
||||||
|
{ id: "gpt-5.4", name: "GPT 5.4" },
|
||||||
// GPT 5.3 Codex - all thinking levels
|
// GPT 5.3 Codex - all thinking levels
|
||||||
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
|
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
|
||||||
{ id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" },
|
{ 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", name: "GPT-5.2" },
|
||||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||||
|
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||||
// GitHub Copilot - Anthropic models
|
// GitHub Copilot - Anthropic models
|
||||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||||
{ id: "claude-opus-4.1", name: "Claude Opus 4.1" },
|
{ 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: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
|
||||||
{ id: "glm-4.7", name: "GLM 4.7" },
|
{ 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: [
|
deepseek: [
|
||||||
{ id: "deepseek-chat", name: "DeepSeek V3.2 Chat" },
|
{ id: "deepseek-chat", name: "DeepSeek V3.2 Chat" },
|
||||||
{ id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" },
|
{ id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" },
|
||||||
|
|
@ -341,6 +352,7 @@ export const PROVIDER_ID_TO_ALIAS = {
|
||||||
minimax: "minimax",
|
minimax: "minimax",
|
||||||
"minimax-cn": "minimax-cn",
|
"minimax-cn": "minimax-cn",
|
||||||
alicode: "alicode",
|
alicode: "alicode",
|
||||||
|
"alicode-intl": "alicode-intl",
|
||||||
deepseek: "deepseek",
|
deepseek: "deepseek",
|
||||||
groq: "groq",
|
groq: "groq",
|
||||||
xai: "xai",
|
xai: "xai",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.3.33",
|
"version": "0.3.35",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
BIN
public/providers/alicode-intl.png
Normal file
BIN
public/providers/alicode-intl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/providers/alicode.png
Normal file
BIN
public/providers/alicode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -836,10 +836,27 @@ PassthroughModelsSection.propTypes = {
|
||||||
onDeleteAlias: PropTypes.func.isRequired,
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-sidebar/50">
|
<div className={`flex items-center gap-3 p-3 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
|
||||||
<span className="material-symbols-outlined text-base text-text-muted">smart_toy</span>
|
<span
|
||||||
|
className="material-symbols-outlined text-base text-text-muted"
|
||||||
|
style={iconColor ? { color: iconColor } : undefined}
|
||||||
|
>
|
||||||
|
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{modelId}</p>
|
<p className="text-sm font-medium truncate">{modelId}</p>
|
||||||
|
|
@ -855,6 +872,18 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
|
||||||
{copied === `model-${modelId}` ? "check" : "content_copy"}
|
{copied === `model-${modelId}` ? "check" : "content_copy"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
{onTest && (
|
||||||
|
<button
|
||||||
|
onClick={onTest}
|
||||||
|
disabled={isTesting}
|
||||||
|
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-colors"
|
||||||
|
title="Test model"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||||
|
{isTesting ? "progress_activity" : "science"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -876,12 +905,35 @@ PassthroughModelRow.propTypes = {
|
||||||
copied: PropTypes.string,
|
copied: PropTypes.string,
|
||||||
onCopy: PropTypes.func.isRequired,
|
onCopy: PropTypes.func.isRequired,
|
||||||
onDeleteAlias: 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 }) {
|
function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) {
|
||||||
const [newModel, setNewModel] = useState("");
|
const [newModel, setNewModel] = useState("");
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [importing, setImporting] = 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(
|
const providerAliases = Object.entries(modelAliases).filter(
|
||||||
([, model]) => model.startsWith(`${providerStorageAlias}/`)
|
([, model]) => model.startsWith(`${providerStorageAlias}/`)
|
||||||
|
|
@ -1008,6 +1060,9 @@ function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, m
|
||||||
copied={copied}
|
copied={copied}
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
onDeleteAlias={() => onDeleteAlias(alias)}
|
onDeleteAlias={() => onDeleteAlias(alias)}
|
||||||
|
onTest={connections.length > 0 ? () => handleTestModel(modelId) : undefined}
|
||||||
|
testStatus={modelTestResults[modelId]}
|
||||||
|
isTesting={testingModelId === modelId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,14 @@ const PROVIDER_MODELS_CONFIG = {
|
||||||
authPrefix: "Bearer ",
|
authPrefix: "Bearer ",
|
||||||
parseResponse: (data) => data.data || []
|
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
|
// OpenAI-compatible API key providers
|
||||||
deepseek: createOpenAIModelsConfig("https://api.deepseek.com/models"),
|
deepseek: createOpenAIModelsConfig("https://api.deepseek.com/models"),
|
||||||
|
|
|
||||||
|
|
@ -309,12 +309,16 @@ async function testApiKeyConnection(connection) {
|
||||||
const valid = res.status !== 401 && res.status !== 403;
|
const valid = res.status !== 401 && res.status !== 403;
|
||||||
return { valid, error: valid ? null : "Invalid API key" };
|
return { valid, error: valid ? null : "Invalid API key" };
|
||||||
}
|
}
|
||||||
case "alicode": {
|
case "alicode":
|
||||||
|
case "alicode-intl": {
|
||||||
// Aliyun Coding Plan uses OpenAI-compatible API
|
// 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",
|
method: "POST",
|
||||||
headers: { "Authorization": `Bearer ${connection.apiKey}`, "content-type": "application/json" },
|
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;
|
const valid = res.status !== 401 && res.status !== 403;
|
||||||
return { valid, error: valid ? null : "Invalid API key" };
|
return { valid, error: valid ? null : "Invalid API key" };
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ export async function POST(request) {
|
||||||
case "kimi":
|
case "kimi":
|
||||||
case "minimax":
|
case "minimax":
|
||||||
case "minimax-cn":
|
case "minimax-cn":
|
||||||
|
case "alicode-intl":
|
||||||
case "alicode": {
|
case "alicode": {
|
||||||
const claudeBaseUrls = {
|
const claudeBaseUrls = {
|
||||||
glm: "https://api.z.ai/api/anthropic/v1/messages",
|
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: "https://api.minimax.io/anthropic/v1/messages",
|
||||||
"minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
|
"minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
|
||||||
alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
|
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
|
// glm-cn, alicode and alicode-intl use OpenAI format
|
||||||
if (provider === "glm-cn" || provider === "alicode") {
|
if (provider === "glm-cn" || provider === "alicode" || provider === "alicode-intl") {
|
||||||
const testModel = getDefaultModel(provider);
|
const testModel = getDefaultModel(provider);
|
||||||
const glmCnRes = await fetch(claudeBaseUrls[provider], {
|
const glmCnRes = await fetch(claudeBaseUrls[provider], {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,47 @@ const HOSTS_FILE = IS_WIN
|
||||||
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
|
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
|
||||||
: "/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)
|
* Execute command with sudo password via stdin (macOS/Linux only)
|
||||||
*/
|
*/
|
||||||
|
|
@ -99,18 +140,37 @@ async function addDNSEntry(tool, sudoPassword) {
|
||||||
try {
|
try {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const hostsPath = HOSTS_FILE.replace(/'/g, "''");
|
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 }`
|
// Build PowerShell script with proper error handling
|
||||||
).join("; ");
|
const scriptLines = [];
|
||||||
const psScript = `${addLines}; ipconfig /flushdns | Out-Null`;
|
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
||||||
await new Promise((resolve, reject) => {
|
scriptLines.push(`$hostsPath = '${hostsPath}'`);
|
||||||
const escaped = psScript.replace(/"/g, '\\"');
|
scriptLines.push(`try {`);
|
||||||
exec(
|
scriptLines.push(` $hostsContent = Get-Content -Path $hostsPath -Raw -ErrorAction SilentlyContinue`);
|
||||||
`powershell -NonInteractive -WindowStyle Hidden -Command "Start-Process powershell -ArgumentList '-NonInteractive -WindowStyle Hidden -Command \\"${escaped}\\"' -Verb RunAs -Wait"`,
|
scriptLines.push(` if (-not $hostsContent) { $hostsContent = '' }`);
|
||||||
{ windowsHide: true },
|
|
||||||
(error) => { if (error) reject(new Error(`Failed to add DNS: ${error.message}`)); else resolve(); }
|
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 {
|
} else {
|
||||||
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||||
await flushDNS(sudoPassword);
|
await flushDNS(sudoPassword);
|
||||||
|
|
@ -139,23 +199,35 @@ async function removeDNSEntry(tool, sudoPassword) {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
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 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");
|
fs.writeFileSync(tmpFile, filtered, "utf8");
|
||||||
|
|
||||||
const tmpEsc = tmpFile.replace(/'/g, "''");
|
const tmpEsc = tmpFile.replace(/'/g, "''");
|
||||||
const hostsEsc = HOSTS_FILE.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) => {
|
// Build PowerShell script with proper error handling
|
||||||
const escaped = psScript.replace(/"/g, '\\"');
|
const scriptLines = [];
|
||||||
exec(
|
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
||||||
`powershell -NonInteractive -WindowStyle Hidden -Command "Start-Process powershell -ArgumentList '-NonInteractive -WindowStyle Hidden -Command \\"${escaped}\\"' -Verb RunAs -Wait"`,
|
scriptLines.push(`try {`);
|
||||||
{ windowsHide: true },
|
scriptLines.push(` Copy-Item -Path '${tmpEsc}' -Destination '${hostsEsc}' -Force -ErrorAction Stop`);
|
||||||
(error) => {
|
scriptLines.push(` Write-Host "Hosts file updated successfully"`);
|
||||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
scriptLines.push(` ipconfig /flushdns | Out-Null`);
|
||||||
if (error) reject(new Error(`Failed to remove DNS: ${error.message}`));
|
scriptLines.push(` Write-Host "DNS cache flushed"`);
|
||||||
else resolve();
|
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 {
|
} else {
|
||||||
for (const host of entriesToRemove) {
|
for (const host of entriesToRemove) {
|
||||||
const sedCmd = IS_MAC
|
const sedCmd = IS_MAC
|
||||||
|
|
@ -191,6 +263,7 @@ module.exports = {
|
||||||
removeDNSEntry,
|
removeDNSEntry,
|
||||||
removeAllDNSEntries,
|
removeAllDNSEntries,
|
||||||
execWithPassword,
|
execWithPassword,
|
||||||
|
executeElevatedPowerShell,
|
||||||
checkDNSEntry,
|
checkDNSEntry,
|
||||||
checkAllDNSStatus,
|
checkAllDNSStatus,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const os = require("os");
|
||||||
const net = require("net");
|
const net = require("net");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const crypto = require("crypto");
|
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 IS_WIN = process.platform === "win32";
|
||||||
const { generateCert } = require("./cert/generate");
|
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)
|
// Step 2: Spawn server (Root CA already installed in Step 1.5)
|
||||||
if (IS_WIN) {
|
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 psSQ = (s) => s.replace(/'/g, "''");
|
||||||
const nodePs = psSQ(process.execPath);
|
const nodePs = psSQ(process.execPath);
|
||||||
const serverPs = psSQ(SERVER_PATH);
|
const serverPs = psSQ(SERVER_PATH);
|
||||||
const flagPs = psSQ(flagFile);
|
|
||||||
|
|
||||||
const psScript = [
|
const psScript = [
|
||||||
|
`$ErrorActionPreference = 'Stop'`,
|
||||||
`$conn = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
`$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 }`,
|
`if ($conn -and $conn.OwningProcess -gt 4) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }`,
|
||||||
`Start-Sleep -Milliseconds 500`,
|
`Start-Sleep -Milliseconds 500`,
|
||||||
`$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`,
|
`$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`,
|
||||||
`Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`,
|
`Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`,
|
||||||
`Start-Sleep -Milliseconds 500`,
|
`Start-Sleep -Milliseconds 500`,
|
||||||
`Set-Content -Path '${flagPs}' -Value 'ready' -Encoding UTF8`,
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
const tmpPs1 = path.join(os.tmpdir(), `mitm_start_${Date.now()}.ps1`);
|
const tmpPs1 = path.join(os.tmpdir(), `mitm_start_${Date.now()}.ps1`);
|
||||||
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
||||||
const vbs = [
|
await executeElevatedPowerShell(tmpPs1, 90000);
|
||||||
`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();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -451,38 +425,27 @@ async function startServer(apiKey, sudoPassword) {
|
||||||
* Stop MITM server — removes ALL tool DNS entries first, then kills server
|
* Stop MITM server — removes ALL tool DNS entries first, then kills server
|
||||||
*/
|
*/
|
||||||
async function stopServer(sudoPassword) {
|
async function stopServer(sudoPassword) {
|
||||||
// Remove all DNS entries first (before killing server)
|
console.log("[MITM] Stopping server...");
|
||||||
console.log("[MITM] Removing all DNS entries before stopping server...");
|
|
||||||
await removeAllDNSEntries(sudoPassword);
|
|
||||||
|
|
||||||
|
// Kill server process
|
||||||
const proc = serverProcess;
|
const proc = serverProcess;
|
||||||
if (proc && !proc.killed) {
|
const pidToKill = proc && !proc.killed
|
||||||
console.log("Stopping MITM server...");
|
? proc.pid
|
||||||
killProcess(proc.pid, false, sudoPassword);
|
: (() => { try { return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); } catch { return null; } })();
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
if (isProcessAlive(proc.pid)) killProcess(proc.pid, true, sudoPassword);
|
if (pidToKill && isProcessAlive(pidToKill)) {
|
||||||
serverProcess = null;
|
console.log(`Killing MITM server (PID: ${pidToKill})...`);
|
||||||
serverPid = null;
|
killProcess(pidToKill, false, sudoPassword);
|
||||||
} else {
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
try {
|
if (isProcessAlive(pidToKill)) killProcess(pidToKill, true, sudoPassword);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
serverProcess = null;
|
||||||
|
serverPid = null;
|
||||||
|
|
||||||
if (IS_WIN) {
|
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 hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
|
||||||
const psSQ = (s) => s.replace(/'/g, "''");
|
const psSQ = (s) => s.replace(/'/g, "''");
|
||||||
const { TOOL_HOSTS } = require("./dns/dnsConfig");
|
|
||||||
const allHosts = Object.values(TOOL_HOSTS).flat();
|
const allHosts = Object.values(TOOL_HOSTS).flat();
|
||||||
|
|
||||||
let hostsContent = "";
|
let hostsContent = "";
|
||||||
|
|
@ -490,41 +453,25 @@ async function stopServer(sudoPassword) {
|
||||||
const filtered = hostsContent.split(/\r?\n/)
|
const filtered = hostsContent.split(/\r?\n/)
|
||||||
.filter(l => !allHosts.some(h => l.includes(h)))
|
.filter(l => !allHosts.some(h => l.includes(h)))
|
||||||
.join("\r\n");
|
.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");
|
fs.writeFileSync(tmpHosts, filtered, "utf8");
|
||||||
|
|
||||||
const flagFile = path.join(os.tmpdir(), "mitm_stop_done.flag");
|
|
||||||
const psScript = [
|
const psScript = [
|
||||||
`Copy-Item -Path '${psSQ(tmpHosts)}' -Destination '${psSQ(hostsFile)}' -Force`,
|
`$ErrorActionPreference = 'Stop'`,
|
||||||
`& ipconfig /flushdns | Out-Null`,
|
`try {`,
|
||||||
`Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`,
|
` Copy-Item -Path '${psSQ(tmpHosts)}' -Destination '${psSQ(hostsFile)}' -Force -ErrorAction Stop`,
|
||||||
`Set-Content -Path '${psSQ(flagFile)}' -Value 'done' -Encoding UTF8`,
|
` ipconfig /flushdns | Out-Null`,
|
||||||
|
` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`,
|
||||||
|
`} catch {`,
|
||||||
|
` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`,
|
||||||
|
`}`,
|
||||||
].join("\n");
|
].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");
|
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
||||||
|
await executeElevatedPowerShell(tmpPs1, 30000);
|
||||||
const vbs = [
|
} else {
|
||||||
`Set oShell = CreateObject("Shell.Application")`,
|
await removeAllDNSEntries(sudoPassword);
|
||||||
`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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ export const PROVIDER_ENDPOINTS = {
|
||||||
minimax: "https://api.minimax.io/anthropic/v1/messages",
|
minimax: "https://api.minimax.io/anthropic/v1/messages",
|
||||||
"minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
|
"minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages",
|
||||||
alicode: "https://coding.dashscope.aliyuncs.com/v1/chat/completions",
|
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",
|
openai: "https://api.openai.com/v1/chat/completions",
|
||||||
anthropic: "https://api.anthropic.com/v1/messages",
|
anthropic: "https://api.anthropic.com/v1/messages",
|
||||||
gemini: "https://generativelanguage.googleapis.com/v1beta/models",
|
gemini: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||||
|
|
|
||||||
|
|
@ -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: { 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" },
|
"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: { 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" },
|
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" },
|
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" },
|
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue