diff --git a/.gitignore b/.gitignore index 70993ce..fabb175 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ package-lock.json # Camoufox browser camoufox/ +camoufox-linux/ +camoufox-macos/ camoufox*.zip # Authentication files diff --git a/README.md b/README.md index ae29cbd..173ad75 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,18 @@ ## 🚀 快速开始 -### 💻 本地运行(仅支持 Windows) +### 💻 本地运行(Windows / macOS / Linux) 1. 克隆仓库: -```powershell +```bash git clone https://github.com/iBenzene/AIStudioToAPI.git cd AIStudioToAPI ``` 2. 运行快速设置脚本: -```powershell +```bash npm run setup-auth ``` @@ -40,8 +40,7 @@ npm run setup-auth 3. 启动服务: -```powershell -npm install +```bash npm start ``` @@ -49,7 +48,7 @@ API 服务将在 `http://localhost:7860` 上运行。 服务启动后,您可以在浏览器中访问 `http://localhost:7860` 打开 Web 控制台主页,在这里可以查看账号状态和服务状态。 -> ⚠ **注意:** Windows 本地运行不支持通过 VNC 在线添加账号,需要使用 `npm run setup-auth` 脚本添加账号。当前 VNC 登录功能仅在 Linux 服务器上的 Docker 容器中可用。 +> ⚠ **注意:** 本地运行不支持通过 VNC 在线添加账号,需要使用 `npm run setup-auth` 脚本添加账号。当前 VNC 登录功能仅在 Linux 服务器上的 Docker 容器中可用。 ### ☁ 云端部署(Linux VPS) @@ -131,7 +130,7 @@ sudo docker compose down **方法 2:上传认证文件** -- 在 Windows 机器上运行 `npm run setup-auth` 生成认证文件 +- 在本地机器上运行 `npm run setup-auth` 生成认证文件 - 在网页控制台,点击「上传 Auth」,上传 auth 的 JSON 文件,或手动上传到挂载的 `/path/to/auth` 目录 > 💡 **提示**:您也可以从已有的服务器下载 auth 文件,然后上传到新的服务器。在网页控制台点击对应账号的「下载 Auth」按钮即可下载 auth 文件。 diff --git a/README_EN.md b/README_EN.md index 79584f6..e423b5c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -17,18 +17,18 @@ A tool that wraps Google AI Studio web interface to provide OpenAI API and Gemin ## 🚀 Quick Start -### 💻 Local Development (Windows Only) +### 💻 Local Development (Windows / macOS / Linux) 1. Clone the repository: -```powershell +```bash git clone https://github.com/iBenzene/AIStudioToAPI.git cd AIStudioToAPI ``` 2. Run the setup script: -```powershell +```bash npm run setup-auth ``` @@ -40,8 +40,7 @@ This script will: 3. Start the service: -```powershell -npm install +```bash npm start ``` @@ -49,7 +48,7 @@ The API server will be available at `http://localhost:7860` After the service starts, you can access `http://localhost:7860` in your browser to open the web console homepage, where you can view account status and service status. -> ⚠ **Note:** Windows local deployment does not support adding accounts via VNC online. You need to use the `npm run setup-auth` script to add accounts. VNC login is only available in Docker deployments on Linux servers. +> ⚠ **Note:** Local deployment does not support adding accounts via VNC online. You need to use the `npm run setup-auth` script to add accounts. VNC login is only available in Docker deployments on Linux servers. ### ☁ Cloud Deployment (Linux VPS) @@ -131,7 +130,7 @@ After deployment, you need to add Google accounts using one of these methods: **Method 2: Upload Auth Files** -- Run `npm run setup-auth` on a Windows machine to generate auth files +- Run `npm run setup-auth` on your local machine to generate auth files - In the web console, click "Upload Auth" to upload the auth JSON file, or manually upload to the mounted `/path/to/auth` directory > 💡 **Tip**: You can also download auth files from an existing server and upload them to a new server. Click the "Download Auth" button for the corresponding account in the web console to download the auth file. diff --git a/docs/en/claw-cloud-run.md b/docs/en/claw-cloud-run.md index b212e35..db3c9d3 100644 --- a/docs/en/claw-cloud-run.md +++ b/docs/en/claw-cloud-run.md @@ -54,6 +54,7 @@ After deployment, you need to add Google accounts. There are two methods: **Method 2: Upload Auth Files** - Run `npm run setup-auth` on a Windows machine to generate auth files +- Run `npm run setup-auth` on your local machine to generate auth files - In the web console, click "Upload Auth" to upload the auth JSON file > 💡 **Tip**: You can also download auth files from an existing server and upload them to a new server. Click the "Download Auth" button for the corresponding account in the web console to download the auth file. diff --git a/docs/en/zeabur.md b/docs/en/zeabur.md index 60e0c6d..113eb63 100644 --- a/docs/en/zeabur.md +++ b/docs/en/zeabur.md @@ -53,7 +53,7 @@ After deployment, you need to add Google accounts. There are two methods: **Method 2: Upload Auth Files** -- Run `npm run setup-auth` on a Windows machine to generate auth files +- Run `npm run setup-auth` on your local machine to generate auth files - In the web console, click "Upload Auth" to upload the auth JSON file > 💡 **Tip**: You can also download auth files from an existing server and upload them to a new server. Click the "Download Auth" button for the corresponding account in the web console to download the auth file. diff --git a/docs/zh/claw-cloud-run.md b/docs/zh/claw-cloud-run.md index 6bdb954..b5b3288 100644 --- a/docs/zh/claw-cloud-run.md +++ b/docs/zh/claw-cloud-run.md @@ -53,7 +53,7 @@ **方法 2:上传认证文件** -- 在 Windows 机器上运行 `npm run setup-auth` 生成认证文件 +- 在本地机器上运行 `npm run setup-auth` 生成认证文件 - 在网页控制台,点击「上传 Auth」,上传 auth 的 JSON 文件 > 💡 **提示**:您也可以从已有的服务器下载 auth 文件,然后上传到新的服务器。在网页控制台点击对应账号的「下载 Auth」按钮即可下载 auth 文件。 diff --git a/docs/zh/zeabur.md b/docs/zh/zeabur.md index 778ad9c..1769822 100644 --- a/docs/zh/zeabur.md +++ b/docs/zh/zeabur.md @@ -53,7 +53,7 @@ **方法 2:上传认证文件** -- 在 Windows 机器上运行 `npm run setup-auth` 生成认证文件 +- 在本地机器上运行 `npm run setup-auth` 生成认证文件 - 在网页控制台,点击「上传 Auth」,上传 auth 的 JSON 文件 > 💡 **提示**:您也可以从已有的服务器下载 auth 文件,然后上传到新的服务器。在网页控制台点击对应账号的「下载 Auth」按钮即可下载 auth 文件。 diff --git a/package.json b/package.json index d44c5cc..eafadca 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "start": "cross-env NODE_ENV=production node main.js", "prestart": "npm run build:ui", "save-auth": "node scripts/auth/saveAuth.js", - "setup-auth": "scripts\\auth\\setupAuth.bat", + "setup-auth": "node scripts/auth/setupAuth.js", "prepare": "husky install", "lint": "eslint . && stylelint \"ui/**/*.{css,less}\"", "lint:fix": "eslint . --fix && stylelint \"ui/**/*.{css,less}\" --fix", diff --git a/scripts/auth/saveAuth.js b/scripts/auth/saveAuth.js index 516d689..8691804 100644 --- a/scripts/auth/saveAuth.js +++ b/scripts/auth/saveAuth.js @@ -8,10 +8,20 @@ const { firefox } = require("playwright"); const fs = require("fs"); +const os = require("os"); const path = require("path"); // --- Configuration Constants --- -const browserExecutablePath = path.join(__dirname, "..", "..", "camoufox", "camoufox.exe"); +const getDefaultBrowserExecutablePath = () => { + const platform = os.platform(); + if (platform === "linux") return path.join(__dirname, "..", "..", "camoufox-linux", "camoufox"); + if (platform === "win32") return path.join(__dirname, "..", "..", "camoufox", "camoufox.exe"); + if (platform === "darwin") + return path.join(__dirname, "..", "..", "camoufox-macos", "Camoufox.app", "Contents", "MacOS", "camoufox"); + return null; +}; + +const browserExecutablePath = process.env.CAMOUFOX_EXECUTABLE_PATH || getDefaultBrowserExecutablePath(); const VALIDATION_LINE_THRESHOLD = 200; // Validation line threshold const CONFIG_DIR = "configs/auth"; // Authentication files directory @@ -59,6 +69,13 @@ const getNextAuthIndex = () => { console.log(`▶️ Preparing to create new authentication file for account #${newIndex}...`); console.log(`▶️ Launching browser: ${browserExecutablePath}`); + if (!browserExecutablePath || !fs.existsSync(browserExecutablePath)) { + console.error("❌ Camoufox executable not found."); + console.error(` -> Checked: ${browserExecutablePath || "(null)"}`); + console.error(' -> Please run "npm run setup-auth" first, or set CAMOUFOX_EXECUTABLE_PATH.'); + process.exit(1); + } + const browser = await firefox.launch({ executablePath: browserExecutablePath, headless: false, diff --git a/scripts/auth/setupAuth.js b/scripts/auth/setupAuth.js new file mode 100644 index 0000000..e1aed54 --- /dev/null +++ b/scripts/auth/setupAuth.js @@ -0,0 +1,445 @@ +/** + * File: scripts/auth/setupAuth.js + * Description: Cross-platform auth setup helper. Installs dependencies, downloads Camoufox, and runs saveAuth.js. + * + * Maintainers: iBenzene, bbbugg + */ + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const https = require("https"); +const os = require("os"); +const path = require("path"); + +const DEFAULT_CAMOUFOX_VERSION = "135.0.1-beta.24"; +const GITHUB_RELEASE_TAG_PREFIX = "v"; + +const PROJECT_ROOT = path.join(__dirname, "..", ".."); + +const execOrThrow = (command, args, options) => { + const result = spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + + if (result.error) throw result.error; + if (typeof result.status === "number" && result.status !== 0) { + throw new Error(`Command failed: ${command} ${args.join(" ")}`); + } +}; + +const pathExists = p => { + try { + fs.accessSync(p); + return true; + } catch { + return false; + } +}; + +const ensureDir = dirPath => { + if (!pathExists(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); +}; + +const npmCommand = () => (process.platform === "win32" ? "npm.cmd" : "npm"); + +const getCamoufoxInstallConfig = () => { + const platform = process.platform; + + if (platform === "win32") { + const dir = path.join(PROJECT_ROOT, "camoufox"); + return { + platform, + installDir: dir, + expectedExecutablePath: path.join(dir, "camoufox.exe"), + expectedExecutableName: "camoufox.exe", + expectedAppDirName: null, + }; + } + + if (platform === "linux") { + const dir = path.join(PROJECT_ROOT, "camoufox-linux"); + return { + platform, + installDir: dir, + expectedExecutablePath: path.join(dir, "camoufox"), + expectedExecutableName: "camoufox", + expectedAppDirName: null, + }; + } + + if (platform === "darwin") { + const dir = path.join(PROJECT_ROOT, "camoufox-macos"); + return { + platform, + installDir: dir, + expectedExecutablePath: path.join(dir, "Camoufox.app", "Contents", "MacOS", "camoufox"), + expectedExecutableName: "camoufox", + expectedAppDirName: "Camoufox.app", + }; + } + + throw new Error(`Unsupported operating system: ${platform}`); +}; + +const downloadFile = async (url, outFilePath) => { + const maxRedirects = 10; + + const fetchOnce = (currentUrl, redirectsLeft) => + new Promise((resolve, reject) => { + const request = https.get( + currentUrl, + { + headers: { + "User-Agent": "aistudio-to-api setup-auth", + Accept: "*/*", + }, + }, + res => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + res.resume(); + if (redirectsLeft <= 0) { + reject(new Error(`Too many redirects while downloading: ${url}`)); + return; + } + resolve(fetchOnce(res.headers.location, redirectsLeft - 1)); + return; + } + + if (res.statusCode !== 200) { + const chunks = []; + res.on("data", chunk => chunks.push(chunk)); + res.on("end", () => { + const body = Buffer.concat(chunks).toString("utf-8"); + reject(new Error(`Download failed (${res.statusCode}): ${body.slice(0, 300)}`)); + }); + return; + } + + const fileStream = fs.createWriteStream(outFilePath); + res.pipe(fileStream); + fileStream.on("finish", () => fileStream.close(() => resolve())); + fileStream.on("error", err => { + try { + fs.unlinkSync(outFilePath); + } catch { + // ignore cleanup error + } + reject(err); + }); + } + ); + request.on("error", reject); + }); + + await fetchOnce(url, maxRedirects); +}; + +const fetchJson = async url => + new Promise((resolve, reject) => { + https.get( + url, + { + headers: { + "User-Agent": "aistudio-to-api setup-auth", + Accept: "application/vnd.github+json", + }, + }, + res => { + const chunks = []; + res.on("data", chunk => chunks.push(chunk)); + res.on("end", () => { + const body = Buffer.concat(chunks).toString("utf-8"); + if (res.statusCode !== 200) { + reject(new Error(`GitHub API request failed (${res.statusCode}): ${body.slice(0, 300)}`)); + return; + } + try { + resolve(JSON.parse(body)); + } catch (error) { + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)); + } + }); + } + ).on("error", reject); + }); + +const selectCamoufoxAsset = (assets, platform, arch) => { + if (!Array.isArray(assets)) return null; + + const isZip = a => typeof a?.name === "string" && a.name.toLowerCase().endsWith(".zip"); + const nameOf = a => String(a?.name || "").toLowerCase(); + + const hasAny = (name, tokens) => tokens.some(t => name.includes(t)); + + const isWindows = name => hasAny(name, ["win", "windows"]); + const isLinux = name => hasAny(name, ["lin", "linux"]); + const isDarwin = name => hasAny(name, ["mac", "macos", "osx", "darwin"]); + + const isArm64 = name => hasAny(name, ["arm64", "aarch64"]); + const isX64 = name => hasAny(name, ["x86_64", "x64", "amd64"]); + + const platformMatcher = name => { + if (platform === "win32") return isWindows(name); + if (platform === "linux") return isLinux(name); + if (platform === "darwin") return isDarwin(name); + return false; + }; + + const archMatcher = name => { + if (arch === "arm64") return isArm64(name); + if (arch === "x64") return isX64(name); + return false; + }; + + const candidates = assets + .filter(a => isZip(a)) + .filter(a => platformMatcher(nameOf(a))) + .filter(a => archMatcher(nameOf(a))); + + if (candidates.length === 0) return null; + + candidates.sort((a, b) => (b.size || 0) - (a.size || 0)); + return candidates[0]; +}; + +const extractZip = (zipFilePath, destinationDir) => { + if (process.platform === "win32") { + execOrThrow( + "powershell", + [ + "-NoProfile", + "-Command", + `Expand-Archive -Path "${zipFilePath}" -DestinationPath "${destinationDir}" -Force`, + ], + { cwd: PROJECT_ROOT } + ); + return; + } + + const unzipCheck = spawnSync("unzip", ["-v"], { stdio: "ignore" }); + if (unzipCheck.error || unzipCheck.status !== 0) { + throw new Error( + 'Missing "unzip" command. Please install it (macOS usually has it), or set CAMOUFOX_URL and extract manually.' + ); + } + + execOrThrow("unzip", ["-q", zipFilePath, "-d", destinationDir], { cwd: PROJECT_ROOT }); +}; + +const walkFiles = (rootDir, maxDepth, onEntry) => { + const stack = [{ dir: rootDir, depth: 0 }]; + while (stack.length > 0) { + const current = stack.pop(); + const entries = fs.readdirSync(current.dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(current.dir, entry.name); + onEntry(fullPath, entry); + if (entry.isDirectory() && current.depth < maxDepth) { + stack.push({ dir: fullPath, depth: current.depth + 1 }); + } + } + } +}; + +const locatePath = (rootDir, maxDepth, predicate) => { + let found = null; + walkFiles(rootDir, maxDepth, (fullPath, entry) => { + if (found) return; + if (predicate(fullPath, entry)) found = fullPath; + }); + return found; +}; + +const ensureCamoufoxExecutable = async () => { + const { installDir, expectedExecutablePath, expectedExecutableName, expectedAppDirName } = getCamoufoxInstallConfig(); + + if (pathExists(expectedExecutablePath)) return expectedExecutablePath; + + const version = process.env.CAMOUFOX_VERSION || DEFAULT_CAMOUFOX_VERSION; + const tag = `${GITHUB_RELEASE_TAG_PREFIX}${version}`; + + const camoufoxUrlFromEnv = process.env.CAMOUFOX_URL; + let downloadUrl = camoufoxUrlFromEnv; + + if (!downloadUrl) { + const apiUrl = `https://api.github.com/repos/daijro/camoufox/releases/tags/${tag}`; + const release = await fetchJson(apiUrl); + const asset = selectCamoufoxAsset(release?.assets, process.platform, process.arch); + + if (!asset?.browser_download_url) { + const assetNames = Array.isArray(release?.assets) + ? release.assets.map(a => a?.name).filter(Boolean) + : []; + throw new Error( + [ + `Unable to find a Camoufox asset for platform=${process.platform} arch=${process.arch}.`, + `Please set CAMOUFOX_URL to a direct download URL, or download it manually into ${path.relative( + PROJECT_ROOT, + installDir + )}.`, + assetNames.length > 0 ? `Available assets: ${assetNames.join(", ")}` : "No assets found in release.", + ].join("\n") + ); + } + + downloadUrl = asset.browser_download_url; + } + + ensureDir(installDir); + + const zipFilePath = path.join(PROJECT_ROOT, "camoufox.zip"); + + console.log(`[2/4] Checking Camoufox...`); + console.log(`Downloading Camoufox (${version})...`); + console.log(`Download URL: ${downloadUrl}`); + + await downloadFile(downloadUrl, zipFilePath); + console.log("Download complete."); + + console.log(`[3/4] Extracting Camoufox...`); + extractZip(zipFilePath, installDir); + + try { + fs.unlinkSync(zipFilePath); + } catch { + // ignore cleanup error + } + + if (!pathExists(expectedExecutablePath)) { + if (expectedAppDirName) { + const foundApp = locatePath(installDir, 4, (fullPath, entry) => entry.isDirectory() && entry.name === expectedAppDirName); + if (foundApp) { + const targetApp = path.join(installDir, expectedAppDirName); + if (!pathExists(targetApp)) { + fs.renameSync(foundApp, targetApp); + } + } + } else { + const foundExe = locatePath( + installDir, + 4, + (fullPath, entry) => entry.isFile() && entry.name === expectedExecutableName + ); + if (foundExe) { + const targetExe = path.join(installDir, expectedExecutableName); + if (!pathExists(targetExe)) { + fs.renameSync(foundExe, targetExe); + } + } + } + } + + if (!pathExists(expectedExecutablePath)) { + throw new Error( + [ + "Camoufox extraction completed, but the executable was not found.", + `Expected: ${expectedExecutablePath}`, + "Try deleting the camoufox directory and rerun setup, or set CAMOUFOX_EXECUTABLE_PATH manually.", + ].join("\n") + ); + } + + if (process.platform !== "win32") { + try { + fs.chmodSync(expectedExecutablePath, 0o755); + } catch { + // ignore chmod error + } + } + + return expectedExecutablePath; +}; + +const ensureNodeModules = () => { + console.log(`[1/4] Checking Node.js dependencies...`); + const nodeModulesDir = path.join(PROJECT_ROOT, "node_modules"); + if (pathExists(nodeModulesDir)) { + console.log("Dependencies exist, skipping installation."); + return; + } + console.log("Installing npm dependencies..."); + execOrThrow(npmCommand(), ["install"], { cwd: PROJECT_ROOT }); +}; + +const runSaveAuth = camoufoxExecutablePath => { + console.log(`[4/4] Starting auth save tool...`); + console.log(""); + console.log("=========================================="); + console.log(" Please follow the prompts to login"); + console.log("=========================================="); + console.log(""); + + const env = { + ...process.env, + CAMOUFOX_EXECUTABLE_PATH: camoufoxExecutablePath, + }; + + const result = spawnSync(process.execPath, [path.join("scripts", "auth", "saveAuth.js")], { + cwd: PROJECT_ROOT, + stdio: "inherit", + env, + }); + + if (result.error) throw result.error; + if (typeof result.status === "number" && result.status !== 0) { + throw new Error("Auth save failed. Please check error messages above."); + } +}; + +const main = async () => { + const args = process.argv.slice(2); + if (args.includes("-h") || args.includes("--help")) { + console.log("Usage: npm run setup-auth"); + console.log(""); + console.log("Optional env vars:"); + console.log(" CAMOUFOX_VERSION=135.0.1-beta.24"); + console.log(" CAMOUFOX_URL="); + console.log(" CAMOUFOX_EXECUTABLE_PATH="); + process.exit(0); + } + + console.log("=========================================="); + console.log(" AI Studio To API - Auth Setup"); + console.log("=========================================="); + console.log(`OS: ${os.platform()} Arch: ${os.arch()}`); + console.log(""); + + ensureNodeModules(); + + let camoufoxExecutablePath = process.env.CAMOUFOX_EXECUTABLE_PATH; + if (!camoufoxExecutablePath) { + camoufoxExecutablePath = await ensureCamoufoxExecutable(); + } + + console.log(`Camoufox executable: ${camoufoxExecutablePath}`); + + if (process.platform === "darwin") { + console.log(""); + console.log( + 'macOS 提示:如果首次运行被 Gatekeeper 阻止,请到「系统设置 -> 隐私与安全性」允许该应用后重试。' + ); + } + + runSaveAuth(camoufoxExecutablePath); + + console.log(""); + console.log("=========================================="); + console.log(" Auth setup complete!"); + console.log("=========================================="); + console.log(""); + console.log('Auth files saved to "configs/auth".'); + console.log('You can now run "npm start" to start the server.'); +}; + +main().catch(error => { + console.error(""); + console.error("ERROR:", error?.message || error); + process.exit(1); +}); +