#!/usr/bin/env bash set -Eeuo pipefail BASE_URL="https://code.aiserverai.online/v1" NODE_MIRROR="https://npmmirror.com/mirrors/node" NPM_MIRROR="https://registry.npmmirror.com" SUPPORT_EMAIL="alexisty233@gmail.com" INSTALL_ROOT="$HOME/.local" BIN_DIR="$INSTALL_ROOT/bin" LIB_DIR="$INSTALL_ROOT/lib" CODEX_HOME_DIR="$HOME/.codex" TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT fail() { trap - ERR if command -v zenity >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then zenity --error --title="Codex 安装失败" --text="$1\n\n请附完整失败截图联系管理员:$SUPPORT_EMAIL\n我们会在 72 小时内更新脚本,请更新后重试。" >/dev/null 2>&1 || true fi printf '安装失败:%s\n请附完整失败截图联系管理员:%s\n我们会在 72 小时内更新脚本,请更新后重试。\n' "$1" "$SUPPORT_EMAIL" >&2 exit 1 } handle_error() { local status="$?" local line="$1" fail "安装步骤执行失败(第 $line 行,退出码 $status)" } trap 'handle_error "$LINENO"' ERR download_text() { if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 "$1" elif command -v wget >/dev/null 2>&1; then wget -q -t 3 -O - "$1" else fail "系统缺少 curl 或 wget,请先安装其中一个下载工具" fi } download_file() { if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 -o "$2" "$1" elif command -v wget >/dev/null 2>&1; then wget -q -t 3 -O "$2" "$1" else fail "系统缺少 curl 或 wget,请先安装其中一个下载工具" fi } ensure_node() { if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || printf '0')" if [ "$NODE_MAJOR" -ge 20 ]; then return fi fi printf '正在安装 Node.js LTS…\n' if ! NODE_INDEX="$(download_text "$NODE_MIRROR/index.tab")"; then NODE_INDEX="$(download_text "https://nodejs.org/dist/index.tab")" fi NODE_VERSION="$(printf '%s\n' "$NODE_INDEX" | awk 'NR > 1 && $10 != "-" { print $1; exit }')" [ -n "$NODE_VERSION" ] || fail "无法获取 Node.js LTS 版本" case "$(uname -m)" in aarch64|arm64) NODE_ARCH="arm64" ;; x86_64|amd64) NODE_ARCH="x64" ;; *) fail "暂不支持此 Linux 架构:$(uname -m)" ;; esac ARCHIVE="$TMP_DIR/node.tar.gz" ARCHIVE_NAME="node-$NODE_VERSION-linux-$NODE_ARCH.tar.gz" if ! download_file "$NODE_MIRROR/$NODE_VERSION/$ARCHIVE_NAME" "$ARCHIVE"; then download_file "https://nodejs.org/dist/$NODE_VERSION/$ARCHIVE_NAME" "$ARCHIVE" fi if ! CHECKSUMS="$(download_text "$NODE_MIRROR/$NODE_VERSION/SHASUMS256.txt")"; then CHECKSUMS="$(download_text "https://nodejs.org/dist/$NODE_VERSION/SHASUMS256.txt")" fi EXPECTED_HASH="$(printf '%s\n' "$CHECKSUMS" | awk -v file="$ARCHIVE_NAME" '$2 == file { print $1; exit }')" ACTUAL_HASH="$(sha256sum "$ARCHIVE" | awk '{ print $1 }')" [ -n "$EXPECTED_HASH" ] && [ "$EXPECTED_HASH" = "$ACTUAL_HASH" ] || fail "Node.js 安装包校验失败" tar -xzf "$ARCHIVE" -C "$TMP_DIR" EXTRACTED="$TMP_DIR/node-$NODE_VERSION-linux-$NODE_ARCH" NODE_HOME="$LIB_DIR/node-$NODE_VERSION-linux-$NODE_ARCH" mkdir -p "$LIB_DIR" "$BIN_DIR" if [ ! -d "$NODE_HOME" ]; then mv "$EXTRACTED" "$NODE_HOME" fi ln -sf "$NODE_HOME/bin/node" "$BIN_DIR/node" ln -sf "$NODE_HOME/bin/npm" "$BIN_DIR/npm" ln -sf "$NODE_HOME/bin/npx" "$BIN_DIR/npx" export PATH="$BIN_DIR:$PATH" } persist_path() { PROFILE_FILE="$HOME/.profile" PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' touch "$PROFILE_FILE" if ! grep -Fq "$PATH_LINE" "$PROFILE_FILE"; then printf '\n%s\n' "$PATH_LINE" >> "$PROFILE_FILE" fi export PATH="$BIN_DIR:$PATH" } prompt_key() { if command -v zenity >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then zenity --password --title="Codex 一键配置" --text="请输入你的 Sub2API Key" return fi printf '请输入你的 Sub2API Key:' >&2 read -r -s API_KEY_INPUT printf '\n' >&2 printf '%s' "$API_KEY_INPUT" } write_config() { mkdir -p "$CODEX_HOME_DIR" chmod 700 "$CODEX_HOME_DIR" STAMP="$(date +%Y%m%d-%H%M%S)" MAIN_CONFIG="$CODEX_HOME_DIR/config.toml" [ ! -f "$MAIN_CONFIG" ] || cp "$MAIN_CONFIG" "$MAIN_CONFIG.backup-$STAMP" PRESERVED_CONFIG="$TMP_DIR/config.preserved.toml" if [ -f "$MAIN_CONFIG" ]; then awk ' BEGIN { in_table = 0 } /^\[/ { in_table = 1 skip_provider = ($0 ~ /^\[model_providers\.aiserver(\.|\])/) } skip_provider { next } !in_table && /^[[:space:]]*(profile|model|model_provider|model_reasoning_effort)[[:space:]]*=/ { next } { print } ' "$MAIN_CONFIG" > "$PRESERVED_CONFIG" else : > "$PRESERVED_CONFIG" fi MAIN_TMP="$TMP_DIR/config.toml" { cat <<'CONFIG' model = "gpt-5.6-sol" model_provider = "aiserver" model_reasoning_effort = "medium" CONFIG cat "$PRESERVED_CONFIG" cat <<'CONFIG' [model_providers.aiserver] name = "AIServer" base_url = "https://code.aiserverai.online/v1" wire_api = "responses" requires_openai_auth = true CONFIG } > "$MAIN_TMP" mv "$MAIN_TMP" "$MAIN_CONFIG" chmod 600 "$MAIN_CONFIG" } printf '正在准备 Codex…\n' ensure_node persist_path mkdir -p "$INSTALL_ROOT/cache/npm" CODEX_BIN="$BIN_DIR/codex" if ! npm install -g --prefix "$INSTALL_ROOT" --cache="$INSTALL_ROOT/cache/npm" --registry="$NPM_MIRROR" --include=optional --no-audit --no-fund @openai/codex@latest || ! "$CODEX_BIN" --version >/dev/null 2>&1; then printf '中国 npm 镜像安装不完整,正在使用官方源修复…\n' npm install -g --force --prefix "$INSTALL_ROOT" --cache="$INSTALL_ROOT/cache/npm" --registry="https://registry.npmjs.org" --include=optional --no-audit --no-fund @openai/codex@latest fi "$CODEX_BIN" --version || fail "Codex CLI 或当前系统对应的原生组件安装未完成" API_KEY="$(prompt_key)" [ -n "$API_KEY" ] || fail "API Key 不能为空" curl -fsS --retry 2 -o /dev/null -H "Authorization: Bearer $API_KEY" "$BASE_URL/models" || fail "API Key 或网络验证失败" write_config printf '%s' "$API_KEY" | "$CODEX_BIN" login --with-api-key >/dev/null unset API_KEY if command -v zenity >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then zenity --info --title="配置完成" --text="Codex 已安装并完成 AIServer 配置。\n打开新的终端,进入项目目录运行 codex。" >/dev/null 2>&1 || true fi printf '配置完成。打开新的终端并运行 codex。\n'