diff --git a/AGENTS.md b/AGENTS.md index 34c2738..8ba2d0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,30 @@ pnpm run dev - **`ASSETS` binding name must be `ASSETS`** (matched in `src/index.ts` as `env.ASSETS`). - **Base64 MIME parts** are decoded with `atob()` in `extractMailBodies()`. Cloudflare Workers runtime supports `atob`/`btoa`. +## AI Provider Configuration + +The AI layer uses a single unified function `callProvider(config: ProviderConfig, prompt)` in `src/index.ts`. All provider-specific logic is contained there — do not add new per-provider methods. + +**Env vars (set in `wrangler.toml` `[vars]` or as Cloudflare Secrets):** + +| Variable | Required | Description | +|---|---|---| +| `AI_BASE_URL` | ✅ | Provider base URL, no trailing slash | +| `AI_API_KEY` | ✅ | API key (use Secret in production) | +| `AI_API_FORMAT` | ✅ | `openai` \| `responses` \| `anthropic` | +| `AI_MODEL` | ✅ | Model ID | +| `AI_FALLBACK_BASE_URL` | optional | Fallback provider base URL | +| `AI_FALLBACK_API_KEY` | optional | Fallback API key | +| `AI_FALLBACK_API_FORMAT` | optional | Fallback format | +| `AI_FALLBACK_MODEL` | optional | Fallback model ID | + +Fallback is only active when all four `AI_FALLBACK_*` vars are set. Primary retries 3× before fallback is attempted (1×). + +**Format → endpoint mapping:** +- `openai` → `POST /v1/chat/completions` (OpenAI, Gemini OpenAI-compat, DeepSeek, Groq, …) +- `responses` → `POST /v1/responses` (OpenAI Responses API) +- `anthropic` → `POST /v1/messages` (Anthropic Claude direct; sends `anthropic-version: 2023-06-01`, `max_tokens: 1024`) + ## Coding Style - Tabs, LF, UTF-8, no trailing whitespace (see `.editorconfig`). diff --git a/CLAUDE.md b/CLAUDE.md index df794d2..902a8b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,12 @@ pnpm run test 3. `src/index.html` is a live fallback. Do not delete. 4. DOMPurify + sandboxed iframe on all email HTML rendering. Do not relax. 5. Basic Auth gate is first in `WorkerEntrypoint.fetch()`. Do not move or bypass. +6. All AI calls go through `callProvider()`. Do not add per-provider methods alongside it. + +## AI Provider Env Vars + +Required: `AI_BASE_URL`, `AI_API_KEY`, `AI_API_FORMAT` (`openai`|`responses`|`anthropic`), `AI_MODEL`. +Optional fallback: `AI_FALLBACK_BASE_URL`, `AI_FALLBACK_API_KEY`, `AI_FALLBACK_API_FORMAT`, `AI_FALLBACK_MODEL` (all four or none). ## Frontend Component Rules diff --git a/README.md b/README.md index 5b5810a..f20732e 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ [English](https://github.com/TooonyChen/AuthInbox/blob/main/README.md) | [简体中文](https://github.com/TooonyChen/AuthInbox/blob/main/README_CN.md) -**Auth Inbox** is an open-source project that securely receive and views authentication emails using [Cloudflare](https://cloudflare.com/)'s free serverless services. It automatically processes incoming emails, extracts verification codes or links, and stores them in a database. A user-friendly web interface is provided for administrators to easily review the extracted information. AuthInbox also supports real-time notifications via Bark, making it a comprehensive and hassle-free solution for email authentication management. - -Don't wanna receive ads and spams on your main email? Want a bunch of alternative email for register services and websites? Try this **secure**, **serverless**, **light** service! +**Auth Inbox** is an open-source, self-hosted email verification code platform built on [Cloudflare](https://cloudflare.com/)'s free serverless services. It automatically processes incoming emails, filters out promotional mail before hitting the AI, extracts verification codes or links, and stores them in a database. A modern React dashboard lets administrators review extracted codes, inspect raw emails, and render HTML email previews — all protected by Basic Auth. +Don't want ads and spam in your main inbox? Need a bunch of alternative addresses for signups? Try this **secure**, **serverless**, **lightweight** service! [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) @@ -15,205 +14,177 @@ Don't wanna receive ads and spams on your main email? Want a bunch of alternativ ## Table of Contents 📑 -- [Features](#features) -- [Technologies Used](#technologies-used) -- [Installation](#installation) -- [License](#license) -- [Screenshots](#Screenshots) +- [Features](#features-) +- [Technologies Used](#technologies-used-) +- [Installation](#installation-) +- [License](#license-) +- [Screenshots](#screenshots-) --- ## Features ✨ -- **Email Processing**: Automatically captures and stores incoming emails. -- **Code Extraction**: Utilizes AI to extract verification codes, links, and organization names from emails. -- **Secure Front-End**: Provides a web interface protected by Basic Access Authentication for viewing extracted codes. -- **Real-Time Notifications**: Optionally sends notifications via Bark when new codes are extracted. -- **Database Integration**: Stores raw and processed email data in a Cloudflare D1 Database. +- **Promotional Filter**: Detects and skips bulk/marketing emails via headers (`List-Unsubscribe`, `Precedence: bulk`, etc.) before calling the AI — saves tokens. +- **AI Code Extraction**: Uses Google Gemini (with OpenAI as fallback) to extract verification codes, links, and organization names. +- **Modern Dashboard**: React 18 + shadcn/ui interface with mail list, detail panel, and three tabs — Extracted, Raw Email, Rendered HTML preview. +- **Safe HTML Preview**: Email HTML is sanitized with DOMPurify and rendered in a sandboxed iframe. +- **One-click Copy**: Verification codes and links have copy buttons with toast confirmation. +- **Real-Time Notifications**: Optionally sends Bark push notifications when new codes arrive. +- **Database**: Stores all raw emails and AI-extracted results in Cloudflare D1. --- ## Technologies Used 🛠️ -- **Cloudflare Workers**: Serverless platform for handling email processing and web requests. -- **Cloudflare D1**: Cloudflare's serverless SQL database for storing email data. -- **TypeScript**: Strongly typed programming language for robust and maintainable code. -- **Google AI Studio API**: Utilized for extracting relevant information from emails with optimized AI prompts to enhance data accuracy and reliability. -- **AI Prompt Optimization**: Custom-crafted prompts ensure precise extraction of titles, codes, and topics from varied email formats. -- **Bark API**: Optional integration for sending real-time notifications. -- **HTML/CSS**: Front-end interface with responsive and modern design. - ---- - -## AI Prompt Optimization 🧠 - -To ensure accurate extraction of information from incoming emails, we've implemented AI prompt optimization using the Google AI Studio API. By crafting precise and context-aware prompts, the AI can reliably identify and extract key elements such as: - -- **Organization Name (Title)**: Identifies the sender's organization or company. -- **Verification Code/Link**: Extracts codes, links, or passwords necessary for account verification. -- **Email Topic**: Summarizes the main purpose of the email, such as 'account verification' or 'password reset'. - -**Prompt:** -```plaintext -Email content: [Insert raw email content here]. - -Please read the email and extract the following information: -1. Code/Link/Password from the email (if available). -2. Organization name (title) from which the email is sent. -3. A brief summary of the email's topic (e.g., 'account verification'). - -Format the output as JSON with this structure: -{ - "title": "The organization or company that sent the verification code (e.g., 'Netflix')", - "code": "The extracted verification code, link, or password (e.g., '123456' or 'https://example.com/verify?code=123456')", - "topic": "A brief summary of the email's topic (e.g., 'account verification')", - "codeExist": 1 -} - -If both a code and a link are present, include both in the 'code' field like this: -"code": "code, link" - -If there is no code, clickable link, or this is an advertisement email, return: -{ - "codeExist": 0 -} -``` +- **Cloudflare Workers** — Serverless runtime for email handling and API. +- **Cloudflare D1** — SQLite-compatible serverless database. +- **Cloudflare Email Routing** — Routes incoming emails to the Worker. +- **React 18 + Vite + Tailwind CSS + shadcn/ui** — Frontend dashboard. +- **Any OpenAI-compatible / Anthropic AI provider** — Configurable via env vars; Gemini, OpenAI, DeepSeek, Groq, Anthropic, and more. +- **Bark API** — Optional iOS push notifications. +- **TypeScript** — End-to-end type safety. --- ## Installation ⚙️ -0. **Prerequisites** - - Create a [Google AI Studio API](https://aistudio.google.com/) +### Prerequisites - - Bind a domain to your [Cloudflare](https://dash.cloudflare.com/) account +- A [Google AI Studio API key](https://aistudio.google.com/) +- A domain bound to your [Cloudflare](https://dash.cloudflare.com/) account +- Cloudflare Account ID and API Token from [here](https://dash.cloudflare.com/profile/api-tokens) +- *(Optional)* [Bark App](https://bark.day.app/) for iOS push notifications - - Get Your Cloudflare Account ID from [here](https://dash.cloudflare.com/profile) +--- - - Generate a Cloudflare Workers API Token from [here](https://dash.cloudflare.com/profile/api-tokens) +### Option 1 — Deploy via GitHub Actions - - (Optional) Download the [Bark App](https://bark.day.app/) and get a Bark Token from the App +1. **Create D1 Database** -1. **Install using Github Pages** + Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → `Workers & Pages` → `D1 SQL Database` → `Create`. Name it `inbox-d1`. - 1. **Creating D1 Database** + Open the database → `Console`, paste and run the contents of [`db/schema.sql`](https://github.com/TooonyChen/AuthInbox/blob/main/db/schema.sql). - 1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) -> `Workers & Pages` -> `D1 SQL Database` -> `Create` + Copy the `database_id` for the next step. - 2. Input Name `inbox-d1` and click `Create` +2. **Fork & Deploy** - 3. After Creating `inbox-d1`, click in to it and find `Console` + [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) - 4. Execute the following SQL command from [db/schema.sql](https://github.com/TooonyChen/AuthInbox/blob/main/db/schema.sql) in the console, just copy, paste and execute it. + In your forked repository, go to `Settings` → `Secrets and variables` → `Actions` and add: + - `CLOUDFLARE_ACCOUNT_ID` + - `CLOUDFLARE_API_TOKEN` + - `TOML` — use the [comment-free template](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example.clear) to avoid parse errors. - 5. Copy the `database_id` and `database_name` for the next step when you configure the `TOML` file + Then go to `Actions` → `Deploy Auth Inbox to Cloudflare Workers` → `Run workflow`. - 2. **Deploy the Cloudflare Worker** + After success, **delete the workflow logs** to avoid leaking your config. - [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) +3. Jump to [Set Email Forwarding](#3-set-email-forwarding-). - 1. Click the button on the top to fork this repository or directly fork this repository. +--- - 2. Open the repository that you fork, find the `Actions` page, find `Deploy Auth Inbox to Cloudflare Workers`, and click `enable workflow` to activate the workflows. +### Option 2 — Deploy via CLI - 3. Then, in the repository page, navigate to `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets` and add the following secrets: - - `CLOUDFLARE_ACCOUNT_ID`: Cloudflare account ID. - - `CLOUDFLARE_API_TOKEN`: Cloudflare API Token. - - `TOML`: Configuration file, refer to [wrangler.toml with comments](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example). **Please use the [version without comments](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example.clear) in adding the secret in case of unknown errors.** +1. **Clone and install** - 4. Back to `Action` Page in your repository, find `Deploy Auth Inbox to Cloudflare Workers` and press `Run workflow` to deploy the worker. If you can't find the `Run workflow` button, you can trigger GitHub Actions automatically by making a small edit to the `readme.md` file in your repository. + ```bash + git clone https://github.com/TooonyChen/AuthInbox.git + cd AuthInbox + pnpm install + ``` - 5. After the deployment is successful, you can find the URL of your worker in the `Deploy Auth Inbox to Cloudflare Workers` workflow log. +2. **Create D1 database** - 6. Find the `delete all logs` button in the upper-right corner of the workflow logs page. Delete the logs to avoid data leakage. + ```bash + pnpm wrangler d1 create inbox-d1 + pnpm wrangler d1 execute inbox-d1 --remote --file=./db/schema.sql + ``` - 7. Done! ✅ Jump to step 3: Set Email Forwarding. + Copy the `database_id` from the output. +3. **Configure** -2. **Install using command-line** + ```bash + cp wrangler.toml.example wrangler.toml + ``` - 1. **Initialization** + Edit `wrangler.toml` — at minimum fill in: - ```bash - npm install wrangler -g # install wrangler - git clone https://github.com/TooonyChen/AuthInbox.git # clone the repository - cd AuthInbox # change directory - npm install # install dependencies - npm --prefix web install # install dashboard dependencies - ``` + ```toml + [vars] + FrontEndAdminID = "your-username" + FrontEndAdminPassword = "your-password" + UseBark = "false" - 2. **create d1 database** + # AI provider — choose any compatible service + AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" + AI_API_KEY = "your-api-key" + AI_API_FORMAT = "openai" + AI_MODEL = "gemini-2.0-flash" - When you execute the [Wrangler](https://developers.cloudflare.com/workers/wrangler/get-started/) login command for the first time, you will be prompted to log in. Just follow the prompts. + [[d1_databases]] + binding = "DB" + database_name = "inbox-d1" + database_id = "" + ``` - ```bash - npx wrangler d1 create inbox-d1 # creating a d1 database called 'inbox-d1' - npx wrangler d1 execute inbox-d1 --remote --file=./db/schema.sql # write the schema.sql to the database - ``` - you will get the result like this: - ```bash - ✅ Successfully created DB 'inbox-d1' + **`AI_API_FORMAT`** options: - [[d1_databases]] - binding = "DB" # available in your Worker on env.DB - database_name = "inbox-d1" - database_id = "" - ``` - please copy the result from your terminal, you will use them in the next step + | Value | Endpoint | Compatible providers | + |---|---|---| + | `openai` | `/v1/chat/completions` | OpenAI, Gemini (OpenAI-compat), DeepSeek, Groq, Mistral, … | + | `responses` | `/v1/responses` | OpenAI Responses API | + | `anthropic` | `/v1/messages` | Anthropic Claude | - 3. **Configure Environment Variables** + **Common `AI_BASE_URL` values:** + ``` + OpenAI: https://api.openai.com + Gemini (OAI-compat): https://generativelanguage.googleapis.com/v1beta/openai + Anthropic: https://api.anthropic.com + DeepSeek: https://api.deepseek.com + Groq: https://api.groq.com/openai + ``` - Use `wrangler.toml` file in the project root with the necessary environment variables: + **Optional fallback provider** (triggered if primary fails after 3 retries): + ```toml + # AI_FALLBACK_BASE_URL = "https://api.openai.com" + # AI_FALLBACK_API_KEY = "your-fallback-key" + # AI_FALLBACK_API_FORMAT = "openai" + # AI_FALLBACK_MODEL = "gpt-4o-mini" + ``` - ```toml - name = "auth-inbox" - type = "typescript" + Optional Bark vars: `barkTokens`, `barkUrl`. - [vars] - UseBark = "true" # set 'true' to use or 'false' to not use - barkUrl = "https://api.day.app" - barkTokens = "[token1, token2]" # set to your bark tokens on your iOS device, download it from https://bark.day.app/, you can use multiple tokens, if you only use one, then set it to '[token1]' - FrontEndAdminID = "admin" # your login - FrontEndAdminPassword = "password" # your password - GoogleAPIKey = "xxxxxxxxxxx" # your google api, go to https://aistudio.google.com/ to generate one if u dont have +4. **Build and deploy** - [[d1_databases]] # Copy the lines obtained from step 2 from your terminal. - binding = "DB" - database_name = "inbox-d1" # Copy from step 2 - database_id = "" # Copy from step 2 - ``` + ```bash + pnpm run deploy + ``` - 4. **Deploy your own worker** 🌐 - Deploy your Worker to make your project accessible on the Internet. Run: - ```bash - npm run build:web - npx wrangler deploy - ``` - You will get output like this: - ``` - Outputs: https://auth-inbox..workers.dev - ``` - You can now visit the URL for your newly depolyed Auth Inbox for checking the email results. + Output: `https://auth-inbox..workers.dev` -3. **Set Email Forwarding** ✉️ +--- - Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) -> `Websites` -> `` -> `Email` -> `Email-Routing` -> `Routing Rules` +### 3. Set Email Forwarding ✉️ - if you want to use `catch-all address`: - ![image](https://github.com/user-attachments/assets/53e5a939-6b03-4ca6-826a-7a5f02f361ac) +Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → `Websites` → `` → `Email` → `Email Routing` → `Routing Rules`. - if you want to use `custom address`: - ![image](https://github.com/user-attachments/assets/b0d0ab94-c2ad-4870-ac08-d53e64b2c880) +**Catch-all** (forwards all addresses to the Worker): +![image](https://github.com/user-attachments/assets/53e5a939-6b03-4ca6-826a-7a5f02f361ac) -4. **Enjoy!** 🎉 +**Custom address** (forwards a specific address): +![image](https://github.com/user-attachments/assets/b0d0ab94-c2ad-4870-ac08-d53e64b2c880) - All set! ✅ You can now receive and view your authentication emails securely and efficiently using Auth Inbox! +### 4. Done! 🎉 + +Visit your Worker URL, log in with the credentials you set, and start receiving verification emails. --- ## License 📜 -This project is licensed under the [MIT License](LICENSE). +[MIT License](LICENSE) --- @@ -221,27 +192,26 @@ This project is licensed under the [MIT License](LICENSE). ![image](https://github.com/user-attachments/assets/ec14d226-ae82-4689-b44d-22850002c34c) - --- ## Acknowledgements 🙏 -- **Cloudflare Workers** for providing a powerful serverless platform. -- **Google Gemini AI** for enabling intelligent email content extraction. -- **Bark** for real-time notification capabilities. -- **Open Source Community** for inspiring and supporting projects like AuthInbox. -- **ChatGPT** for helping me writing some of the code. +- **Cloudflare Workers** for the serverless platform. +- **Google Gemini AI** for intelligent email extraction. +- **Bark** for real-time notifications. +- **shadcn/ui** for the component library. +- **Open Source Community** for the inspiration. --- ## TODO 📝 -- [x] **Github Pages Deployment**: Automatically deploy to Cloudflare Workers via Github Actions. -- [ ] **Regular Expressions**: Using regular expressions instead of Google Gemini AI for privacy, make Gemini AI to an optional function. -- [ ] **Multi-User Support**: Add functionality to manage multiple users for increased flexibility and broader usage. -- [ ] **Enhance Front-End Design**: Improve the UI/UX of the web interface for a more modern and user-friendly experience. -- [ ] **API**: Add API functionality. -- [ ] **Sending Email**: Add the ability to send emails. -- [ ] **More Notification Methods**: Support additional notification methods such as email, Slack, etc. - ---- +- [x] GitHub Actions deployment +- [x] OpenAI fallback support +- [x] React dashboard with shadcn/ui +- [x] Promotional email filter (skip ads before LLM) +- [x] Raw email viewer + sandboxed HTML preview +- [ ] Regex-based extraction as an AI-free option +- [ ] Multi-user support +- [ ] More notification methods (Slack, webhook, etc.) +- [ ] Sending email support diff --git a/README_CN.md b/README_CN.md index 72ab042..25a19a5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,9 +2,11 @@ [English](https://github.com/TooonyChen/AuthInbox/blob/main/README.md) | [简体中文](https://github.com/TooonyChen/AuthInbox/blob/main/README_CN.md) -**Auth Inbox** 是一个自建的开源多邮箱验证码的接码平台,基于 [Cloudflare](https://cloudflare.com/) 的免费服务。它可以自动处理收到的邮件,提取验证码或链接,并将其存储在数据库中。管理员可以通过一个用户友好的网页界面轻松查看提取的信息。AuthInbox 还支持通过 Bark 进行实时通知,使其成为一个全面且省心的邮件认证管理解决方案。 +**Auth Inbox** 是一个开源的自建验证码接码平台,基于 [Cloudflare](https://cloudflare.com/) 的免费无服务器服务。它会自动处理收到的邮件,在调用 AI 之前过滤掉广告邮件,提取验证码或链接并存入数据库。现代化的 React 仪表盘让管理员可以查看提取的验证码、查看原始邮件内容、渲染 HTML 邮件预览,全程受 Basic Auth 保护。 -不想在主邮箱中收到广告和垃圾邮件?想要多个备用邮箱用于注册服务和网站?试试这个吧! +不想在主邮箱中收到广告和垃圾邮件?需要多个备用邮箱用于注册各类服务?试试这个**安全**、**无服务器**、**轻量**的解决方案吧! + +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) ![框架](https://github.com/user-attachments/assets/f2ea1ef1-b500-4e47-88ba-a00d19f99595) @@ -12,208 +14,177 @@ ## 目录 📑 -- [功能](#features) -- [使用的技术](#technologies-used) -- [安装](#installation) -- [许可证](#license) -- [截图](#Screenshots) +- [功能](#功能-) +- [使用的技术](#使用的技术-) +- [安装](#安装-) +- [许可证](#许可证-) +- [截图](#截图-) --- ## 功能 ✨ -- **邮件处理**:自动捕获和存储收到的邮件。 -- **验证码提取**:利用 AI 从邮件中提取验证码、链接和组织名称。 -- **安全前端**:提供受 Basic Access Authentication 保护的网页界面,用于查看提取的验证码。 -- **实时通知**:当提取到新的验证码时,可选通过 Bark 发送通知。 -- **数据库集成**:将原始和处理过的邮件数据存储在 D1Database 中。 +- **广告过滤**:通过邮件头(`List-Unsubscribe`、`Precedence: bulk` 等)识别并跳过营销邮件,不调用 AI,节省 token。 +- **AI 验证码提取**:使用 Google Gemini(OpenAI 作为备用)提取验证码、链接和组织名称。 +- **现代化仪表盘**:React 18 + shadcn/ui 界面,包含邮件列表、详情面板和三个 Tab —— 提取内容、原始邮件、HTML 渲染预览。 +- **安全 HTML 预览**:邮件 HTML 经 DOMPurify 净化后在沙箱 iframe 中渲染。 +- **一键复制**:验证码和链接均有复制按钮,复制后有 Toast 提示。 +- **实时通知**:可选接入 Bark,收到新验证码时推送到 iOS 设备。 +- **数据库存储**:所有原始邮件和 AI 提取结果均存入 Cloudflare D1。 --- ## 使用的技术 🛠️ -- **Cloudflare Workers**: 无服务器平台,用于处理邮件处理和Web请求。 -- **Cloudflare D1**: Cloudflare的无服务器SQL数据库,用于存储邮件数据。 -- **TypeScript**: 强类型编程语言,确保代码的稳健性和可维护性。 -- **AI 提示词优化**: 定制的提示确保从多种邮件格式中精确提取标题、代码和主题。 -- **Google AI Studio API**: 利用优化的AI提示从邮件中提取相关信息,以提升数据的准确性和可靠性。 -- **Bark API**: 可选集成,用于发送实时通知。 -- **HTML/CSS**: 前端界面,具有响应式和现代化设计。 -- **Google Fonts**: 通过一致的排版增强Web界面的视觉吸引力。 - - ---- - -## AI 提示词优化 🧠 - -为了确保从收到的电子邮件中准确提取信息,我们使用Google AI Studio API实施了AI提示优化。通过设计精确且具有上下文意识的提示,AI可以可靠地识别和提取关键要素,如: - -- **组织名称(标题)**: 识别发件人的组织或公司。 -- **验证码/链接**: 提取账户验证所需的代码、链接或密码。 -- **电子邮件主题**: 总结电子邮件的主要目的,例如“账户验证”或“密码重置”。 - -**提示词如下:** -```plaintext -Email content: [Insert raw email content here]. - -Please read the email and extract the following information: -1. Code/Link/Password from the email (if available). -2. Organization name (title) from which the email is sent. -3. A brief summary of the email's topic (e.g., 'account verification'). - -Format the output as JSON with this structure: -{ - "title": "The organization or company that sent the verification code (e.g., 'Netflix')", - "code": "The extracted verification code, link, or password (e.g., '123456' or 'https://example.com/verify?code=123456')", - "topic": "A brief summary of the email's topic (e.g., 'account verification')", - "codeExist": 1 -} - -If both a code and a link are present, include both in the 'code' field like this: -"code": "code, link" - -If there is no code, clickable link, or this is an advertisement email, return: -{ - "codeExist": 0 -} - -``` +- **Cloudflare Workers** — 处理邮件和 API 的无服务器运行时。 +- **Cloudflare D1** — 兼容 SQLite 的无服务器数据库。 +- **Cloudflare Email Routing** — 将收到的邮件路由到 Worker。 +- **React 18 + Vite + Tailwind CSS + shadcn/ui** — 前端仪表盘。 +- **任意 OpenAI 兼容 / Anthropic AI 提供商** — 通过环境变量配置,支持 Gemini、OpenAI、DeepSeek、Groq、Anthropic 等。 +- **Bark API** — iOS 实时推送通知(可选)。 +- **TypeScript** — 端到端类型安全。 --- ## 安装 ⚙️ -0. **先决条件** - - 创建一个 [Google AI Studio API](https://aistudio.google.com/) +### 先决条件 - - 在你的 [Cloudflare](https://dash.cloudflare.com/) 账户上绑定一个域名 +- 一个 [Google AI Studio API Key](https://aistudio.google.com/) +- 在 [Cloudflare](https://dash.cloudflare.com/) 账户上绑定一个域名 +- Cloudflare 账户 ID 和 API Token,可在 [此处](https://dash.cloudflare.com/profile/api-tokens) 获取 +- *(可选)* [Bark App](https://bark.day.app/),用于 iOS 推送通知 - - 获取你的Cloudflare 账户 ID, 可在 [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) 中找到 +--- - - 获取你的 Cloudflare API Token,可在 [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) 中找到 +### 方式一 — 通过 GitHub Actions 部署 - - (可选)下载[Bark App](https://bark.day.app/),在App中获得一个Bark Token +1. **创建 D1 数据库** -1. **使用 Github Pages 进行安装** + 进入 [Cloudflare 仪表盘](https://dash.cloudflare.com/) → `Workers & Pages` → `D1 SQL Database` → `Create`,名称填 `inbox-d1`。 - 1. **创建 D1 数据库** + 点击进入数据库 → `Console`,粘贴并执行 [`db/schema.sql`](https://github.com/TooonyChen/AuthInbox/blob/main/db/schema.sql) 中的内容。 - 1. 进入 [Cloudflare 仪表盘](https://dash.cloudflare.com/) -> `Workers & Pages` -> `D1 SQL Database` -> `Create` + 复制 `database_id`,下一步会用到。 - 2. 输入名称 `inbox-d1` 并点击 `Create` +2. **Fork 并部署** - 3. 创建 `inbox-d1` 后,点击进入并找到 `Console` + [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) - 4. 在控制台中执行 [db/schema.sql](https://github.com/TooonyChen/AuthInbox/blob/main/db/schema.sql) 中的 SQL 命令,直接复制、粘贴并执行它。 + 在你 Fork 的仓库中,进入 `Settings` → `Secrets and variables` → `Actions`,添加以下 Secrets: + - `CLOUDFLARE_ACCOUNT_ID` + - `CLOUDFLARE_API_TOKEN` + - `TOML` — 使用[不带注释的模板](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example.clear),避免解析报错。 - 5. 复制 `database_id` 和 `database_name`,用于下一步配置 `TOML` 文件时使用 + 然后进入 `Actions` → `Deploy Auth Inbox to Cloudflare Workers` → `Run workflow`。 - 2. **部署 Cloudflare Worker** + 部署成功后,**务必删除 workflow 日志**,防止配置信息泄露。 - [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) +3. 跳转到[设置邮件转发](#3-设置邮件转发-)。 - 1. 点击上方按钮 fork 此仓库,或直接 fork 此仓库。 +--- - 2. 打开你 fork 的仓库,找到 `Actions` 页面,找到 `Deploy Auth Inbox to Cloudflare Workers`,并点击 `enable workflow` 激活 workflow。 +### 方式二 — 通过命令行部署 - 3. 然后,在仓库页面中,导航到 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`,并添加以下 secrets: - - `CLOUDFLARE_ACCOUNT_ID`: Cloudflare 账户 ID。 - - `CLOUDFLARE_API_TOKEN`: Cloudflare API Token。 - - `TOML`: 配置文件,参考 [带有注释的wrangler.toml](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example)。**添加至secrets时,请使用[不带注释的版本](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example.clear)来避免奇怪的报错。** +1. **克隆并安装依赖** - 4. 返回你仓库的 `Actions` 页面,找到 `Deploy Auth Inbox to Cloudflare Workers`,并按 `Run workflow` 来部署 worker。如果没找到 `Run workflow` 按钮,请你随便修改一下repo中的`readme.md`文件,让Github Actions自动运行。 + ```bash + git clone https://github.com/TooonyChen/AuthInbox.git + cd AuthInbox + pnpm install + ``` - 5. 部署成功后,你可以在 `Deploy Auth Inbox to Cloudflare Workers` 的 workflow 日志中找到你的 worker URL。 +2. **创建 D1 数据库** - 6. 在 workflow 日志页面的右上角找到 `delete all logs` 来删除日志,删除它来避免数据外泄。 + ```bash + pnpm wrangler d1 create inbox-d1 + pnpm wrangler d1 execute inbox-d1 --remote --file=./db/schema.sql + ``` - 7. 完成!✅ 请前往第三步:设置邮件转发。 + 复制输出中的 `database_id`。 -2. **使用 Wrangler 命令行部署到 Cloudflare Workers** +3. **配置** - 1. **初始化** + ```bash + cp wrangler.toml.example wrangler.toml + ``` - ```bash - npm install wrangler -g # 安装 wrangler - git clone https://github.com/TooonyChen/AuthInbox.git # 克隆仓库 - cd AuthInbox # 切换目录 - npm install # 安装依赖 - npm --prefix web install # 安装前端仪表盘依赖 - ``` - - 2. **创建 d1 数据库** - - 当你第一次执行 [Wrangler](https://developers.cloudflare.com/workers/wrangler/get-started/) 登录命令时,系统会提示你登录。按提示操作即可。 - - ```bash - npx wrangler d1 create inbox-d1 # 创建名为 'inbox-d1' 的 d1 数据库 - npx wrangler d1 execute inbox-d1 --remote --file=./db/schema.sql # 执行 schema.sql 文件 - ``` - 你将会看到如下结果: - ```bash - ✅ Successfully created DB 'inbox-d1' - - [[d1_databases]] - binding = "DB" # 在你的 Worker 中通过 env.DB 访问 - database_name = "inbox-d1" - database_id = "<你的数据库的唯一ID>" - ``` - 请从终端复制结果,你将在下一步中使用它们。 - - 3. **配置环境变量** - - 使用项目根目录下的 `wrangler.toml` 文件,并添加所需的环境变量: + 编辑 `wrangler.toml`,至少填写以下内容: ```toml - name = "auth-inbox" - type = "typescript" - [vars] - UseBark = 'true' # 设置为 'true' 启用 Bark,设置为 'false' 禁用 - barkUrl = "https://api.day.app" - barkTokens = "[token1, token2]" # 填写你 iOS 设备上的 Bark tokens,可从 https://bark.day.app/ 下载应用获取,可以填写多个。如果你只想用一个,那么填写 '[token1]' - FrontEndAdminID = "admin" # 你的登录 ID - FrontEndAdminPassword = "password" # 你的登录密码 - GoogleAPIKey = "xxxxxxxxxxx" # 你的 Google API key,如果没有可以前往 https://aistudio.google.com/ 生成一个 + FrontEndAdminID = "你的登录账号" + FrontEndAdminPassword = "你的登录密码" + UseBark = "false" - [[d1_databases]] # 从步骤 2 的终端结果中复制这些行。 - binding = "DB" - database_name = "inbox-d1" # 从步骤 2 中复制 - database_id = "<你的数据库的唯一ID>" # 从步骤 2 中复制 + # AI 提供商配置 + AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" + AI_API_KEY = "你的 API Key" + AI_API_FORMAT = "openai" + AI_MODEL = "gemini-2.0-flash" + + [[d1_databases]] + binding = "DB" + database_name = "inbox-d1" + database_id = "<你的数据库ID>" ``` - 4. **部署你的 worker** 🌐 + **`AI_API_FORMAT`** 三选一: - 部署你的 Worker 以使项目在互联网上可访问。运行以下命令: - ```bash - npm run build:web - npx wrangler deploy - ``` - 你将看到如下输出: + | 值 | 实际请求路径 | 适用提供商 | + |---|---|---| + | `openai` | `/v1/chat/completions` | OpenAI、Gemini(OpenAI 兼容)、DeepSeek、Groq、Mistral 等 | + | `responses` | `/v1/responses` | OpenAI Responses API | + | `anthropic` | `/v1/messages` | Anthropic Claude 直连 | + + **常用 `AI_BASE_URL`:** ``` - output: https://auth-inbox.<你的子域名>.workers.dev + OpenAI: https://api.openai.com + Gemini(OpenAI 兼容): https://generativelanguage.googleapis.com/v1beta/openai + Anthropic: https://api.anthropic.com + DeepSeek: https://api.deepseek.com + Groq: https://api.groq.com/openai ``` - 你现在可以访问该 URL 来查看你部署的 Auth Inbox 的邮件面板。 -3. **设置邮件转发** ✉️ + **备用模型(可选)**,主模型失败重试 3 次后触发: + ```toml + # AI_FALLBACK_BASE_URL = "https://api.openai.com" + # AI_FALLBACK_API_KEY = "备用 API Key" + # AI_FALLBACK_API_FORMAT = "openai" + # AI_FALLBACK_MODEL = "gpt-4o-mini" + ``` - 前往 [Cloudflare Dashboard](https://dash.cloudflare.com/) -> `Websites` -> `<你的域名>` -> `Email` -> `Email-Routing` -> `Routing Rules` + 可选 Bark 配置:`barkTokens`、`barkUrl`。 - 如果你想使用“接收所有地址”: - ![image](https://github.com/user-attachments/assets/53e5a939-6b03-4ca6-826a-7a5f02f361ac) +4. **构建并部署** - 如果你想使用“自定义地址”: - ![image](https://github.com/user-attachments/assets/b0d0ab94-c2ad-4870-ac08-d53e64b2c880) + ```bash + pnpm run deploy + ``` -4. **完成!** ✅ + 输出:`https://auth-inbox.<你的子域名>.workers.dev` - 现在你可以访问你的新部署的 Auth Inbox URL,查看电子邮件结果。 +--- + +### 3. 设置邮件转发 ✉️ + +进入 [Cloudflare 仪表盘](https://dash.cloudflare.com/) → `Websites` → `<你的域名>` → `Email` → `Email Routing` → `Routing Rules`。 + +**接收所有地址**(将所有邮件转发给 Worker): +![image](https://github.com/user-attachments/assets/53e5a939-6b03-4ca6-826a-7a5f02f361ac) + +**自定义地址**(转发特定地址): +![image](https://github.com/user-attachments/assets/b0d0ab94-c2ad-4870-ac08-d53e64b2c880) + +### 4. 完成!🎉 + +访问你的 Worker URL,用你设置的账号密码登录,即可开始接收验证邮件。 --- ## 许可证 📜 -本项目基于 [MIT License](LICENSE) 许可证。 +[MIT License](LICENSE) --- @@ -221,24 +192,26 @@ If there is no code, clickable link, or this is an advertisement email, return: ![image](https://github.com/user-attachments/assets/41db550c-8340-4315-ba87-85330abc5dfb) - --- ## 鸣谢 🙏 -- 感谢 **Cloudflare Workers** 提供强大的无服务器平台。 -- 感谢 **Google Gemini AI** 提供智能的邮件内容提取功能。 -- 感谢 **Bark** 提供实时通知能力。 -- 感谢 **开源社区** 为像 Auth Inbox 这样的项目提供灵感和支持。 -- 感谢 **ChatGPT** 协助我写代码 +- **Cloudflare Workers** 提供强大的无服务器平台。 +- **Google Gemini AI** 提供智能邮件内容提取能力。 +- **Bark** 提供实时推送通知。 +- **shadcn/ui** 提供组件库。 +- **开源社区** 提供灵感与支持。 --- + ## TODO 📝 -- [x] **Github Pages 部署**:通过 Github Actions 自动部署到 Cloudflare Workers。 -- [ ] **正则表达式**:使用正则表达式替代 Google Gemini AI 以保护隐私,并将使用 AI 作为一个可选功能。 -- [ ] **多用户支持**:增加管理多用户的功能,以提高灵活性并扩展使用范围。 -- [ ] **增强前端设计**:改进网页界面的UI/UX设计,使其更加现代化和用户友好。 -- [ ] **API**: 添加API接口。 -- [ ] **发送邮件**:添加发送邮件功能。 -- [ ] **更多通知方式**:支持更多通知方式,如邮件、Slack等。 +- [x] GitHub Actions 自动部署 +- [x] OpenAI 备用模型支持 +- [x] React 仪表盘(shadcn/ui) +- [x] 广告邮件过滤(调用 AI 前拦截) +- [x] 原始邮件查看 + 沙箱 HTML 预览 +- [ ] 正则表达式提取(无 AI 选项) +- [ ] 多用户支持 +- [ ] 更多通知方式(Slack、Webhook 等) +- [ ] 发送邮件功能 diff --git a/src/index.ts b/src/index.ts index 44c62f5..0932d6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,34 +10,35 @@ import { RPCEmailMessage } from "./rpcEmail"; import indexHtml from "./index.html"; +type ApiFormat = "openai" | "responses" | "anthropic"; + +interface ProviderConfig { + baseUrl: string; + apiKey: string; + format: ApiFormat; + model: string; +} + interface Env { - // If you set another name in wrangler.toml as the value for 'binding', - // replace "DB" with the variable name you defined. DB: D1Database; ASSETS?: Fetcher; FrontEndAdminID: string; FrontEndAdminPassword: string; + UseBark: string; barkTokens: string; barkUrl: string; - GoogleAPIKey: string; - UseBark: string; - GeminiModel?: string; - OpenAIAPIKey?: string; - OpenAIBaseUrl?: string; - OpenAIModel?: string; + // Primary AI provider + AI_BASE_URL: string; + AI_API_KEY: string; + AI_API_FORMAT: ApiFormat; + AI_MODEL: string; + // Fallback AI provider (all four must be set to enable fallback) + AI_FALLBACK_BASE_URL?: string; + AI_FALLBACK_API_KEY?: string; + AI_FALLBACK_API_FORMAT?: ApiFormat; + AI_FALLBACK_MODEL?: string; } -interface ModelResponse { - ok: boolean; - status: number; - statusText: string; - payload: unknown; -} - -const DEFAULT_GEMINI_MODEL = "gemini-2.0-flash"; -const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com"; -const DEFAULT_OPENAI_MODEL = "gpt-4o-mini"; - // Normalize model output into JSON text // 将模型输出规范化为可解析的 JSON 文本 function extractJsonFromText(rawText: string): Record | null { let candidate = rawText.trim(); @@ -58,49 +59,98 @@ function extractJsonFromText(rawText: string): Record | null { } } -// Read structured content from Gemini response // 读取 Gemini 响应中的结构化文本 -function extractGeminiText(payload: unknown): string | null { - const candidate = (payload as any)?.candidates?.[0]?.content?.parts?.[0]; - if (!candidate || typeof candidate.text !== "string") { - console.error("Gemini response is missing expected data structure"); - return null; - } - return candidate.text; -} - -// Read structured content from OpenAI response // 读取 OpenAI 响应中的结构化文本 -function extractOpenAIText(payload: unknown): string | null { - const message = (payload as any)?.choices?.[0]?.message; - if (!message) { - console.error("OpenAI response is missing choices"); - return null; - } - - const content = message.content; - if (typeof content === "string") { - return content; - } - - if (Array.isArray(content)) { - const textPart = content.find((part: any) => part?.type === "text" && typeof part.text === "string"); - if (textPart) { - return textPart.text; - } - } - - if (typeof message?.content?.[0]?.text === "string") { - return message.content[0].text; - } - - console.error("OpenAI response is missing text content"); - return null; -} - // Remove trailing slashes to avoid double separators // 移除结尾斜杠防止重复拼接 function normaliseBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, ""); } +// Unified AI provider caller supporting openai / responses / anthropic formats +async function callProvider(config: ProviderConfig, prompt: string): Promise { + const base = normaliseBaseUrl(config.baseUrl); + let endpoint: string; + let body: unknown; + const headers: Record = { "Content-Type": "application/json" }; + + if (config.format === "openai") { + endpoint = `${base}/v1/chat/completions`; + headers["Authorization"] = `Bearer ${config.apiKey}`; + body = { + model: config.model, + temperature: 0, + response_format: { type: "json_object" }, + messages: [ + { role: "system", content: "You return only valid JSON that matches the requested schema." }, + { role: "user", content: prompt }, + ], + }; + } else if (config.format === "responses") { + endpoint = `${base}/v1/responses`; + headers["Authorization"] = `Bearer ${config.apiKey}`; + body = { + model: config.model, + input: [{ role: "user", content: prompt }], + text: { format: { type: "json_object" } }, + }; + } else { + // anthropic + endpoint = `${base}/v1/messages`; + headers["x-api-key"] = config.apiKey; + headers["anthropic-version"] = "2023-06-01"; + body = { + model: config.model, + max_tokens: 1024, + messages: [{ role: "user", content: prompt }], + }; + } + + let response: Response; + try { + response = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(body) }); + } catch (err) { + console.error(`[callProvider:${config.format}] fetch error:`, err); + return null; + } + + if (!response.ok) { + console.error(`[callProvider:${config.format}] HTTP ${response.status} ${response.statusText}`); + return null; + } + + let payload: unknown; + try { + payload = await response.json(); + } catch { + console.error(`[callProvider:${config.format}] failed to parse JSON response`); + return null; + } + + // Extract text from response based on format + if (config.format === "openai") { + const content = (payload as any)?.choices?.[0]?.message?.content; + if (typeof content === "string") return content; + if (Array.isArray(content)) { + const part = content.find((p: any) => p?.type === "text" && typeof p.text === "string"); + return part?.text ?? null; + } + console.error("[callProvider:openai] unexpected response shape"); + return null; + } + + if (config.format === "responses") { + const text = (payload as any)?.output?.[0]?.content?.[0]?.text + ?? (payload as any)?.output?.[0]?.text; + if (typeof text === "string") return text; + console.error("[callProvider:responses] unexpected response shape"); + return null; + } + + // anthropic + const text = (payload as any)?.content?.[0]?.text; + if (typeof text === "string") return text; + console.error("[callProvider:anthropic] unexpected response shape"); + return null; +} + function jsonResponse(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, @@ -406,113 +456,27 @@ export default class extends WorkerEntrypoint { } } - // Call Gemini for primary extraction // 调用 Gemini 执行主解析 - private async callGemini(prompt: string, apiKey: string, model: string): Promise { - const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; - const init: RequestInit = { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - contents: [ - { - parts: [{ text: prompt }], - }, - ], - }), - }; - - try { - const response = await fetch(endpoint, init); - let payload: unknown = null; - try { - payload = await response.json(); - } catch (parseError) { - console.error("Gemini JSON parse error:", parseError); - } - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - payload, - }; - } catch (error) { - console.error("Gemini request threw:", error); - return { - ok: false, - status: 0, - statusText: "FETCH_ERROR", - payload: null, - }; - } - } - - // Call OpenAI as the fallback extractor // 调用 OpenAI 作为兜底解析 - private async callOpenAI( - prompt: string, - baseUrl: string, - apiKey: string, - model: string - ): Promise { - const endpoint = `${normaliseBaseUrl(baseUrl)}/v1/chat/completions`; - const init: RequestInit = { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model, - temperature: 0, - response_format: { type: "json_object" }, - messages: [ - { - role: "system", - content: "You return only valid JSON that matches the requested schema.", - }, - { - role: "user", - content: prompt, - }, - ], - }), - }; - - try { - const response = await fetch(endpoint, init); - let payload: unknown = null; - try { - payload = await response.json(); - } catch (parseError) { - console.error("OpenAI JSON parse error:", parseError); - } - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - payload, - }; - } catch (error) { - console.error("OpenAI request threw:", error); - return { - ok: false, - status: 0, - statusText: "FETCH_ERROR", - payload: null, - }; - } - } - // Primary email handler // 主要邮件处理入口 async email(message: ForwardableEmailMessage): Promise { const env: Env = this.env; const useBark = env.UseBark.toLowerCase() === "true"; - const googleApiKey = env.GoogleAPIKey; - const geminiModel = env.GeminiModel || DEFAULT_GEMINI_MODEL; - const openAIKey = env.OpenAIAPIKey; - const openAIBaseUrl = env.OpenAIBaseUrl || DEFAULT_OPENAI_BASE_URL; - const openAIModel = env.OpenAIModel || DEFAULT_OPENAI_MODEL; + + const primary: ProviderConfig = { + baseUrl: env.AI_BASE_URL, + apiKey: env.AI_API_KEY, + format: env.AI_API_FORMAT ?? "openai", + model: env.AI_MODEL, + }; + + const fallback: ProviderConfig | null = + env.AI_FALLBACK_BASE_URL && env.AI_FALLBACK_API_KEY && env.AI_FALLBACK_MODEL + ? { + baseUrl: env.AI_FALLBACK_BASE_URL, + apiKey: env.AI_FALLBACK_API_KEY, + format: env.AI_FALLBACK_API_FORMAT ?? "openai", + model: env.AI_FALLBACK_MODEL, + } + : null; // Pull raw email content // 获取原始邮件内容 const rawEmail = @@ -569,63 +533,41 @@ export default class extends WorkerEntrypoint { try { const maxRetries = 3; - let retryCount = 0; let extractedData: Record | null = null; - let geminiStatusError = false; - // First attempt Gemini with simple retries // 先使用 Gemini 并进行简单重试 - while (retryCount < maxRetries && !extractedData && !geminiStatusError) { - const geminiResult = await this.callGemini(aiPrompt, googleApiKey, geminiModel); - console.log(`Gemini response attempt ${retryCount + 1}:`, geminiResult.payload); - - if (!geminiResult.ok) { - geminiStatusError = true; - console.error( - `Gemini request failed with status ${geminiResult.status} ${geminiResult.statusText}` - ); - break; - } - - const extractedText = extractGeminiText(geminiResult.payload); - if (extractedText) { - console.log(`Extracted Text before parsing: "${extractedText}"`); - const parsed = extractJsonFromText(extractedText); + // Primary provider: up to 3 attempts + for (let attempt = 1; attempt <= maxRetries; attempt++) { + console.log(`[primary:${primary.format}] attempt ${attempt}`); + const text = await callProvider(primary, aiPrompt); + if (text) { + const parsed = extractJsonFromText(text); if (parsed) { extractedData = parsed; - console.log("Parsed Extracted Data:", extractedData); + console.log("[primary] extracted data:", extractedData); break; } } - - retryCount += 1; - if (retryCount < maxRetries) { - console.log("Retrying AI request..."); - } else { - console.error("Max retries reached. Unable to get valid AI response."); - } + if (attempt < maxRetries) console.log("[primary] retrying..."); + else console.error("[primary] max retries reached"); } - if (!extractedData && geminiStatusError && openAIKey) { - // Gemini failed with non-200, switch to OpenAI fallback // 当 Gemini 返回非 200 时切换到 OpenAI 兜底 - const openAIResult = await this.callOpenAI(aiPrompt, openAIBaseUrl, openAIKey, openAIModel); - console.log("OpenAI fallback response:", openAIResult.payload); - if (!openAIResult.ok) { - console.error( - `OpenAI fallback failed with status ${openAIResult.status} ${openAIResult.statusText}` - ); - } else { - const openAIText = extractOpenAIText(openAIResult.payload); - if (openAIText) { - console.log(`OpenAI text before parsing: "${openAIText}"`); - const parsed = extractJsonFromText(openAIText); - if (parsed) { - extractedData = parsed; - console.log("Parsed Extracted Data from OpenAI:", extractedData); - } + // Fallback provider: one attempt if primary produced nothing and fallback is configured + if (!extractedData && fallback) { + console.log(`[fallback:${fallback.format}] attempting`); + const text = await callProvider(fallback, aiPrompt); + if (text) { + const parsed = extractJsonFromText(text); + if (parsed) { + extractedData = parsed; + console.log("[fallback] extracted data:", extractedData); + } else { + console.error("[fallback] failed to parse response"); } + } else { + console.error("[fallback] provider returned nothing"); } - } else if (!extractedData && geminiStatusError) { - console.error("Gemini request failed and OpenAI fallback is not configured."); + } else if (!extractedData) { + console.error("[primary] failed and no fallback configured"); } if (extractedData) { diff --git a/wrangler.toml.example b/wrangler.toml.example index 76cc10d..9f7cf6f 100644 --- a/wrangler.toml.example +++ b/wrangler.toml.example @@ -57,30 +57,36 @@ FrontEndAdminPassword = "password" # 你的登录密码 **请更改为你自己的登录密码** -GoogleAPIKey = "xxxxxxxxxxx" -# GoogleAPIKey: String -# your google AI API, you can have one from https://aistudio.google.com -# 你的谷歌AI API,你可以从 https://aistudio.google.com 获取 +# --- Primary AI provider --- +AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" +# Base URL of your AI provider (no trailing slash). +# Examples: +# OpenAI: https://api.openai.com +# Gemini (OAI-compat): https://generativelanguage.googleapis.com/v1beta/openai +# Anthropic: https://api.anthropic.com +# DeepSeek: https://api.deepseek.com +# Groq: https://api.groq.com/openai -GeminiModel = "gemini-2.0-flash" -# GeminiModel: String -# optional override for the Gemini model, defaults to "gemini-2.0-flash" -# 可选配置,覆盖默认的 Gemini 模型名称,默认值为 "gemini-2.0-flash" +AI_API_KEY = "your-api-key" +# API key for the primary provider. Use a Cloudflare Secret for production. +# 主模型的 API Key,生产环境请使用 Cloudflare Secret 存储 -OpenAIAPIKey = "" -# OpenAIAPIKey: String -# optional key for OpenAI fallback when Gemini fails with a non-200 response -# 可选配置,当 Gemini 返回非 200 时使用 OpenAI 作为兜底 +AI_API_FORMAT = "openai" +# Request/response format. One of: +# openai — POST /v1/chat/completions (OpenAI, Gemini compat, DeepSeek, Groq, etc.) +# responses — POST /v1/responses (OpenAI Responses API) +# anthropic — POST /v1/messages (Anthropic Claude direct) +# 请求格式,三选一:openai | responses | anthropic -OpenAIBaseUrl = "https://api.openai.com" -# OpenAIBaseUrl: String -# optional, change only if you proxy the OpenAI API -# 可选配置,仅在自建代理时更改 +AI_MODEL = "gemini-2.0-flash" +# Model ID to use, e.g. "gpt-4o-mini", "gemini-2.0-flash", "claude-3-5-haiku-20241022" +# 模型 ID -OpenAIModel = "gpt-4o-mini" -# OpenAIModel: String -# optional override for the OpenAI model, defaults to "gpt-4o-mini" -# 可选配置,覆盖默认的 OpenAI 模型 +# --- Fallback AI provider (optional — remove all four lines to disable) --- +# AI_FALLBACK_BASE_URL = "https://api.openai.com" +# AI_FALLBACK_API_KEY = "your-fallback-api-key" +# AI_FALLBACK_API_FORMAT = "openai" +# AI_FALLBACK_MODEL = "gpt-4o-mini" # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases diff --git a/wrangler.toml.example.clear b/wrangler.toml.example.clear index 44d839d..c590d85 100644 --- a/wrangler.toml.example.clear +++ b/wrangler.toml.example.clear @@ -16,11 +16,14 @@ barkUrl = "https://api.day.app" barkTokens = "[token1, token2]" FrontEndAdminID = "admin" FrontEndAdminPassword = "password" -GoogleAPIKey = "xxxxxxxxxxx" -GeminiModel = "gemini-2.0-flash" -OpenAIAPIKey = "" -OpenAIBaseUrl = "https://api.openai.com" -OpenAIModel = "gpt-4o-mini" +AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" +AI_API_KEY = "your-api-key" +AI_API_FORMAT = "openai" +AI_MODEL = "gemini-2.0-flash" +# AI_FALLBACK_BASE_URL = "https://api.openai.com" +# AI_FALLBACK_API_KEY = "your-fallback-api-key" +# AI_FALLBACK_API_FORMAT = "openai" +# AI_FALLBACK_MODEL = "gpt-4o-mini" [[d1_databases]] binding = "DB"