diff --git a/.gitignore b/.gitignore index 3dfae3a06..a2d6c6916 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ node_modules +logs/ .serena/ .claude/ CLAUDE.md config.json provider_pools.json +custom_models.json plugins.json fetch_system_prompt.txt input_system_prompt.txt @@ -11,8 +13,10 @@ token-store.json usage-cache.json *_oauth_creds.json *-auth-token.json +*_codex-*.json api-potluck-keys.json api-potluck-data.json model-usage-stats.json AGENTS.md +src/plugins-user/ diff --git a/Dockerfile b/Dockerfile index 15ca582c2..10c001245 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ # ── Stage 1: 编译 Go TLS sidecar ── FROM golang:1.22-alpine AS sidecar-builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ENV HTTP_PROXY=$HTTP_PROXY +ENV HTTPS_PROXY=$HTTPS_PROXY + RUN apk add --no-cache git WORKDIR /build @@ -19,6 +24,10 @@ FROM node:20-alpine LABEL maintainer="AIClient2API Team" LABEL description="Docker image for AIClient2API server" +# 代理参数仅用于构建时,不持久化到最终镜像 +ARG HTTP_PROXY +ARG HTTPS_PROXY + # 安装必要的系统工具(tar 用于更新功能,git 用于版本检查,procps 用于系统监控) RUN apk add --no-cache tar git procps @@ -28,10 +37,8 @@ WORKDIR /app # 复制package.json和package-lock.json(如果存在) COPY package*.json ./ -# 安装依赖 -# 使用--production标志只安装生产依赖,减小镜像大小 -# 使用--omit=dev来排除开发依赖 -RUN npm install +# 构建时代理(如果提供了的话) +RUN npm install || npm install --ignore-scripts # 复制源代码 COPY . . diff --git a/README-JA.md b/README-JA.md index d251d0613..17cc44828 100644 --- a/README-JA.md +++ b/README-JA.md @@ -2,18 +2,34 @@ logo -# AIClient-2-API 🚀 +# AIClient2API(A2)🚀 **複数のクライアント専用大規模言語モデルAPI(Gemini CLI、Antigravity、Codex, Grok、Kiro ...)を模擬リクエストし、ローカルのOpenAI互換インターフェースに統一的にラッピングする強力なプロキシ。** -justlovemaki%2FAIClient-2-API | Trendshift -
-Ask DeepWiki + + + + + + + + + + + + + +
Dockerダウンロード数10万超Trendshiftで2位にランクイン
+ AIClient2API + + justlovemaki%2FAIClient-2-API | Trendshift +
+[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/justlovemaki/AIClient-2-API) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/) [![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://hub.docker.com/r/justlikemaki/aiclient-2-api) @@ -24,8 +40,13 @@
+--- + + ## 💎 スポンサー +*スポンサーは先着順に掲載されており、すべてのアカウント登録と利用を推奨します。* + + + + + + + + +
@@ -39,24 +60,48 @@
- - AICodeMirror Sponsor + + APIKEY.FUN Sponsor - AICodeMirror の本プロジェクトへのスポンサーシップに感謝します!AICodeMirror は、Claude Code / Codex / Gemini CLI 向けに公式の高安定性リレーサービスを提供しており、企業レベルの同時実行性、迅速な請求書発行、24時間365日の専用技術サポートを備えています。Claude Code / Codex / Gemini の公式チャンネルを、元の価格の 38% / 2% / 9% で利用でき、チャージ時にはさらなる割引もあります!AICodeMirror は AIClient-2-API ユーザーに特別な特典を提供しています:このリンクから登録すると、初回チャージが 20% オフになり、法人のお客様は最大 25% オフになります! + APIKEY.FUN による本プロジェクトへのスポンサーに感謝します!APIKEY.FUN はプロフェッショナルな企業向け AI リレーサービスであり、企業や個人開発者に安定、効率的、低コストな AI モデル API アクセスサービスを提供することに尽力しています。Claude、OpenAI、Gemini などの主要な人気モデルをサポートしており、価格は公式価格のわずか 7% からです。本プロジェクトの専用リンクから登録すると、永続的なチャージに対して最大 5% オフ(95折) の特別割引を受けることができます。
- - LingtrueAPI Sponsor + + VisionCoder Sponsor - LingtrueAPIによる本プロジェクトへのスポンサーに感謝します!LingtrueAPIは世界的な大規模言語モデルAPI中継プラットフォームであり、Claude opus 4.6、GPT 5.4、Gemini 3.1 proなど各種モデルのAPI呼び出しサービスを提供しています。低コスト、高安定性で世界中のAI機能に接続し、生産性を最大化することを目指しています。LingtrueAPIは本ソフトウェアユーザー向けに特別優遇を提供しています。このリンクから登録し、初回チャージ時に「LingtrueAPI」のクーポンコードを入力すると、10%オフで利用できます。 + VisionCoder による本プロジェクトへのスポンサーに感謝します!VisionCoder 開発プラットフォームは信頼性が高く効率的な API 中継サービスプロバイダーであり、Claude Code、Codex、Gemini などの主要な AI モデルへのアクセスを提供しています。開発者やチームが AI 機能をより簡単に統合し、生産性を向上させるのを支援します。VisionCoder は本ソフトウェアのユーザー向けに期間限定の Token Plan 特典を提供しています:1ヶ月の購入で1ヶ月分を無料で進呈
+ + Atlas Cloud Sponsor + + + Atlas Cloud による本プロジェクトへのスポンサーに感謝します!Atlas Cloud は、開発者が動画生成、画像生成、および LLM API にアクセスするための单二の AI API を提供する全モーダル AI 推論プラットフォームです。複数のベンダーの統合を管理する代わりに、一度接続するだけですべてのモダリティにわたる 300 以上の厳選されたモデルに統合アクセスできます。よりリーズナブルな API アクセスのために、Atlas Cloud の新しいコーディングプランプロモーション (coding plan)をぜひチェックしてください。 +
Sponsor Contact @@ -82,7 +140,7 @@ ## 🚀 概要 -`AIClient2API` はクライアント制限を突破するAPIプロキシサービスで、Gemini、Antigravity、Codex, Grok、Kiroなど、元々クライアント内でのみ使用可能な無料大規模モデルを、あらゆるアプリケーションから呼び出せる標準OpenAI互換インターフェースに変換します。Node.jsをベースに構築され、OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、Cherry-Studio、NextChat、Clineなどのツールで、Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plusなどの高度なモデルを大規模に無料で使用できるようにします。プロジェクトはストラテジーパターンとアダプターパターンに基づくモジュラーアーキテクチャを採用し、アカウントプール管理、インテリジェントポーリング、自動フェイルオーバー、ヘルスチェック機構を内蔵し、99.9%のサービス可用性を保証します。 +`AIClient2API` はクライアント制限を突破するAPIプロキシサービスで、Gemini、Antigravity、Codex, Grok、Kiroなど、元々クライアント内でのみ使用可能な無料大規模モデルを、あらゆるアプリケーションから呼び出せる標準OpenAI互換インターフェースに変換します。Node.jsをベースに構築され、OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、Cherry-Studio、NextChat、Clineなどのツールで、Claude Opus、Gemini Proなどの高度なモデルを大規模に無料で使用できるようにします。プロジェクトはストラテジーパターンとアダプターパターンに基づくモジュラーアーキテクチャを採用し、アカウントプール管理、インテリジェントポーリング、自動フェイルオーバー、ヘルスチェック機構を内蔵し、99.9%のサービス可用性を保証します。 > [!NOTE] > **🎉 重要なマイルストーン** @@ -94,7 +152,10 @@ >
> クリックして詳細なバージョン履歴を展開 > -> - **2026.03.02** - Grokプロトコルサポートを追加:Cookie/SSO方式でxAI Grokシリーズモデル(Grok 3/4)へのアクセスに対応し、マルチモーダル入力、画像/動画生成、自動トークンリフレッシュ、ストリーミング出力をサポート +> - **2026.06.03** - Grok Build(Grok CLI)サポートを追加:`grok-cli-oauth` の xAI OAuth / Responses API 呼び出しフローに対応し、Grok Build テキストモデル、マルチプロトコル変換、組み込みツール(Web検索、X検索、コードインタープリター、コレクション/添付ファイル検索)、画像/動画生成モデルをサポート。 +> - **2026.05.04 (v3.0.0)** - **マイルストーンアップデート:高度な AI 統合と自己発見アーキテクチャ**。自動化された Skill ガイドとリモート `/api/help`、`/api/example` エンドポイントを追加し、AI 代理が 50 以上の全 API エンドポイントをシームレスに理解・操作できるようになりました。CLI と REST API の出力結果を完全に統一し、構造化 JSON サポートを強化しました。 +> - **2026.04.29** - OpenAI 標準の画像生成 (`/v1/images/generations`) および画像編集 (`/v1/images/edits`) インターフェースを完全にサポート。OpenAI 形式のリクエストを各モデルのネイティブ画像生成プロトコルに自動変換し、プロバイダープールのポーリングや自动リトライメカニズムに完全対応。マルチモーダル制作の安定性を大幅に向上。 +> - **2026.03.02** - Grokプロトコルサポートを追加:Cookie/SSO方式でxAI Grokシリーズモデル(Grok)へのアクセスに対応し、マルチモーダル入力、画像/動画生成、自動トークンリフレッシュ、ストリーミング出力をサポート > - **2026.01.26** - Codexプロトコルサポートを追加:OpenAI Codex OAuth認証での接続に対応 > - **2026.01.25** - AI 監視プラグインの強化:AI プロトコル変換前後のリクエストパラメータとレスポンスの監視をサポート。ログ管理の最適化:統一されたログ形式、ビジュアル設定 > - **2026.01.15** - プロバイダープールマネージャーの最適化:非同期リフレッシュキューメカニズム、バッファキュー重複排除、グローバル並行制御、ノードウォームアップと自動期限切れ検出を追加 @@ -102,11 +163,10 @@ > - **2025.12.30** - メインプロセス管理と自動更新機能を追加 > - **2025.12.25** - 設定ファイル統一管理:すべての設定を `configs/` ディレクトリに集約。Dockerユーザーはマウントパスを `-v "ローカルパス:/app/configs"` に更新が必要 > - **2025.12.11** - Dockerイメージが自動的にビルドされ、Docker Hubで公開されました: [justlikemaki/aiclient-2-api](https://hub.docker.com/r/justlikemaki/aiclient-2-api) -> - **2025.11.30** - Antigravityプロトコルサポートの追加、Google内部インターフェース経由でGemini 3 Pro、Claude Sonnet 4.5などのモデルへのアクセスをサポート +> - **2025.11.30** - Antigravityプロトコルサポートの追加、Google内部インターフェース経由でGemini Pro、Claude Sonnetなどのモデルへのアクセスをサポート > - **2025.11.11** - Web UI管理コントロールコンソールの追加、リアルタイム設定管理と健康状態モニタリングをサポート -> - **2025.11.06** - Gemini 3 プレビュー版のサポートを追加、モデル互換性とパフォーマンス最適化を向上 -> - **2025.10.18** - Kiroオープン登録、新規アカウントに500クレジット付与、Claude Sonnet 4.5を完全サポート -> - **2025.09.01** - Qwen Code CLIを統合、`qwen3-coder-plus`モデルサポートを追加 +> - **2025.11.06** - Gemini プレビュー版のサポートを追加、モデル互換性とパフォーマンス最適化を向上 +> - **2025.10.18** - Kiroオープン登録、新規アカウントに500クレジット付与、Claude Sonnetを完全サポート > - **2025.08.29** - アカウントプール管理機能をリリース、マルチアカウントポーリング、自動フェイルオーバー、自動ダウングレード戦略をサポート > - 設定方法:config.jsonに`PROVIDER_POOLS_FILE_PATH`パラメータを追加 > - 参考設定:[provider_pools.json](./configs/provider_pools.json.example) @@ -119,8 +179,24 @@ ## 💡 コアアドバンテージ +### 🤖 AI 優先、Agent 連携サポート + +> **AI 優先設計**:本プロジェクトは、OpenClaw, Hermes, Claude Code などの主要な AI Agent との効率的な連携をネイティブにサポートしています。 +> +> **💡 クイックコマンド**:AI に直接以下の文章を伝えると、本プロジェクトのすべての使用方法を自动的にマスターします: +> +> - **リモートデプロイ**: +> ```text +> https://raw.githubusercontent.com/justlovemaki/AIClient2API/main/docs/skills/aiclient-cli-usage.md にある Skill をロードして学習し(サービスアドレス:実際のドメインまたは IP、ログインパスワード:実際のパスワード)、AIClient2API のすべての使用方法をマスターしてください。 +> ``` +> - **ローカルモード**: +> ローカル環境で AI 代理を直接実行している場合は、以下を送信してください: +> ```text +> docs/skills/aiclient-cli-usage.md にある Skill をロードして学習し、AIClient2API サービスのローカルでの起動、設定、管理をサポートしてください。 +> ``` + ### 🎯 統一アクセス、ワンストップ管理 -* **マルチモデル統一インターフェース**:標準OpenAI互換プロトコルを通じて、一度の設定でGemini、Claude、Grok、Codex、 K2、MiniMax M2などの主流大規模モデルにアクセス +* **マルチモデル統一インターフェース**:標準OpenAI互換プロトコルを通じて、一度の設定でGemini、Claude、Grok、Codex、Kimi、MiniMaxなどの主流大規模モデルにアクセス * **柔軟な切り替えメカニズム**:Pathルーティング、起動パラメータ、環境変数の3つの方法で動的にモデルを切り替え、異なるシナリオのニーズに対応 * **ゼロコスト移行**:OpenAI API仕様と完全互換、Cherry-Studio、NextChat、Clineなどのツールを変更なしで使用可能 * **マルチプロトコルインテリジェント変換**:OpenAI、Claude、Geminiの3大プロトコル間のインテリジェント変換をサポートし、クロスプロトコルモデル呼び出しを実現 @@ -128,7 +204,7 @@ ### 🚀 制限を突破、効率を向上 * **公式制限の回避**:OAuth認証メカニズムを利用して、Gemini、Antigravityなどの無料APIのレート制限と割り当て制限を効果的に突破 * **TLS 指紋の回避**:内蔵の TLS Sidecar (Go uTLS) によりブラウザの特徴をシミュレートし、Grok などのサービスの Cloudflare 403 ブロックを効果的に回避 -* **無料高度モデル**:Kiro APIモードでClaude Opus 4.5を無料使用、Qwen OAuthモードでQwen3 Coder Plusを使用し、使用コストを削減 +* **無料高度モデル**:Kiro APIモードでClaude Opusを無料使用、使用コストを削減 * **インテリジェントアカウントプールスケジューリング**:マルチアカウントポーリング、自動フェイルオーバー、設定ダウングレードをサポートし、99.9%のサービス可用性を保証 ### 🛡️ 安全で制御可能、データ透明 @@ -164,19 +240,20 @@ ### 🚀 クイックスタート -AIClient-2-APIを使い始める最も推奨される方法は、自動起動スクリプトを使用し、**Web UIコンソール**で直接ビジュアル設定を行うことです。 +AIClient2APIを使い始める最も推奨される方法は、自動起動スクリプトを使用し、**Web UIコンソール**で直接ビジュアル設定を行うことです。 #### 🐳 Docker クイックスタート (推奨) ```bash -docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 19876-19880:19876-19880 --restart=always -v "指定パス:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 56121:56121 -p 19876-19880:19876-19880 --restart=always -v "指定パス/configs:/app/configs" -v "指定パス/plugins:/app/src/plugins-user" --name aiclient2api justlikemaki/aiclient-2-api ``` **パラメータ説明**: - `-d`:バックグラウンドでコンテナを実行 -- `-p 3000:3000 ...`:ポートマッピング。3000はWeb UI用、その他はOAuthコールバック用(Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880) +- `-p 3000:3000 ...`:ポートマッピング。3000はWeb UI用、その他はOAuthコールバック用(Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880) - `--restart=always`:コンテナ自動再起動ポリシー -- `-v "指定パス:/app/configs"`:設定ディレクトリをマウント(「指定パス」を実際のパスに置き換えてください、例:`/home/user/aiclient-configs`) +- `-v "指定パス/configs:/app/configs"`:設定ディレクトリをマウント(「指定パス」を実際のパスに置き換えてください、例:`/home/user/aiclient2api`) +- `-v "指定パス/plugins:/app/src/plugins-user"`:ユーザープラグインディレクトリをマウント - `--name aiclient2api`:コンテナ名 #### 🐳 Docker Compose デプロイ @@ -198,13 +275,19 @@ docker compose up -d * **Linux/macOS**: `chmod +x install-and-run.sh && ./install-and-run.sh` * **Windows**: `install-and-run.bat` をダブルクリックして実行 -> **💡 スクリプトの実行に失敗した場合は、手動で依存関係をインストールして起動できます:** +> **💡 手動インストールと起動(カスタムパラメータ対応):** > ```bash > npm install +> # デフォルト起動 > npm start +> # ヘルプ情報を表示 +> npm run help +> # API 呼び出しの例を表示 +> npm run example:api +> # バックエンドのみモード(フロントエンド管理画面と) +> npm start -- --no-ui > ``` - #### 2. コンソールへのアクセス サーバー起動後、ブラウザで以下にアクセスしてください: 👉 [**http://localhost:3000**](http://localhost:3000) @@ -261,7 +344,7 @@ docker compose up -d **📊 ダッシュボード**:システム概要、インタラクティブなルーティング例、クライアント設定ガイド -**⚙️ 設定管理**:全プロバイダー(Gemini、Antigravity、OpenAI、Claude、Kiro、Qwen)のリアルタイムパラメータ修正、高度設定、ファイルアップロード対応 +**⚙️ 設定管理**:全プロバイダー(Gemini、Antigravity、OpenAI、Claude、Kiro)のリアルタイムパラメータ修正、高度設定、ファイルアップロード対応 **🔗 プロバイダープール**:アクティブ接続監視、プロバイダー健全性統計、有効化/無効化管理 @@ -278,11 +361,10 @@ docker compose up -d #### 最新モデルサポート 以下の最新大規模モデルをシームレスにサポート、Web UIまたは[`config.json`](./configs/config.json)で対応するエンドポイントを設定するだけで使用可能: -* **Grok 3 / Grok 4** - xAIのフラッグシップモデル。Grok Cookie/SSO経由でサポートされ、思考モデル、画像生成、動画生成に対応 -* **Claude 4.5 Opus** - Anthropic史上最強モデル、Kiro、Antigravity経由でサポート -* **Gemini 3 Pro** - Google次世代アーキテクチャプレビュー版、Gemini、Antigravity経由でサポート -* **Qwen3 Coder Plus** - アリババ通義千問の最新コード専用モデル、Qwen Code経由でサポート -* **Kimi K2 / MiniMax M2** - 国内トップフラッグシップモデルの同期サポート、カスタムOpenAI、Claude経由でサポート +* **Grok / Grok Build** - xAIのフラッグシップモデル。Grok Cookie/SSOおよびGrok CLI OAuth経由でサポートされ、思考モデル、Grok Build、組み込みツール、画像生成、動画生成に対応 +* **Claude Opus** - Anthropic史上最強モデル、Kiro、Antigravity経由でサポート +* **Gemini Pro** - Google次世代アーキテクチャプレビュー版、Gemini、Antigravity経由でサポート +* **Kimi / MiniMax** - 国内トップフラッグシップモデルの同期サポート、カスタムOpenAI、Claude経由でサポート --- @@ -295,8 +377,8 @@ docker compose up -d #### 🌐 Web UI クイック認証 (推奨) Web UI管理インターフェースでは、極めて迅速に認証設定を完了できます: -1. **認証の生成**:**「プロバイダープール」** ページまたは **「設定管理」** ページで、対応するプロバイダー(Gemini、Qwenなど)の右上にある **「認証生成」** ボタンをクリックします。 -2. **スキャン/ログイン**:認証ダイアログが表示されるので、**「ブラウザで開く」** をクリックしてログイン検証を行います。Qwenの場合はウェブログインを完了するだけ、Gemini、Antigravityの場合はGoogleアカウントの認証を完了させます。 +1. **認証の生成**:**「プロバイダープール」** ページまたは **「設定管理」** ページで、対応するプロバイダー(Geminiなど)の右上にある **「認証生成」** ボタンをクリックします。 +2. **スキャン/ログイン**:認証ダイアログが表示されるので、**「ブラウザで開く」** をクリックしてログイン検証を行います。Gemini、Antigravityの場合はGoogleアカウントの認証を完了させます。 3. **自動保存**:認証成功後、システムは自動的に資格情報を取得し、`configs/` の対応するディレクトリに保存します。**「設定ファイル」** ページで新しく生成された資格情報を確認できます。 4. **ビジュアル管理**:Web UIでいつでも資格情報のアップロードや削除、または **「クイック関連付け」** 機能を使用して既存の資格情報ファイルをワンクリックでプロバイダーにバインドできます。 @@ -310,16 +392,6 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を 2. **Pro会員**:Antigravity は一時的に Pro 会員に開放されています。まず Pro 会員を購入する必要があります。 3. **組織アカウント**:組織アカウントは個別に認証が必要です。管理者に連絡して認証を取得してください。 -#### Qwen Code OAuth設定 -1. **初回認証**:Qwenサービス設定後、システムが自動的にブラウザで認証ページを開きます -2. **推奨パラメータ**:最良の結果を得るために公式デフォルトパラメータを使用 - ```json - { - "temperature": 0, - "top_p": 1 - } - ``` - #### Kiro API設定 1. **環境準備**:[Kiroクライアントをダウンロードしてインストール](https://kiro.dev/pricing/) 2. **認証完了**:クライアントでアカウントにログインし、`kiro-auth-token.json`認証情報ファイルを生成 @@ -327,7 +399,7 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を 4. **重要なお知らせ**:Kiroサービス使用ポリシーが更新されました、最新の使用制限と条件については公式ウェブサイトをご確認ください。 #### Kiro 拡張思考 (Claude モデル) -AIClient-2-API は、`claude-kiro-oauth` にルーティングされた Claude 互換リクエスト (`/v1/messages`) または OpenAI 互換リクエスト (`/v1/chat/completions`) を使用する場合、Kiro 拡張思考をサポートします。 +AIClient2API は、`claude-kiro-oauth` にルーティングされた Claude 互換リクエスト (`/v1/messages`) または OpenAI 互換リクエスト (`/v1/chat/completions`) を使用する場合、Kiro 拡張思考をサポートします。 **Claude 互換インターフェース (`/v1/messages`)**: ```bash @@ -372,11 +444,17 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ 3. **自動保存**:認証成功後、システムがCodexのOAuth認証情報ファイルを自動保存 4. **コールバックポート**:OAuthコールバックポート `1455` が占有されていないことを確認 +#### Grok CLI OAuth設定 +1. **認証の生成**:Web UIの「プロバイダープール」または「設定管理」ページで、Grok CLIの「認証生成」ボタンをクリック +2. **ブラウザログイン**:システムがxAI認証ページを開き、OAuthログインを完了 +3. **自動保存**:認証成功後、システムがGrok CLI OAuth認証情報ファイルを `configs/grok-cli/` に自動保存 +4. **コールバックポート**:OAuthコールバックポート `56121` が占有されていないことを確認 + #### Grok Cookie/SSO 設定 1. **SSOトークンの取得**: [Grok公式サイト](https://grok.com/)にログインし、ブラウザの開発者ツールの Application -> Cookies から `sso` の値をコピーします。 2. **設定の入力**: Web UIの「設定管理」ページ、または設定ファイルを直接編集して、トークンを `GROK_COOKIE_TOKEN` に入力します。 3. **サポート機能**: - - チャットおよび思考モデル (Grok 3 Thinking) + - チャットおよび思考モデル (Grok Thinking) - 画像生成 (Grok Imagine) - 動画生成 (Grok Video) 4. **注意事項**: ブロックを避けるため、`GROK_USER_AGENT` がCookie取得時と同じブラウザのものであることを確認してください。 @@ -400,9 +478,9 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ |------|---------|------| | **Gemini** | `~/.gemini/oauth_creds.json` | OAuth認証情報 | | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro認証トークン | -| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth認証情報 | -| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth認証情報 (Claude 4.5 Opus サポート) | +| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth認証情報 (Claude Opus サポート) | | **Codex** | `~/.codex/oauth_creds.json` | Codex OAuth認証情報 | +| **Grok CLI** | `configs/grok-cli/..._xai-..._oauth_creds.json` | Grok CLI OAuth認証情報 | > **説明**:`~`はユーザーホームディレクトリを表します(Windows: `C:\Users\ユーザー名`、Linux/macOS: `/home/ユーザー名`または`/Users/ユーザー名`) @@ -441,10 +519,11 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ "gemini-cli-oauth", "gemini-antigravity", "claude-kiro-oauth", - "grok-custom" + "grok-web" ] -} - ``` + } + ``` + 3. **プロバイダー独自のプロキシ済みエンドポイント**:一部のプロバイダー(OpenAI、Claudeなど)はプロキシ済みAPIエンドポイントの設定をサポートしています @@ -579,7 +658,7 @@ Grok などの TLS 指紋(JA3/JA4)を厳密に検証するサービスに対 **解決策**: - **ネットワーク接続を確認**:Google、アリババクラウドなどのサービスに正常にアクセスできることを確認 -- **ポート占有を確認**:OAuthコールバックには特定のポートが必要です(Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880)、これらのポートが占有されていないことを確認 +- **ポート占有を確認**:OAuthコールバックには特定のポートが必要です(Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880)、これらのポートが占有されていないことを確認 - **ブラウザキャッシュをクリア**:シークレットモードを使用するか、ブラウザキャッシュをクリアして再試行 - **ファイアウォール設定を確認**:ファイアウォールがローカルコールバックポートへのアクセスを許可していることを確認 - **Dockerユーザー**:すべてのOAuthコールバックポートが正しくマッピングされていることを確認 @@ -628,6 +707,7 @@ kill -9 **解決策**: - **アカウントプールを設定**:`provider_pools.json` に複数のアカウントを追加し、ポーリングメカニズムを有効化 - **フォールバックを設定**:`config.json` で `providerFallbackChain` を設定し、クロスタイプ降格を実現 +- **429クールダウンを有効化**:`RATE_LIMIT_COOLDOWN_ENABLED` を `true` にし、`RATE_LIMIT_COOLDOWN_MS` で既定の冷却時間を設定すると、レート制限されたアカウントは一時的にプールから外れ、自動的に復帰します - **リクエスト頻度を下げる**:リクエスト間隔を適切に増やし、レート制限のトリガーを回避 - **割り当てリセットを待つ**:無料割り当ては通常、毎日または毎分リセットされます @@ -714,6 +794,26 @@ kill -9 - **リクエスト頻度を確認**:一部のプロバイダーはリクエスト頻度に厳しい制限があります。リクエスト頻度を下げて再試行 - **プロバイダードキュメントを確認**:対応するプロバイダーの公式ドキュメントにアクセスして、具体的なアクセス制限と要件を理解 +### 14. なぜ「OAuthトークンの自動更新」を有効にする必要があるのですか? + +**問題の説明**:トークンの自動更新機能が必要かどうかがわからない。 + +**解決策**: +OAuthトークン(Gemini、Antigravity、Codexなど)には通常、有効期限(例:1時間)があります。 +- **有効な場合**:システムはバックグラウンドで期限が切れる前にトークンを自動的にチェックして更新します。これにより、24時間365日の安定したAPIサービスが保証され、トークンの期限切れによる `401 Unauthorized` や `403 Forbidden` エラーを回避できます。 +- **無効な場合**:トークンが期限切れになると、システムは新しいトークンを自動的に取得できず、手動で再度OAuth認証を行うまでAPIリクエストが失敗します。 + +### 15. 「モデルプロバイダーのプリロード」が未有効の場合、トークンの維持にどのような影響がありますか? + +**問題の説明**:「モデルプロバイダーのプリロード」設定の役割と、それがトークンの更新にどう影響するかがわからない。 + +**解決策**: +システムは、**アクティブなプールにロードされている**プロバイダーに対してのみ自動更新タスクを実行します。 +- **影響**:設定で特定のプロバイダーを「モデルプロバイダーのプリロード」として選択していない場合、システム起動時にそのプロバイダーは初期化されません。プールに含まれていないため、バックグラウンド更新タスクはそのプロバイダーのトークンを**処理しません**。 +- **結果**:そのプロバイダーを長時間使用しない場合、トークンはバックグラウンドで静かに期限切れになります。たまに特定のルートを介して呼び出すと、トークン切れによりリクエストが失敗します。 +- **推奨事項**:頻繁に使用し、アクティブな状態を維持する必要があるプロバイダーは、必ず「モデルプロバイダーのプリロード」で選択してください。 + +
--- @@ -728,7 +828,7 @@ kill -9 ### 貢献者リスト -AIClient-2-APIプロジェクトに貢献してくれたすべての開発者に感謝します: +AIClient2APIプロジェクトに貢献してくれたすべての開発者に感謝します: [![Contributors](https://contrib.rocks/image?repo=justlovemaki/AIClient-2-API)](https://github.com/justlovemaki/AIClient-2-API/graphs/contributors) @@ -743,7 +843,7 @@ AIClient-2-APIプロジェクトに貢献してくれたすべての開発者に ## ⚠️ 免責事項 ### 使用リスクの注意 -本プロジェクト(AIClient-2-API)は学習と研究目的のみです。ユーザーは本プロジェクト使用時、すべてのリスクを自己負担する必要があります。作者は本プロジェクトの使用により生じた直接的、間接的、または結果的な損失について一切の責任を負いません。 +本プロジェクト(AIClient2API)は学習と研究目的のみです。ユーザーは本プロジェクト使用時、すべてのリスクを自己負担する必要があります。作者は本プロジェクトの使用により生じた直接的、間接的、または結果的な損失について一切の責任を負いません。 ### サードパーティサービスの責任説明 本プロジェクトはAPIプロキシツールであり、AIモデルサービスを提供していません。すべてのAIモデルサービスは対応するサードパーティプロバイダー(Google、OpenAI、Anthropicなど)により提供されます。ユーザーが本プロジェクトを通じてこれらのサードパーティサービスにアクセスする際は、各サードパーティサービスの利用規約とポリシーを遵守する必要があります。作者はサードパーティサービスの可用性、品質、セキュリティ、合法性について責任を負いません。 diff --git a/README-ZH.md b/README-ZH.md index d4be81894..2f66b3e1d 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -2,17 +2,34 @@ logo -# AIClient-2-API 🚀 +# AIClient2API(A2)🚀 **一个能将多种仅客户端内使用的大模型 API(Gemini CLI, Antigravity, Codex, Grok, Kiro ...),模拟请求,统一封装为本地 OpenAI 兼容接口的强大代理。** -justlovemaki%2FAIClient-2-API | Trendshift
-Ask DeepWiki + + + + + + + + + + + + + +
镜像下载量超100ktrendshift.io上过榜2
+ AIClient2API + + justlovemaki%2FAIClient-2-API | Trendshift +
+[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/justlovemaki/AIClient-2-API) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/) [![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://hub.docker.com/r/justlikemaki/aiclient-2-api) @@ -23,8 +40,13 @@
+--- + + ## 💎 赞助商 +*排序按赞助先后顺序排列,均推荐注册使用。* + + + + + + + + +
@@ -38,24 +60,48 @@
- - AICodeMirror Sponsor + + APIKEY.FUN Sponsor - 感谢 AICodeMirror 赞助本项目!AICodeMirror 为 Claude Code / Codex / Gemini CLI 提供官方高稳定性中转服务,具备企业级并发能力、快速开票和 7/24 专属技术支持。Claude Code / Codex / Gemini 官方渠道价格仅为原价的 38% / 2% / 9%,充值还有额外优惠!AICodeMirror 为 AIClient-2-API 用户提供专属福利:通过此链接注册即可享受首充 8折(20% off) 优惠,企业客户最高可享 75折(25% off)! + 感谢 APIKEY.FUN 赞助本项目!APIKEY.FUN 是一家专业的企业级 AI 中转站,致力于为企业和个人开发者提供稳定、高效、低成本的其他 AI 模型 API 接入服务。平台支持 Claude、OpenAI、Gemini 等主流热门模型,价格低至官方原价的 7%。通过本项目专属链接注册,还可享受最高 充值永久 95 折 专属优惠。
- - LingtrueAPI Sponsor + + VisionCoder Sponsor - 感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude opus 4.6、GPT 5.4、Gemini 3.1 pro等多种模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力,最大化生产效率。LingtrueAPI为本软件用户提供了特别优惠:通过此链接注册并在首次充值时输入 LingtrueAPI 优惠码即可享受 9折优惠。 + 感谢 VisionCoder 对本项目的支持。VisionCoder 开发平台 是一个可靠高效的 API 中继服务提供商,提供 Claude Code、Codex、Gemini 等主流 AI 模型,帮助开发者和团队更轻松地集成 AI 功能,提升工作效率。VisionCoder 还为我们的用户提供 Token Plan 限时活动:购买 1 个月,赠送 1 个月
+ + Atlas Cloud Sponsor + + + 感谢 Atlas Cloud 赞助本项目!Atlas Cloud 是一款全模态 AI 推理平台,为开发者提供单一的 AI API 以轻松接入视频生成、图像生成和大语言模型 API。无需管理多个供应商集成,您只需连接一次,即可统一访问跨所有模态的 300 多种精选模型。欢迎查看 Atlas Cloud 全新的 编码计划促销活动 (coding plan),以获取更具性价比的 API 接入服务。 +
Sponsor Contact @@ -81,7 +140,7 @@ ## 🚀 项目概览 -`AIClient2API` 是一个突破客户端限制的 API 代理服务,将 Gemini、Antigravity、Codex, Grok、Kiro 等原本仅限客户端内使用的免费大模型,转换为可供任何应用调用的标准 OpenAI 兼容接口。基于 Node.js 构建,支持 OpenAI、Claude、Gemini 三大协议的智能互转,让 Cherry-Studio、NextChat、Cline 等工具能够免费大量使用 Claude Opus 4.5、Gemini 3.0 Pro、Qwen3 Coder Plus 等高级模型。项目采用策略模式和适配器模式的模块化架构,内置账号池管理、智能轮询、自动故障转移和健康检查机制,确保 99.9% 的服务可用性。 +`AIClient2API` 是一个突破客户端限制的 API 代理服务,将 Gemini、Antigravity、Codex, Grok、Kiro 等原本仅限客户端内使用的免费大模型,转换为可供任何应用调用的标准 OpenAI 兼容接口。基于 Node.js 构建,支持 OpenAI、Claude、Gemini 三大协议的智能互转,让 Cherry-Studio、NextChat、Cline 等工具能够免费大量使用 Claude Opus、Gemini Pro 等高级模型。项目采用策略模式和适配器模式的模块化架构,内置账号池管理、智能轮询、自动故障转移和健康检查机制,确保 99.9% 的服务可用性。 > [!NOTE] > **🎉 重要里程碑** @@ -93,7 +152,10 @@ >
> 点击展开查看详细版本历史 > -> - **2026.03.02** - 新增 Grok 协议支持,支持通过 Cookie/SSO 方式访问 xAI Grok 系列模型(Grok 3/4),支持多模态输入、图片/视频生成、自动 token 刷新及流式输出 +> - **2026.06.03** - 新增 Grok Build(Grok CLI)支持:接入 `grok-cli-oauth` 的 xAI OAuth / Responses API 调用链,支持 Grok Build 文本模型、多协议转换、内置工具(网页搜索、X 搜索、代码解释器、集合/附件搜索)以及图片/视频生成模型。 +> - **2026.05.04 (v3.0.0)** - **里程碑更新:AI 深度集成与自发现架构**。新增自动化 Skill 指南与远程 `/api/help`、`/api/example` 接口,支持 AI 代理无缝理解并操作 50+ 个全量 API 端点;实现了 CLI 与 REST API 输出结果的完全统一,增强了结构化 JSON 支持。 +> - **2026.04.29** - 全面支持 OpenAI 标准的图片生成 (`/v1/images/generations`) 与编辑 (`/v1/images/edits`) 接口。支持自动将 OpenAI 格式请求转换为各模型对应的原生生图协议,并适配号池轮询与自动重试机制,大幅提升多模态创作的稳定性。 +> - **2026.03.02** - 新增 Grok 协议支持,支持通过 Cookie/SSO 方式访问 xAI Grok 系列模型(Grok),支持多模态输入、图片/视频生成、自动 token 刷新及流式输出 > - **2026.01.26** - 新增 Codex 协议支持:支持 OpenAI Codex OAuth 授权接入 > - **2026.01.25** - 增强 AI 监控插件:支持监控 AI 协议转换前后的请求参数和响应。优化日志管理:统一日志格式,可视化配置 > - **2026.01.15** - 优化提供商池管理器:新增异步刷新队列机制、缓冲队列去重、全局并发控制,支持节点预热和自动过期检测 @@ -101,11 +163,10 @@ > - **2025.12.30** - 添加主进程管理和自动更新功能 > - **2025.12.25** - 配置文件统一管理:所有配置集中到 `configs/` 目录,Docker 用户需更新挂载路径为 `-v "本地路径:/app/configs"` > - **2025.12.11** - Docker 镜像自动构建并发布到 Docker Hub: [justlikemaki/aiclient-2-api](https://hub.docker.com/r/justlikemaki/aiclient-2-api) -> - **2025.11.30** - 新增 Antigravity 协议支持,支持通过 Google 内部接口访问 Gemini 3 Pro、Claude Sonnet 4.5 等模型 +> - **2025.11.30** - 新增 Antigravity 协议支持,支持通过 Google 内部接口访问 Gemini Pro、Claude Sonnet 等模型 > - **2025.11.11** - 新增 Web UI 管理控制台,支持实时配置管理和健康状态监控 -> - **2025.11.06** - 新增对 Gemini 3 预览版的支持,增强模型兼容性和性能优化 -> - **2025.10.18** - Kiro 开放注册,新用户赠送 500 额度,已完整支持 Claude Sonnet 4.5 -> - **2025.09.01** - 集成 Qwen Code CLI,新增 `qwen3-coder-plus` 模型支持 +> - **2025.11.06** - 新增对 Gemini 预览版的支持,增强模型兼容性和性能优化 +> - **2025.10.18** - Kiro 开放注册,新用户赠送 500 额度,已完整支持 Claude Sonnet > - **2025.08.29** - 发布账号池管理功能,支持多账号轮询、智能故障转移和自动降级策略 > - 配置方式:在 `configs/config.json` 中添加 `PROVIDER_POOLS_FILE_PATH` 参数 > - 参考配置:[provider_pools.json](./configs/provider_pools.json.example) @@ -117,8 +178,24 @@ ## 💡 核心优势 +### 🤖 AI 优先,Agent 交互支持 + +> **AI 优先设计**:本项目原生支持与 OpenClaw, Hermes, Claude Code 等主流 AI Agent 的高效交互。 +> +> **💡 快速指令**:你可以直接对 AI 说下面这句话,它将自动掌握本项目的所有用法: +> +> - **远程部署**: +> ```text +> 请加载并学习 https://raw.githubusercontent.com/justlovemaki/AIClient2API/main/docs/skills/aiclient-cli-usage.md 中的 Skill(服务地址为你的实际域名或 IP,登录密码为你的实际密码),以掌握 AIClient2API 的所有用法。 +> ``` +> - **本地模式**: +> 如果你在本地环境直接运行 AI 代理,可以直接发送: +> ```text +> 请加载并学习本项目的 docs/skills/aiclient-cli-usage.md 这一 Skill,以协助我本地启动、配置并管理 AIClient2API 服务。 +> ``` + ### 🎯 统一接入,一站式管理 -* **多模型统一接口**:通过标准 OpenAI 兼容协议,一次配置即可接入 Gemini、Claude、Grok、Codex、Kimi K2、MiniMax M2 等主流大模型 +* **多模型统一接口**:通过标准 OpenAI 兼容协议,一次配置即可接入 Gemini、Claude、Grok、Codex、Kimi、MiniMax 等主流大模型 * **灵活切换机制**:Path 路由、支持通过启动参数、环境变量三种方式动态切换模型,满足不同场景需求 * **零成本迁移**:完全兼容 OpenAI API 规范,Cherry-Studio、NextChat、Cline 等工具无需修改即可使用 * **多协议智能转换**:支持 OpenAI、Claude、Gemini 三大协议间的智能转换,实现跨协议模型调用 @@ -126,7 +203,7 @@ ### 🚀 突破限制,提升效率 * **绕过官方限制**:利用 OAuth 授权机制,有效突破 Gemini, Antigravity 等服务的免费 API 速率和配额限制 * **TLS 指纹绕过**:内置 TLS Sidecar (Go uTLS) 模拟浏览器特征,有效绕过 Grok 等服务的 Cloudflare 403 封锁 -* **免费高级模型**:通过 Kiro API 模式免费使用 Claude Opus 4.5,通过 Qwen OAuth 模式使用 Qwen3 Coder Plus,降低使用成本 +* **免费高级模型**:通过 Kiro API 模式免费使用 Claude Opus,降低使用成本 * **账号池智能调度**:支持多账号轮询、自动故障转移和配置降级,确保 99.9% 服务可用性 ### 🛡️ 安全可控,数据透明 @@ -162,19 +239,20 @@ ### 🚀 快速启动 -使用 AIClient-2-API 最推荐的方式是通过自动化脚本启动,并直接在 **Web UI 控制台** 进行可视化配置。 +使用 AIClient2API 最推荐的方式是通过自动化脚本启动,并直接在 **Web UI 控制台** 进行可视化配置。 #### 🐳 Docker 快捷启动 (推荐) ```bash -docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 19876-19880:19876-19880 --restart=always -v "指定路径:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 56121:56121 -p 19876-19880:19876-19880 --restart=always -v "指定路径/configs:/app/configs" -v "指定路径/plugins:/app/src/plugins-user" --name aiclient2api justlikemaki/aiclient-2-api ``` **参数说明**: - `-d`:后台运行容器 -- `-p 3000:3000 ...`:端口映射。3000 为 Web UI,其余为 OAuth 回调端口(Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880) +- `-p 3000:3000 ...`:端口映射。3000 为 Web UI,其余为 OAuth 回调端口(Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880) - `--restart=always`:容器自动重启策略 -- `-v "指定路径:/app/configs"`:挂载配置目录(请将"指定路径"替换为实际路径,如 `/home/user/aiclient-configs`) +- `-v "指定路径/configs:/app/configs"`:挂载配置目录(请将"指定路径"替换为实际路径,如 `/home/user/aiclient2api`) +- `-v "指定路径/plugins:/app/src/plugins-user"`:挂载用户插件目录 - `--name aiclient2api`:容器名称 #### 🐳 Docker Compose 部署 @@ -199,10 +277,16 @@ docker compose up -d > **💡 如果脚本运行失败,可以尝试手动安装依赖并启动:** > ```bash > npm install +> # 默认启动 > npm start +> # 查看帮助信息 +> npm run help +> # 查看 API 调用示例 +> npm run example:api +> # 纯后端模式(禁用前端管理界面) +> npm start -- --no-ui > ``` - #### 2. 访问控制台 服务器启动后,打开浏览器访问: 👉 [**http://localhost:3000**](http://localhost:3000) @@ -259,7 +343,7 @@ docker compose up -d **📊 仪表盘**:系统概览、交互式路由示例、客户端配置指南 -**⚙️ 配置管理**:实时参数修改,支持所有提供商(Gemini、Antigravity、OpenAI、Claude、Kiro、Qwen),包含高级设置和文件上传 +**⚙️ 配置管理**:实时参数修改,支持所有提供商(Gemini、Antigravity、OpenAI、Claude、Kiro),包含高级设置和文件上传 **🔗 提供商池**:监控活动连接、提供商健康统计、启用/禁用管理 @@ -276,11 +360,10 @@ docker compose up -d #### 最新模型支持 无缝支持以下最新大模型,仅需在 Web UI 或 [`configs/config.json`](./configs/config.json) 中配置相应的端点: -* **Grok 3 / Grok 4** - xAI 旗舰模型,现已通过 Grok Cookie/SSO 支持,支持思考模型、图片生成及视频生成 -* **Claude 4.5 Opus** - Anthropic 史上最强模型,现已通过 Kiro, Antigravity 支持 -* **Gemini 3 Pro** - Google 下一代架构预览版,现已通过 Gemini, Antigravity 支持 -* **Qwen3 Coder Plus** - 阿里通义千问最新代码专用模型,现已通过Qwen Code 支持 -* **Kimi K2 / MiniMax M2** - 国内顶级旗舰模型同步支持,现已通过自定义OpenAI,Claude 支持 +* **Grok / Grok Build** - xAI 旗舰模型,现已通过 Grok Cookie/SSO 与 Grok CLI OAuth 支持,支持思考模型、Grok Build、内置工具、图片生成及视频生成 +* **Claude Opus** - Anthropic 史上最强模型,现已通过 Kiro, Antigravity 支持 +* **Gemini Pro** - Google 下一代架构预览版,现已通过 Gemini, Antigravity 支持 +* **Kimi / MiniMax** - 国内顶级旗舰模型同步支持,现已通过自定义OpenAI,Claude 支持 --- @@ -293,8 +376,8 @@ docker compose up -d #### 🌐 Web UI 快捷授权 (推荐) 在 Web UI 管理界面中,您可以极速完成授权配置: -1. **生成授权**:在 **“提供商池”** 页面或**“配置管理”** 页面,点击对应提供商(如 Gemini, Qwen)右上角的 **“生成授权”** 按钮。 -2. **扫码/登录**:系统将弹出授权对话框,您可以点击 **“在浏览器中打开”** 进行登录验证。对于 Qwen,只需完成网页登录;对于 Gemini,Antigravity 需完成 Google 账号授权。 +1. **生成授权**:在 **“提供商池”** 页面或**“配置管理”** 页面,点击对应提供商(如 Gemini)右上角的 **“生成授权”** 按钮。 +2. **扫码/登录**:系统将弹出授权对话框,您可以点击 **“在浏览器中打开”** 进行登录验证。对于 Gemini,Antigravity 需完成 Google 账号授权。 3. **自动保存**:授权成功后,系统会自动获取凭据并保存至 `configs/` 对应目录下,您可以在 **“配置文件”** 页面看到新生成的凭据。 4. **可视化管理**:您可以随时在 Web UI 中上传、删除凭据,或通过 **“快速关联”** 功能将已有的凭据文件一键绑定到提供商。 @@ -308,16 +391,6 @@ docker compose up -d 2. **Pro会员**:Antigravity 暂时对 Pro 会员开放,需要先购买 Pro 会员。 3. **组织账号**:组织账号需要单独授权,联系管理员获取授权。 -#### Qwen Code OAuth 配置 -1. **首次授权**:配置Qwen服务后,系统会自动在浏览器中打开授权页面 -2. **推荐参数**:使用官方默认参数以获得最佳效果 - ```json - { - "temperature": 0, - "top_p": 1 - } - ``` - #### Kiro API 配置 1. **环境准备**:[下载并安装 Kiro 客户端](https://kiro.dev/pricing/) 2. **完成授权**:在客户端中登录账号,生成 `kiro-auth-token.json` 凭据文件 @@ -325,7 +398,7 @@ docker compose up -d 4. **重要提示**:Kiro 服务使用政策已更新,请访问官方网站查看最新使用限制和条款 #### Kiro 扩展思考 (Claude 模型) -AIClient-2-API 在使用路由到 `claude-kiro-oauth` 的 Claude 兼容请求 (`/v1/messages`) 或 OpenAI 兼容请求 (`/v1/chat/completions`) 时支持 Kiro 扩展思考。 +AIClient2API 在使用路由到 `claude-kiro-oauth` 的 Claude 兼容请求 (`/v1/messages`) 或 OpenAI 兼容请求 (`/v1/chat/completions`) 时支持 Kiro 扩展思考。 **Claude 兼容接口 (`/v1/messages`)**: ```bash @@ -370,11 +443,17 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ 3. **自动保存**:授权成功后,系统会自动保存 Codex 的 OAuth 凭据文件 4. **回调端口**:确保 OAuth 回调端口 `1455` 未被占用 +#### Grok CLI OAuth 配置 +1. **生成授权**:在 Web UI 的“提供商池”或“配置管理”页面,点击 Grok CLI 的“生成授权”按钮 +2. **浏览器登录**:系统将打开 xAI 授权页面,完成 OAuth 登录 +3. **自动保存**:授权成功后,系统会自动保存 Grok CLI OAuth 凭据文件到 `configs/grok-cli/` +4. **回调端口**:确保 OAuth 回调端口 `56121` 未被占用 + #### Grok Cookie/SSO 配置 1. **获取 SSO 令牌**:登录 [Grok 官网](https://grok.com/),在浏览器开发者工具的 Application -> Cookies 中复制 `sso` 的值 2. **填入配置**:在 Web UI 的“配置管理”或直接修改配置文件,将令牌填入 `GROK_COOKIE_TOKEN` 3. **支持功能**: - - 聊天与思考模型(Grok 3 Thinking) + - 聊天与思考模型(Grok Thinking) - 图片生成(Grok Imagine) - 视频生成(Grok Video) 4. **注意事项**:确保 `GROK_USER_AGENT` 与您获取 Cookie 时使用的浏览器一致以避免被拦截 @@ -398,9 +477,9 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ |------|---------|------| | **Gemini** | `~/.gemini/oauth_creds.json` | OAuth 认证凭据 | | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro 认证令牌 | -| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth 凭据 | -| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth 凭据 (支持 Claude 4.5 Opus) | +| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth 凭据 (支持 Claude Opus) | | **Codex** | `~/.codex/oauth_creds.json` | Codex OAuth 凭据 | +| **Grok CLI** | `configs/grok-cli/..._xai-..._oauth_creds.json` | Grok CLI OAuth 凭据 | > **说明**:`~` 表示用户主目录(Windows: `C:\Users\用户名`,Linux/macOS: `/home/用户名` 或 `/Users/用户名`) @@ -439,10 +518,11 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ "gemini-cli-oauth", "gemini-antigravity", "claude-kiro-oauth", - "grok-custom" + "grok-web" ] -} - ``` + } + ``` + 3. **提供商自带代理端点**:某些提供商(如 OpenAI、Claude)支持配置已代理的 API 端点 @@ -577,7 +657,7 @@ curl http://localhost:3000/claude-kiro-oauth/v1/chat/completions \ **解决方案**: - **检查网络连接**:确保能够正常访问 Google、阿里云等服务 -- **检查端口占用**:OAuth 回调需要特定端口(Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880),确保这些端口未被占用 +- **检查端口占用**:OAuth 回调需要特定端口(Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880),确保这些端口未被占用 - **清除浏览器缓存**:尝试使用无痕模式或清除浏览器缓存后重试 - **检查防火墙设置**:确保防火墙允许本地回调端口的访问 - **Docker 用户**:确保已正确映射所有 OAuth 回调端口 @@ -626,6 +706,7 @@ kill -9 **解决方案**: - **配置账号池**:添加多个账号到 `provider_pools.json`,启用轮询机制 - **配置 Fallback**:在 `config.json` 中配置 `providerFallbackChain`,实现跨类型降级 +- **启用 429 短冷却**:将 `RATE_LIMIT_COOLDOWN_ENABLED` 设为 `true`,并通过 `RATE_LIMIT_COOLDOWN_MS` 设置默认冷却时间,让被限流账号短暂退出账号池后自动恢复 - **降低请求频率**:适当增加请求间隔,避免触发速率限制 - **等待配额重置**:免费配额通常每日或每分钟重置 @@ -714,6 +795,25 @@ kill -9 - **检查请求频率**:某些提供商对请求频率有严格限制,降低请求频率后重试 - **查看提供商文档**:访问对应提供商的官方文档,了解具体的访问限制和要求 +### 14. 为什么要开启“启用OAuth令牌自动刷新”? + +**问题描述**:不确定是否需要开启令牌自动刷新功能。 + +**解决方案**: +OAuth 令牌(如 Gemini, Antigravity, Codex)通常有一定的有效期(如 1 小时)。 +- **开启后**:系统会在后台自动检查并刷新即将过期的令牌。这能确保提供 24/7 稳定的 API 服务,避免因令牌过期导致的 `401 Unauthorized` 或 `403 Forbidden` 错误。 +- **不开启**:令牌过期后,系统无法自动获取新令牌,导致 API 请求失败,直到您手动重新进行 OAuth 授权。 + +### 15. “预加载模型提供商”未开启对 Token 保持有什么影响? + +**问题描述**:不理解“预加载模型提供商”配置的作用及其对 Token 的影响。 + +**解决方案**: +系统仅会对**已加载到活跃池中**的提供商执行自动刷新任务。 +- **影响**:如果您在配置中未勾选某个提供商作为“预加载模型提供商”,系统启动时不会初始化该提供商。即使开启了“自动刷新”,后台任务也**不会**去刷新这个未激活提供商的令牌。 +- **后果**:如果您长时间不使用该提供商,其 Token 会在后台静默过期。当您偶尔通过特定路由调用它时,会因为 Token 过期而请求失败。 +- **建议**:对于需要长期稳定使用的提供商,请务必在“预加载模型提供商”中进行勾选。 +
--- @@ -726,7 +826,7 @@ kill -9 本项目的开发受到了官方 Google Gemini CLI 的极大启发,并参考了Cline 3.18.0 版本 `gemini-cli.ts` 的部分代码实现。在此对 Google 官方团队和 Cline 开发团队的卓越工作表示衷心的感谢! ### 贡献者列表 -感谢以下所有为 AIClient-2-API 项目做出贡献的开发者: +感谢以下所有为 AIClient2API 项目做出贡献的开发者: [![Contributors](https://contrib.rocks/image?repo=justlovemaki/AIClient-2-API)](https://github.com/justlovemaki/AIClient-2-API/graphs/contributors) @@ -741,7 +841,7 @@ kill -9 ## ⚠️ 免责声明 ### 使用风险提示 -本项目(AIClient-2-API)仅供学习和研究使用。用户在使用本项目时,应自行承担所有风险。作者不对因使用本项目而导致的任何直接、间接或 consequential 损失承担责任。 +本项目(AIClient2API)仅供学习和研究使用。用户在使用本项目时,应自行承担所有风险。作者不对因使用本项目而导致的任何直接、间接或 consequential 损失承担责任。 ### 第三方服务责任说明 本项目是一个API代理工具,不提供任何AI模型服务。所有AI模型服务由相应的第三方提供商(如Google、OpenAI、Anthropic等)提供。用户在使用本项目访问这些第三方服务时,应遵守各第三方服务的使用条款和政策。作者不对第三方服务的可用性、质量、安全性或合法性承担责任。 diff --git a/README.md b/README.md index 8629d9c90..34ab322e2 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,34 @@ logo -# AIClient-2-API 🚀 +# AIClient2API(A2)🚀 **A powerful proxy that can unify the requests of various client-only large model APIs (Gemini CLI, Antigravity, Codex, Grok, Kiro ...), simulate requests, and encapsulate them into a local OpenAI-compatible interface.** -justlovemaki%2FAIClient-2-API | Trendshift -
-Ask DeepWiki + + + + + + + + + + + + + +
Docker Downloads > 100kRanked #2 on Trendshift
+ AIClient2API + + justlovemaki%2FAIClient-2-API | Trendshift +
+[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/justlovemaki/AIClient-2-API) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Node.js](https://img.shields.io/badge/Node.js-≥20.0.0-green.svg)](https://nodejs.org/) [![Docker](https://img.shields.io/badge/docker-≥20.0.0-blue.svg)](https://hub.docker.com/r/justlikemaki/aiclient-2-api) @@ -24,8 +40,13 @@
+--- + + ## 💎 Sponsors +*Sponsors are listed in chronological order; all are recommended for registration and use.* + + + + + + + + +
@@ -39,24 +60,48 @@
- - AICodeMirror Sponsor + + APIKEY.FUN Sponsor - Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support. Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for AIClient-2-API users: register via this link to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off! + Thanks to APIKEY.FUN for sponsoring this project! APIKEY.FUN is a professional enterprise-grade AI relay station, dedicated to providing stable, efficient, and low-cost AI model API access services for enterprises and individual developers. The platform supports mainstream popular models such as Claude, OpenAI, and Gemini, with prices as low as 7% of the official original price. Register through the project's exclusive link to enjoy an exclusive discount of up to 5% off (95% of original price) for permanent recharges.
- - LingtrueAPI Sponsor + + VisionCoder Sponsor - Thanks to LingtrueAPI for its sponsorship of this project! LingtrueAPI is a global large-model API intermediary service platform that offers API calling services for various models such as Claude opus 4.6, GPT 5.4, and Gemini 3.1 pro. It is committed to enabling users to connect to global AI capabilities at low cost and with high stability, maximizing production efficiency. LingtrueAPI provides special discounts for users of this software: register using this link and enter the LingtrueAPI promo code when making the first recharge to enjoy a 10% discount. + Thanks to VisionCoder for supporting this project. VisionCoder Developer Platform is a reliable and efficient API relay service provider, offering access to mainstream AI models such as Claude Code, Codex, and Gemini. It helps developers and teams integrate AI capabilities more easily and improve productivity. VisionCoder is also offering our users a limited-time Token Plan promotion: buy 1 month and get 1 month free. +
+ + Atlas Cloud Sponsor + + + Thanks to Atlas Cloud for sponsoring this project! Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access.
Sponsor Contact @@ -82,7 +140,7 @@ ## 🚀 Overview -`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini, Antigravity, Codex, Grok, Kiro) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between OpenAI, Claude, and Gemini protocols, enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Opus 4.5, Gemini 3.0 Pro, and Qwen3 Coder Plus at scale. The project adopts a modular architecture based on strategy and adapter patterns, with built-in account pool management, intelligent polling, automatic failover, and health check mechanisms, ensuring 99.9% service availability. +`AIClient2API` is an API proxy service that breaks through client limitations, converting free large models originally restricted to client use only (such as Gemini, Antigravity, Codex, Grok, Kiro) into standard OpenAI-compatible interfaces that can be called by any application. Built on Node.js, it supports intelligent conversion between OpenAI, Claude, and Gemini protocols, enabling tools like Cherry-Studio, NextChat, and Cline to freely use advanced models such as Claude Opus and Gemini Pro at scale. The project adopts a modular architecture based on strategy and adapter patterns, with built-in account pool management, intelligent polling, automatic failover, and health check mechanisms, ensuring 99.9% service availability. > [!NOTE] > **🎉 Important Milestone** @@ -94,7 +152,10 @@ >
> Click to expand detailed version history > -> - **2026.03.02** - Added Grok protocol support, supporting access to xAI Grok series models (Grok 3/4) via Cookie/SSO, supporting multimodal input, image/video generation, automatic token refresh and streaming output +> - **2026.06.03** - Added Grok Build (Grok CLI) support: integrated the `grok-cli-oauth` xAI OAuth / Responses API flow, covering Grok Build text models, multi-protocol conversion, built-in tools (web search, X search, code interpreter, collections/file attachments), and image/video generation models. +> - **2026.05.04 (v3.0.0)** - **Milestone Update: Deep AI Integration & Self-Discovery Architecture**. Added automated Skill guides and remote `/api/help`, `/api/example` endpoints, enabling AI agents to seamlessly understand and operate 50+ full API endpoints; achieved full unification of CLI and REST API output results with enhanced structured JSON support. +> - **2026.04.29** - Comprehensive support for OpenAI standard Image Generation (`/v1/images/generations`) and Image Editing (`/v1/images/edits`) interfaces. Supports automatic conversion from OpenAI format to native image generation protocols of various models, fully compatible with provider pool polling and retry mechanisms, significantly improving the stability of multimodal creation. +> - **2026.03.02** - Added Grok protocol support, supporting access to xAI Grok series models (Grok) via Cookie/SSO, supporting multimodal input, image/video generation, automatic token refresh and streaming output > - **2026.01.26** - Added Codex protocol support: supports OpenAI Codex OAuth authorization access > - **2026.01.25** - Enhanced AI Monitor plugin: supports monitoring request parameters and responses before and after AI protocol conversion. Optimized log management: unified log format, visual configuration > - **2026.01.15** - Optimized provider pool manager: added async refresh queue mechanism, buffer queue deduplication, global concurrency control, node warmup and automatic expiry detection @@ -102,11 +163,10 @@ > - **2025.12.30** - Added main process management and automatic update functionality > - **2025.12.25** - Unified configuration management: All configs centralized to `configs/` directory. Docker users need to update mount path to `-v "local_path:/app/configs"` > - **2025.12.11** - Automatically built Docker images are now available on Docker Hub: [justlikemaki/aiclient-2-api](https://hub.docker.com/r/justlikemaki/aiclient-2-api) -> - **2025.11.30** - Added Antigravity protocol support, enabling access to Gemini 3 Pro, Claude Sonnet 4.5, and other models via Google internal interfaces +> - **2025.11.30** - Added Antigravity protocol support, enabling access to Gemini Pro, Claude Sonnet, and other models via Google internal interfaces > - **2025.11.11** - Added Web UI management console, supporting real-time configuration management and health status monitoring -> - **2025.11.06** - Added support for Gemini 3 Preview, enhanced model compatibility and performance optimization -> - **2025.10.18** - Kiro open registration, new accounts get 500 credits, full support for Claude Sonnet 4.5 -> - **2025.09.01** - Integrated Qwen Code CLI, added `qwen3-coder-plus` model support +> - **2025.11.06** - Added support for Gemini Preview, enhanced model compatibility and performance optimization +> - **2025.10.18** - Kiro open registration, new accounts get 500 credits, full support for Claude Sonnet > - **2025.08.29** - Released account pool management feature, supporting multi-account polling, intelligent failover, and automatic degradation strategies > - Configuration: Add `PROVIDER_POOLS_FILE_PATH` parameter in `configs/config.json` > - Reference configuration: [provider_pools.json](./configs/provider_pools.json.example) @@ -119,8 +179,24 @@ ## 💡 Core Advantages +### 🤖 AI-First, Agent Interaction Support + +> **AI-First Design**: This project natively supports efficient interaction with mainstream AI Agents such as OpenClaw, Hermes, and Claude Code. +> +> **💡 Quick Command**: You can tell the AI this sentence directly, and it will automatically master all usage of this project: +> +> - **Remote Deployment**: +> ```text +> Please load and learn the Skill in https://raw.githubusercontent.com/justlovemaki/AIClient2API/main/docs/skills/aiclient-cli-usage.md (Service Address: your actual domain or IP, Login Password: your actual password) to master all usage of AIClient2API. +> ``` +> - **Local Mode**: +> If you are running the AI agent directly in your local environment, just send: +> ```text +> Please load and learn the Skill in docs/skills/aiclient-cli-usage.md to help me start, configure, and manage the AIClient2API service locally. +> ``` + ### 🎯 Unified Access, One-Stop Management -* **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, Grok, Codex, Kimi K2, MiniMax M2 +* **Multi-Model Unified Interface**: Through standard OpenAI-compatible protocol, configure once to access mainstream large models including Gemini, Claude, Grok, Codex, Kimi, MiniMax * **Flexible Switching Mechanism**: Path routing, support dynamic model switching via startup parameters or environment variables to meet different scenario requirements * **Zero-Cost Migration**: Fully compatible with OpenAI API specifications, tools like Cherry-Studio, NextChat, Cline can be used without modification * **Multi-Protocol Intelligent Conversion**: Support intelligent conversion between OpenAI, Claude, and Gemini protocols for cross-protocol model invocation @@ -128,7 +204,7 @@ ### 🚀 Break Through Limitations, Improve Efficiency * **Bypass Official Restrictions**: Utilize OAuth authorization mechanism to effectively break through rate and quota limits of services like Gemini, Antigravity * **TLS Fingerprint Bypass**: Built-in TLS Sidecar (Go uTLS) to simulate browser features, effectively bypassing Cloudflare 403 blocks for services like Grok -* **Free Advanced Models**: Use Claude Opus 4.5 for free via Kiro API mode, use Qwen3 Coder Plus via Qwen OAuth mode, reducing usage costs +* **Free Advanced Models**: Use Claude Opus for free via Kiro API mode, reducing usage costs * **Intelligent Account Pool Scheduling**: Support multi-account polling, automatic failover, and configuration degradation, ensuring 99.9% service availability ### 🛡️ Secure and Controllable, Data Transparent @@ -164,19 +240,20 @@ ### 🚀 Quick Start -The most recommended way to use AIClient-2-API is to start it through an automated script and configure it visually directly in the **Web UI console**. +The most recommended way to use AIClient2API is to start it through an automated script and configure it visually directly in the **Web UI console**. #### 🐳 Docker Quick Start (Recommended) ```bash -docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 19876-19880:19876-19880 --restart=always -v "your_path:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085-8086:8085-8086 -p 1455:1455 -p 56121:56121 -p 19876-19880:19876-19880 --restart=always -v "your_path/configs:/app/configs" -v "your_path/plugins:/app/src/plugins-user" --name aiclient2api justlikemaki/aiclient-2-api ``` **Parameter Description**: - `-d`: Run container in background -- `-p 3000:3000 ...`: Port mapping. 3000 is for Web UI, others are for OAuth callbacks (Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880) +- `-p 3000:3000 ...`: Port mapping. 3000 is for Web UI, others are for OAuth callbacks (Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880) - `--restart=always`: Container auto-restart policy -- `-v "your_path:/app/configs"`: Mount configuration directory (replace "your_path" with actual path, e.g., `/home/user/aiclient-configs`) +- `-v "your_path/configs:/app/configs"`: Mount configuration directory (replace "your_path" with actual path, e.g., `/home/user/aiclient2api`) +- `-v "your_path/plugins:/app/src/plugins-user"`: Mount user plugins directory - `--name aiclient2api`: Container name #### 🐳 Docker Compose Deployment @@ -198,13 +275,19 @@ To build from source instead of using the pre-built image, edit `docker-compose. * **Linux/macOS**: `chmod +x install-and-run.sh && ./install-and-run.sh` * **Windows**: Double-click `install-and-run.bat` -> **💡 If the script fails, you can try manually installing dependencies and starting:** +> **💡 Manual installation and startup (supports custom parameters):** > ```bash > npm install +> # Default startup > npm start +> # Show help information +> npm run help +> # Show API calling examples +> npm run example:api +> # Backend-only mode (disable frontend management UI) +> npm start -- --no-ui > ``` - #### 2. Access the console After the server starts, open your browser and visit: 👉 [**http://localhost:3000**](http://localhost:3000) @@ -217,7 +300,7 @@ Go to the **"Configuration"** page, you can: * ✅ Switch default model providers in real-time * ✅ Monitor health status and real-time request logs -#### 4. Local Environment Preparation (Non-Docker Users) +#### 4. Local Environment Preparation (Non-Docker Users)s If you are running directly on your local machine (via script or Node.js) and need to bypass TLS detection for services like Grok, please ensure: * ✅ **Install Go Language**: Go to the [official Go website](https://go.dev/) to download and install (1.20+). * ✅ **Manually Compile Sidecar**: Execute the following command to compile the TLS proxy component: @@ -261,7 +344,7 @@ A functional Web management interface, including: **📊 Dashboard**: System overview, interactive routing examples, client configuration guide -**⚙️ Configuration**: Real-time parameter modification, supporting all providers (Gemini, Antigravity, OpenAI, Claude, Kiro, Qwen), including advanced settings and file uploads +**⚙️ Configuration**: Real-time parameter modification, supporting all providers (Gemini, Antigravity, OpenAI, Claude, Kiro), including advanced settings and file uploads **🔗 Provider Pools**: Monitor active connections, provider health statistics, enable/disable management @@ -278,11 +361,10 @@ Supports various input types such as images and documents, providing you with a #### Latest Model Support Seamlessly support the following latest large models, just configure the corresponding endpoint in Web UI or [`configs/config.json`](./configs/config.json): -* **Grok 3 / Grok 4** - xAI's flagship models, now supported via Grok Cookie/SSO, supporting thinking models, image generation, and video generation -* **Claude 4.5 Opus** - Anthropic's strongest model ever, now supported via Kiro, Antigravity -* **Gemini 3 Pro** - Google's next-generation architecture preview, now supported via Gemini, Antigravity -* **Qwen3 Coder Plus** - Alibaba Tongyi Qianwen's latest code-specific model, now supported via Qwen Code -* **Kimi K2 / MiniMax M2** - Synchronized support for top domestic flagship models, now supported via custom OpenAI, Claude +* **Grok / Grok Build** - xAI's flagship models, now supported via Grok Cookie/SSO and Grok CLI OAuth, supporting thinking models, Grok Build, built-in tools, image generation, and video generation +* **Claude Opus** - Anthropic's strongest model ever, now supported via Kiro, Antigravity +* **Gemini Pro** - Google's next-generation architecture preview, now supported via Gemini, Antigravity +* **Kimi / MiniMax** - Synchronized support for top domestic flagship models, now supported via custom OpenAI, Claude --- @@ -295,8 +377,8 @@ Seamlessly support the following latest large models, just configure the corresp #### 🌐 Web UI Quick Authorization (Recommended) In the Web UI management interface, you can complete authorization configuration rapidly: -1. **Generate Authorization**: On the **"Provider Pools"** page or **"Configuration"** page, click the **"Generate Authorization"** button in the upper right corner of the corresponding provider (e.g., Gemini, Qwen). -2. **Scan/Login**: An authorization dialog will pop up, you can click **"Open in Browser"** for login verification. For Qwen, just complete the web login; for Gemini and Antigravity, complete the Google account authorization. +1. **Generate Authorization**: On the **"Provider Pools"** page or **"Configuration"** page, click the **"Generate Authorization"** button in the upper right corner of the corresponding provider (e.g., Gemini). +2. **Scan/Login**: An authorization dialog will pop up, you can click **"Open in Browser"** for login verification. For Gemini and Antigravity, complete the Google account authorization. 3. **Auto-Save**: After successful authorization, the system will automatically obtain credentials and save them to the corresponding directory in `configs/`. You can see the newly generated credentials on the **"Config Files"** page. 4. **Visual Management**: You can upload or delete credentials at any time in the Web UI, or use the **"Quick Associate"** function to bind existing credential files to providers with one click. @@ -310,16 +392,6 @@ In the Web UI management interface, you can complete authorization configuration 2. **Pro Member**: Antigravity is temporarily open to Pro members, you need to purchase a Pro membership first. 3. **Organization Account**: Organization accounts require separate authorization, contact the administrator to obtain authorization. -#### Qwen Code OAuth Configuration -1. **First Authorization**: After configuring the Qwen service, the system will automatically open the authorization page in the browser -2. **Recommended Parameters**: Use official default parameters for best results - ```json - { - "temperature": 0, - "top_p": 1 - } - ``` - #### Kiro API Configuration 1. **Environment Preparation**: [Download and install Kiro client](https://kiro.dev/pricing/) 2. **Complete Authorization**: Log in to your account in the client to generate `kiro-auth-token.json` credential file @@ -327,7 +399,7 @@ In the Web UI management interface, you can complete authorization configuration 4. **Important Notice**: Kiro service usage policy has been updated, please visit the official website for the latest usage restrictions and terms #### Kiro Extended Thinking (Claude Models) -AIClient-2-API supports Kiro extended thinking when using Claude-compatible requests (`/v1/messages`) or OpenAI-compatible requests (`/v1/chat/completions`) routed to `claude-kiro-oauth`. +AIClient2API supports Kiro extended thinking when using Claude-compatible requests (`/v1/messages`) or OpenAI-compatible requests (`/v1/chat/completions`) routed to `claude-kiro-oauth`. **Claude-compatible (`/v1/messages`)**: ```bash @@ -372,11 +444,17 @@ Notes: 3. **Auto Save**: After successful authorization, the system automatically saves the Codex OAuth credential file 4. **Callback Port**: Ensure the OAuth callback port `1455` is not occupied +#### Grok CLI OAuth Configuration +1. **Generate Authorization**: On the Web UI "Provider Pools" or "Configuration" page, click the "Generate Authorization" button for Grok CLI +2. **Browser Login**: The system opens the xAI authorization page to complete OAuth login +3. **Auto Save**: After successful authorization, the system automatically saves the Grok CLI OAuth credential file to `configs/grok-cli/` +4. **Callback Port**: Ensure the OAuth callback port `56121` is not occupied + #### Grok Cookie/SSO Configuration 1. **Obtain SSO Token**: Log in to the [Grok official website](https://grok.com/), copy the value of `sso` from Application -> Cookies in browser developer tools 2. **Enter Configuration**: In the Web UI "Configuration" page or directly modify the configuration file, enter the token into `GROK_COOKIE_TOKEN` 3. **Supported Features**: - - Chat and Thinking models (Grok 3 Thinking) + - Chat and Thinking models (Grok Thinking) - Image generation (Grok Imagine) - Video generation (Grok Video) 4. **Notes**: Ensure `GROK_USER_AGENT` matches the browser used when obtaining the cookie to avoid being blocked @@ -400,9 +478,9 @@ Default storage locations for authorization credential files of each service: |------|---------|------| | **Gemini** | `~/.gemini/oauth_creds.json` | OAuth authentication credentials | | **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro authentication token | -| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth credentials | -| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth credentials (supports Claude 4.5 Opus) | +| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth credentials (supports Claude Opus) | | **Codex** | `~/.codex/oauth_creds.json` | Codex OAuth credentials | +| **Grok CLI** | `configs/grok-cli/..._xai-..._oauth_creds.json` | Grok CLI OAuth credentials | > **Note**: `~` represents the user home directory (Windows: `C:\Users\username`, Linux/macOS: `/home/username` or `/Users/username`) @@ -441,10 +519,11 @@ This project supports flexible proxy configuration, allowing you to configure a "gemini-cli-oauth", "gemini-antigravity", "claude-kiro-oauth", - "grok-custom" + "grok-web" ] -} - ``` + } + ``` + 3. **Provider-Specific Proxied Endpoints**: Some providers (like OpenAI, Claude) support configuring proxied API endpoints @@ -579,7 +658,7 @@ For services like Grok that strictly validate TLS fingerprints (JA3/JA4), this p **Solutions**: - **Check Network Connection**: Ensure you can access Google, Alibaba Cloud, and other services normally -- **Check Port Occupation**: OAuth callbacks require specific ports (Gemini: 8085, Antigravity: 8086, Codex: 1455, Kiro: 19876-19880), ensure these ports are not occupied +- **Check Port Occupation**: OAuth callbacks require specific ports (Gemini: 8085, Antigravity: 8086, Codex: 1455, Grok CLI: 56121, Kiro: 19876-19880), ensure these ports are not occupied - **Clear Browser Cache**: Try using incognito mode or clearing browser cache and retry - **Check Firewall Settings**: Ensure the firewall allows access to local callback ports - **Docker Users**: Ensure all OAuth callback ports are correctly mapped @@ -628,6 +707,7 @@ Or modify the port configuration in `configs/config.json` to use a different por **Solutions**: - **Configure Account Pool**: Add multiple accounts to `provider_pools.json`, enable polling mechanism - **Configure Fallback**: Configure `providerFallbackChain` in `config.json` for cross-type degradation +- **Enable 429 Cooldown**: Set `RATE_LIMIT_COOLDOWN_ENABLED` to `true` and tune `RATE_LIMIT_COOLDOWN_MS` so rate-limited accounts temporarily leave the pool and recover automatically - **Reduce Request Frequency**: Appropriately increase request intervals to avoid triggering rate limits - **Wait for Quota Reset**: Free quotas usually reset daily or per minute @@ -714,6 +794,25 @@ Or modify the port configuration in `configs/config.json` to use a different por - **Check Request Frequency**: Some providers have strict request frequency limits; reduce request frequency and retry - **View Provider Documentation**: Visit the official documentation of the corresponding provider to understand specific access restrictions and requirements +### 14. Why should I enable "OAuth Token Auto-Refresh"? + +**Problem Description**: Unsure if token auto-refresh is necessary. + +**Solution**: +OAuth tokens (e.g., Gemini, Antigravity, Codex) typically have a limited lifespan (e.g., 1 hour). +- **With it enabled**: The system automatically checks and refreshes tokens before they expire in the background. This ensures 24/7 stable API service and avoids `401 Unauthorized` or `403 Forbidden` errors due to expired tokens. +- **Without it**: Once a token expires, the system cannot automatically obtain a new one, causing API requests to fail until you manually re-authorize. + +### 15. What is the impact of not enabling "Preload Model Providers" on token maintenance? + +**Problem Description**: Confusion about the "Preload Model Providers" configuration and its relation to token refresh. + +**Solution**: +The system only performs auto-refresh tasks for providers that are **loaded into the active pool**. +- **Impact**: If a provider is not checked as a "Preload Model Provider" in the configuration, it won't be initialized when the system starts. Since it's not in the pool, the background refresh task will **not** process its token. +- **Consequence**: If you don't use that provider for a long time, its token will expire silently. When you eventually call it via a specific route, the request will fail due to the expired token. +- **Recommendation**: Always check providers you intend to use frequently and need to keep active in the "Preload Model Providers" list. +
--- @@ -728,7 +827,7 @@ The development of this project was greatly inspired by the official Google Gemi ### Contributor List -Thanks to all the developers who contributed to the AIClient-2-API project: +Thanks to all the developers who contributed to the AIClient2API project: [![Contributors](https://contrib.rocks/image?repo=justlovemaki/AIClient-2-API)](https://github.com/justlovemaki/AIClient-2-API/graphs/contributors) @@ -743,7 +842,7 @@ Thanks to all the developers who contributed to the AIClient-2-API project: ## ⚠️ Disclaimer ### Usage Risk Warning -This project (AIClient-2-API) is for learning and research purposes only. Users assume all risks when using this project. The author is not responsible for any direct, indirect, or consequential losses resulting from the use of this project. +This project (AIClient2API) is for learning and research purposes only. Users assume all risks when using this project. The author is not responsible for any direct, indirect, or consequential losses resulting from the use of this project. ### Third-Party Service Responsibility Statement This project is an API proxy tool and does not provide any AI model services. All AI model services are provided by their respective third-party providers (such as Google, OpenAI, Anthropic, etc.). Users should comply with the terms of service and policies of each third-party service when accessing them through this project. The author is not responsible for the availability, quality, security, or legality of third-party services. diff --git a/VERSION b/VERSION index ea55a03fa..75a557aba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.13.7 +3.2.2.2 \ No newline at end of file diff --git a/configs/config.json.example b/configs/config.json.example index 6a4bf1dd6..be9e6f8ce 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -13,9 +13,14 @@ "PROMPT_LOG_MODE": "none", "REQUEST_MAX_RETRIES": 3, "REQUEST_BASE_DELAY": 1000, + "RATE_LIMIT_COOLDOWN_ENABLED": false, + "RATE_LIMIT_COOLDOWN_MS": 30000, + "RATE_LIMIT_COOLDOWN_JITTER_MS": 5000, + "RATE_LIMIT_COOLDOWN_MAX_MS": 300000, "CRON_NEAR_MINUTES": 1, "CRON_REFRESH_TOKEN": false, "PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json", + "CUSTOM_MODELS_FILE_PATH": "configs/custom_models.json", "MAX_ERROR_COUNT": 3, "GROK_COOKIE_TOKEN": "your-sso-cookie-token", "GROK_CF_CLEARANCE": "your-cf-clearance-cookie", @@ -63,5 +68,6 @@ "LOG_MAX_FILE_SIZE": 10485760, "LOG_MAX_FILES": 10, "TLS_SIDECAR_ENABLED": false, - "TLS_SIDECAR_PORT": 9090 + "TLS_SIDECAR_PORT": 9090, + "UI_ENABLED": true } diff --git a/configs/custom_models.json.example b/configs/custom_models.json.example new file mode 100644 index 000000000..c7c48250d --- /dev/null +++ b/configs/custom_models.json.example @@ -0,0 +1,35 @@ +[ + { + "id": "atlas-custom-model", + "name": "Atlas Custom Model", + "alias": "gpt-4o", + "provider": "atlascloud", + "actualProvider": "atlascloud", + "actualModel": "gpt-4o", + "contextLength": 128000, + "temperature": 0.7 + }, + { + "id": "my-custom-gpt-4", + "name": "Custom GPT-4", + "alias": "gpt-4", + "provider": "openai-custom", + "actualProvider": "openai-custom", + "actualModel": "gpt-4-0613", + "contextLength": 8192, + "maxTokens": 4096, + "temperature": 0.7, + "topP": 1.0, + "description": "Custom configuration for GPT-4" + }, + { + "id": "claude-3-7-sonnet-custom", + "name": "Claude 3.7 Sonnet Custom", + "alias": "claude-3-7-sonnet", + "provider": "claude-custom", + "actualProvider": "claude-kiro-oauth", + "actualModel": "claude-3-7-sonnet-20250219", + "contextLength": 200000, + "temperature": 0.5 + } +] diff --git a/configs/market.json b/configs/market.json new file mode 100644 index 000000000..52179c500 --- /dev/null +++ b/configs/market.json @@ -0,0 +1,13 @@ +[ + { + "id": "ip-node-proxy", + "name": "IP 节点代理绑定", + "version": "1.0.0", + "minSystemVersion": "3.1.1", + "isPaid": true, + "price": "¥699", + "description": "按客户端 IP 为指定提供商节点1对1绑定出站代理,实现精准的链路控制和 TLS 指纹伪装覆盖。", + "paymentUrl": "https://pay.ldxp.cn/item/xq10cf", + "qrCode": "ip2node.png" + } +] \ No newline at end of file diff --git a/configs/market.json.example b/configs/market.json.example new file mode 100644 index 000000000..c3f35f396 --- /dev/null +++ b/configs/market.json.example @@ -0,0 +1,37 @@ +[ + { + "id": "api-potluck", + "name": "API 大锅饭", + "version": "1.0.2", + "minSystemVersion": "3.0.0", + "description": "多用户 Key 管理系统。支持每日配额限制、Token 用量统计、自定义过期时间及独立的用户查询界面。", + "downloadUrl": "https://raw.githubusercontent.com/justlovemaki/AIClient-2-API/main/plugins/api-potluck.zip" + }, + { + "id": "model-usage-stats", + "name": "模型用量统计", + "version": "1.0.0", + "minSystemVersion": "2.8.0", + "description": "全自动统计 Provider 和 Model 维度的请求次数、Token 消耗,并提供直观的可视化仪表盘和趋势图。", + "downloadUrl": "https://raw.githubusercontent.com/justlovemaki/AIClient-2-API/main/plugins/model-usage-stats.zip" + }, + { + "id": "ai-monitor", + "name": "AI 实时监控", + "version": "1.0.0", + "minSystemVersion": "3.0.0", + "description": "监控系统吞吐量(QPS/TPS)、延迟和错误率,帮助您实时掌握各个节点的运行状况。", + "downloadUrl": "https://raw.githubusercontent.com/justlovemaki/AIClient-2-API/main/plugins/ai-monitor.zip" + }, + { + "id": "premium-export", + "name": "高级数据导出插件", + "version": "1.2.0", + "minSystemVersion": "3.0.0", + "isPaid": true, + "price": "¥19.9", + "description": "支持将所有用量统计、监控日志导出为 Excel、PDF 及自动同步至外部数据库。", + "paymentUrl": "https://example.com/pay/export-plugin", + "qrCode": "sponsor.png" + } +] diff --git a/configs/provider_pools.json.example b/configs/provider_pools.json.example index 98e56433f..2aa4c1f47 100644 --- a/configs/provider_pools.json.example +++ b/configs/provider_pools.json.example @@ -1,4 +1,21 @@ { + "atlascloud": [ + { + "customName": "AtlasCloud节点1", + "OPENAI_API_KEY": "sk-atlascloud-key1", + "OPENAI_BASE_URL": "https://api.openai.com/v1", + "checkModelName": null, + "checkHealth": false, + "notSupportedModels": [], + "uuid": "4f579c65-d3c5-41b1-9985-9f6e3d7bf39d", + "isHealthy": true, + "isDisabled": false, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null + } + ], "openai-custom": [ { "customName": "OpenAI节点1", diff --git a/docker/VERSION b/docker/VERSION new file mode 100644 index 000000000..56fea8a08 --- /dev/null +++ b/docker/VERSION @@ -0,0 +1 @@ +3.0.0 \ No newline at end of file diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml index 31fa518d0..9064cb418 100644 --- a/docker/docker-compose.build.yml +++ b/docker/docker-compose.build.yml @@ -1,7 +1,7 @@ services: aiclient-api: # 方式二:从 Dockerfile 本地构建 - # 使用方法: docker compose -f docker-compose.build.yml up -d --build + # 使用方法: docker compose -f docker/docker-compose.build.yml -f docker/docker-compose.dev.yml up -d --build build: context: .. dockerfile: Dockerfile @@ -9,11 +9,13 @@ services: restart: unless-stopped ports: - "3000:3000" - - "8085-8087:8085-8087" + - "8085-8087:8085-8087" - "1455:1455" + - "56121:56121" - "19876-19880:19876-19880" volumes: - - ./configs:/app/configs + - ../configs:/app/configs + - ../plugins:/app/src/plugins-user environment: - ARGS= healthcheck: @@ -21,4 +23,4 @@ services: interval: 30s timeout: 3s start_period: 5s - retries: 3 \ No newline at end of file + retries: 3 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 000000000..4abd77a81 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,17 @@ +# 开发模式覆盖配置 - 热重载 +# 使用方法: docker compose -f docker/docker-compose.build.yml -f docker/docker-compose.dev.yml up -d --build + +services: + aiclient-api: + build: + context: .. + dockerfile: Dockerfile + volumes: + # 从项目根目录挂载 configs,使用绝对路径确保正确 + - ../configs:/app/configs + - ../plugins:/app/src/plugins-user + # 挂载源代码以支持热重载,但保留 node_modules + - ../src:/app/src:ro + - ../package.json:/app/package.json:ro + # 使用 shell form 以支持 $ARGS 展开 + command: ["sh", "-c", "node --watch src/core/master.js $ARGS"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a9d2eaf22..91828182a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,9 +9,11 @@ services: - "3000:3000" - "8085-8087:8085-8087" - "1455:1455" + - "56121:56121" - "19876-19880:19876-19880" volumes: - ./configs:/app/configs + - ./plugins:/app/src/plugins-user environment: - ARGS= healthcheck: diff --git a/docs/OPENCLAW_CONFIG_GUIDE-JA.md b/docs/OPENCLAW_CONFIG_GUIDE-JA.md index d33f60e48..cb22b89bf 100644 --- a/docs/OPENCLAW_CONFIG_GUIDE-JA.md +++ b/docs/OPENCLAW_CONFIG_GUIDE-JA.md @@ -1,12 +1,12 @@ # OpenClaw 設定ガイド -OpenClaw で AIClient-2-API を使用するためのクイック設定ガイド。 +OpenClaw で AIClient2API を使用するためのクイック設定ガイド。 --- ## 前提条件 -1. AIClient-2-API サービスを起動 +1. AIClient2API サービスを起動 2. Web UI (`http://localhost:3000`) で少なくとも1つのプロバイダーを設定 3. 設定ファイルから API Key を記録 4. OpenClaw をインストール @@ -195,7 +195,7 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "あなたの質問" ## よくある質問 **Q: 接続に失敗しますか?** -- AIClient-2-API サービスが実行中であることを確認 +- AIClient2API サービスが実行中であることを確認 - Base URL が正しいか確認(OpenAI プロトコルには `/v1` サフィックスが必要) - `localhost` の代わりに `127.0.0.1` を使用してみる @@ -204,10 +204,10 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "あなたの質問" - 環境変数 `AICLIENT2API_KEY` が設定されているか確認 **Q: モデルが利用できない?** -- AIClient-2-API Web UI でプロバイダーが設定されているか確認 +- AIClient2API Web UI でプロバイダーが設定されているか確認 - `openclaw gateway restart` を実行してゲートウェイを再起動 - `openclaw models list` を実行してモデルリストを確認 --- -詳細については、[AIClient-2-API ドキュメント](../README-JA.md) を参照してください +詳細については、[AIClient2API ドキュメント](../README-JA.md) を参照してください diff --git a/docs/OPENCLAW_CONFIG_GUIDE-ZH.md b/docs/OPENCLAW_CONFIG_GUIDE-ZH.md index e45b8cc86..d5770dcea 100644 --- a/docs/OPENCLAW_CONFIG_GUIDE-ZH.md +++ b/docs/OPENCLAW_CONFIG_GUIDE-ZH.md @@ -1,12 +1,12 @@ # OpenClaw 配置指南 -在 OpenClaw 中使用 AIClient-2-API 的快速配置指南。 +在 OpenClaw 中使用 AIClient2API 的快速配置指南。 --- ## 前置准备 -1. 启动 AIClient-2-API 服务 +1. 启动 AIClient2API 服务 2. 在 Web UI (`http://localhost:3000`) 配置至少一个提供商 3. 记录配置文件中的 API Key 4. 安装 OpenClaw @@ -195,7 +195,7 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "你的问题" ## 常见问题 **Q: 连接失败?** -- 确认 AIClient-2-API 服务运行中 +- 确认 AIClient2API 服务运行中 - 检查 Base URL 是否正确(OpenAI 协议需要 `/v1` 后缀) - 尝试使用 `127.0.0.1` 替代 `localhost` @@ -204,10 +204,10 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "你的问题" - 确认环境变量 `AICLIENT2API_KEY` 已设置 **Q: 模型不可用?** -- 在 AIClient-2-API Web UI 确认已配置对应提供商 +- 在 AIClient2API Web UI 确认已配置对应提供商 - 运行 `openclaw gateway restart` 重启网关 - 运行 `openclaw models list` 验证模型列表 --- -更多信息请参考 [AIClient-2-API 文档](../README-ZH.md) +更多信息请参考 [AIClient2API 文档](../README-ZH.md) diff --git a/docs/OPENCLAW_CONFIG_GUIDE.md b/docs/OPENCLAW_CONFIG_GUIDE.md index f62529e08..e016c6ce5 100644 --- a/docs/OPENCLAW_CONFIG_GUIDE.md +++ b/docs/OPENCLAW_CONFIG_GUIDE.md @@ -1,12 +1,12 @@ # OpenClaw Configuration Guide -Quick configuration guide for using AIClient-2-API with OpenClaw. +Quick configuration guide for using AIClient2API with OpenClaw. --- ## Prerequisites -1. Start AIClient-2-API service +1. Start AIClient2API service 2. Configure at least one provider in Web UI (`http://localhost:3000`) 3. Note the API Key from configuration file 4. Install OpenClaw @@ -195,7 +195,7 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "your question" ## FAQ **Q: Connection failed?** -- Confirm AIClient-2-API service is running +- Confirm AIClient2API service is running - Check if Base URL is correct (OpenAI protocol needs `/v1` suffix) - Try using `127.0.0.1` instead of `localhost` @@ -204,10 +204,10 @@ openclaw chat --model aiclient2api/gemini-3-flash-preview "your question" - Confirm environment variable `AICLIENT2API_KEY` is set **Q: Model unavailable?** -- Confirm provider is configured in AIClient-2-API Web UI +- Confirm provider is configured in AIClient2API Web UI - Run `openclaw gateway restart` to restart gateway - Run `openclaw models list` to verify model list --- -For more information, see [AIClient-2-API Documentation](../README.md) +For more information, see [AIClient2API Documentation](../README.md) diff --git a/docs/OPENCODE_CONFIG_EXAMPLE.md b/docs/OPENCODE_CONFIG_EXAMPLE.md index 76016bd2c..c6a1734f9 100644 --- a/docs/OPENCODE_CONFIG_EXAMPLE.md +++ b/docs/OPENCODE_CONFIG_EXAMPLE.md @@ -24,19 +24,6 @@ } } }, - "qwen": { - "npm": "@ai-sdk/openai-compatible", - "name": "AIClient2API-qwen", - "options": { - "baseURL": "http://localhost:3000/openai-qwen-oauth/v1", - "apiKey": "123456" - }, - "models": { - "qwen3-coder-plus": { - "name": "Qwen3 Coder Plus Openai " - } - } - }, "gemini-antigravity": { "npm": "@ai-sdk/google", "name": "AIClient2API-antigravity", @@ -83,12 +70,12 @@ ## 配置重点解释 ### 1. `provider` (服务提供商配置) -这是配置的核心部分,每个键(如 `kiro`, `qwen`, `gemini-cli`)代表一个独立的服务提供商实例。 +这是配置的核心部分,每个键(如 `kiro`, `gemini-cli`)代表一个独立的服务提供商实例。 * **`npm` (SDK 适配器)**: * 指定底层使用的 AI SDK。例如: * `@ai-sdk/anthropic`: 用于 Anthropic (Claude) 系列模型。 - * `@ai-sdk/openai-compatible`: 用于兼容 OpenAI 接口标准的模型(如通义千问 Qwen)。 + * `@ai-sdk/openai-compatible`: 用于兼容 OpenAI 接口标准的模型。 * `@ai-sdk/google`: 用于 Google Gemini 系列模型。 * **重点**: 必须确保 `npm` 字段与您要使用的模型协议匹配,否则会导致连接失败。 diff --git a/docs/skills/aiclient-cli-usage.md b/docs/skills/aiclient-cli-usage.md new file mode 100644 index 000000000..15698ecbe --- /dev/null +++ b/docs/skills/aiclient-cli-usage.md @@ -0,0 +1,89 @@ +--- +name: aiclient-cli-usage +description: Use when an agent needs to understand, configure, or call the AIClient2API service via its CLI tools or REST APIs. +--- + +# AIClient2API CLI & API Usage Skill + +## Overview +This skill provides instructions for AI agents to interact with the AIClient2API service. It covers how to discover available commands and navigate the exhaustive API surface using both local CLI and remote REST interfaces. + +## Self-Discovery Modes + +**CRITICAL**: Regardless of how you access the service, using REST APIs (for management or AI tasks) requires the `Server Address` and appropriate credentials. **Always ask the user for the `Server Address` (use `http://localhost:3000` as default for local) and `Admin Password` before attempting API-based tasks.** + +### 1. Local Mode (CLI & REST API) +Use these when you have shell access to the server environment. In this mode, you can use both local CLI tools and REST APIs: +- **CLI Help**: `npm run help` (Add `--json` for structured data) +- **CLI API Guide**: `npm run example:api` (Add `--json` for structured data) +- **REST API**: You can access all endpoints via `http://localhost:` (Default: 3000). + +### 2. Remote Mode (REST API Only) +Use these when interacting with a running instance over the network without shell access: +- **Help JSON**: `GET /api/help` (Public, No Auth) +- **API Guide JSON**: `GET /api/example` (Public, No Auth) +- **REST API**: Use the user-provided `Server Address`. + +## When to Use +- When you need to understand, configure, or call the AIClient2API service. +- To programmatically manage model providers or account pools. +- To monitor system health, logs, or usage. + +## Core API Categories (AI vs. Management) + +### 0. Public Endpoints (No Auth Required) +Use these for self-discovery or system monitoring: +- `GET /api/help`: Get full API help documentation (JSON). +- `GET /api/example`: Get API calling examples (JSON). +- `GET /provider_health`: Detailed health status of all model providers. +- `POST /api/login`: Exchange `Admin Password` for a dynamic `Token`. + +### 1. AI Business Path (`/v1/*`, `/v1beta/*`, `/count_tokens`) +- **Purpose**: AI model inference (chat, image, token counting). +- **Auth**: Static `API Key`. **Ask the user for this if not provided.** +- **Header**: `Authorization: Bearer ` + +### 2. Management Path (`/api/*`, `/health`, `/provider_health`) +- **Purpose**: Server config, node pool management, logs, stats. +- **Auth**: Dynamic `Token` via `/api/login`. **Ask the user for the `Server Address` and `Admin Password`.** +- **Header**: `Authorization: Bearer ` (Except for `/api/login` and public health checks). + +## Advanced Patterns + +### Path Routing +Force a specific provider by prefixing the AI business path: +- `/gemini-cli-oauth/v1/chat/completions` +- `/claude-custom/v1/messages` + +### Real-time Logs (SSE) +Subscribe to `GET /api/events` via `EventSource` for live system output. + +## Quick Reference + +| Mode | Method | Command/Endpoint | Format | +|------|--------|------------------|--------| +| Local | CLI | `npm run help -- --json` | JSON | +| Local | CLI | `npm run example:api` | Text | +| Local | REST | `GET http://localhost:3000/api/help` | JSON | +| Remote | REST | `GET /api/help` | JSON | +| Remote | REST | `GET /api/example?format=text` | Text | + +## Common Mistakes +- **Wrong Key**: Using the static AI Key for management APIs (causes 401). +- **No Login**: Attempting to fetch `/api/config` without first calling `/api/login`. +- **Format Mismatch**: Expecting JSON from a CLI command without the `--json` flag. + +## Code Example: Management API Flow +```javascript +// 1. Login to get token +const login = await fetch('/api/login', { + method: 'POST', + body: JSON.stringify({ password: 'admin' }) +}); +const { token } = await login.json(); + +// 2. Use token for management +const nodes = await fetch('/api/providers', { + headers: { 'Authorization': `Bearer ${token}` } +}); +``` diff --git a/package-lock.json b/package-lock.json index 8e81846b3..70ae2aee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,28 @@ { - "name": "AIClient2API", + "name": "aiclient2api", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "aiclient2api", + "version": "3.0.0", "dependencies": { + "@ai-sdk/openai": "^3.0.61", "@anthropic-ai/tokenizer": "^0.0.4", "adm-zip": "^0.5.16", - "axios": "^1.10.0", + "ai": "^6.0.175", + "axios": "^1.14.0", + "busboy": "^1.6.0", "deepmerge": "^4.3.1", - "dotenv": "^16.4.5", + "dotenv": "^16.6.1", "google-auth-library": "^10.1.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "multer": "^2.0.2", "open": "^10.2.0", + "openai": "^6.36.0", "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", "uuid": "^11.1.0", @@ -31,6 +38,68 @@ "supertest": "^6.3.3" } }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.110", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.110.tgz", + "integrity": "sha512-sbv8+1L9/BRKydn8dMNwoMQKupA4iLJ9N+yvxgW6wMQ/94UepDf3FeYWMj/dLdzolAHZ6izRUP4s5WqQkmJ2Zg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.26", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "3.0.61", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.61.tgz", + "integrity": "sha512-L5FdlTo+7IBOdXbnTZqVzowRdLGWnxPn01vHe4leLPp0G4ydoXhySu0tp0ttkoO0ZSzPjHo7iiAkYoT0J91Q8Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.26" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", + "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.26.tgz", + "integrity": "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -71,13 +140,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -127,14 +196,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -231,9 +300,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -255,29 +324,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -300,9 +369,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -360,9 +429,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -370,9 +439,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -419,13 +488,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1225,16 +1294,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1760,33 +1829,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1794,14 +1863,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2209,6 +2278,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", @@ -2246,6 +2324,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2369,6 +2453,15 @@ "dev": true, "license": "ISC" }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/adm-zip": { "version": "0.5.16", "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz", @@ -2387,6 +2480,24 @@ "node": ">= 14" } }, + "node_modules/ai": { + "version": "6.0.175", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.175.tgz", + "integrity": "sha512-6fFFHzbh6FIZnYc31V6osOxq25ABJYCShfG0O6ajHiA4FB/DgnPi1mP8cO5aAU3HNSbQHiMazdlh9bIsp97mVA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.110", + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.26", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2473,14 +2584,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-jest": { @@ -2675,9 +2786,9 @@ } }, "node_modules/babel-jest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2916,9 +3027,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3011,7 +3122,7 @@ }, "node_modules/busboy": { "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { "streamsearch": "^1.1.0" @@ -3594,6 +3705,15 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3725,9 +3845,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -3745,9 +3865,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4149,9 +4269,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -5021,9 +5141,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5063,6 +5183,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5088,12 +5214,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -5138,9 +5264,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -5293,9 +5419,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5305,27 +5431,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5333,21 +5438,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/natural-compare": { @@ -5432,15 +5538,6 @@ "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5498,6 +5595,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.36.0.tgz", + "integrity": "sha512-Has2YbIusMq9wQEierFsgf9c783dy1y9arX459LmphNacEkkM5yxi2RIyXP0LmkOroQyW19iTwALHL8Yf26UKA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5617,9 +5735,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5695,10 +5813,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pure-rand": { "version": "6.1.0", @@ -5718,9 +5839,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6393,9 +6514,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", - "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -6490,9 +6611,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -6592,9 +6713,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6627,15 +6748,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6694,6 +6806,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 5b1b23bc9..fcffc5c23 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,23 @@ { + "name": "aiclient2api", + "version": "3.0.0", "type": "module", "dependencies": { + "@ai-sdk/openai": "^3.0.61", "@anthropic-ai/tokenizer": "^0.0.4", "adm-zip": "^0.5.16", + "ai": "^6.0.175", "axios": "^1.14.0", + "busboy": "^1.6.0", "deepmerge": "^4.3.1", - "dotenv": "^16.4.5", + "dotenv": "^16.6.1", "google-auth-library": "^10.1.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "multer": "^2.0.2", "open": "^10.2.0", + "openai": "^6.36.0", "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", "uuid": "^11.1.0", @@ -37,6 +43,8 @@ "test:silent": "jest --silent", "test:unit": "node run-tests.js --unit", "test:integration": "node run-tests.js --integration", - "test:summary": "node test-summary.js" + "test:summary": "node test-summary.js", + "help": "node src/scripts/help.js", + "example:api": "node src/scripts/example-api.js" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3da5f0706..a5bc0ed91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,20 +8,29 @@ importers: .: dependencies: + '@ai-sdk/openai': + specifier: ^3.0.61 + version: 3.0.63(zod@4.4.3) '@anthropic-ai/tokenizer': specifier: ^0.0.4 version: 0.0.4 adm-zip: specifier: ^0.5.16 version: 0.5.16 + ai: + specifier: ^6.0.175 + version: 6.0.177(zod@4.4.3) axios: specifier: ^1.14.0 version: 1.14.0 + busboy: + specifier: ^1.6.0 + version: 1.6.0 deepmerge: specifier: ^4.3.1 version: 4.3.1 dotenv: - specifier: ^16.4.5 + specifier: ^16.6.1 version: 16.6.1 google-auth-library: specifier: ^10.1.0 @@ -41,6 +50,9 @@ importers: open: specifier: ^10.2.0 version: 10.2.0 + openai: + specifier: ^6.36.0 + version: 6.37.0(ws@8.19.0)(zod@4.4.3) socks-proxy-agent: specifier: ^8.0.5 version: 8.0.5 @@ -78,6 +90,28 @@ importers: packages: + '@ai-sdk/gateway@3.0.112': + resolution: {integrity: sha512-jiBao9pR4owWyjo0BnuNc7WSQBGOD0thysE4AFgZXaG+zMFbISQXUkJr7ePw/phBvePy7jE5FSA2Lf7lwqUiiQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.63': + resolution: {integrity: sha512-4yY/m8a57MNNVoJCsXuNblKf6BO4yuAuLKRX4tzSNffBEBSp1FlcWdPE0Z4FkqUeS0AJhYSSqp0GIiA/cIcDNA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.27': + resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + engines: {node: '>=18'} + '@anthropic-ai/tokenizer@0.0.4': resolution: {integrity: sha512-EHRKbxlxlc8W4KCBEseByJ7YwyYCmgu9OyN59H9+IYIGPoKv8tXyQXinkeGDI+cI8Tiuz9wk2jZb/kK7AyvL7g==} @@ -774,6 +808,10 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -793,6 +831,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -834,6 +875,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} adm-zip@0.5.16: resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} @@ -843,6 +889,12 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@6.0.177: + resolution: {integrity: sha512-1xQtbeWwNcLyyM86ixZhkKvT+WRXc1lvarIKqPVtsyn8F9NDikwUMBqYu+aQKDgMht50SMXh4qboYuU8MeHZZA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1203,6 +1255,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1615,6 +1671,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1766,6 +1825,18 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + openai@6.37.0: + resolution: {integrity: sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2182,8 +2253,35 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@ai-sdk/gateway@3.0.112(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/openai@3.0.63(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.27(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.10': + dependencies: + json-schema: 0.4.0 + '@anthropic-ai/tokenizer@0.0.4': dependencies: '@types/node': 18.19.130 @@ -3164,6 +3262,8 @@ snapshots: '@noble/hashes@1.8.0': {} + '@opentelemetry/api@1.9.0': {} + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -3183,6 +3283,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -3236,10 +3338,20 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vercel/oidc@3.2.0': {} + adm-zip@0.5.16: {} agent-base@7.1.4: {} + ai@6.0.177(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 3.0.112(zod@4.4.3) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@opentelemetry/api': 1.9.0 + zod: 4.4.3 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -3612,6 +3724,8 @@ snapshots: esutils@2.0.3: {} + eventsource-parser@3.0.8: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -4253,6 +4367,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema@0.4.0: {} + json5@2.2.3: {} jwa@2.0.1: @@ -4384,6 +4500,11 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + openai@6.37.0(ws@8.19.0)(zod@4.4.3): + optionalDependencies: + ws: 8.19.0 + zod: 4.4.3 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -4766,3 +4887,5 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zod@4.4.3: {} diff --git a/src/auth/codex-import-normalizer.js b/src/auth/codex-import-normalizer.js new file mode 100644 index 000000000..c2de533dd --- /dev/null +++ b/src/auth/codex-import-normalizer.js @@ -0,0 +1,208 @@ +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function cleanString(value) { + return typeof value === 'string' ? value.trim() : ''; +} + +function parseJwtPayload(token) { + const tokenValue = cleanString(token); + if (!tokenValue) return null; + + const parts = tokenValue.split('.'); + if (parts.length !== 3) return null; + + try { + const payload = Buffer.from(parts[1], 'base64url').toString('utf8'); + return JSON.parse(payload); + } catch { + return null; + } +} + +function getAuthClaims(claims) { + return claims?.['https://api.openai.com/auth'] || {}; +} + +function getProfileClaims(claims) { + return claims?.['https://api.openai.com/profile'] || {}; +} + +function expiryFromValues({ expired, expiresAt, expires_at, expires_in, claims }) { + const explicit = expired || expiresAt; + if (explicit) { + const date = new Date(explicit); + if (!Number.isNaN(date.getTime())) return date.toISOString(); + } + + if (expires_at) { + const timestamp = Number(expires_at); + if (Number.isFinite(timestamp)) { + const milliseconds = timestamp > 1000000000000 ? timestamp : timestamp * 1000; + const date = new Date(milliseconds); + if (!Number.isNaN(date.getTime())) return date.toISOString(); + } + } + + if (expires_in) { + const seconds = Number(expires_in); + if (Number.isFinite(seconds) && seconds > 0) { + return new Date(Date.now() + seconds * 1000).toISOString(); + } + } + + if (claims?.exp) { + const exp = Number(claims.exp); + if (Number.isFinite(exp)) { + return new Date(exp * 1000).toISOString(); + } + } + + return new Date(Date.now() + 3600 * 1000).toISOString(); +} + +function makeFallbackEmail(accountId, index) { + if (accountId) return `codex-${accountId}`; + return `codex-import-${index}`; +} + +function normalizeCredential(raw, source, index, exportedAt = null) { + if (!isPlainObject(raw)) { + return { + error: '凭据必须是 JSON 对象', + source, + index + }; + } + + const nestedCredentials = isPlainObject(raw.credentials) ? raw.credentials : {}; + const nestedExtra = isPlainObject(raw.extra) ? raw.extra : {}; + const data = { ...raw, ...nestedCredentials }; + + const accessToken = cleanString(data.access_token); + if (!accessToken) { + return { + error: '缺少 access_token', + source, + index, + email: cleanString(raw.email || raw.name || nestedExtra.email) + }; + } + + const idToken = cleanString(data.id_token); + const accessClaims = parseJwtPayload(accessToken); + const idClaims = parseJwtPayload(idToken); + const claims = idClaims || accessClaims || {}; + const authClaims = getAuthClaims(claims); + const profileClaims = getProfileClaims(claims); + + const accountId = cleanString( + data.account_id || + data.chatgpt_account_id || + authClaims.chatgpt_account_id || + claims.sub + ); + const email = cleanString( + raw.email || + nestedExtra.email || + raw.name || + data.email || + profileClaims.email || + claims.email + ) || makeFallbackEmail(accountId, index); + + if (!accountId) { + return { + error: '无法获取 account_id 或 chatgpt_account_id', + source, + index, + email + }; + } + + const refreshToken = cleanString(data.refresh_token); + const expired = expiryFromValues({ + expired: data.expired, + expiresAt: data.expiresAt, + expires_at: data.expires_at, + expires_in: data.expires_in, + claims + }); + + return { + source, + id_token: idToken, + access_token: accessToken, + refresh_token: refreshToken, + account_id: accountId, + chatgpt_account_id: accountId, + email, + name: email, + type: 'codex', + last_refresh: cleanString(data.last_refresh || exportedAt) || new Date().toISOString(), + expired, + access_token_only: !refreshToken + }; +} + +export function normalizeCpaCodexCredentials(payload) { + const items = Array.isArray(payload) ? payload : [payload]; + return items.map((item, index) => normalizeCredential(item, 'cpa', index + 1)); +} + +export function normalizeSub2ApiCodexCredentials(payload) { + let accounts; + let exportedAt = null; + + if (Array.isArray(payload)) { + accounts = payload; + } else if (isPlainObject(payload) && Array.isArray(payload.accounts)) { + accounts = payload.accounts; + exportedAt = payload.exported_at || null; + } else if (isPlainObject(payload) && isPlainObject(payload.credentials)) { + accounts = [payload]; + } else { + throw new Error('sub2api 数据必须是完整导出对象、账号数组或单个账号对象'); + } + + const results = []; + + accounts.forEach((account, index) => { + if (!isPlainObject(account)) { + results.push({ + error: '账号条目必须是 JSON 对象', + source: 'sub2api', + index: index + 1 + }); + return; + } + + if (account.platform && account.platform !== 'openai') { + results.push({ + skipped: true, + source: 'sub2api', + index: index + 1, + email: cleanString(account.extra?.email || account.name), + reason: `跳过非 openai 平台账号: ${account.platform}` + }); + return; + } + + results.push(normalizeCredential(account, 'sub2api', index + 1, exportedAt)); + }); + + return results; +} + +export function normalizeCodexExternalCredentials(source, payload) { + if (source === 'cpa') { + return normalizeCpaCodexCredentials(payload); + } + + if (source === 'sub2api') { + return normalizeSub2ApiCodexCredentials(payload); + } + + throw new Error(`Unsupported Codex import source: ${source}`); +} diff --git a/src/auth/codex-oauth.js b/src/auth/codex-oauth.js index 6e4e44241..446e499ee 100644 --- a/src/auth/codex-oauth.js +++ b/src/auth/codex-oauth.js @@ -28,6 +28,16 @@ const CODEX_OAUTH_CONFIG = { */ const activeServers = new Map(); +function sanitizeCodexCredentialFilenamePart(value) { + const sanitized = String(value || 'default') + .trim() + .replace(/[^a-zA-Z0-9@._+-]/g, '_') + .replace(/_+/g, '_') + .slice(0, 120); + + return sanitized || 'default'; +} + /** * 关闭指定端口的活动服务器 */ @@ -493,6 +503,7 @@ class CodexAuth { */ async saveCredentials(creds) { const email = creds.email || this.config.CODEX_EMAIL || 'default'; + const safeEmail = sanitizeCodexCredentialFilenamePart(email); // 优先使用配置中指定的路径,否则保存到 configs/codex 目录 let credsPath; @@ -504,7 +515,7 @@ class CodexAuth { const targetDir = path.join(projectDir, 'configs', 'codex'); await fs.promises.mkdir(targetDir, { recursive: true }); const timestamp = Date.now(); - const filename = `${timestamp}_codex-${email}.json`; + const filename = `${timestamp}_codex-${safeEmail}_oauth_creds.json`; credsPath = path.join(targetDir, filename); } @@ -674,24 +685,52 @@ export async function batchImportCodexTokensStream(tokens, onProgress = null, sk }; try { - // 验证 token 数据 - if (!tokenData.access_token || !tokenData.id_token) { - throw new Error('Token 缺少必需字段 (access_token 或 id_token)'); + if (!tokenData || typeof tokenData !== 'object') { + throw new Error('Token 数据必须是 JSON 对象'); + } + + if (tokenData.skipped || tokenData.error) { + throw new Error(tokenData.reason || tokenData.error || 'skipped'); + } + + // 验证 token 数据:access_token 是唯一必需字段,id_token/refresh_token 可为空。 + if (!tokenData.access_token) { + throw new Error('Token 缺少必需字段 access_token'); } - // 解析 JWT 提取账户信息 - const claims = auth.parseJWT(tokenData.id_token); - const accountId = claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub; - const email = claims.email; + // 解析 JWT 提取账户信息。外部导入格式可能没有 id_token,因此回退解析 access_token。 + let claims = {}; + for (const candidate of [tokenData.id_token, tokenData.access_token]) { + if (!candidate) continue; + try { + claims = auth.parseJWT(candidate); + break; + } catch { + // access_token-only 导入允许无法解析 JWT,只要外部字段提供了账号信息。 + } + } + + const authClaims = claims['https://api.openai.com/auth'] || {}; + const profileClaims = claims['https://api.openai.com/profile'] || {}; + const accountId = tokenData.account_id || tokenData.chatgpt_account_id || authClaims.chatgpt_account_id || claims.sub; + const email = tokenData.email || tokenData.name || profileClaims.email || claims.email || (accountId ? `codex-${accountId}` : null); + + if (!accountId) { + throw new Error('Token 缺少 account_id/chatgpt_account_id,且无法从 JWT 中解析账号 ID'); + } + + const refreshToken = tokenData.refresh_token || ''; // 检查重复 if (!skipDuplicateCheck) { - const duplicateCheck = await auth.checkDuplicate(accountId, tokenData.refresh_token); + const duplicateCheck = await auth.checkDuplicate(accountId, refreshToken); if (duplicateCheck.isDuplicate) { progressData.current = { index: i + 1, success: false, error: 'duplicate', + email, + accountId, existingPath: duplicateCheck.existingPath }; results.failed++; @@ -707,16 +746,44 @@ export async function batchImportCodexTokensStream(tokens, onProgress = null, sk } } + const expiredValue = tokenData.expired || tokenData.expiresAt || tokenData.expire || tokenData.expires_at; + let expired; + if (expiredValue) { + const parsed = typeof expiredValue === 'number' + ? new Date(expiredValue > 1000000000000 ? expiredValue : expiredValue * 1000) + : new Date(expiredValue); + expired = Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); + } + if (!expired && tokenData.expires_in) { + const seconds = Number(tokenData.expires_in); + if (Number.isFinite(seconds) && seconds > 0) { + expired = new Date(Date.now() + seconds * 1000).toISOString(); + } + } + if (!expired && claims.exp) { + const claimExp = Number(claims.exp); + if (Number.isFinite(claimExp)) { + const parsed = new Date(claimExp * 1000); + if (!Number.isNaN(parsed.getTime())) { + expired = parsed.toISOString(); + } + } + } + if (!expired) { + expired = new Date(Date.now() + 3600 * 1000).toISOString(); + } + // 构建凭据对象 const credentials = { - id_token: tokenData.id_token, + id_token: tokenData.id_token || '', access_token: tokenData.access_token, - refresh_token: tokenData.refresh_token, + refresh_token: refreshToken, account_id: accountId, - last_refresh: new Date().toISOString(), + last_refresh: tokenData.last_refresh || new Date().toISOString(), email: email, type: 'codex', - expired: new Date(Date.now() + (tokenData.expires_in || 3600) * 1000).toISOString() + expired, + access_token_only: !refreshToken }; // 保存凭据 @@ -728,6 +795,9 @@ export async function batchImportCodexTokensStream(tokens, onProgress = null, sk progressData.current = { index: i + 1, success: true, + email, + accountId, + accessTokenOnly: !refreshToken, path: relativePath }; results.success++; @@ -744,6 +814,8 @@ export async function batchImportCodexTokensStream(tokens, onProgress = null, sk progressData.current = { index: i + 1, success: false, + email: tokenData?.email || tokenData?.name, + accountId: tokenData?.account_id || tokenData?.chatgpt_account_id, error: error.message }; results.failed++; @@ -1053,4 +1125,4 @@ export async function handleCodexOAuthCallback(code, state) { error: error.message }; } -} \ No newline at end of file +} diff --git a/src/auth/gemini-oauth.js b/src/auth/gemini-oauth.js index 4842efb36..fb51d7337 100644 --- a/src/auth/gemini-oauth.js +++ b/src/auth/gemini-oauth.js @@ -19,7 +19,7 @@ const OAUTH_PROVIDERS = { port: 8085, credentialsDir: '.gemini', credentialsFile: 'oauth_creds.json', - scope: ['https://www.googleapis.com/auth/cloud-platform'], + scope: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/userinfo.email'], logPrefix: '[Gemini Auth]' }, 'gemini-antigravity': { @@ -28,7 +28,7 @@ const OAUTH_PROVIDERS = { port: 8086, credentialsDir: '.antigravity', credentialsFile: 'oauth_creds.json', - scope: ['https://www.googleapis.com/auth/cloud-platform'], + scope: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/userinfo.email'], logPrefix: '[Antigravity Auth]' } }; diff --git a/src/auth/grok-auth.js b/src/auth/grok-auth.js new file mode 100644 index 000000000..30ce04d52 --- /dev/null +++ b/src/auth/grok-auth.js @@ -0,0 +1,170 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import logger from '../utils/logger.js'; +import { broadcastEvent } from '../services/ui-manager.js'; +import { getProviderPoolManager } from '../services/service-manager.js'; +import { CONFIG } from '../core/config-manager.js'; +import { createProviderConfig } from '../utils/provider-utils.js'; +import { withFileLock, atomicWriteFile } from '../utils/file-lock.js'; + +/** + * 批量导入 Grok SSO Tokens (流式处理) + * @param {Array} tokens - Token 数组 (可以是字符串数组 or 对象数组) + * @param {Function} onProgress - 进度回调函数 + * @param {Boolean} skipDuplicateCheck - 是否跳过重复检查 + * @returns {Promise} 导入结果统计 + */ +export async function batchImportGrokTokensStream(tokens, onProgress = null, skipDuplicateCheck = false) { + const results = { + total: tokens.length, + success: 0, + failed: 0, + details: [] + }; + + const providerType = 'grok-web'; + const poolManager = getProviderPoolManager(); + const allPools = poolManager ? poolManager.providerPools : (CONFIG.providerPools || {}); + if (!allPools[providerType]) allPools[providerType] = []; + + const pool = allPools[providerType]; + + for (let i = 0; i < tokens.length; i++) { + let ssoToken = tokens[i]; + + // 支持多种输入格式:直接是字符串或者是包含 sso 字段的对象 + if (typeof ssoToken === 'object' && ssoToken !== null) { + ssoToken = ssoToken.sso || ssoToken.GROK_COOKIE_TOKEN || ssoToken.token; + } + + const progressData = { + index: i + 1, + total: tokens.length, + current: null + }; + + try { + if (!ssoToken || typeof ssoToken !== 'string') { + throw new Error('无效的 SSO Token 格式'); + } + + // 清理 token 字符串(去除前后空格及可能的引号) + let cleanedToken = ssoToken.trim(); + if (cleanedToken.startsWith('"') && cleanedToken.endsWith('"')) { + cleanedToken = cleanedToken.substring(1, cleanedToken.length - 1).trim(); + } + if (cleanedToken.startsWith("'") && cleanedToken.endsWith("'")) { + cleanedToken = cleanedToken.substring(1, cleanedToken.length - 1).trim(); + } + + if (!cleanedToken) { + throw new Error('SSO Token 不能为空'); + } + + // 使用 token 的哈希作为 ID 防止重复 + const tokenId = crypto.createHash('md5').update(cleanedToken).digest('hex').substring(0, 12); + + // 检查重复 + if (!skipDuplicateCheck) { + const existingProvider = pool.find(p => { + // 1. 精确匹配 Token 字段 + if (p.GROK_COOKIE_TOKEN === cleanedToken) return true; + + // 2. 模糊匹配:检查对象的所有字符串属性值是否包含该 Token (处理可能的不同字段名) + return Object.values(p).some(val => + typeof val === 'string' && val.trim() === cleanedToken + ); + }); + + if (existingProvider) { + progressData.current = { + index: i + 1, + success: false, + error: 'duplicate', + existingPath: existingProvider.customName || existingProvider.uuid + }; + results.failed++; + results.details.push(progressData.current); + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: results.failed + }); + } + continue; + } + } + + // 创建新的提供商配置 + const newProvider = createProviderConfig({ + credPathKey: 'GROK_COOKIE_TOKEN', + credPath: cleanedToken, // 直接存储 Token 字符串 + defaultCheckModel: 'grok-4.1-mini', + needsProjectId: false, + urlKeys: ['GROK_BASE_URL', 'GROK_CF_CLEARANCE', 'GROK_USER_AGENT'] + }); + + // 补充 Grok 默认配置 + newProvider.GROK_BASE_URL = 'https://grok.com'; + newProvider.customName = `Imported Token ${tokenId}`; + + // 添加到 Pool + pool.push(newProvider); + + progressData.current = { + index: i + 1, + success: true, + path: `Token ${tokenId}` + }; + results.success++; + + } catch (error) { + logger.error(`[Grok Batch Import] Token ${i + 1} import failed:`, error.message); + progressData.current = { + index: i + 1, + success: false, + error: error.message + }; + results.failed++; + } + + results.details.push(progressData.current); + + // 发送进度更新 + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: results.failed + }); + } + } + + // 如果有成功的,更新 ProviderPoolManager 并广播事件 + if (results.success > 0) { + try { + // 确保 CONFIG.providerPools 与 allPools 同步 + CONFIG.providerPools = allPools; + + // 更新 ProviderPoolManager + if (poolManager) { + poolManager.providerPools = allPools; + poolManager.initializeProviderStatus(); + } + + // 广播更新事件 + broadcastEvent('config_update', { + action: 'batch_add', + provider: 'grok', + count: results.success, + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error(`[Grok Batch Import] Failed to update provider pools: ${error.message}`); + } + } + + return results; +} diff --git a/src/auth/grok-cli-oauth.js b/src/auth/grok-cli-oauth.js new file mode 100644 index 000000000..8f2088649 --- /dev/null +++ b/src/auth/grok-cli-oauth.js @@ -0,0 +1,885 @@ +import http from 'http'; +import logger from '../utils/logger.js'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import axios from 'axios'; +import { broadcastEvent } from '../services/ui-manager.js'; +import { autoLinkProviderConfigs } from '../services/service-manager.js'; +import { CONFIG } from '../core/config-manager.js'; +import { getProxyConfigForProvider } from '../utils/proxy-utils.js'; + +const GROK_CLI_PROVIDER = 'grok-cli-oauth'; + +/** + * Grok CLI OAuth 配置。 + * clientId/scope/redirectUri 与 xAI Grok CLI OAuth public client 保持一致。 + */ +const GROK_CLI_OAUTH_CONFIG = { + issuer: 'https://auth.x.ai', + discoveryUrl: 'https://auth.x.ai/.well-known/openid-configuration', + clientId: 'b1a00492-073a-47ea-816f-4c329264a828', + scope: 'openid profile email offline_access grok-cli:access api:access', + redirectUri: 'http://127.0.0.1:56121/callback', + port: 56121, + defaultApiBaseUrl: 'https://api.x.ai/v1', + logPrefix: '[Grok CLI Auth]' +}; + +const activeServers = new Map(); + +function sanitizeGrokCliCredentialFilenamePart(value) { + const sanitized = String(value || 'default') + .trim() + .replace(/[^a-zA-Z0-9@._+-]/g, '_') + .replace(/_+/g, '_') + .slice(0, 120); + + return sanitized || 'default'; +} + +function generateResponsePage(isSuccess, message, provider = GROK_CLI_PROVIDER) { + const title = isSuccess ? '授权成功' : '授权失败'; + const countdownHtml = isSuccess ? ` +

此窗口将在 10 秒后自动关闭。

+ ` : ''; + + return ` + + + + + ${title} + + + +
+

${title}

+

${message}

+ ${countdownHtml} +
+ +`; +} + +async function closeActiveServer(provider, port = null) { + const existing = activeServers.get(provider); + + if (existing) { + try { + const closePromise = new Promise((resolve, reject) => { + existing.server.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000); + }); + + await Promise.race([closePromise, timeoutPromise]); + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} ${provider} server closed successfully`); + } catch (error) { + logger.warn(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Server close failed or timed out: ${error.message}`); + } finally { + activeServers.delete(provider); + } + } + + if (port) { + for (const [p, info] of activeServers.entries()) { + if (info.port === port) { + await closeActiveServer(p); + } + } + } +} + +function validateOAuthEndpoint(rawUrl, field) { + const value = String(rawUrl || '').trim(); + if (!value) { + throw new Error(`xAI discovery ${field} is empty`); + } + + const parsed = new URL(value); + const host = parsed.hostname.toLowerCase(); + if (parsed.protocol !== 'https:' || (host !== 'x.ai' && !host.endsWith('.x.ai'))) { + throw new Error(`xAI discovery ${field} host is not on x.ai: ${value}`); + } + + return value; +} + +class GrokCliAuth { + constructor(config) { + this.config = config; + const axiosConfig = { timeout: 30000 }; + const proxyConfig = getProxyConfigForProvider(config, GROK_CLI_PROVIDER); + if (proxyConfig) { + axiosConfig.httpAgent = proxyConfig.httpAgent; + axiosConfig.httpsAgent = proxyConfig.httpsAgent; + axiosConfig.proxy = false; + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Proxy enabled for OAuth requests`); + } + + this.httpClient = axios.create(axiosConfig); + this.server = null; + this.discovery = null; + } + + generatePKCECodes() { + const verifier = crypto.randomBytes(96).toString('base64url'); + const challenge = crypto.createHash('sha256') + .update(verifier) + .digest('base64url'); + + return { verifier, challenge }; + } + + async discoverEndpoints() { + if (this.discovery) return this.discovery; + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Discovering xAI OAuth endpoints...`); + const response = await this.httpClient.get(GROK_CLI_OAUTH_CONFIG.discoveryUrl, { + headers: { Accept: 'application/json' } + }); + + const authorizationEndpoint = validateOAuthEndpoint(response.data?.authorization_endpoint, 'authorization_endpoint'); + const tokenEndpoint = validateOAuthEndpoint(response.data?.token_endpoint, 'token_endpoint'); + + this.discovery = { + authorizationEndpoint, + tokenEndpoint + }; + + return this.discovery; + } + + async generateAuthUrl() { + const pkce = this.generatePKCECodes(); + const state = crypto.randomBytes(16).toString('hex'); + const nonce = crypto.randomBytes(16).toString('hex'); + const discovery = await this.discoverEndpoints(); + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Generating auth URL...`); + + const server = await this.startCallbackServer(); + this.server = server; + + const authUrl = new URL(discovery.authorizationEndpoint); + authUrl.searchParams.set('response_type', 'code'); + authUrl.searchParams.set('client_id', GROK_CLI_OAUTH_CONFIG.clientId); + authUrl.searchParams.set('redirect_uri', GROK_CLI_OAUTH_CONFIG.redirectUri); + authUrl.searchParams.set('scope', GROK_CLI_OAUTH_CONFIG.scope); + authUrl.searchParams.set('code_challenge', pkce.challenge); + authUrl.searchParams.set('code_challenge_method', 'S256'); + authUrl.searchParams.set('state', state); + authUrl.searchParams.set('nonce', nonce); + authUrl.searchParams.set('plan', 'generic'); + authUrl.searchParams.set('referrer', 'grok-cli'); + + return { + authUrl: authUrl.toString(), + state, + nonce, + pkce, + tokenEndpoint: discovery.tokenEndpoint, + server + }; + } + + async completeOAuthFlow(code, state, expectedState, pkce, tokenEndpoint) { + if (state !== expectedState) { + throw new Error('State mismatch - possible CSRF attack'); + } + + const tokens = await this.exchangeCodeForTokens(code, pkce.verifier, tokenEndpoint); + const claims = this.parseJWT(tokens.id_token); + const credentials = this.buildCredentials(tokens, { + email: claims.email, + sub: claims.sub, + tokenEndpoint + }); + + const saveResult = await this.saveCredentials(credentials); + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Authentication successful`); + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Email: ${credentials.email || 'unknown'}`); + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Subject: ${credentials.sub || 'unknown'}`); + + if (this.server) { + this.server.close(); + this.server = null; + } + + return { + ...credentials, + credPath: saveResult.credsPath, + relativePath: saveResult.relativePath + }; + } + + async startCallbackServer() { + await closeActiveServer(GROK_CLI_PROVIDER, GROK_CLI_OAUTH_CONFIG.port); + + return new Promise((resolve, reject) => { + const server = http.createServer(); + + server.on('request', (req, res) => { + if (!req.url.startsWith('/callback')) { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url, GROK_CLI_OAUTH_CONFIG.redirectUri); + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const error = url.searchParams.get('error'); + const errorDescription = url.searchParams.get('error_description'); + + if (error) { + const message = errorDescription || error; + res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, message)); + server.emit('auth-error', new Error(message)); + return; + } + + if (code && state) { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(true, 'Grok CLI 授权成功,凭据将自动保存。')); + server.emit('auth-success', { code, state }); + return; + } + + res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, '回调缺少授权码或 state。')); + }); + + server.listen(GROK_CLI_OAUTH_CONFIG.port, GROK_CLI_OAUTH_CONFIG.redirectUri.includes('127.0.0.1') ? '127.0.0.1' : undefined, () => { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Callback server listening on ${GROK_CLI_OAUTH_CONFIG.redirectUri}`); + activeServers.set(GROK_CLI_PROVIDER, { server, port: GROK_CLI_OAUTH_CONFIG.port }); + resolve(server); + }); + + server.on('error', (error) => { + if (error.code === 'EADDRINUSE') { + reject(new Error(`Port ${GROK_CLI_OAUTH_CONFIG.port} is already in use. Please close other applications using this port.`)); + } else { + reject(error); + } + }); + }); + } + + async exchangeCodeForTokens(code, codeVerifier, tokenEndpoint) { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Exchanging authorization code for tokens...`); + const endpoint = tokenEndpoint || (await this.discoverEndpoints()).tokenEndpoint; + + try { + const response = await this.httpClient.post( + endpoint, + new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: GROK_CLI_OAUTH_CONFIG.redirectUri, + client_id: GROK_CLI_OAUTH_CONFIG.clientId, + code_verifier: codeVerifier + }).toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + } + } + ); + + return response.data; + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Token exchange failed:`, error.response?.data || error.message); + throw new Error(`Failed to exchange code for tokens: ${error.response?.data?.error_description || error.message}`); + } + } + + async refreshTokens(refreshToken, tokenEndpoint, fallback = {}) { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Refreshing access token...`); + const endpoint = tokenEndpoint || fallback.token_endpoint || (await this.discoverEndpoints()).tokenEndpoint; + + try { + const response = await this.httpClient.post( + endpoint, + new URLSearchParams({ + grant_type: 'refresh_token', + client_id: GROK_CLI_OAUTH_CONFIG.clientId, + refresh_token: refreshToken + }).toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + } + } + ); + + const tokens = response.data; + const claims = this.parseJWT(tokens.id_token); + return this.buildCredentials(tokens, { + ...fallback, + email: claims.email || fallback.email, + sub: claims.sub || fallback.sub, + tokenEndpoint: endpoint + }); + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Token refresh failed:`, error.response?.data || error.message); + throw new Error(`Failed to refresh Grok CLI tokens: ${error.response?.data?.error_description || error.message}`); + } + } + + parseJWT(token) { + if (!token) return {}; + try { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid JWT token format'); + } + + const payload = Buffer.from(parts[1], 'base64url').toString('utf8'); + return JSON.parse(payload); + } catch (error) { + logger.warn(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Failed to parse JWT: ${error.message}`); + return {}; + } + } + + buildCredentials(tokens, fallback = {}) { + const expiresIn = tokens.expires_in || fallback.expires_in || 3600; + return { + id_token: tokens.id_token || fallback.id_token || '', + access_token: tokens.access_token, + refresh_token: tokens.refresh_token || fallback.refresh_token || '', + token_type: tokens.token_type || fallback.token_type || 'Bearer', + expires_in: expiresIn, + last_refresh: new Date().toISOString(), + email: fallback.email || '', + sub: fallback.sub || '', + type: 'xai', + auth_kind: 'oauth', + expired: new Date(Date.now() + expiresIn * 1000).toISOString(), + base_url: fallback.base_url || GROK_CLI_OAUTH_CONFIG.defaultApiBaseUrl, + redirect_uri: fallback.redirect_uri || GROK_CLI_OAUTH_CONFIG.redirectUri, + token_endpoint: fallback.tokenEndpoint || fallback.token_endpoint || '' + }; + } + + async saveCredentials(creds) { + const safeEmail = sanitizeGrokCliCredentialFilenamePart(creds.email || creds.sub || 'default'); + let credsPath; + + if (this.config.GROK_CLI_OAUTH_CREDS_FILE_PATH || this.config.XAI_OAUTH_CREDS_FILE_PATH) { + credsPath = this.config.GROK_CLI_OAUTH_CREDS_FILE_PATH || this.config.XAI_OAUTH_CREDS_FILE_PATH; + } else { + const targetDir = path.join(process.cwd(), 'configs', 'grok-cli'); + await fs.promises.mkdir(targetDir, { recursive: true }); + const suffix = crypto.randomBytes(4).toString('hex'); + credsPath = path.join(targetDir, `${Date.now()}_xai-${safeEmail}-${suffix}_oauth_creds.json`); + } + + await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); + await fs.promises.writeFile(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 }); + + const relativePath = path.relative(process.cwd(), credsPath); + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Credentials saved to ${relativePath}`); + + return { credsPath, relativePath }; + } + + async checkDuplicate({ sub, email, refreshToken, accessToken }) { + const targetDir = path.join(process.cwd(), 'configs', 'grok-cli'); + + try { + if (!fs.existsSync(targetDir)) { + return { isDuplicate: false }; + } + + const files = await fs.promises.readdir(targetDir); + for (const file of files) { + if (!file.endsWith('.json')) continue; + + try { + const fullPath = path.join(targetDir, file); + const credentials = JSON.parse(await fs.promises.readFile(fullPath, 'utf8')); + const matched = + (sub && credentials.sub === sub) || + (email && credentials.email === email) || + (refreshToken && credentials.refresh_token === refreshToken) || + (accessToken && credentials.access_token === accessToken); + + if (matched) { + return { + isDuplicate: true, + existingPath: path.relative(process.cwd(), fullPath) + }; + } + } catch { + // 忽略无法解析的历史文件。 + } + } + } catch (error) { + logger.warn(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Error checking duplicates: ${error.message}`); + } + + return { isDuplicate: false }; + } +} + +export async function refreshGrokCliTokensWithRetry(refreshToken, config = {}, fallback = {}, maxRetries = 3) { + const auth = new GrokCliAuth(config); + let lastError; + + for (let i = 0; i < maxRetries; i++) { + try { + return await auth.refreshTokens(refreshToken, fallback.token_endpoint, fallback); + } catch (error) { + lastError = error; + logger.warn(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Retry ${i + 1}/${maxRetries} failed:`, error.message); + + if (i < maxRetries - 1) { + const delay = Math.min(1000 * Math.pow(2, i), 10000); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw lastError; +} + +export async function batchImportGrokCliTokensStream(tokens, onProgress = null, skipDuplicateCheck = false) { + const auth = new GrokCliAuth({}); + const results = { + total: tokens.length, + success: 0, + failed: 0, + details: [] + }; + + for (let i = 0; i < tokens.length; i++) { + const tokenData = tokens[i]; + const progressData = { + index: i + 1, + total: tokens.length, + current: null + }; + + try { + if (!tokenData || typeof tokenData !== 'object' || Array.isArray(tokenData)) { + throw new Error('Token 数据必须是 JSON 对象'); + } + + if (tokenData.skipped || tokenData.error) { + throw new Error(tokenData.reason || tokenData.error || 'skipped'); + } + + if (!tokenData.access_token) { + throw new Error('Token 缺少必需字段 access_token'); + } + + let claims = {}; + for (const candidate of [tokenData.id_token, tokenData.access_token]) { + if (!candidate) continue; + claims = auth.parseJWT(candidate); + if (Object.keys(claims).length > 0) break; + } + + const sub = tokenData.sub || tokenData.subject || tokenData.user_id || claims.sub || ''; + const email = tokenData.email || tokenData.name || claims.email || (sub ? `xai-${sub}` : ''); + const refreshToken = tokenData.refresh_token || ''; + + if (!skipDuplicateCheck) { + const duplicateCheck = await auth.checkDuplicate({ + sub, + email, + refreshToken, + accessToken: tokenData.access_token + }); + + if (duplicateCheck.isDuplicate) { + progressData.current = { + index: i + 1, + success: false, + error: 'duplicate', + email, + sub, + existingPath: duplicateCheck.existingPath + }; + results.failed++; + results.details.push(progressData.current); + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: results.failed + }); + } + continue; + } + } + + let expired = null; + const expiredValue = tokenData.expired || tokenData.expiresAt || tokenData.expires_at || tokenData.expire; + if (expiredValue) { + const parsed = typeof expiredValue === 'number' + ? new Date(expiredValue > 1000000000000 ? expiredValue : expiredValue * 1000) + : new Date(expiredValue); + expired = Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); + } + if (!expired && tokenData.expires_in) { + const seconds = Number(tokenData.expires_in); + if (Number.isFinite(seconds) && seconds > 0) { + expired = new Date(Date.now() + seconds * 1000).toISOString(); + } + } + if (!expired && claims.exp) { + const claimExp = Number(claims.exp); + if (Number.isFinite(claimExp)) { + const parsed = new Date(claimExp * 1000); + if (!Number.isNaN(parsed.getTime())) { + expired = parsed.toISOString(); + } + } + } + if (!expired) { + expired = new Date(Date.now() + 3600 * 1000).toISOString(); + } + + const expiresIn = Math.max(0, Math.floor((new Date(expired).getTime() - Date.now()) / 1000)); + const credentials = { + id_token: tokenData.id_token || '', + access_token: tokenData.access_token, + refresh_token: refreshToken, + token_type: tokenData.token_type || 'Bearer', + expires_in: Number.isFinite(expiresIn) ? expiresIn : 3600, + last_refresh: tokenData.last_refresh || new Date().toISOString(), + email, + sub, + type: 'xai', + auth_kind: 'oauth', + expired, + base_url: tokenData.base_url || tokenData.xai_base_url || GROK_CLI_OAUTH_CONFIG.defaultApiBaseUrl, + redirect_uri: tokenData.redirect_uri || GROK_CLI_OAUTH_CONFIG.redirectUri, + token_endpoint: tokenData.token_endpoint || '', + access_token_only: !refreshToken + }; + + const saveResult = await auth.saveCredentials(credentials); + const relativePath = saveResult.relativePath; + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Token ${i + 1} imported: ${relativePath}`); + + progressData.current = { + index: i + 1, + success: true, + email, + sub, + accessTokenOnly: !refreshToken, + path: relativePath + }; + results.success++; + results.details.push(progressData.current); + + await autoLinkProviderConfigs(CONFIG, { + onlyCurrentCred: true, + credPath: relativePath + }); + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Token ${i + 1} import failed: ${error.message}`); + + progressData.current = { + index: i + 1, + success: false, + email: tokenData?.email || tokenData?.name, + sub: tokenData?.sub || tokenData?.subject, + error: error.message + }; + results.failed++; + results.details.push(progressData.current); + } + + if (onProgress) { + onProgress({ + ...progressData, + successCount: results.success, + failedCount: results.failed + }); + } + } + + if (results.success > 0) { + broadcastEvent('oauth_batch_success', { + provider: GROK_CLI_PROVIDER, + count: results.success, + timestamp: new Date().toISOString() + }); + } + + return results; +} + +export async function handleGrokCliOAuth(currentConfig, options = {}) { + const auth = new GrokCliAuth(currentConfig); + + try { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Generating OAuth URL...`); + + if (global.grokCliOAuthSessions && global.grokCliOAuthSessions.size > 0) { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Cleaning up old OAuth sessions...`); + for (const [sessionId, session] of global.grokCliOAuthSessions.entries()) { + try { + if (session.pollTimer) { + clearInterval(session.pollTimer); + } + global.grokCliOAuthSessions.delete(sessionId); + } catch (error) { + logger.warn(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Failed to clean up session ${sessionId}: ${error.message}`); + } + } + } + + const { authUrl, state, pkce, tokenEndpoint, server } = await auth.generateAuthUrl(); + + if (!global.grokCliOAuthSessions) { + global.grokCliOAuthSessions = new Map(); + } + + const sessionId = state; + let pollCount = 0; + const maxPollCount = 100; + const pollInterval = 3000; + let isCompleted = false; + + const session = { + auth, + state, + pkce, + tokenEndpoint, + server, + pollTimer: null, + createdAt: Date.now() + }; + + global.grokCliOAuthSessions.set(sessionId, session); + + const pollTimer = setInterval(() => { + pollCount++; + if (pollCount <= maxPollCount && !isCompleted) { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Waiting for callback... (${pollCount}/${maxPollCount})`); + } + + if (pollCount >= maxPollCount && !isCompleted) { + clearInterval(pollTimer); + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Polling timeout, releasing session for next authorization`); + global.grokCliOAuthSessions.delete(sessionId); + } + }, pollInterval); + + session.pollTimer = pollTimer; + + server.once('auth-success', async (result) => { + isCompleted = true; + clearInterval(pollTimer); + + try { + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Received auth callback, completing OAuth flow...`); + + const session = global.grokCliOAuthSessions.get(sessionId); + if (!session) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Session not found`); + return; + } + + const credentials = await auth.completeOAuthFlow( + result.code, + result.state, + session.state, + session.pkce, + session.tokenEndpoint + ); + + global.grokCliOAuthSessions.delete(sessionId); + + broadcastEvent('oauth_success', { + provider: GROK_CLI_PROVIDER, + credPath: credentials.credPath, + relativePath: credentials.relativePath, + timestamp: new Date().toISOString(), + email: credentials.email, + sub: credentials.sub + }); + + await autoLinkProviderConfigs(CONFIG, { + onlyCurrentCred: true, + credPath: credentials.relativePath + }); + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} OAuth flow completed successfully`); + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Failed to complete OAuth flow: ${error.message}`); + + broadcastEvent('oauth_error', { + provider: GROK_CLI_PROVIDER, + error: error.message, + timestamp: new Date().toISOString() + }); + } + }); + + server.once('auth-error', (error) => { + isCompleted = true; + clearInterval(pollTimer); + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Auth error: ${error.message}`); + global.grokCliOAuthSessions.delete(sessionId); + + broadcastEvent('oauth_error', { + provider: GROK_CLI_PROVIDER, + error: error.message, + timestamp: new Date().toISOString() + }); + }); + + return { + success: true, + authUrl, + authInfo: { + provider: GROK_CLI_PROVIDER, + method: 'oauth2-pkce', + sessionId, + redirectUri: GROK_CLI_OAUTH_CONFIG.redirectUri, + port: GROK_CLI_OAUTH_CONFIG.port, + instructions: [ + '1. 点击下方按钮在浏览器中打开授权链接', + '2. 使用您的 xAI/Grok 账户登录', + '3. 授权 Grok CLI 访问 xAI API', + '4. 授权成功后会自动保存凭据', + '5. 如果浏览器未自动跳转,请手动复制回调 URL' + ] + } + }; + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Failed to generate OAuth URL: ${error.message}`); + + return { + success: false, + error: error.message, + authInfo: { + provider: GROK_CLI_PROVIDER, + method: 'oauth2-pkce', + instructions: [ + `1. 确保端口 ${GROK_CLI_OAUTH_CONFIG.port} 未被占用`, + '2. 确保可以访问 auth.x.ai', + '3. 确保浏览器可以正常打开', + '4. 如果问题持续,请检查网络连接' + ] + } + }; + } +} + +export async function handleGrokCliOAuthCallback(code, state) { + try { + if (!global.grokCliOAuthSessions || !global.grokCliOAuthSessions.has(state)) { + throw new Error('Invalid or expired OAuth session'); + } + + const session = global.grokCliOAuthSessions.get(state); + const { auth, state: expectedState, pkce, tokenEndpoint } = session; + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} Processing OAuth callback...`); + + const result = await auth.completeOAuthFlow(code, state, expectedState, pkce, tokenEndpoint); + global.grokCliOAuthSessions.delete(state); + + broadcastEvent('oauth_success', { + provider: GROK_CLI_PROVIDER, + credPath: result.credPath, + relativePath: result.relativePath, + timestamp: new Date().toISOString(), + email: result.email, + sub: result.sub + }); + + await autoLinkProviderConfigs(CONFIG, { + onlyCurrentCred: true, + credPath: result.relativePath + }); + + logger.info(`${GROK_CLI_OAUTH_CONFIG.logPrefix} OAuth callback processed successfully`); + + return { + success: true, + message: 'Grok CLI authentication successful', + credentials: result, + email: result.email, + sub: result.sub, + credPath: result.credPath, + relativePath: result.relativePath + }; + } catch (error) { + logger.error(`${GROK_CLI_OAUTH_CONFIG.logPrefix} OAuth callback failed: ${error.message}`); + + broadcastEvent('oauth_error', { + provider: GROK_CLI_PROVIDER, + error: error.message, + timestamp: new Date().toISOString() + }); + + return { + success: false, + error: error.message + }; + } +} diff --git a/src/auth/index.js b/src/auth/index.js index dc5cfb022..e17947d75 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -33,3 +33,17 @@ export { handleIFlowOAuth, refreshIFlowTokens } from './iflow-oauth.js'; + +// Grok Auth +export { + batchImportGrokTokensStream +} from './grok-auth.js'; + +// Grok CLI OAuth +export { + refreshGrokCliTokensWithRetry, + handleGrokCliOAuth, + handleGrokCliOAuthCallback, + batchImportGrokCliTokensStream +} from './grok-cli-oauth.js'; + diff --git a/src/auth/kiro-oauth.js b/src/auth/kiro-oauth.js index 718f9b4f9..13a5bfa60 100644 --- a/src/auth/kiro-oauth.js +++ b/src/auth/kiro-oauth.js @@ -130,7 +130,9 @@ function generateResponsePage(isSuccess, message) { const title = isSuccess ? '授权成功!' : '授权失败'; const countdownHtml = isSuccess ? `

此窗口将在 10 秒后自动关闭。

+

提示:您可以按 F12 查看详细日志。

diff --git a/static/potluck-user.html b/static/potluck-user.html index 688852872..5014ccff7 100644 --- a/static/potluck-user.html +++ b/static/potluck-user.html @@ -333,6 +333,27 @@ width: 100%; justify-content: center; } + + .calendar-panel { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: var(--radius-xl); padding: 20px; margin-bottom: 2rem; box-shadow: var(--shadow-sm); } + .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } + .calendar-legend { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-tertiary); } + .calendar-legend .level { width: 10px; height: 10px; border-radius: 2px; } + .calendar-wrapper { overflow-x: auto; padding: 25px 0 10px; scrollbar-width: thin; margin-top: -15px; display: flex; justify-content: center; } + .calendar-grid { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 11px); gap: 3px; min-width: max-content; padding: 2px; } + .calendar-day { width: 11px; height: 11px; border-radius: 2px; background: var(--bg-secondary); position: relative; cursor: pointer; transition: transform 0.1s; } + .calendar-day:hover { transform: scale(1.2); z-index: 10; outline: 1px solid var(--primary-color); } + .calendar-footer { margin-top: 10px; font-size: 12px; color: var(--text-tertiary); text-align: right; } + .level-0, .calendar-day[data-level="0"] { background: var(--bg-secondary); } + .level-1, .calendar-day[data-level="1"] { background: #9be9a8; } + .level-2, .calendar-day[data-level="2"] { background: #40c463; } + .level-3, .calendar-day[data-level="3"] { background: #30a14e; } + .level-4, .calendar-day[data-level="4"] { background: #216e39; } + [data-theme="dark"] .level-1, [data-theme="dark"] .calendar-day[data-level="1"] { background: #0e4429; } + [data-theme="dark"] .level-2, [data-theme="dark"] .calendar-day[data-level="2"] { background: #006d32; } + [data-theme="dark"] .level-3, [data-theme="dark"] .calendar-day[data-level="3"] { background: #26a641; } + [data-theme="dark"] .level-4, [data-theme="dark"] .calendar-day[data-level="4"] { background: #39d353; } + .calendar-tooltip { position: fixed; padding: 6px 10px; background: rgba(0,0,0,0.9); color: #fff; font-size: 11px; border-radius: 4px; pointer-events: none; z-index: 1000; display: none; box-shadow: 0 3px 10px rgba(0,0,0,0.3); white-space: pre-line; line-height: 1.4; } + @@ -399,6 +420,7 @@

个人使用统计0 / 0 +
QPS: 0.00 (峰值: 0.00) | RPM: 0.00 (峰值: 0.00)
剩余额度
@@ -413,6 +435,7 @@

个人使用统计
今日 Tokens
0
+
TPS: 0.00 (峰值: 0.00)

今日缓存 Tokens
@@ -430,8 +453,23 @@

个人使用统计