Spaces:
Paused
Paused
Upload 5 files
Browse files
deprecated_javascript_version/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Studio Proxy Server (Javascript Version - DEPRECATED)
|
| 2 |
+
|
| 3 |
+
**⚠️ 警告:此 Javascript 版本 (`server.cjs`, `auto_connect_aistudio.cjs`) 已被弃用且不再维护。推荐使用项目根目录下的 Python 版本,该版本采用了模块化架构设计,具有更好的稳定性和可维护性。**
|
| 4 |
+
|
| 5 |
+
**📖 查看最新文档**: 请参考项目根目录下的 [`README.md`](../README.md) 了解当前Python版本的完整使用说明。
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
[点击查看项目使用演示视频](https://drive.google.com/file/d/1efR-cNG2CNboNpogHA1ASzmx45wO579p/view?usp=drive_link)
|
| 10 |
+
|
| 11 |
+
这是一个 Node.js + Playwright 服务器,通过模拟 OpenAI API 的方式来访问 Google AI Studio 网页版,服务器无缝交互转发 Gemini 对话。这使得兼容 OpenAI API 的客户端(如 Open WebUI, NextChat 等)可以使用 AI Studio 的无限额度及能力。
|
| 12 |
+
|
| 13 |
+
## ✨ 特性 (Javascript 版本)
|
| 14 |
+
|
| 15 |
+
* **OpenAI API 兼容**: 提供 `/v1/chat/completions` 和 `/v1/models` 端点,兼容大多数 OpenAI 客户端。
|
| 16 |
+
* **流式响应**: 支持 `stream=true`,实现打字机效果。
|
| 17 |
+
* **非流式响应**: 支持 `stream=false`,一次性返回完整 JSON 响应。
|
| 18 |
+
* **系统提示词 (System Prompt)**: 支持通过请求体中的 `messages` 数组的 `system` 角色或额外的 `system_prompt` 字段传递系统提示词。
|
| 19 |
+
* **内部 Prompt 优化**: 自动包装用户输入,指导 AI Studio 输出特定格式(流式为 Markdown 代码块,非流式为 JSON),并包含起始标记 `<<<START_RESPONSE>>>` 以便解析。
|
| 20 |
+
* **自动连接脚本 (`auto_connect_aistudio.cjs`)**:
|
| 21 |
+
* 自动查找并启动 Chrome/Chromium 浏览器,开启调试端口,**并设置特定窗口宽度 (460px)** 以优化布局,确保"清空聊天"按钮可见。
|
| 22 |
+
* 自动检测并尝试连接已存在的 Chrome 调试实例。
|
| 23 |
+
* 提供交互式选项,允许用户选择连接现有实例或自动结束冲突进程。
|
| 24 |
+
* 自动查找或打开 AI Studio 的 `New chat` 页面。
|
| 25 |
+
* 自动启动 `server.cjs`。
|
| 26 |
+
* **服务端 (`server.cjs`)**:
|
| 27 |
+
* 连接到由 `auto_connect_aistudio.cjs` 管理的 Chrome 实例。
|
| 28 |
+
* **自动清空上下文**: 当检测到来自客户端的请求可能是"新对话"时(基于消息历史长度),自动模拟点击 AI Studio 页面上的"Clear chat"按钮及其确认对话框,并验证清空效果,以实现更好的会话隔离。
|
| 29 |
+
* 处理 API 请求,通过 Playwright 操作 AI Studio 页面。
|
| 30 |
+
* 解析 AI Studio 的响应,提取有效内容。
|
| 31 |
+
* 提供简单的 Web UI (`/`) 进行基本测试。
|
| 32 |
+
* 提供健康检查端点 (`/health`)。
|
| 33 |
+
* **错误快照**: 在 Playwright 操作、响应解析或**清空聊天**出错时,自动在项目根目录下的 `errors/` 目录下保存页面截图和 HTML,方便调试。(注意: Python 版本错误快照在 `errors_py/`)
|
| 34 |
+
* **依赖检测**: 两个脚本在启动时都会检查所需依赖,并提供安装指导。
|
| 35 |
+
* **跨平台设计**: 旨在支持 macOS, Linux 和 Windows (WSL 推荐)。
|
| 36 |
+
|
| 37 |
+
## ⚠️ 重要提示 (Javascript 版本)
|
| 38 |
+
|
| 39 |
+
* **非官方项目**: 本项目与 Google 无关,依赖于对 AI Studio Web 界面的自动化操作,可能因 AI Studio 页面更新而失效。
|
| 40 |
+
* **自动清空功能的脆弱性**: 自动清空上下文的功能依赖于精确的 UI 元素选择器 (`CLEAR_CHAT_BUTTON_SELECTOR`, `CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR` 在 `server.cjs` 中)。如果 AI Studio 页面结构发生变化,此功能可能会失效。届时需要更新这些选择器。
|
| 41 |
+
* **不支持历史编辑/分叉**: 即使实现了新对话的上下文清空,本代理仍然无法支持客户端进行历史消息编辑并从该点重新生成对话的功能。AI Studio 内部维护的对话历史是线性的。
|
| 42 |
+
* **固定窗口宽度**: `auto_connect_aistudio.cjs` 会以固定的宽度 (460px) 启动 Chrome 窗口,以确保清空按钮可见。
|
| 43 |
+
* **安全性**: 启动 Chrome 时开启了远程调试端口 (默认为 `8848`),请确保此端口仅在受信任的网络环境中使用,或通过防火墙规则限制访问。切勿将此端口暴露到公网。
|
| 44 |
+
* **稳定性**: 由于依赖浏览器自动化,其稳定性不如官方 API。长时间运行或频繁请求可能导致页面无响应或连接中断,可能需要重启浏览器或服务器。
|
| 45 |
+
* **AI Studio 限制**: AI Studio 本身可能有请求频率限制、内容策略限制等,代理服务器无法绕过这些限制。
|
| 46 |
+
* **参数配置**: **像模型选择、温度、输出长度等参数,需要您直接在 AI Studio 页面的右侧设置面板中进行调整。本代理服务器目前不处理或转发这些通过 API 请求传递的参数。** 您需要预先在 AI Studio Web UI 中设置好所需的模型和参数。
|
| 47 |
+
|
| 48 |
+
## 🛠️ 配置 (Javascript 版本)
|
| 49 |
+
|
| 50 |
+
虽然不建议频繁修改,但了解以下常量可能有助于理解脚本行为或在特殊情况下进行调整:
|
| 51 |
+
|
| 52 |
+
**`auto_connect_aistudio.cjs`:**
|
| 53 |
+
|
| 54 |
+
* `DEBUGGING_PORT`: (默认 `8848`) Chrome 浏览器启动时使用的远程调试端口。
|
| 55 |
+
* `TARGET_URL`: (默认 `'https://aistudio.google.com/prompts/new_chat'`) 脚本尝试打开或导航到的 AI Studio 页面。
|
| 56 |
+
* `SERVER_SCRIPT_FILENAME`: (默认 `'server.cjs'`) 由此脚本自动启动的 API 服务器文件名。
|
| 57 |
+
* `CONNECT_TIMEOUT_MS`: (默认 `20000`) 连接到 Chrome 调试端口的超时时间 (毫秒)。
|
| 58 |
+
* `NAVIGATION_TIMEOUT_MS`: (默认 `35000`) Playwright 等待页面导航完成的超时时间 (毫秒)。
|
| 59 |
+
* `--window-size=460,...`: 启动 Chrome 时传递的参数,固定宽度以保证 UI 元素(如清空按钮)位置相对稳定。
|
| 60 |
+
|
| 61 |
+
**`server.cjs`:**
|
| 62 |
+
|
| 63 |
+
* `SERVER_PORT`: (默认 `2048`) API 服务器监听的端口。
|
| 64 |
+
* `AI_STUDIO_URL_PATTERN`: (默认 `'aistudio.google.com/'`) 用于识别 AI Studio 页面的 URL 片段。
|
| 65 |
+
* `RESPONSE_COMPLETION_TIMEOUT`: (默认 `300000`) 等待 AI Studio 响应完成的总超时时间 (毫秒,5分钟)。
|
| 66 |
+
* `POLLING_INTERVAL`: (默认 `300`) 轮询检查 AI Studio 页面状态的间隔 (毫秒)。
|
| 67 |
+
* `SILENCE_TIMEOUT_MS`: (默认 `3000`) 判断 AI Studio 是否停止输出的静默超时时间 (毫秒)。
|
| 68 |
+
* `CLEAR_CHAT_VERIFY_TIMEOUT_MS`: (默认 `5000`) 等待并验证清空聊天操作完成的超时时间 (毫秒)。
|
| 69 |
+
* **CSS 选择器**: (`INPUT_SELECTOR`, `SUBMIT_BUTTON_SELECTOR`, `RESPONSE_CONTAINER_SELECTOR`, `LOADING_SPINNER_SELECTOR`, `ERROR_TOAST_SELECTOR`, `CLEAR_CHAT_BUTTON_SELECTOR`, `CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR`) 这些常量定义了脚本用于查找页面元素的选择器。**修改这些值需要具备前端知识,并且如果 AI Studio 页面更新,这些是最可能需要调整的部分。**
|
| 70 |
+
|
| 71 |
+
## ⚙️ Prompt 内部处理 (Javascript 版本)
|
| 72 |
+
|
| 73 |
+
为了让代理能够解析 AI Studio 的输出,`server.cjs` 会在将你的 Prompt 发送到 AI Studio 前进行包装,加入特定的指令,要求 AI:
|
| 74 |
+
|
| 75 |
+
1. **对于非流式请求 (`stream=false`)**: 将整个回复包裹在一个 JSON 对象中,格式为 `{"response": "<<<START_RESPONSE>>>[AI的实际回复]"}`。
|
| 76 |
+
2. **对于流式请求 (`stream=true`)**: 将整个回复(包括开始和结束)包裹在一个 Markdown 代码块 (```) 中,并在实际回复前加上标记 `<<<START_RESPONSE>>>`,形如:
|
| 77 |
+
```markdown
|
| 78 |
+
```
|
| 79 |
+
<<<START_RESPONSE>>>[AI的实际回复第一部分]
|
| 80 |
+
[AI的实际回复第二部分]
|
| 81 |
+
...
|
| 82 |
+
```
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
`server.cjs` 会查找 `<<<START_RESPONSE>>>` 标记来提取真正的回复内容。这意味着你通过 API 得到的回复是经过这个内部处理流程的,AI Studio 页面的原始输出格式会被改变。
|
| 86 |
+
|
| 87 |
+
## 🚀 开始使用 (Javascript 版本)
|
| 88 |
+
|
| 89 |
+
### 1. 先决条件
|
| 90 |
+
|
| 91 |
+
* **Node.js**: v16 或更高版本。
|
| 92 |
+
* **NPM / Yarn / PNPM**: 用于安装依赖。
|
| 93 |
+
* **Google Chrome / Chromium**: 需要安装浏览器本体。
|
| 94 |
+
* **Google AI Studio 账号**: 并能正常访问和使用。
|
| 95 |
+
|
| 96 |
+
### 2. 安装
|
| 97 |
+
|
| 98 |
+
1. **进入弃用版本目录**:
|
| 99 |
+
```bash
|
| 100 |
+
cd deprecated_javascript_version
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
2. **安装依赖**:
|
| 104 |
+
根据 `package.json` 文件,脚本运行需要以下核心依赖:
|
| 105 |
+
* `express`: Web 框架,用于构建 API 服务器。
|
| 106 |
+
* `cors`: 处理跨域资源共享。
|
| 107 |
+
* `playwright`: 浏览器自动化库。
|
| 108 |
+
* `@playwright/test`: Playwright 的测试库,`server.cjs` 使用其 `expect` 功能进行断言。
|
| 109 |
+
|
| 110 |
+
使用你的包管理器安装:
|
| 111 |
+
```bash
|
| 112 |
+
npm install
|
| 113 |
+
# 或
|
| 114 |
+
yarn install
|
| 115 |
+
# 或
|
| 116 |
+
pnpm install
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 3. 运行
|
| 120 |
+
|
| 121 |
+
只需要运行 `auto_connect_aistudio.cjs` 脚本即可启动所有服务:
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
node auto_connect_aistudio.cjs
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
这个脚本会执行以下操作:
|
| 128 |
+
|
| 129 |
+
1. **检查依赖**: 确认上述 Node.js 模块已安装,且 `server.cjs` 文件存在。
|
| 130 |
+
2. **检查 Chrome 调试端口 (`8848`)**:
|
| 131 |
+
* 如果端口空闲,尝试自动查找并启动一个新的 Chrome 实例(窗口宽度固定为 460px),并打开远程调试端口。
|
| 132 |
+
* 如果端口被占用,询问用户是连接现有实例还是尝试清理端口后启动新实例。
|
| 133 |
+
3. **连接 Playwright**: 尝试连接到 Chrome 的调试端口 (`http://127.0.0.1:8848`)。
|
| 134 |
+
4. **管理 AI Studio 页面**: 查找或打开 AI Studio 的 `New chat` 页面 (`https://aistudio.google.com/prompts/new_chat`),并尝试置于前台。
|
| 135 |
+
5. **启动 API 服务器**: 如果以上步骤成功,脚本会自动在后台启动 `node server.cjs`。
|
| 136 |
+
|
| 137 |
+
当 `server.cjs` 成功启动并连接到 Playwright 后,您将在终端看到类似以下的输出(来自 `server.cjs`):
|
| 138 |
+
|
| 139 |
+
```
|
| 140 |
+
=============================================================
|
| 141 |
+
🚀 AI Studio Proxy Server (vX.XX - Queue & Auto Clear) 🚀
|
| 142 |
+
=============================================================
|
| 143 |
+
🔗 监听地址: http://localhost:2048
|
| 144 |
+
- Web UI (测试): http://localhost:2048/
|
| 145 |
+
- API 端点: http://localhost:2048/v1/chat/completions
|
| 146 |
+
- 模型接口: http://localhost:2048/v1/models
|
| 147 |
+
- 健康检查: http://localhost:2048/health
|
| 148 |
+
-------------------------------------------------------------
|
| 149 |
+
✅ Playwright 连接成功,服务已准备就绪!
|
| 150 |
+
-------------------------------------------------------------
|
| 151 |
+
```
|
| 152 |
+
*(版本号可能不同)*
|
| 153 |
+
|
| 154 |
+
此时,代理服务已准备就绪,监听在 `http://localhost:2048`。
|
| 155 |
+
|
| 156 |
+
### 4. 配置客户端 (以 Open WebUI 为例)
|
| 157 |
+
|
| 158 |
+
1. 打开 Open WebUI。
|
| 159 |
+
2. 进入 "设置" -> "连接"。
|
| 160 |
+
3. 在 "模型" 部分,点击 "添加模型"。
|
| 161 |
+
4. **模型名称**: 输入你想要的名字,例如 `aistudio-gemini-cjs`。
|
| 162 |
+
5. **API 基础 URL**: 输入代理服务器的地址,例如 `http://localhost:2048/v1` (注意包含 `/v1`)。
|
| 163 |
+
6. **API 密钥**: 留空或输入任意字符 (服务器不验证)。
|
| 164 |
+
7. 保存设置。
|
| 165 |
+
8. 现在,你应该可以在 Open WebUI 中选择 `aistudio-gemini-cjs` 模型并开始聊天了。
|
| 166 |
+
|
| 167 |
+
### 5. 使用测试脚本 (可选)
|
| 168 |
+
|
| 169 |
+
本目录下提供了一个 `test.js` 脚本,用于在命令行中直接与代理进行交互式聊天。
|
| 170 |
+
|
| 171 |
+
1. **安装额外依赖**: `test.js` 使用了 OpenAI 的官方 Node.js SDK。
|
| 172 |
+
```bash
|
| 173 |
+
npm install openai
|
| 174 |
+
# 或 yarn add openai / pnpm add openai
|
| 175 |
+
```
|
| 176 |
+
2. **检查配置**: 打开 `test.js`,确认 `LOCAL_PROXY_URL` 指向你的代理服务器地址 (`http://127.0.0.1:2048/v1/`)。`DUMMY_API_KEY` 可以保持不变。
|
| 177 |
+
3. **运行测试**: 在 `deprecated_javascript_version` 目录下运行:
|
| 178 |
+
```bash
|
| 179 |
+
node test.js
|
| 180 |
+
```
|
| 181 |
+
之后就可以在命令行输入问题进行测试了。输入 `exit` 退出。
|
| 182 |
+
|
| 183 |
+
## 💻 多平台指南 (Javascript 版本)
|
| 184 |
+
|
| 185 |
+
* **macOS**:
|
| 186 |
+
* `auto_connect_aistudio.cjs` 通常能自动找到 Chrome。
|
| 187 |
+
* 防火墙可能会提示是否允许 Node.js 接受网络连接,请允许。
|
| 188 |
+
* **Linux**:
|
| 189 |
+
* 确保已安装 `google-chrome-stable` 或 `chromium-browser`。
|
| 190 |
+
* 如果脚本找不到 Chrome,你可能需要修改 `auto_connect_aistudio.cjs` 中的 `getChromePath` 函数,手动指定路径,或者创建一个符号链接 (`/usr/bin/google-chrome`) 指向实际的 Chrome 可执行文件。
|
| 191 |
+
* 某些 Linux 发行版可能需要安装额外的 Playwright 依赖库,参考 [Playwright Linux 文档](https://playwright.dev/docs/intro#system-requirements)。运行 `npx playwright install-deps` 可能有助于安装。
|
| 192 |
+
* **Windows**:
|
| 193 |
+
* **强烈建议使用 WSL (Windows Subsystem for Linux)**。在 WSL 中按照 Linux 指南操作通常更顺畅。
|
| 194 |
+
* **直接在 Windows 上运行 (不推荐)**:
|
| 195 |
+
* `auto_connect_aistudio.cjs` 可能需要手动修改 `getChromePath` 函数来指定 Chrome 的完整路径 (例如 `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`)。注意路径中的反斜杠需要转义 (`\\`)。
|
| 196 |
+
* 防火墙设置需要允许 Node.js 和 Chrome 监听和连接端口 (`8848` 和 `2048`)。
|
| 197 |
+
* 由于文件系统和权限差异,可能会遇到未知问题,例如端口检查或进程结束操作 (`taskkill`) 失败。
|
| 198 |
+
|
| 199 |
+
## 🔧 故障排除 (Javascript 版本)
|
| 200 |
+
|
| 201 |
+
* **`auto_connect_aistudio.cjs` 启动失败或报错**:
|
| 202 |
+
* **依赖未找到**: 确认运行了 `npm install` 等命令。
|
| 203 |
+
* **Chrome 路径找不到**: 确认 Chrome/Chromium 已安装,并按需修改 `getChromePath` 函数或创建符号链接 (Linux)。
|
| 204 |
+
* **端口 (`8848`) 被占用且无法自动清理**: 根据脚本提示,使用系统工具(如 `lsof -i :8848` / `tasklist | findstr "8848"`)手动查找并结束占用端口的进程。
|
| 205 |
+
* **连接 Playwright 超时**: 确认 Chrome 是否已成功启动并监听 `8848` 端口,防火墙是否阻止本地连接 `127.0.0.1:8848`。查看 `auto_connect_aistudio.cjs` 中的 `CONNECT_TIMEOUT_MS` 是否足够。
|
| 206 |
+
* **打开/导航 AI Studio 页面失败**: 检查网络连接,尝试手动在浏览器中打开 `https://aistudio.google.com/prompts/new_chat` 并完成登录。查看 `NAVIGATION_TIMEOUT_MS` 是否足够。
|
| 207 |
+
* **窗口大小问题**: 如果 460px 宽度导致问题,可以尝试修改 `auto_connect_aistudio.cjs` 中的 `--window-size` 参数,但这可能影响自动清空功能。
|
| 208 |
+
* **`server.cjs` 启动时提示端口被占用 (`EADDRINUSE`)**:
|
| 209 |
+
* 检查是否有其他程序 (包括旧的服务器实例) 正在使用 `2048` 端口。关闭冲突程序或修改 `server.cjs` 中的 `SERVER_PORT`。
|
| 210 |
+
* **服务器日志显示 Playwright 未就绪或连接失败 (在 `server.cjs` 启动后)**:
|
| 211 |
+
* 通常意味着 `auto_connect_aistudio.cjs` 启动的 Chrome 实例意外关闭或无响应。检查 Chrome 窗口是否还在,AI Studio 页面是否崩溃。
|
| 212 |
+
* 尝试关闭所有相关进程(`node` 和 `chrome`),然后重新运行 `node auto_connect_aistudio.cjs`。
|
| 213 |
+
* 检查根目录下的 `errors/` 目录是否有截图和 HTML 文件,它们可能包含 AI Studio 页面的错误信息或状态。
|
| 214 |
+
* **客户端 (如 Open WebUI) 无法连接或请求失败**:
|
| 215 |
+
* 确认 API 基础 URL 配置正确 (`http://localhost:2048/v1`)。
|
| 216 |
+
* 检查 `server.cjs` 运行的终端是否有错误输出。
|
| 217 |
+
* 确保客户端和服务器在同一网络中,且防火墙没有阻止从客户端到服务器 `2048` 端口的连接。
|
| 218 |
+
* **API 请求返回 5xx 错误**:
|
| 219 |
+
* **503 Service Unavailable / Playwright not ready**: `server.cjs` 无法连接到 Chrome。
|
| 220 |
+
* **504 Gateway Timeout**: 请求处理时间超过了 `RESPONSE_COMPLETION_TIMEOUT`。可能是 AI Studio 响应慢或卡住了。
|
| 221 |
+
* **502 Bad Gateway / AI Studio Error**: `server.cjs` 在 AI Studio 页面上检测到了错误提示 (`toast` 消息),或无法正确解析 AI 的响应。检查 `errors/` 快照。
|
| 222 |
+
* **500 Internal Server Error**: `server.cjs` 内部发生未捕获的错误。检查服务器日志和 `errors/` 快照。
|
| 223 |
+
* **AI 回复不完整、格式错误或包含 `<<<START_RESPONSE>>>` 标记**:
|
| 224 |
+
* AI Studio 的 Web UI 输出不稳定。服务器尽力解析,但可能失败。
|
| 225 |
+
* 非流式请求:如果返回的 JSON 中缺少 `response` 字段或无法解析,服务器可能返回空内容或原始 JSON 字符串。检查 `errors/` 快照确认 AI Studio 页面的实际输出。
|
| 226 |
+
* 流式请求:如果 AI 未按预期输出 Markdown 代码块或起始标记,流式传输可能提前中断或包含非预期内容。
|
| 227 |
+
* 尝试调整 Prompt 或稍后重试。
|
| 228 |
+
* **自动清空上下文失败**:
|
| 229 |
+
* 服务器日志出现 "清空聊天记录或验证时出错" 或 "验证超时" 的警告。
|
| 230 |
+
* **原因**: AI Studio 网页更新导致 `server.cjs` 中的 `CLEAR_CHAT_BUTTON_SELECTOR` 或 `CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR` 失效。
|
| 231 |
+
* **解决**: 检查 `errors/` 快照,使用浏览器开发者工具检查实际页面元素,并更新 `server.cjs` 文件顶部的选择器常量。
|
| 232 |
+
* **原因**: 清空操作本身耗时超过了 `CLEAR_CHAT_VERIFY_TIMEOUT_MS`。
|
| 233 |
+
* **解决**: 如果网络或机器较慢,可以尝试在 `server.cjs` 中适当增加这个超时时间。
|
deprecated_javascript_version/auto_connect_aistudio.cjs
CHANGED
|
@@ -0,0 +1,595 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
// auto_connect_aistudio.js (v2.9 - Refined Launch & Page Handling + Beautified Output)
|
| 4 |
+
|
| 5 |
+
const { spawn, execSync } = require('child_process');
|
| 6 |
+
const path = require('path');
|
| 7 |
+
const fs = require('fs');
|
| 8 |
+
const readline = require('readline');
|
| 9 |
+
|
| 10 |
+
// --- Configuration ---
|
| 11 |
+
const DEBUGGING_PORT = 8848;
|
| 12 |
+
const TARGET_URL = 'https://aistudio.google.com/prompts/new_chat'; // Target page
|
| 13 |
+
const SERVER_SCRIPT_FILENAME = 'server.cjs'; // Corrected script name
|
| 14 |
+
const CONNECTION_RETRIES = 5;
|
| 15 |
+
const RETRY_DELAY_MS = 4000;
|
| 16 |
+
const CONNECT_TIMEOUT_MS = 20000; // Timeout for connecting to CDP
|
| 17 |
+
const NAVIGATION_TIMEOUT_MS = 35000; // Increased timeout for page navigation
|
| 18 |
+
const CDP_ADDRESS = `http://127.0.0.1:${DEBUGGING_PORT}`;
|
| 19 |
+
|
| 20 |
+
// --- ANSI Colors ---
|
| 21 |
+
const RESET = '\x1b[0m';
|
| 22 |
+
const BRIGHT = '\x1b[1m';
|
| 23 |
+
const DIM = '\x1b[2m';
|
| 24 |
+
const RED = '\x1b[31m';
|
| 25 |
+
const GREEN = '\x1b[32m';
|
| 26 |
+
const YELLOW = '\x1b[33m';
|
| 27 |
+
const BLUE = '\x1b[34m';
|
| 28 |
+
const MAGENTA = '\x1b[35m';
|
| 29 |
+
const CYAN = '\x1b[36m';
|
| 30 |
+
|
| 31 |
+
// --- Globals ---
|
| 32 |
+
const SERVER_SCRIPT_PATH = path.join(__dirname, SERVER_SCRIPT_FILENAME);
|
| 33 |
+
let playwright; // Loaded in checkDependencies
|
| 34 |
+
|
| 35 |
+
// --- Platform-Specific Chrome Path ---
|
| 36 |
+
function getChromePath() {
|
| 37 |
+
switch (process.platform) {
|
| 38 |
+
case 'darwin':
|
| 39 |
+
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
| 40 |
+
case 'win32':
|
| 41 |
+
// 尝试 Program Files 和 Program Files (x86)
|
| 42 |
+
const winPaths = [
|
| 43 |
+
path.join(process.env.ProgramFiles || '', 'Google\Chrome\Application\chrome.exe'),
|
| 44 |
+
path.join(process.env['ProgramFiles(x86)'] || '', 'Google\Chrome\Application\chrome.exe')
|
| 45 |
+
];
|
| 46 |
+
return winPaths.find(p => fs.existsSync(p));
|
| 47 |
+
case 'linux':
|
| 48 |
+
// 尝试常见的 Linux 路径
|
| 49 |
+
const linuxPaths = [
|
| 50 |
+
'/usr/bin/google-chrome',
|
| 51 |
+
'/usr/bin/google-chrome-stable',
|
| 52 |
+
'/opt/google/chrome/chrome',
|
| 53 |
+
// Add path for Flatpak installation if needed
|
| 54 |
+
// '/var/lib/flatpak/exports/bin/com.google.Chrome'
|
| 55 |
+
];
|
| 56 |
+
return linuxPaths.find(p => fs.existsSync(p));
|
| 57 |
+
default:
|
| 58 |
+
return null; // 不支持的平台
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
const chromeExecutablePath = getChromePath();
|
| 63 |
+
|
| 64 |
+
// --- 端口检查函数 ---
|
| 65 |
+
function isPortInUse(port) {
|
| 66 |
+
const platform = process.platform;
|
| 67 |
+
let command;
|
| 68 |
+
// console.log(`${DIM} 检查端口 ${port}...${RESET}`); // Optional: Verbose check
|
| 69 |
+
try {
|
| 70 |
+
if (platform === 'win32') {
|
| 71 |
+
// 在 Windows 上,查找监听状态的 TCP 端口
|
| 72 |
+
command = `netstat -ano | findstr LISTENING | findstr :${port}`;
|
| 73 |
+
execSync(command); // 如果找到,不会抛出错误
|
| 74 |
+
return true;
|
| 75 |
+
} else if (platform === 'darwin' || platform === 'linux') {
|
| 76 |
+
// 在 macOS 或 Linux 上,查找监听该端口的进程
|
| 77 |
+
command = `lsof -i tcp:${port} -sTCP:LISTEN`;
|
| 78 |
+
execSync(command); // 如果找到,不会抛出错误
|
| 79 |
+
return true;
|
| 80 |
+
}
|
| 81 |
+
} catch (error) {
|
| 82 |
+
// 如果命令执行失败(通常意味着找不到匹配的进程),则端口未被占用
|
| 83 |
+
// console.log(`端口 ${port} 检查命令执行失败或未找到进程:`, error.message.split('\n')[0]); // 可选的调试信息
|
| 84 |
+
return false;
|
| 85 |
+
}
|
| 86 |
+
// 对于不支持的平台,保守地假设端口未被占用
|
| 87 |
+
return false;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// --- 查找占用端口的 PID --- (新增)
|
| 91 |
+
function findPidsUsingPort(port) {
|
| 92 |
+
const platform = process.platform;
|
| 93 |
+
const pids = [];
|
| 94 |
+
let command;
|
| 95 |
+
try {
|
| 96 |
+
console.log(`${DIM} 正在查找占用端口 ${port} 的进程...${RESET}`);
|
| 97 |
+
if (platform === 'win32') {
|
| 98 |
+
command = `netstat -ano | findstr LISTENING | findstr :${port}`;
|
| 99 |
+
const output = execSync(command).toString();
|
| 100 |
+
const lines = output.trim().split('\n');
|
| 101 |
+
for (const line of lines) {
|
| 102 |
+
const parts = line.trim().split(/\s+/);
|
| 103 |
+
const pid = parts[parts.length - 1]; // PID is the last column
|
| 104 |
+
if (pid && !isNaN(pid)) {
|
| 105 |
+
pids.push(pid);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
} else { // macOS or Linux
|
| 109 |
+
command = `lsof -t -i tcp:${port} -sTCP:LISTEN`;
|
| 110 |
+
const output = execSync(command).toString();
|
| 111 |
+
const lines = output.trim().split('\n');
|
| 112 |
+
for (const line of lines) {
|
| 113 |
+
const pid = line.trim();
|
| 114 |
+
if (pid && !isNaN(pid)) {
|
| 115 |
+
pids.push(pid);
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
if (pids.length > 0) {
|
| 120 |
+
console.log(` ${YELLOW}找到占用端口 ${port} 的 PID: ${pids.join(', ')}${RESET}`);
|
| 121 |
+
} else {
|
| 122 |
+
console.log(` ${GREEN}未找到明确监听端口 ${port} 的进程。${RESET}`);
|
| 123 |
+
}
|
| 124 |
+
} catch (error) {
|
| 125 |
+
// 命令失败通常意味着没有找到进程
|
| 126 |
+
console.log(` ${GREEN}查找端口 ${port} 进程的命令执行失败或无结果。${RESET}`);
|
| 127 |
+
}
|
| 128 |
+
return [...new Set(pids)]; // 返回去重后的 PID 列表
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// --- 结束进程 --- (新增)
|
| 132 |
+
function killProcesses(pids) {
|
| 133 |
+
if (pids.length === 0) return true; // 没有进程需要结束
|
| 134 |
+
|
| 135 |
+
const platform = process.platform;
|
| 136 |
+
let success = true;
|
| 137 |
+
console.log(`${YELLOW} 正在尝试结束 PID: ${pids.join(', ')}...${RESET}`);
|
| 138 |
+
|
| 139 |
+
for (const pid of pids) {
|
| 140 |
+
try {
|
| 141 |
+
if (platform === 'win32') {
|
| 142 |
+
execSync(`taskkill /F /PID ${pid}`);
|
| 143 |
+
console.log(` ${GREEN}✅ 成功结束 PID ${pid} (Windows)${RESET}`);
|
| 144 |
+
} else { // macOS or Linux
|
| 145 |
+
execSync(`kill -9 ${pid}`);
|
| 146 |
+
console.log(` ${GREEN}✅ 成功结束 PID ${pid} (macOS/Linux)${RESET}`);
|
| 147 |
+
}
|
| 148 |
+
} catch (error) {
|
| 149 |
+
console.warn(` ${RED}⚠️ 结束 PID ${pid} 时出错: ${error.message.split('\n')[0]}${RESET}`);
|
| 150 |
+
// 可能原因:进程已不存在、权限不足等
|
| 151 |
+
success = false; // 标记至少有一个失败了
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
return success;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// --- 创建 Readline Interface ---
|
| 158 |
+
function askQuestion(query) {
|
| 159 |
+
const rl = readline.createInterface({
|
| 160 |
+
input: process.stdin,
|
| 161 |
+
output: process.stdout,
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
return new Promise(resolve => rl.question(query, ans => {
|
| 165 |
+
rl.close();
|
| 166 |
+
resolve(ans);
|
| 167 |
+
}))
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// --- 步骤 1: 检查 Playwright 依赖 ---
|
| 171 |
+
async function checkDependencies() {
|
| 172 |
+
console.log(`${CYAN}-------------------------------------------------${RESET}`);
|
| 173 |
+
console.log(`${CYAN}--- 步骤 1: 检查依赖项 ---${RESET}`);
|
| 174 |
+
console.log('将检查以下模块是否已安装:');
|
| 175 |
+
const requiredModules = ['express', 'playwright', '@playwright/test', 'cors'];
|
| 176 |
+
const missingModules = [];
|
| 177 |
+
let allFound = true;
|
| 178 |
+
|
| 179 |
+
for (const moduleName of requiredModules) {
|
| 180 |
+
process.stdout.write(` - ${moduleName} ... `);
|
| 181 |
+
try {
|
| 182 |
+
require.resolve(moduleName); // Use require.resolve for checking existence without loading
|
| 183 |
+
console.log(`${GREEN}✓ 已找到${RESET}`); // Green checkmark
|
| 184 |
+
} catch (error) {
|
| 185 |
+
if (error.code === 'MODULE_NOT_FOUND') {
|
| 186 |
+
console.log(`${RED}❌ 未找到${RESET}`); // Red X
|
| 187 |
+
missingModules.push(moduleName);
|
| 188 |
+
allFound = false;
|
| 189 |
+
} else {
|
| 190 |
+
console.log(`${RED}❌ 检查时出错: ${error.message}${RESET}`);
|
| 191 |
+
allFound = false;
|
| 192 |
+
// Consider exiting if it's not MODULE_NOT_FOUND?
|
| 193 |
+
// return false;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
process.stdout.write(` - 服务器脚本 (${SERVER_SCRIPT_FILENAME}) ... `);
|
| 199 |
+
if (!fs.existsSync(SERVER_SCRIPT_PATH)) {
|
| 200 |
+
console.log(`${RED}❌ 未找到${RESET}`); // Red X
|
| 201 |
+
console.error(` ${RED}错误: 未在预期路径找到 '${SERVER_SCRIPT_FILENAME}' 文件。${RESET}`);
|
| 202 |
+
console.error(` 预期路径: ${SERVER_SCRIPT_PATH}`);
|
| 203 |
+
console.error(` 请确保 '${SERVER_SCRIPT_FILENAME}' 与此脚本位于同一目录。`);
|
| 204 |
+
allFound = false;
|
| 205 |
+
} else {
|
| 206 |
+
console.log(`${GREEN}✓ 已找到${RESET}`); // Green checkmark
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if (!allFound) {
|
| 210 |
+
console.log(`\n${RED}-------------------------------------------------${RESET}`);
|
| 211 |
+
console.error(`${RED}❌ 错误: 依赖项检查未通过!${RESET}`);
|
| 212 |
+
if (missingModules.length > 0) {
|
| 213 |
+
console.error(` ${RED}缺少以下 Node.js 模块: ${missingModules.join(', ')}${RESET}`);
|
| 214 |
+
console.log(' 请根据您使用的包管理器运行以下命令安装依赖:');
|
| 215 |
+
console.log(` ${MAGENTA}npm install ${missingModules.join(' ')}${RESET}`);
|
| 216 |
+
console.log(' 或');
|
| 217 |
+
console.log(` ${MAGENTA}yarn add ${missingModules.join(' ')}${RESET}`);
|
| 218 |
+
console.log(' 或');
|
| 219 |
+
console.log(` ${MAGENTA}pnpm install ${missingModules.join(' ')}${RESET}`);
|
| 220 |
+
console.log(' (如果已安装但仍提示未找到,请尝试删除 node_modules 目录和 package-lock.json/yarn.lock 文件后重新安装)');
|
| 221 |
+
}
|
| 222 |
+
if (!fs.existsSync(SERVER_SCRIPT_PATH)) {
|
| 223 |
+
console.error(` ${RED}缺少必要的服务器脚本文件: ${SERVER_SCRIPT_FILENAME}${RESET}`);
|
| 224 |
+
console.error(` 请确保它和 auto_connect_aistudio.cjs 在同一个文件夹内。`);
|
| 225 |
+
}
|
| 226 |
+
console.log(`${RED}-------------------------------------------------${RESET}`);
|
| 227 |
+
return false;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
console.log(`\n${GREEN}✅ 所有依赖检查通过。${RESET}`);
|
| 231 |
+
playwright = require('playwright'); // Load playwright only after checks
|
| 232 |
+
return true;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// --- 步骤 2: 检查并启动 Chrome ---
|
| 236 |
+
async function launchChrome() {
|
| 237 |
+
console.log(`${CYAN}-------------------------------------------------${RESET}`);
|
| 238 |
+
console.log(`${CYAN}--- 步骤 2: 启动或连接 Chrome (调试端口 ${DEBUGGING_PORT}) ---${RESET}`);
|
| 239 |
+
|
| 240 |
+
// 首先检查端口是否被占用
|
| 241 |
+
if (isPortInUse(DEBUGGING_PORT)) {
|
| 242 |
+
console.log(`${YELLOW}⚠️ 警告: 端口 ${DEBUGGING_PORT} 已被占用。${RESET}`);
|
| 243 |
+
console.log(' 这通常意味着已经有一个 Chrome 实例在监听此端口。');
|
| 244 |
+
const question = `选择操作: [Y/n]
|
| 245 |
+
${GREEN}Y (默认): 尝试连接现有 Chrome 实例并启动 API 服务器。${RESET}
|
| 246 |
+
${YELLOW}n: 自动强行结束占用端口 ${DEBUGGING_PORT} 的进程,然后启动新的 Chrome 实例。${RESET}
|
| 247 |
+
请输入选项 [Y/n]: `;
|
| 248 |
+
const answer = await askQuestion(question);
|
| 249 |
+
|
| 250 |
+
if (answer.toLowerCase() === 'n') {
|
| 251 |
+
console.log(`\n好的,您选择了启动新实例。将尝试自动清理端口...`);
|
| 252 |
+
const pids = findPidsUsingPort(DEBUGGING_PORT);
|
| 253 |
+
if (pids.length > 0) {
|
| 254 |
+
const killSuccess = killProcesses(pids);
|
| 255 |
+
if (killSuccess) {
|
| 256 |
+
console.log(` ${GREEN}✅ 尝试结束进程完成。等待 1 秒检查端口...${RESET}`);
|
| 257 |
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 短暂等待
|
| 258 |
+
if (isPortInUse(DEBUGGING_PORT)) {
|
| 259 |
+
console.error(`${RED}❌ 错误: 尝试结束后,端口 ${DEBUGGING_PORT} 仍然被占用。${RESET}`);
|
| 260 |
+
console.error(' 可能原因:权限不足,或进程未能正常终止。请尝试手动结束进程。' );
|
| 261 |
+
// 提供手动清理提示
|
| 262 |
+
console.log(`${YELLOW}提示: 您可以使用以下命令查找进程 ID (PID):${RESET}`);
|
| 263 |
+
if (process.platform === 'win32') {
|
| 264 |
+
console.log(` - 在 CMD 或 PowerShell 中: netstat -ano | findstr :${DEBUGGING_PORT}`);
|
| 265 |
+
console.log(' - 找到 PID 后,使用: taskkill /F /PID <PID>');
|
| 266 |
+
} else { // macOS or Linux
|
| 267 |
+
console.log(` - 在终端中: lsof -t -i:${DEBUGGING_PORT}`);
|
| 268 |
+
console.log(' - 找到 PID 后,使用: kill -9 <PID>');
|
| 269 |
+
}
|
| 270 |
+
await askQuestion('请在手动结束进程后,按 Enter 键重试脚本...');
|
| 271 |
+
process.exit(1); // 退出,让用户处理后重跑
|
| 272 |
+
} else {
|
| 273 |
+
console.log(` ${GREEN}✅ 端口 ${DEBUGGING_PORT} 现在空闲。${RESET}`);
|
| 274 |
+
// 端口已清理,继续执行下面的 Chrome 启动流程
|
| 275 |
+
}
|
| 276 |
+
} else {
|
| 277 |
+
console.error(`${RED}❌ 错误: 尝试结束部分或全部占用端口的进程失败。${RESET}`);
|
| 278 |
+
console.error(' 请检查日志中的具体错误信息,可能需要手动结束进程。');
|
| 279 |
+
await askQuestion('请在手动结束进程后,按 Enter 键重试脚本...');
|
| 280 |
+
process.exit(1); // 退出,让用户处理后重跑
|
| 281 |
+
}
|
| 282 |
+
} else {
|
| 283 |
+
console.log(`${YELLOW} 虽然端口被占用,但未能找到具体监听的进程 PID。可能情况复杂,建议手动检查。${RESET}` );
|
| 284 |
+
await askQuestion('请手动检查并确保端口空闲后,按 Enter 键重试脚本...');
|
| 285 |
+
process.exit(1); // 退出
|
| 286 |
+
}
|
| 287 |
+
// 如果代码执行到这里,意味着端口清理成功,将继续启动 Chrome
|
| 288 |
+
console.log(`\n准备启动新的 Chrome 实例...`);
|
| 289 |
+
|
| 290 |
+
} else {
|
| 291 |
+
console.log(`\n好的,将尝试连接到现有的 Chrome 实例...`);
|
| 292 |
+
return 'use_existing'; // 特殊返回值,告知主流程跳过启动,直接连接
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// --- 如果端口未被占用,或者用户选择 'n' 且自动清理成功 ---
|
| 297 |
+
|
| 298 |
+
if (!chromeExecutablePath) {
|
| 299 |
+
console.error(`${RED}❌ 错误: 未能在当前操作系统 (${process.platform}) 的常见路径找到 Chrome 可执行文件。${RESET}`);
|
| 300 |
+
console.error(' 请确保已安装 Google Chrome,或修改脚本中的 getChromePath 函数以指向正确的路径。');
|
| 301 |
+
if (process.platform === 'win32') {
|
| 302 |
+
console.error(' (已尝试查找 %ProgramFiles% 和 %ProgramFiles(x86)% 下的路径)');
|
| 303 |
+
} else if (process.platform === 'linux') {
|
| 304 |
+
console.error(' (已尝试查找 /usr/bin/google-chrome, /usr/bin/google-chrome-stable, /opt/google/chrome/chrome)');
|
| 305 |
+
}
|
| 306 |
+
return false;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
console.log(` ${GREEN}找到 Chrome 路径:${RESET} ${chromeExecutablePath}`);
|
| 310 |
+
|
| 311 |
+
// 只有在明确需要启动新实例时才提示关闭其他实例
|
| 312 |
+
// (如果上面选择了 'n' 并清理成功,这里 isPortInUse 应该返回 false)
|
| 313 |
+
if (!isPortInUse(DEBUGGING_PORT)) {
|
| 314 |
+
console.log(`${YELLOW}⚠️ 重要提示:为了确保新的调试端口生效,建议先手动完全退出所有*其他*可能干扰的 Google Chrome 实例。${RESET}`);
|
| 315 |
+
console.log(' (在 macOS 上通常是 Cmd+Q,Windows/Linux 上是关闭所有窗口)');
|
| 316 |
+
await askQuestion('请确认已处理好其他 Chrome 实例,然后按 Enter 键继续启动...');
|
| 317 |
+
} else {
|
| 318 |
+
// 理论上不应该到这里,因为端口已被清理或选择了 use_existing
|
| 319 |
+
console.warn(` ${YELLOW}警告:端口 ${DEBUGGING_PORT} 意外地仍被占用。继续尝试启动,但这极有可能失败。${RESET}`);
|
| 320 |
+
await askQuestion('请按 Enter 键继续尝试启动...');
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
console.log(`正在尝试启动 Chrome...`);
|
| 325 |
+
console.log(` 路径: "${chromeExecutablePath}"`);
|
| 326 |
+
// --- 修改:添加启动参数 ---
|
| 327 |
+
const chromeArgs = [
|
| 328 |
+
`--remote-debugging-port=${DEBUGGING_PORT}`,
|
| 329 |
+
`--window-size=460,800` // 指定宽度为 460px,高度暂定为 800px (可以根据需要调整)
|
| 330 |
+
// 你可以在这里添加其他需要的 Chrome 启动参数
|
| 331 |
+
];
|
| 332 |
+
console.log(` 参数: ${chromeArgs.join(' ')}`); // 打印所有参数
|
| 333 |
+
|
| 334 |
+
try {
|
| 335 |
+
const chromeProcess = spawn(
|
| 336 |
+
chromeExecutablePath,
|
| 337 |
+
chromeArgs, // 使用包含窗口大小的参数数组
|
| 338 |
+
{ detached: true, stdio: 'ignore' } // Detach to allow script to exit independently if needed
|
| 339 |
+
);
|
| 340 |
+
chromeProcess.unref(); // Allow parent process to exit independently
|
| 341 |
+
|
| 342 |
+
console.log(`${GREEN}✅ Chrome 启动命令已发送 (指定窗口大小)。稍后将尝试连接...${RESET}`);
|
| 343 |
+
console.log(`${DIM}⏳ 等待 3 秒让 Chrome 进程启动...${RESET}`);
|
| 344 |
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
| 345 |
+
return true; // 表示启动流程已尝试
|
| 346 |
+
|
| 347 |
+
} catch (error) {
|
| 348 |
+
console.error(`${RED}❌ 启动 Chrome 时出错: ${error.message}${RESET}`);
|
| 349 |
+
console.error(` 请检查路径 "${chromeExecutablePath}" 是否正确,以及是否有权限执行。`);
|
| 350 |
+
return false;
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
// --- 步骤 3: 连接 Playwright 并管理页面 (带重试) ---
|
| 355 |
+
async function connectAndManagePage() {
|
| 356 |
+
console.log(`${CYAN}-------------------------------------------------${RESET}`);
|
| 357 |
+
console.log(`${CYAN}--- 步骤 3: 连接 Playwright 到 ${CDP_ADDRESS} (最多尝试 ${CONNECTION_RETRIES} 次) ---${RESET}`);
|
| 358 |
+
let browser = null;
|
| 359 |
+
let context = null;
|
| 360 |
+
|
| 361 |
+
for (let i = 0; i < CONNECTION_RETRIES; i++) {
|
| 362 |
+
try {
|
| 363 |
+
console.log(`\n${DIM}尝试连接 Playwright (第 ${i + 1}/${CONNECTION_RETRIES} 次)...${RESET}`);
|
| 364 |
+
browser = await playwright.chromium.connectOverCDP(CDP_ADDRESS, { timeout: CONNECT_TIMEOUT_MS });
|
| 365 |
+
console.log(`${GREEN}✅ 成功连接到 Chrome!${RESET}`);
|
| 366 |
+
|
| 367 |
+
// Simplified context fetching
|
| 368 |
+
await new Promise(resolve => setTimeout(resolve, 500)); // Short delay after connect
|
| 369 |
+
const contexts = browser.contexts();
|
| 370 |
+
if (contexts && contexts.length > 0) {
|
| 371 |
+
context = contexts[0];
|
| 372 |
+
console.log(`-> 获取到浏览器默认上下文。`);
|
| 373 |
+
break; // Connection and context successful
|
| 374 |
+
} else {
|
| 375 |
+
// This case should be rare if connectOverCDP succeeded with a responsive Chrome
|
| 376 |
+
throw new Error('连接成功,但无法获取浏览器上下文。Chrome 可能没有响应或未完全初始化。');
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
} catch (error) {
|
| 380 |
+
console.warn(` ${YELLOW}连接尝试 ${i + 1} 失败: ${error.message.split('\n')[0]}${RESET}`);
|
| 381 |
+
if (browser && browser.isConnected()) {
|
| 382 |
+
// Should not happen if connectOverCDP failed, but good practice
|
| 383 |
+
await browser.close().catch(e => console.error("尝试关闭连接失败的浏览器时出错:", e));
|
| 384 |
+
}
|
| 385 |
+
browser = null;
|
| 386 |
+
context = null;
|
| 387 |
+
|
| 388 |
+
if (i < CONNECTION_RETRIES - 1) {
|
| 389 |
+
console.log(` ${YELLOW}可能原因: Chrome 未完全启动 / 端口 ${DEBUGGING_PORT} 未监听 / 端口被占用。${RESET}`);
|
| 390 |
+
console.log(`${DIM} 等待 ${RETRY_DELAY_MS / 1000} 秒后重试...${RESET}`);
|
| 391 |
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
| 392 |
+
} else {
|
| 393 |
+
console.error(`\n${RED}❌ 在 ${CONNECTION_RETRIES} 次尝试后仍然无法连接。${RESET}`);
|
| 394 |
+
console.error(' 请再次检查:');
|
| 395 |
+
console.error(' 1. Chrome 是否真的已经通过脚本成功启动,并且窗口可见、已加载?(可能需要登录Google)');
|
| 396 |
+
console.error(` 2. 是否有其他程序占用了端口 ${DEBUGGING_PORT}?(检查命令: macOS/Linux: lsof -i :${DEBUGGING_PORT} | Windows: netstat -ano | findstr ${DEBUGGING_PORT})`);
|
| 397 |
+
console.error(' 3. 启动 Chrome 时终端或系统是否有报错信息?');
|
| 398 |
+
console.error(' 4. 防火墙或安全软件是否阻止了本地回环地址(127.0.0.1)的连接?');
|
| 399 |
+
return false;
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
if (!browser || !context) {
|
| 405 |
+
console.error(`${RED}-> 未能成功连接到浏览器或获取上下文。${RESET}`);
|
| 406 |
+
return false;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
// --- 连接成功后的页面管理逻辑 ---
|
| 410 |
+
console.log(`\n${CYAN}--- 页面管理 ---${RESET}`);
|
| 411 |
+
try {
|
| 412 |
+
let targetPage = null;
|
| 413 |
+
let pages = [];
|
| 414 |
+
try {
|
| 415 |
+
pages = context.pages();
|
| 416 |
+
} catch (err) {
|
| 417 |
+
console.error(`${RED}❌ 获取现有页面列表时出错:${RESET}`, err);
|
| 418 |
+
console.log(" 将尝试打开新页面...");
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
console.log(`${DIM}-> 检查 ${pages.length} 个已存在的页面...${RESET}`);
|
| 422 |
+
const aiStudioUrlPattern = 'aistudio.google.com/';
|
| 423 |
+
const loginUrlPattern = 'accounts.google.com/';
|
| 424 |
+
|
| 425 |
+
for (const page of pages) {
|
| 426 |
+
try {
|
| 427 |
+
if (!page.isClosed()) {
|
| 428 |
+
const pageUrl = page.url();
|
| 429 |
+
console.log(`${DIM} 检查页面: ${pageUrl}${RESET}`);
|
| 430 |
+
// Prioritize AI Studio pages, then login pages
|
| 431 |
+
if (pageUrl.includes(aiStudioUrlPattern)) {
|
| 432 |
+
console.log(`-> ${GREEN}找到 AI Studio 页面:${RESET} ${pageUrl}`);
|
| 433 |
+
targetPage = page;
|
| 434 |
+
// Ensure it's the target URL if possible
|
| 435 |
+
if (!pageUrl.includes('/prompts/new_chat')) {
|
| 436 |
+
console.log(`${YELLOW} 非目标页面,尝试导航到 ${TARGET_URL}...${RESET}`);
|
| 437 |
+
try {
|
| 438 |
+
await targetPage.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS });
|
| 439 |
+
console.log(` ${GREEN}导航成功:${RESET} ${targetPage.url()}`);
|
| 440 |
+
} catch (navError) {
|
| 441 |
+
console.warn(` ${YELLOW}警告:导航到 ${TARGET_URL} 失败: ${navError.message.split('\n')[0]}${RESET}`);
|
| 442 |
+
console.warn(` ${YELLOW}将使用当前页面 (${pageUrl}),请稍后手动确认。${RESET}`);
|
| 443 |
+
}
|
| 444 |
+
} else {
|
| 445 |
+
console.log(` ${GREEN}页面已在目标路径或子路径。${RESET}`);
|
| 446 |
+
}
|
| 447 |
+
break; // Found a good AI Studio page
|
| 448 |
+
} else if (pageUrl.includes(loginUrlPattern) && !targetPage) {
|
| 449 |
+
// Keep track of a login page if no AI studio page is found yet
|
| 450 |
+
console.log(`-> ${YELLOW}发现 Google 登录页面,暂存。${RESET}`);
|
| 451 |
+
targetPage = page;
|
| 452 |
+
// Don't break here, keep looking for a direct AI Studio page
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
} catch (pageError) {
|
| 456 |
+
if (!page.isClosed()) {
|
| 457 |
+
console.warn(` ${YELLOW}警告:评估或导航页面时出错: ${pageError.message.split('\n')[0]}${RESET}`);
|
| 458 |
+
}
|
| 459 |
+
// Avoid using a page that caused an error
|
| 460 |
+
if (targetPage === page) {
|
| 461 |
+
targetPage = null;
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
// If after checking all pages, the best we found was a login page
|
| 467 |
+
if (targetPage && targetPage.url().includes(loginUrlPattern)) {
|
| 468 |
+
console.log(`-> ${YELLOW}未找到直接的 AI Studio 页面,将使用之前找到的登录页面。${RESET}`);
|
| 469 |
+
console.log(` ${YELLOW}请确保在该页面手动完成登录。${RESET}`);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
// If no suitable page was found at all
|
| 473 |
+
if (!targetPage) {
|
| 474 |
+
console.log(`-> ${YELLOW}未找到合适的现有页面。正在打开新页面并导航到 ${TARGET_URL}...${RESET}`);
|
| 475 |
+
try {
|
| 476 |
+
targetPage = await context.newPage();
|
| 477 |
+
console.log(`${DIM} 正在导航...${RESET}`);
|
| 478 |
+
await targetPage.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS });
|
| 479 |
+
console.log(`-> ${GREEN}新页面已打开并导航到:${RESET} ${targetPage.url()}`);
|
| 480 |
+
} catch (newPageError) {
|
| 481 |
+
console.error(`${RED}❌ 打开或导航新页面到 ${TARGET_URL} 失败: ${newPageError.message}${RESET}`);
|
| 482 |
+
console.error(" 请检查网络连接,以及 Chrome 是否能正常访问该网址。可能需要手动登录。" );
|
| 483 |
+
await browser.close().catch(e => {});
|
| 484 |
+
return false;
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
try {
|
| 489 |
+
await targetPage.bringToFront();
|
| 490 |
+
console.log('-> 已尝试将目标页面置于前台。');
|
| 491 |
+
} catch (bringToFrontError) {
|
| 492 |
+
console.warn(` ${YELLOW}警告:将页面置于前台失败: ${bringToFrontError.message.split('\n')[0]}${RESET}`);
|
| 493 |
+
console.warn(` (这可能发生在窗口最小化或位于不同虚拟桌面上时,通常不影响连接)`);
|
| 494 |
+
}
|
| 495 |
+
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay after bringToFront
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
console.log(`\n${BRIGHT}${GREEN}🎉 --- AI Studio 连接准备完成 --- 🎉${RESET}`);
|
| 499 |
+
console.log(`${GREEN}Chrome 已启动,Playwright 已连接,相关页面已找到或创建。${RESET}`);
|
| 500 |
+
console.log(`${YELLOW}请确保在 Chrome 窗口中 AI Studio 页面处于可交互状态 (例如,已登录Google, 无弹窗)。${RESET}`);
|
| 501 |
+
|
| 502 |
+
return true;
|
| 503 |
+
|
| 504 |
+
} catch (error) {
|
| 505 |
+
console.error(`\n${RED}❌ --- 步骤 3 页面管理失败 ---${RESET}`);
|
| 506 |
+
console.error(' 在连接成功后,处理页面时发生错误:', error);
|
| 507 |
+
if (browser && browser.isConnected()) {
|
| 508 |
+
await browser.close().catch(e => console.error("关闭浏览器时出错:", e));
|
| 509 |
+
}
|
| 510 |
+
return false;
|
| 511 |
+
} finally {
|
| 512 |
+
// 这里不再打印即将退出的日志,因为脚本会继续运行 server.js
|
| 513 |
+
// console.log("-> auto_connect_aistudio.js 步骤3结束。");
|
| 514 |
+
// 不需要手动断开 browser 连接,因为是 connectOverCDP
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
// --- 步骤 4: 启动 API 服务器 ---
|
| 520 |
+
function startApiServer() {
|
| 521 |
+
console.log(`${CYAN}-------------------------------------------------${RESET}`);
|
| 522 |
+
console.log(`${CYAN}--- 步骤 4: 启动 API 服务器 ('node ${SERVER_SCRIPT_FILENAME}') ---${RESET}`);
|
| 523 |
+
console.log(`${DIM} 脚本路径: ${SERVER_SCRIPT_PATH}${RESET}`);
|
| 524 |
+
|
| 525 |
+
if (!fs.existsSync(SERVER_SCRIPT_PATH)) {
|
| 526 |
+
console.error(`${RED}❌ 错误: 无法启动服务器,文件不存在: ${SERVER_SCRIPT_PATH}${RESET}`);
|
| 527 |
+
process.exit(1);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
console.log(`${DIM}正在启动: node ${SERVER_SCRIPT_PATH}${RESET}`);
|
| 531 |
+
|
| 532 |
+
try {
|
| 533 |
+
const serverProcess = spawn('node', [SERVER_SCRIPT_PATH], {
|
| 534 |
+
stdio: 'inherit',
|
| 535 |
+
cwd: __dirname
|
| 536 |
+
});
|
| 537 |
+
|
| 538 |
+
serverProcess.on('error', (err) => {
|
| 539 |
+
console.error(`${RED}❌ 启动 '${SERVER_SCRIPT_FILENAME}' 失败: ${err.message}${RESET}`);
|
| 540 |
+
console.error(`请检查 Node.js 是否已安装并配置在系统 PATH 中,以及 '${SERVER_SCRIPT_FILENAME}' 文件是否有效。`);
|
| 541 |
+
process.exit(1);
|
| 542 |
+
});
|
| 543 |
+
|
| 544 |
+
serverProcess.on('exit', (code, signal) => {
|
| 545 |
+
console.log(`\n${MAGENTA}👋 '${SERVER_SCRIPT_FILENAME}' 进程已退出 (代码: ${code}, 信号: ${signal})。${RESET}`);
|
| 546 |
+
console.log("自动连接脚本执行结束。");
|
| 547 |
+
process.exit(code ?? 0);
|
| 548 |
+
});
|
| 549 |
+
// Don't print the success message here, let server.cjs print its own ready message
|
| 550 |
+
// console.log("✅ '${SERVER_SCRIPT_FILENAME}' 已启动。脚本将保持运行,直到服务器进程结束或被手动中断。");
|
| 551 |
+
|
| 552 |
+
} catch (error) {
|
| 553 |
+
console.error(`${RED}❌ 启动 '${SERVER_SCRIPT_FILENAME}' 时发生意外错误: ${error.message}${RESET}`);
|
| 554 |
+
process.exit(1);
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
// --- 主执行流程 ---
|
| 560 |
+
(async () => {
|
| 561 |
+
console.log(`${MAGENTA}🚀 欢迎使用 AI Studio 自动连接与启动脚本 (跨平台优化, v2.9 自动端口清理) 🚀${RESET}`);
|
| 562 |
+
console.log(`${MAGENTA}=================================================${RESET}`);
|
| 563 |
+
|
| 564 |
+
if (!await checkDependencies()) {
|
| 565 |
+
process.exit(1);
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
console.log(`${MAGENTA}=================================================${RESET}`);
|
| 569 |
+
|
| 570 |
+
const launchResult = await launchChrome();
|
| 571 |
+
|
| 572 |
+
if (launchResult === false) {
|
| 573 |
+
console.log(`${RED}❌ 启动 Chrome 失败,脚本终止。${RESET}`);
|
| 574 |
+
process.exit(1);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
// 如果 launchResult 是 'use_existing' 或 true, 都需要连接
|
| 578 |
+
console.log(`${MAGENTA}=================================================${RESET}`);
|
| 579 |
+
if (!await connectAndManagePage()) {
|
| 580 |
+
// 如果连接失败,并且我们是尝试连接到现有实例,给出更具体的提示
|
| 581 |
+
if (launchResult === 'use_existing') {
|
| 582 |
+
console.error(`${RED}❌ 连接到现有 Chrome 实例 (端口 ${DEBUGGING_PORT}) 失败。${RESET}`);
|
| 583 |
+
console.error(' 请确认:');
|
| 584 |
+
console.error(' 1. 占用该端口的确实是您想连接的 Chrome 实例。');
|
| 585 |
+
console.error(' 2. 该 Chrome 实例是以 --remote-debugging-port 参数启动的。');
|
| 586 |
+
console.error(' 3. Chrome 实例本身运行正常,没有崩溃或无响应。');
|
| 587 |
+
}
|
| 588 |
+
process.exit(1);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
// 无论 Chrome 是新启动的还是已存在的,只要连接成功,就启动 API 服务器
|
| 592 |
+
console.log(`${MAGENTA}=================================================${RESET}`);
|
| 593 |
+
startApiServer();
|
| 594 |
+
|
| 595 |
+
})();
|
deprecated_javascript_version/package.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dependencies": {
|
| 3 |
+
"cors": "^2.8.5",
|
| 4 |
+
"express": "^4.19.2",
|
| 5 |
+
"playwright": "^1.44.1",
|
| 6 |
+
"@playwright/test": "^1.44.1"
|
| 7 |
+
}
|
| 8 |
+
}
|
deprecated_javascript_version/server.cjs
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// server.cjs (优化版 v2.17 - 增加日志ID & 常量)
|
| 2 |
+
|
| 3 |
+
const express = require('express');
|
| 4 |
+
const fs = require('fs');
|
| 5 |
+
const path = require('path');
|
| 6 |
+
const cors = require('cors');
|
| 7 |
+
|
| 8 |
+
// --- 依赖检查 ---
|
| 9 |
+
let playwright, expect;
|
| 10 |
+
const requiredModules = ['express', 'playwright', '@playwright/test', 'cors'];
|
| 11 |
+
const missingModules = [];
|
| 12 |
+
|
| 13 |
+
for (const modName of requiredModules) {
|
| 14 |
+
try {
|
| 15 |
+
if (modName === 'playwright') {
|
| 16 |
+
playwright = require(modName);
|
| 17 |
+
} else if (modName === '@playwright/test') {
|
| 18 |
+
expect = require(modName).expect;
|
| 19 |
+
} else {
|
| 20 |
+
require(modName);
|
| 21 |
+
}
|
| 22 |
+
// console.log(`✅ 模块 ${modName} 已加载。`); // Optional: Log success
|
| 23 |
+
} catch (e) {
|
| 24 |
+
console.error(`❌ 模块 ${modName} 未找到。`);
|
| 25 |
+
missingModules.push(modName);
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
if (missingModules.length > 0) {
|
| 30 |
+
console.error("-------------------------------------------------------------");
|
| 31 |
+
console.error("❌ 错误:缺少必要的依赖模块!");
|
| 32 |
+
console.error("请根据您使用的包管理器运行以下命令安装依赖:");
|
| 33 |
+
console.error("-------------------------------------------------------------");
|
| 34 |
+
console.error(` npm install ${missingModules.join(' ')}`);
|
| 35 |
+
console.error(" 或");
|
| 36 |
+
console.error(` yarn add ${missingModules.join(' ')}`);
|
| 37 |
+
console.error(" 或");
|
| 38 |
+
console.error(` pnpm install ${missingModules.join(' ')}`);
|
| 39 |
+
console.error("-------------------------------------------------------------");
|
| 40 |
+
process.exit(1);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// --- 配置 ---
|
| 44 |
+
const SERVER_PORT = process.env.PORT || 2048;
|
| 45 |
+
const CHROME_DEBUGGING_PORT = 8848;
|
| 46 |
+
const CDP_ADDRESS = `http://127.0.0.1:${CHROME_DEBUGGING_PORT}`;
|
| 47 |
+
const AI_STUDIO_URL_PATTERN = 'aistudio.google.com/';
|
| 48 |
+
const RESPONSE_COMPLETION_TIMEOUT = 300000; // 5分钟总超时
|
| 49 |
+
const POLLING_INTERVAL = 300; // 非流式/通用检查间隔
|
| 50 |
+
const POLLING_INTERVAL_STREAM = 200; // 流式检查轮询间隔 (ms)
|
| 51 |
+
// v2.12: Timeout for secondary checks *after* spinner disappears
|
| 52 |
+
const POST_SPINNER_CHECK_DELAY_MS = 500; // Spinner消失后稍作等待再检查其他状态
|
| 53 |
+
const FINAL_STATE_CHECK_TIMEOUT_MS = 1500; // 检查按钮和输入框最终状态的超时
|
| 54 |
+
const SPINNER_CHECK_TIMEOUT_MS = 1000; // 检查Spinner状态的超时
|
| 55 |
+
const POST_COMPLETION_BUFFER = 1000; // JSON模式下可以缩短检查后等待时间
|
| 56 |
+
const SILENCE_TIMEOUT_MS = 1500; // 文本静默多久后认为稳定 (Spinner消失后)
|
| 57 |
+
|
| 58 |
+
// --- 常量 ---
|
| 59 |
+
const MODEL_NAME = 'google-ai-studio-via-playwright-cdp-json';
|
| 60 |
+
const CHAT_COMPLETION_ID_PREFIX = 'chatcmpl-';
|
| 61 |
+
|
| 62 |
+
// --- 选择器常量 ---
|
| 63 |
+
const INPUT_SELECTOR = 'ms-prompt-input-wrapper textarea';
|
| 64 |
+
const SUBMIT_BUTTON_SELECTOR = 'button[aria-label="Run"]';
|
| 65 |
+
const RESPONSE_CONTAINER_SELECTOR = 'ms-chat-turn .chat-turn-container.model'; // 选择器指向 AI 模型回复的容器
|
| 66 |
+
const RESPONSE_TEXT_SELECTOR = 'ms-cmark-node.cmark-node';
|
| 67 |
+
const LOADING_SPINNER_SELECTOR = 'button[aria-label="Run"] svg .stoppable-spinner';
|
| 68 |
+
const ERROR_TOAST_SELECTOR = 'div.toast.warning, div.toast.error';
|
| 69 |
+
// !! 新增:清空聊天记录相关选择器 !!
|
| 70 |
+
const CLEAR_CHAT_BUTTON_SELECTOR = 'button[aria-label="Clear chat"][data-test-clear="outside"]:has(span.material-symbols-outlined:has-text("refresh"))'; // 清空按钮 (带图标确认)
|
| 71 |
+
const CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR = 'button.mdc-button:has-text("Continue")'; // 确认对话框中的 "Continue" 按钮
|
| 72 |
+
// !! 新增:清空验证相关常量 !!
|
| 73 |
+
const CLEAR_CHAT_VERIFY_TIMEOUT_MS = 5000; // 等待清空生效的总超时时间 (ms)
|
| 74 |
+
const CLEAR_CHAT_VERIFY_INTERVAL_MS = 300; // 检查清空状态的轮询间隔 (ms)
|
| 75 |
+
|
| 76 |
+
// v2.16: JSON Structure Prompt (Restored for non-streaming)
|
| 77 |
+
const prepareAIStudioPrompt = (userPrompt, systemPrompt = null) => {
|
| 78 |
+
let fullPrompt = `
|
| 79 |
+
IMPORTANT: Your entire response MUST be a single JSON object. Do not include any text outside of this JSON object.
|
| 80 |
+
The JSON object must have a single key named "response".
|
| 81 |
+
Inside the value of the "response" key (which is a string), you MUST put the exact marker "<<<START_RESPONSE>>>"" at the very beginning of your actual answer. There should be NO text before this marker within the response string.
|
| 82 |
+
`;
|
| 83 |
+
|
| 84 |
+
if (systemPrompt && systemPrompt.trim() !== '') {
|
| 85 |
+
fullPrompt += `\nSystem Instruction: ${systemPrompt}\n`;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
fullPrompt += `
|
| 89 |
+
Example 1:
|
| 90 |
+
User asks: "What is the capital of France?"
|
| 91 |
+
Your response MUST be:
|
| 92 |
+
{
|
| 93 |
+
"response": "<<<START_RESPONSE>>>The capital of France is Paris."
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
Example 2:
|
| 97 |
+
User asks: "Write a python function to add two numbers"
|
| 98 |
+
Your response MUST be:
|
| 99 |
+
{
|
| 100 |
+
"response": "<<<START_RESPONSE>>>\\\`\\\`\\\`python\\\\ndef add(a, b):\\\\n return a + b\\\\n\\\`\\\`\\\`"
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
Now, answer the following user prompt, ensuring your output strictly adheres to the JSON format AND the start marker requirement described above:
|
| 104 |
+
|
| 105 |
+
User Prompt: "${userPrompt}"
|
| 106 |
+
|
| 107 |
+
Your JSON Response:
|
| 108 |
+
`;
|
| 109 |
+
return fullPrompt;
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
// v2.26: Use JSON prompt for streaming as well -> vNEXT: Use Markdown Code Block for streaming
|
| 113 |
+
// vNEXT: Instruct AI to output *incomplete* JSON for streaming -> vNEXT: Instruct AI to output Markdown Code Block
|
| 114 |
+
const prepareAIStudioPromptStream = (userPrompt, systemPrompt = null) => {
|
| 115 |
+
let fullPrompt = `
|
| 116 |
+
IMPORTANT: For this streaming request, your entire response MUST be enclosed in a single markdown code block (like \`\`\` block \`\`\`).
|
| 117 |
+
Inside this code block, your actual answer text MUST start immediately after the exact marker "<<<START_RESPONSE>>>".
|
| 118 |
+
Start your response exactly with "\`\`\`\n<<<START_RESPONSE>>>" followed by your answer content.
|
| 119 |
+
Continue outputting your answer content. You SHOULD include the final closing "\`\`\`" at the very end of your full response stream.
|
| 120 |
+
`;
|
| 121 |
+
|
| 122 |
+
if (systemPrompt && systemPrompt.trim() !== '') {
|
| 123 |
+
fullPrompt += `\nSystem Instruction: ${systemPrompt}\n`;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
fullPrompt += `
|
| 127 |
+
Example 1 (Streaming):
|
| 128 |
+
User asks: "What is the capital of France?"
|
| 129 |
+
Your streamed response MUST look like this over time:
|
| 130 |
+
Stream part 1: \`\`\`\n<<<START_RESPONSE>>>The capital
|
| 131 |
+
Stream part 2: of France is
|
| 132 |
+
Stream part 3: Paris.\n\`\`\`
|
| 133 |
+
|
| 134 |
+
Example 2 (Streaming):
|
| 135 |
+
User asks: "Write a python function to add two numbers"
|
| 136 |
+
Your streamed response MUST look like this over time:
|
| 137 |
+
Stream part 1: \`\`\`\n<<<START_RESPONSE>>>\`\`\`python\ndef add(a, b):
|
| 138 |
+
Stream part 2: \n return a + b\n
|
| 139 |
+
Stream part 3: \`\`\`\n\`\`\`
|
| 140 |
+
|
| 141 |
+
Now, answer the following user prompt, ensuring your output strictly adheres to the markdown code block, start marker, and streaming requirements described above:
|
| 142 |
+
|
| 143 |
+
User Prompt: "${userPrompt}"
|
| 144 |
+
|
| 145 |
+
Your Response (Streaming, within a markdown code block):
|
| 146 |
+
`;
|
| 147 |
+
return fullPrompt;
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
const app = express();
|
| 151 |
+
|
| 152 |
+
// --- 全局变量 ---
|
| 153 |
+
let browser = null;
|
| 154 |
+
let page = null;
|
| 155 |
+
let isPlaywrightReady = false;
|
| 156 |
+
let isInitializing = false;
|
| 157 |
+
// v2.18: 请求队列和处理状态
|
| 158 |
+
let requestQueue = [];
|
| 159 |
+
let isProcessing = false;
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
// --- Playwright 初始化函数 ---
|
| 163 |
+
async function initializePlaywright() {
|
| 164 |
+
if (isPlaywrightReady || isInitializing) return;
|
| 165 |
+
isInitializing = true;
|
| 166 |
+
console.log(`--- 初始化 Playwright: 连接到 ${CDP_ADDRESS} ---`);
|
| 167 |
+
|
| 168 |
+
try {
|
| 169 |
+
browser = await playwright.chromium.connectOverCDP(CDP_ADDRESS, { timeout: 20000, ignoreHTTPSErrors: true });
|
| 170 |
+
console.log('✅ 成功连接到正在运行的 Chrome 实例!');
|
| 171 |
+
|
| 172 |
+
browser.once('disconnected', () => {
|
| 173 |
+
console.error('❌ Playwright 与 Chrome 的连接已断开!');
|
| 174 |
+
isPlaywrightReady = false;
|
| 175 |
+
browser = null;
|
| 176 |
+
page = null;
|
| 177 |
+
// v2.18: Clear queue on disconnect? Maybe not, let requests fail naturally.
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
await new Promise(resolve => setTimeout(resolve, 500));
|
| 181 |
+
|
| 182 |
+
const contexts = browser.contexts();
|
| 183 |
+
let context;
|
| 184 |
+
if (!contexts || contexts.length === 0) {
|
| 185 |
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
| 186 |
+
const retryContexts = browser.contexts();
|
| 187 |
+
if (!retryContexts || retryContexts.length === 0) {
|
| 188 |
+
throw new Error('无法获取浏览器上下文。请检查 Chrome 是否已正确启动并响应。');
|
| 189 |
+
}
|
| 190 |
+
context = retryContexts[0];
|
| 191 |
+
} else {
|
| 192 |
+
context = contexts[0];
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
let foundPage = null;
|
| 196 |
+
const pages = context.pages();
|
| 197 |
+
console.log(`-> 发现 ${pages.length} 个页面。正在搜索 AI Studio (匹配 "${AI_STUDIO_URL_PATTERN}")...`);
|
| 198 |
+
for (const p of pages) {
|
| 199 |
+
try {
|
| 200 |
+
if (p.isClosed()) continue;
|
| 201 |
+
const url = p.url();
|
| 202 |
+
if (url.includes(AI_STUDIO_URL_PATTERN) && url.includes('/prompts/')) {
|
| 203 |
+
console.log(`-> 找到 AI Studio 页面: ${url}`);
|
| 204 |
+
foundPage = p;
|
| 205 |
+
break;
|
| 206 |
+
}
|
| 207 |
+
} catch (pageError) {
|
| 208 |
+
if (!p.isClosed()) {
|
| 209 |
+
console.warn(` 警告:评估页面 URL 时出错: ${pageError.message.split('\\n')[0]}`);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if (!foundPage) {
|
| 215 |
+
throw new Error(`未在已连接的 Chrome 中找到包含 "${AI_STUDIO_URL_PATTERN}" 和 "/prompts/" 的页面。请确保 auto_connect_aistudio.js 已成功运行,并且 AI Studio 页面 (例如 prompts/new_chat) 已打开。`);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
page = foundPage;
|
| 219 |
+
console.log('-> 已定位到 AI Studio 页面。');
|
| 220 |
+
await page.bringToFront();
|
| 221 |
+
console.log('-> 尝试将页面置于前台。检查加载状态...');
|
| 222 |
+
await page.waitForLoadState('domcontentloaded', { timeout: 15000 });
|
| 223 |
+
console.log('-> 页面 DOM 已加载。');
|
| 224 |
+
|
| 225 |
+
try {
|
| 226 |
+
console.log("-> 尝试定位核心输入区域以确��页面就绪...");
|
| 227 |
+
await page.locator('ms-prompt-input-wrapper').waitFor({ state: 'visible', timeout: 15000 });
|
| 228 |
+
console.log("-> 核心输入区域容器已找到。");
|
| 229 |
+
} catch(initCheckError) {
|
| 230 |
+
console.warn(`⚠️ 初始化检查警告:未能快速定位到核心输入区域容器。页面可能仍在加载或结构有变: ${initCheckError.message.split('\\n')[0]}`);
|
| 231 |
+
await saveErrorSnapshot('init_check_fail');
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
isPlaywrightReady = true;
|
| 235 |
+
console.log('✅ Playwright 已准备就绪。');
|
| 236 |
+
// v2.18: Start processing queue if playwright just became ready and queue has items
|
| 237 |
+
if (requestQueue.length > 0 && !isProcessing) {
|
| 238 |
+
console.log(`[Queue] Playwright 就绪,队列中有 ${requestQueue.length} 个请求,开始处理...`);
|
| 239 |
+
processQueue();
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
} catch (error) {
|
| 243 |
+
console.error(`❌ 初始化 Playwright 失败: ${error.message}`);
|
| 244 |
+
await saveErrorSnapshot('init_fail');
|
| 245 |
+
isPlaywrightReady = false;
|
| 246 |
+
browser = null;
|
| 247 |
+
page = null;
|
| 248 |
+
} finally {
|
| 249 |
+
isInitializing = false;
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// --- 中间件 ---
|
| 254 |
+
app.use(cors());
|
| 255 |
+
app.use(express.json({ limit: '20mb' }));
|
| 256 |
+
app.use(express.urlencoded({ limit: '20mb', extended: true })); // Also for urlencoded
|
| 257 |
+
|
| 258 |
+
// --- Web UI Route ---
|
| 259 |
+
app.get('/', (req, res) => {
|
| 260 |
+
const htmlPath = path.join(__dirname, 'index.html');
|
| 261 |
+
if (fs.existsSync(htmlPath)) {
|
| 262 |
+
res.sendFile(htmlPath);
|
| 263 |
+
} else {
|
| 264 |
+
res.status(404).send('Error: index.html not found.');
|
| 265 |
+
}
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// --- 健康检查 ---
|
| 269 |
+
app.get('/health', (req, res) => {
|
| 270 |
+
const isConnected = browser?.isConnected() ?? false;
|
| 271 |
+
const isPageValid = page && !page.isClosed();
|
| 272 |
+
const queueLength = requestQueue.length;
|
| 273 |
+
const status = {
|
| 274 |
+
status: 'Unknown',
|
| 275 |
+
message: '',
|
| 276 |
+
playwrightReady: isPlaywrightReady,
|
| 277 |
+
browserConnected: isConnected,
|
| 278 |
+
pageValid: isPageValid,
|
| 279 |
+
initializing: isInitializing,
|
| 280 |
+
processing: isProcessing,
|
| 281 |
+
queueLength: queueLength
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
if (isPlaywrightReady && isPageValid && isConnected) {
|
| 285 |
+
status.status = 'OK';
|
| 286 |
+
status.message = `Server running, Playwright connected, page valid. Currently ${isProcessing ? 'processing' : 'idle'} with ${queueLength} item(s) in queue.`;
|
| 287 |
+
res.status(200).json(status);
|
| 288 |
+
} else {
|
| 289 |
+
status.status = 'Error';
|
| 290 |
+
const reasons = [];
|
| 291 |
+
if (!isPlaywrightReady) reasons.push("Playwright not initialized or ready");
|
| 292 |
+
if (!isPageValid) reasons.push("Target page not found or closed");
|
| 293 |
+
if (!isConnected) reasons.push("Browser disconnected");
|
| 294 |
+
if (isInitializing) reasons.push("Playwright is currently initializing");
|
| 295 |
+
status.message = `Service Unavailable. Issues: ${reasons.join(', ')}. Currently ${isProcessing ? 'processing' : 'idle'} with ${queueLength} item(s) in queue.`;
|
| 296 |
+
res.status(503).json(status);
|
| 297 |
+
}
|
| 298 |
+
});
|
| 299 |
+
|
| 300 |
+
// --- 新增:API 辅助函数 ---
|
| 301 |
+
|
| 302 |
+
// 验证聊天请求
|
| 303 |
+
// v2.19: Updated validation to handle array content (text parts only)
|
| 304 |
+
function validateChatRequest(messages) {
|
| 305 |
+
const reqId = messages?.[0]?.reqId || 'validation'; // Get reqId if passed, fallback
|
| 306 |
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
| 307 |
+
throw new Error(`[${reqId}] Invalid request: "messages" array is missing or empty.`);
|
| 308 |
+
}
|
| 309 |
+
const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
|
| 310 |
+
if (!lastUserMessage) {
|
| 311 |
+
throw new Error(`[${reqId}] Invalid request: No user message found in the "messages" array.`);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
let userPromptContentInput = lastUserMessage.content;
|
| 315 |
+
let processedUserPrompt = ""; // Initialize as empty string
|
| 316 |
+
|
| 317 |
+
// 1. Handle null/undefined content
|
| 318 |
+
if (userPromptContentInput === null || userPromptContentInput === undefined) {
|
| 319 |
+
console.warn(`[${reqId}] (Validation) Warning: Last user message content is null or undefined. Treating as empty string.`);
|
| 320 |
+
processedUserPrompt = "";
|
| 321 |
+
}
|
| 322 |
+
// 2. Handle string content (most common case)
|
| 323 |
+
else if (typeof userPromptContentInput === 'string') {
|
| 324 |
+
processedUserPrompt = userPromptContentInput;
|
| 325 |
+
}
|
| 326 |
+
// 3. Handle array content (attempt compatibility with OpenAI vision format)
|
| 327 |
+
else if (Array.isArray(userPromptContentInput)) {
|
| 328 |
+
console.log(`[${reqId}] (Validation) Info: Last user message content is an array. Processing text parts...`);
|
| 329 |
+
let textParts = [];
|
| 330 |
+
let unsupportedParts = false;
|
| 331 |
+
for (const item of userPromptContentInput) {
|
| 332 |
+
if (typeof item === 'object' && item !== null && item.type === 'text' && typeof item.text === 'string') {
|
| 333 |
+
textParts.push(item.text);
|
| 334 |
+
} else if (typeof item === 'object' && item !== null && item.type === 'image_url') {
|
| 335 |
+
console.warn(`[${reqId}] (Validation) Warning: Found 'image_url' content part. This proxy cannot process images via AI Studio web UI. Ignoring image.`);
|
| 336 |
+
unsupportedParts = true;
|
| 337 |
+
// Optionally, include the URL as text, but it might confuse the AI:
|
| 338 |
+
// textParts.push(`[Image URL (Unsupported): ${item.image_url?.url || 'N/A'}]`);
|
| 339 |
+
} else {
|
| 340 |
+
// Handle other unexpected items in the array - stringify them?
|
| 341 |
+
console.warn(`[${reqId}] (Validation) Warning: Found unexpected item in content array (Type: ${typeof item}). Converting to JSON string.`);
|
| 342 |
+
try {
|
| 343 |
+
textParts.push(JSON.stringify(item));
|
| 344 |
+
unsupportedParts = true;
|
| 345 |
+
} catch (e) {
|
| 346 |
+
console.error(`[${reqId}] (Validation) Error stringifying array item: ${e}. Skipping item.`);
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
processedUserPrompt = textParts.join('\\n'); // Join text parts with newline
|
| 351 |
+
if (unsupportedParts) {
|
| 352 |
+
console.warn(`[${reqId}] (Validation) Warning: Some parts of the array content were unsupported or ignored (e.g., images). Only text parts were included in the final prompt.`);
|
| 353 |
+
}
|
| 354 |
+
if (!processedUserPrompt) {
|
| 355 |
+
console.warn(`[${reqId}] (Validation) Warning: Processed array content resulted in an empty prompt.`);
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
// 4. Handle other object types (fallback to JSON stringify)
|
| 359 |
+
else if (typeof userPromptContentInput === 'object' && userPromptContentInput !== null) {
|
| 360 |
+
console.warn(`[${reqId}] (Validation) Warning: Last user message content is an object but not a recognized array format. Converting to JSON string.`);
|
| 361 |
+
try {
|
| 362 |
+
processedUserPrompt = JSON.stringify(userPromptContentInput);
|
| 363 |
+
} catch (stringifyError) {
|
| 364 |
+
console.error(`[${reqId}] (Validation) Error stringifying object user content: ${stringifyError}. Falling back to empty string.`);
|
| 365 |
+
processedUserPrompt = "";
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
// 5. Handle other primitive types (e.g., number, boolean) - convert to string
|
| 369 |
+
else {
|
| 370 |
+
console.warn(`[${reqId}] (Validation) Warning: Last user message content is an unexpected primitive type (${typeof userPromptContentInput}). Converting to string.`);
|
| 371 |
+
processedUserPrompt = String(userPromptContentInput);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Final check - should always be a string here
|
| 375 |
+
if (typeof processedUserPrompt !== 'string') {
|
| 376 |
+
console.error(`[${reqId}] (Validation) CRITICAL ERROR: Failed to process user prompt content into a string. Type after processing: ${typeof processedUserPrompt}. Using empty string.`);
|
| 377 |
+
processedUserPrompt = ""; // Safeguard
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
// Extract system prompt (remains the same logic)
|
| 382 |
+
const systemPromptContent = messages.find(msg => msg.role === 'system')?.content;
|
| 383 |
+
// Basic validation for system prompt (ensure it's a string if provided)
|
| 384 |
+
let processedSystemPrompt = null;
|
| 385 |
+
if (systemPromptContent !== null && systemPromptContent !== undefined) {
|
| 386 |
+
if (typeof systemPromptContent === 'string') {
|
| 387 |
+
processedSystemPrompt = systemPromptContent;
|
| 388 |
+
} else {
|
| 389 |
+
console.warn(`[${reqId}] (Validation) Warning: System prompt content is not a string (Type: ${typeof systemPromptContent}). Ignoring system prompt.`);
|
| 390 |
+
// Optionally stringify it: processedSystemPrompt = JSON.stringify(systemPromptContent);
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
return {
|
| 396 |
+
userPrompt: processedUserPrompt, // Ensure this is always a string
|
| 397 |
+
systemPrompt: processedSystemPrompt // Ensure this is null or a string
|
| 398 |
+
};
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
// 与页面交互并提交 Prompt
|
| 402 |
+
async function interactAndSubmitPrompt(page, prompt, reqId) {
|
| 403 |
+
console.log(`[${reqId}] 开始页面交互...`);
|
| 404 |
+
const inputField = page.locator(INPUT_SELECTOR);
|
| 405 |
+
const submitButton = page.locator(SUBMIT_BUTTON_SELECTOR);
|
| 406 |
+
const loadingSpinner = page.locator(LOADING_SPINNER_SELECTOR); // Keep spinner locator here for later use
|
| 407 |
+
|
| 408 |
+
console.log(`[${reqId}] - 等待输入框可用...`);
|
| 409 |
+
try {
|
| 410 |
+
await inputField.waitFor({ state: 'visible', timeout: 10000 });
|
| 411 |
+
} catch (e) {
|
| 412 |
+
console.error(`[${reqId}] ❌ 查找输入框失败!`);
|
| 413 |
+
await saveErrorSnapshot(`input_field_not_visible_${reqId}`);
|
| 414 |
+
throw new Error(`[${reqId}] Failed to find visible input field. Error: ${e.message}`);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
console.log(`[${reqId}] - 清空并填充输入框...`);
|
| 418 |
+
await inputField.fill(prompt, { timeout: 60000 });
|
| 419 |
+
|
| 420 |
+
console.log(`[${reqId}] - 等待运行按钮可用...`);
|
| 421 |
+
try {
|
| 422 |
+
await expect(submitButton).toBeEnabled({ timeout: 10000 });
|
| 423 |
+
} catch (e) {
|
| 424 |
+
console.error(`[${reqId}] ❌ 等待运行按钮变为可用状态超时!`);
|
| 425 |
+
await saveErrorSnapshot(`submit_button_not_enabled_before_click_${reqId}`);
|
| 426 |
+
throw new Error(`[${reqId}] Submit button not enabled before click. Error: ${e.message}`);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
console.log(`[${reqId}] - 点击运行按钮...`);
|
| 430 |
+
await submitButton.click({ timeout: 10000 });
|
| 431 |
+
|
| 432 |
+
return { inputField, submitButton, loadingSpinner }; // Return locators
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
// 定位最新的回复元素
|
| 436 |
+
async function locateResponseElements(page, { inputField, submitButton, loadingSpinner }, reqId) {
|
| 437 |
+
console.log(`[${reqId}] 定位 AI 回复元素...`);
|
| 438 |
+
let lastResponseContainer;
|
| 439 |
+
let responseElement;
|
| 440 |
+
let locatedResponseElements = false;
|
| 441 |
+
|
| 442 |
+
for (let i = 0; i < 3 && !locatedResponseElements; i++) {
|
| 443 |
+
try {
|
| 444 |
+
console.log(`[${reqId}] 尝试定位最新回复容器及文本元素 (第 ${i + 1} 次)`);
|
| 445 |
+
await page.waitForTimeout(500 + i * 500); // 固有延迟
|
| 446 |
+
|
| 447 |
+
const isEndState = await checkEndConditionQuickly(page, loadingSpinner, inputField, submitButton, 250, reqId);
|
| 448 |
+
const locateTimeout = isEndState ? 3000 : 60000;
|
| 449 |
+
if (isEndState) {
|
| 450 |
+
console.log(`[${reqId}] -> 检测到结束条件已满足,使用 ${locateTimeout / 1000}s 超时进行定位。`);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
lastResponseContainer = page.locator(RESPONSE_CONTAINER_SELECTOR).last();
|
| 454 |
+
await lastResponseContainer.waitFor({ state: 'attached', timeout: locateTimeout });
|
| 455 |
+
|
| 456 |
+
responseElement = lastResponseContainer.locator(RESPONSE_TEXT_SELECTOR);
|
| 457 |
+
await responseElement.waitFor({ state: 'attached', timeout: locateTimeout });
|
| 458 |
+
|
| 459 |
+
console.log(`[${reqId}] 回复容器和文本元素定位成功。`);
|
| 460 |
+
locatedResponseElements = true;
|
| 461 |
+
} catch (locateError) {
|
| 462 |
+
console.warn(`[${reqId}] 第 ${i + 1} 次定位回复元素失败: ${locateError.message.split('\n')[0]}`);
|
| 463 |
+
if (i === 2) {
|
| 464 |
+
await saveErrorSnapshot(`response_locate_fail_${reqId}`);
|
| 465 |
+
throw new Error(`[${reqId}] Failed to locate response elements after multiple attempts.`);
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
if (!locatedResponseElements) throw new Error(`[${reqId}] Could not locate response elements.`);
|
| 470 |
+
return { responseElement, lastResponseContainer }; // Return located elements
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// --- 新增:处理流式响应 (vNEXT: 标记优先,静默结束,无JSON处理) ---
|
| 474 |
+
async function handleStreamingResponse(res, responseElement, page, { inputField, submitButton, loadingSpinner }, operationTimer, reqId, isRequestCancelled) {
|
| 475 |
+
console.log(`[${reqId}] - 流式传输开始 (vNEXT: Marker priority, silence end, no JSON handling)...`); // TODO: Update version
|
| 476 |
+
let lastRawText = "";
|
| 477 |
+
let lastSentResponseContent = ""; // Tracks content *after* the marker that has been SENT
|
| 478 |
+
let responseStarted = false; // Tracks if <<<START_RESPONSE>>> has been seen
|
| 479 |
+
const startTime = Date.now();
|
| 480 |
+
let spinnerHasDisappeared = false;
|
| 481 |
+
let lastTextChangeTimestamp = Date.now();
|
| 482 |
+
const startMarker = '<<<START_RESPONSE>>>';
|
| 483 |
+
let streamFinishedNaturally = false;
|
| 484 |
+
|
| 485 |
+
while (Date.now() - startTime < RESPONSE_COMPLETION_TIMEOUT && !streamFinishedNaturally) {
|
| 486 |
+
// --- 添加检查:请求是否已取消 ---
|
| 487 |
+
const cancelled = isRequestCancelled(); // 调用检查函数
|
| 488 |
+
// 添加日志记录检查结果
|
| 489 |
+
// console.log(`[${reqId}] (Streaming Loop Check) isRequestCancelled() returned: ${cancelled}`); // 可选:过于频繁,暂时注释掉
|
| 490 |
+
if (cancelled) {
|
| 491 |
+
console.log(`[${reqId}] (Streaming) 检测到请求已取消 (isRequestCancelled() is true),停止处理。`); // 修改日志
|
| 492 |
+
clearTimeout(operationTimer); // 确保定时器清除
|
| 493 |
+
if (!res.writableEnded) res.end(); // 确保响应结束
|
| 494 |
+
return; // 退出函数
|
| 495 |
+
}
|
| 496 |
+
// --- 结束检查 ---
|
| 497 |
+
|
| 498 |
+
const loopStartTime = Date.now();
|
| 499 |
+
|
| 500 |
+
// 1. Get current raw text
|
| 501 |
+
const currentRawText = await getRawTextContent(responseElement, lastRawText, reqId);
|
| 502 |
+
|
| 503 |
+
if (currentRawText !== lastRawText) {
|
| 504 |
+
lastTextChangeTimestamp = Date.now();
|
| 505 |
+
let potentialNewDelta = "";
|
| 506 |
+
let currentContentAfterMarker = "";
|
| 507 |
+
|
| 508 |
+
// 2. Marker Check & Delta Calculation
|
| 509 |
+
const markerIndex = currentRawText.indexOf(startMarker);
|
| 510 |
+
if (markerIndex !== -1) {
|
| 511 |
+
if (!responseStarted) {
|
| 512 |
+
console.log(`[${reqId}] (流式 Simple) 检测到 ${startMarker},开始传输...`);
|
| 513 |
+
responseStarted = true;
|
| 514 |
+
}
|
| 515 |
+
// Content after marker in the current raw text
|
| 516 |
+
currentContentAfterMarker = currentRawText.substring(markerIndex + startMarker.length);
|
| 517 |
+
// Calculate new content since last *sent* content
|
| 518 |
+
potentialNewDelta = currentContentAfterMarker.substring(lastSentResponseContent.length);
|
| 519 |
+
} else if(responseStarted) {
|
| 520 |
+
// If marker was seen before, but now disappears (e.g., AI cleared output?), treat as no new delta.
|
| 521 |
+
potentialNewDelta = "";
|
| 522 |
+
console.warn(`[${reqId}] Marker disappeared after being seen. Raw: ${currentRawText.substring(0,100)}`);
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
// 3. Send Delta if found
|
| 526 |
+
if (potentialNewDelta) {
|
| 527 |
+
// console.log(`[${reqId}] (Send Stream Simple) Sending Delta (len: ${potentialNewDelta.length})`);
|
| 528 |
+
sendStreamChunk(res, potentialNewDelta, reqId);
|
| 529 |
+
lastSentResponseContent += potentialNewDelta; // Update tracking
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
// Update last raw text
|
| 533 |
+
lastRawText = currentRawText;
|
| 534 |
+
|
| 535 |
+
} // End if(currentRawText !== lastRawText)
|
| 536 |
+
|
| 537 |
+
// 4. Check Spinner status
|
| 538 |
+
if (!spinnerHasDisappeared) {
|
| 539 |
+
try {
|
| 540 |
+
await expect(loadingSpinner).toBeHidden({ timeout: 50 });
|
| 541 |
+
spinnerHasDisappeared = true;
|
| 542 |
+
lastTextChangeTimestamp = Date.now(); // Reset silence timer when spinner disappears
|
| 543 |
+
console.log(`[${reqId}] Spinner 已消失,进入静默期检测...`);
|
| 544 |
+
} catch (e) { /* Spinner still visible */ }
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// 5. Silence Check (Standard)
|
| 548 |
+
const isSilent = spinnerHasDisappeared && (Date.now() - lastTextChangeTimestamp > SILENCE_TIMEOUT_MS);
|
| 549 |
+
|
| 550 |
+
if (isSilent) {
|
| 551 |
+
console.log(`[${reqId}] Silence detected. Finishing stream.`);
|
| 552 |
+
streamFinishedNaturally = true;
|
| 553 |
+
break; // Exit loop
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
// 6. Control polling interval
|
| 557 |
+
const loopEndTime = Date.now();
|
| 558 |
+
const loopDuration = loopEndTime - loopStartTime;
|
| 559 |
+
const waitTime = Math.max(0, POLLING_INTERVAL_STREAM - loopDuration);
|
| 560 |
+
await page.waitForTimeout(waitTime);
|
| 561 |
+
|
| 562 |
+
} // --- End main loop ---
|
| 563 |
+
|
| 564 |
+
// --- Cleanup and End --- (如果循环是因取消而退出,下面的代码不会执行)
|
| 565 |
+
clearTimeout(operationTimer); // Clear the specific timer for THIS request
|
| 566 |
+
|
| 567 |
+
if (!streamFinishedNaturally && Date.now() - startTime >= RESPONSE_COMPLETION_TIMEOUT) {
|
| 568 |
+
// Timeout case
|
| 569 |
+
console.warn(`[${reqId}] - 流式传输(Simple模式)因总超时 (${RESPONSE_COMPLETION_TIMEOUT / 1000}s) 结束。`);
|
| 570 |
+
await saveErrorSnapshot(`streaming_simple_timeout_${reqId}`);
|
| 571 |
+
if (!res.writableEnded) {
|
| 572 |
+
sendStreamError(res, "Stream processing timed out on server (Simple mode).", reqId);
|
| 573 |
+
}
|
| 574 |
+
} else if (streamFinishedNaturally && !res.writableEnded) {
|
| 575 |
+
// Natural end (Silence detected)
|
| 576 |
+
// --- Final Sync (Simple Mode) ---
|
| 577 |
+
// Check one last time for any content received after the last delta was sent but before silence was declared.
|
| 578 |
+
console.log(`[${reqId}] (Simple Stream) Loop ended naturally, performing final sync check...`);
|
| 579 |
+
const finalRawText = await getRawTextContent(responseElement, lastRawText, reqId);
|
| 580 |
+
console.log(`[${reqId}] (Simple Stream) Performing final marker check and delta calculation...`);
|
| 581 |
+
try {
|
| 582 |
+
let finalExtractedContent = ""; // Content after marker
|
| 583 |
+
const finalMarkerIndex = finalRawText.indexOf(startMarker);
|
| 584 |
+
if (finalMarkerIndex !== -1) {
|
| 585 |
+
finalExtractedContent = finalRawText.substring(finalMarkerIndex + startMarker.length);
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
const finalDelta = finalExtractedContent.substring(lastSentResponseContent.length);
|
| 589 |
+
|
| 590 |
+
if (finalDelta){
|
| 591 |
+
console.log(`[${reqId}] (Final Sync Simple) Sending final delta (len: ${finalDelta.length})`);
|
| 592 |
+
sendStreamChunk(res, finalDelta, reqId);
|
| 593 |
+
} else {
|
| 594 |
+
console.log(`[${reqId}] (Final Sync Simple) No final delta to send based on lastSent comparison.`);
|
| 595 |
+
}
|
| 596 |
+
} catch (e) { console.warn(`[${reqId}] (Simple Stream) Final sync error during marker/delta calc: ${e.message}`); }
|
| 597 |
+
// --- End Final Sync ---
|
| 598 |
+
|
| 599 |
+
res.write('data: [DONE]\n\n');
|
| 600 |
+
res.end();
|
| 601 |
+
console.log(`[${reqId}] ✅ 流式(Simple模式)响应 [DONE] 已发送。`);
|
| 602 |
+
} else if (res.writableEnded) {
|
| 603 |
+
console.log(`[${reqId}] 流(Simple模式)已提前结束 (writableEnded=true),不再发送 [DONE]。`);
|
| 604 |
+
} else {
|
| 605 |
+
console.log(`[${reqId}] 流(Simple模式)结束时状态异常 (finishedNaturally=${streamFinishedNaturally}, writableEnded=${res.writableEnded}),不再发送 [DONE]。`);
|
| 606 |
+
}
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// --- 新增:处理非流式响应 --- vNEXT: Restore JSON Parsing
|
| 610 |
+
async function handleNonStreamingResponse(res, page, locators, operationTimer, reqId, isRequestCancelled) {
|
| 611 |
+
console.log(`[${reqId}] - 等待 AI 处理完成 (检查 Spinner 消失 + 输入框空 + 按钮禁用)...`);
|
| 612 |
+
let processComplete = false;
|
| 613 |
+
const nonStreamStartTime = Date.now();
|
| 614 |
+
let finalStateCheckInitiated = false;
|
| 615 |
+
const { inputField, submitButton, loadingSpinner } = locators;
|
| 616 |
+
|
| 617 |
+
// Completion check logic
|
| 618 |
+
while (!processComplete && Date.now() - nonStreamStartTime < RESPONSE_COMPLETION_TIMEOUT) {
|
| 619 |
+
// --- 添加检查:请求是否已取消 ---
|
| 620 |
+
if (isRequestCancelled()) {
|
| 621 |
+
console.log(`[${reqId}] (Non-Streaming) 检测到请求已取消,停止等待完成状态。`);
|
| 622 |
+
clearTimeout(operationTimer); // 确保定时器清除
|
| 623 |
+
if (!res.headersSent) {
|
| 624 |
+
// 如果头还没发送,可以发送一个取消错误
|
| 625 |
+
res.status(499).json({ error: { message: `[${reqId}] Client closed request`, type: 'client_error' } });
|
| 626 |
+
} else if (!res.writableEnded) {
|
| 627 |
+
res.end(); // 否则只结束响应
|
| 628 |
+
}
|
| 629 |
+
return; // 退出函数
|
| 630 |
+
}
|
| 631 |
+
// --- 结束检查 ---
|
| 632 |
+
|
| 633 |
+
let isSpinnerHidden = false;
|
| 634 |
+
let isInputEmpty = false;
|
| 635 |
+
let isButtonDisabled = false;
|
| 636 |
+
|
| 637 |
+
try {
|
| 638 |
+
await expect(loadingSpinner).toBeHidden({ timeout: SPINNER_CHECK_TIMEOUT_MS });
|
| 639 |
+
isSpinnerHidden = true;
|
| 640 |
+
} catch { /* Spinner still visible */ }
|
| 641 |
+
|
| 642 |
+
if (isSpinnerHidden) {
|
| 643 |
+
try {
|
| 644 |
+
await expect(inputField).toHaveValue('', { timeout: FINAL_STATE_CHECK_TIMEOUT_MS });
|
| 645 |
+
isInputEmpty = true;
|
| 646 |
+
} catch { /* Input not empty */ }
|
| 647 |
+
|
| 648 |
+
if (isInputEmpty) {
|
| 649 |
+
try {
|
| 650 |
+
await expect(submitButton).toBeDisabled({ timeout: FINAL_STATE_CHECK_TIMEOUT_MS });
|
| 651 |
+
isButtonDisabled = true;
|
| 652 |
+
} catch { /* Button not disabled */ }
|
| 653 |
+
}
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
if (isSpinnerHidden && isInputEmpty && isButtonDisabled) {
|
| 657 |
+
if (!finalStateCheckInitiated) {
|
| 658 |
+
finalStateCheckInitiated = true;
|
| 659 |
+
console.log(`[${reqId}] 检测到潜在最终状态。等待 ${POST_COMPLETION_BUFFER}ms 进行确认...`); // Use constant
|
| 660 |
+
await page.waitForTimeout(POST_COMPLETION_BUFFER); // Wait a bit first
|
| 661 |
+
console.log(`[${reqId}] ${POST_COMPLETION_BUFFER}ms 等待结束,重新检查状态...`);
|
| 662 |
+
try {
|
| 663 |
+
await expect(loadingSpinner).toBeHidden({ timeout: 500 });
|
| 664 |
+
await expect(inputField).toHaveValue('', { timeout: 500 });
|
| 665 |
+
await expect(submitButton).toBeDisabled({ timeout: 500 });
|
| 666 |
+
console.log(`[${reqId}] 状态确认成功。开始文本静默检查...`);
|
| 667 |
+
|
| 668 |
+
// --- NEW: Text Silence Check ---
|
| 669 |
+
let lastCheckText = '';
|
| 670 |
+
let currentCheckText = '';
|
| 671 |
+
let textStable = false;
|
| 672 |
+
const silenceCheckStartTime = Date.now();
|
| 673 |
+
// Re-locate response element here for the check
|
| 674 |
+
const { responseElement: checkResponseElement } = await locateResponseElements(page, locators, reqId);
|
| 675 |
+
|
| 676 |
+
while (Date.now() - silenceCheckStartTime < SILENCE_TIMEOUT_MS * 2) { // Check for up to 2*silence duration
|
| 677 |
+
lastCheckText = currentCheckText;
|
| 678 |
+
currentCheckText = await getRawTextContent(checkResponseElement, lastCheckText, reqId);
|
| 679 |
+
if (currentCheckText === lastCheckText) {
|
| 680 |
+
// Text hasn't changed since last check in this loop
|
| 681 |
+
if (Date.now() - silenceCheckStartTime >= SILENCE_TIMEOUT_MS) {
|
| 682 |
+
// And enough time has passed
|
| 683 |
+
console.log(`[${reqId}] 文本内容静默 ${SILENCE_TIMEOUT_MS}ms,确认处理完成。`);
|
| 684 |
+
textStable = true;
|
| 685 |
+
break;
|
| 686 |
+
}
|
| 687 |
+
} else {
|
| 688 |
+
// Text changed, reset silence timer within this check
|
| 689 |
+
// silenceCheckStartTime = Date.now(); // Option: Reset timer on any change
|
| 690 |
+
console.log(`[${reqId}] (静默检查) 文本仍在变化...`);
|
| 691 |
+
}
|
| 692 |
+
await page.waitForTimeout(POLLING_INTERVAL); // Use standard poll interval for checks
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
if (textStable) {
|
| 696 |
+
processComplete = true; // Mark process as complete
|
| 697 |
+
} else {
|
| 698 |
+
console.warn(`[${reqId}] 警告: 文本静默检查超时,可能仍在输出。将继续尝试解析。`);
|
| 699 |
+
processComplete = true; // Proceed anyway after timeout, but log warning
|
| 700 |
+
}
|
| 701 |
+
// --- END NEW: Text Silence Check ---
|
| 702 |
+
|
| 703 |
+
} catch (recheckError) {
|
| 704 |
+
console.log(`[${reqId}] 状态在确认期间发生变化 (${recheckError.message.split('\\n')[0]})。继续轮询...`);
|
| 705 |
+
finalStateCheckInitiated = false;
|
| 706 |
+
}
|
| 707 |
+
}
|
| 708 |
+
} else {
|
| 709 |
+
if (finalStateCheckInitiated) {
|
| 710 |
+
console.log(`[${reqId}] 最终状态不再满足,重置确认标志。`);
|
| 711 |
+
finalStateCheckInitiated = false;
|
| 712 |
+
}
|
| 713 |
+
await page.waitForTimeout(POLLING_INTERVAL * 2); // Longer wait if not in final state check
|
| 714 |
+
}
|
| 715 |
+
} // --- End Completion check logic loop ---
|
| 716 |
+
|
| 717 |
+
// --- 添加检查:如果在循环结束后发现请求已取消 ---
|
| 718 |
+
if (isRequestCancelled()) {
|
| 719 |
+
console.log(`[${reqId}] (Non-Streaming) 请求在等待完成后被取消,不再继续处理。`);
|
| 720 |
+
// 定时器和响应应该已经被上面的检查处理了,这里只退出
|
| 721 |
+
return;
|
| 722 |
+
}
|
| 723 |
+
// --- 结束检查 ---
|
| 724 |
+
|
| 725 |
+
// Check for Page Errors BEFORE attempting to parse JSON
|
| 726 |
+
console.log(`[${reqId}] - 检查页面上是否存在错误提示...`);
|
| 727 |
+
const pageError = await detectAndExtractPageError(page, reqId);
|
| 728 |
+
if (pageError) {
|
| 729 |
+
console.error(`[${reqId}] ❌ 检测到 AI Studio 页面错误: ${pageError}`);
|
| 730 |
+
await saveErrorSnapshot(`page_error_detected_${reqId}`);
|
| 731 |
+
throw new Error(`[${reqId}] AI Studio Error: ${pageError}`);
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
if (!processComplete) {
|
| 735 |
+
console.warn(`[${reqId}] 警告:等待最终完成状态超时或未能稳定确认 (${(Date.now() - nonStreamStartTime) / 1000}s)。将直接尝试获取并解析JSON。`);
|
| 736 |
+
await saveErrorSnapshot(`nonstream_final_state_timeout_${reqId}`);
|
| 737 |
+
} else {
|
| 738 |
+
console.log(`[${reqId}] - 开始获取并解析最终 JSON...`);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
// Get and Parse JSON
|
| 742 |
+
let aiResponseText = null;
|
| 743 |
+
const maxRetries = 3;
|
| 744 |
+
let attempts = 0;
|
| 745 |
+
|
| 746 |
+
while (attempts < maxRetries && aiResponseText === null) {
|
| 747 |
+
attempts++;
|
| 748 |
+
console.log(`[${reqId}] - 尝试获取原始文本并解析 JSON (第 ${attempts} 次)...`);
|
| 749 |
+
try {
|
| 750 |
+
// Re-locate response element within the retry loop for robustness
|
| 751 |
+
const { responseElement: currentResponseElement } = await locateResponseElements(page, locators, reqId);
|
| 752 |
+
|
| 753 |
+
const rawText = await getRawTextContent(currentResponseElement, '', reqId);
|
| 754 |
+
|
| 755 |
+
if (!rawText || rawText.trim() === '') {
|
| 756 |
+
console.warn(`[${reqId}] - 第 ${attempts} 次获取的原始文本为空。`);
|
| 757 |
+
throw new Error("Raw text content is empty.");
|
| 758 |
+
}
|
| 759 |
+
console.log(`[${reqId}] - 获取到原始文本 (长度: ${rawText.length}): \"${rawText.substring(0,100)}...\"`);
|
| 760 |
+
|
| 761 |
+
const parsedJson = tryParseJson(rawText, reqId);
|
| 762 |
+
|
| 763 |
+
if (parsedJson) {
|
| 764 |
+
if (typeof parsedJson.response === 'string') {
|
| 765 |
+
aiResponseText = parsedJson.response;
|
| 766 |
+
console.log(`[${reqId}] - 成功解析 JSON 并提取 'response' 字段。`);
|
| 767 |
+
} else {
|
| 768 |
+
// JSON 有效但无 response 字段
|
| 769 |
+
try {
|
| 770 |
+
aiResponseText = JSON.stringify(parsedJson);
|
| 771 |
+
console.log(`[${reqId}] - 警告: 未找到 'response' 字段,但解析到有效 JSON。将整个 JSON 字符串化作为回复。`);
|
| 772 |
+
} catch (stringifyError) {
|
| 773 |
+
console.error(`[${reqId}] - 错误:无法将解析出的 JSON 字符串化: ${stringifyError.message}`);
|
| 774 |
+
aiResponseText = null;
|
| 775 |
+
throw new Error("Failed to stringify the parsed JSON object.");
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
+
} else {
|
| 779 |
+
// JSON 解析失败
|
| 780 |
+
console.warn(`[${reqId}] - 第 ${attempts} 次未能解析 JSON。`);
|
| 781 |
+
aiResponseText = null;
|
| 782 |
+
if (attempts >= maxRetries) {
|
| 783 |
+
await saveErrorSnapshot(`json_parse_fail_final_attempt_${reqId}`);
|
| 784 |
+
}
|
| 785 |
+
throw new Error("Failed to parse JSON from raw text.");
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
break;
|
| 789 |
+
|
| 790 |
+
} catch (e) {
|
| 791 |
+
console.warn(`[${reqId}] - 第 ${attempts} 次获取或解析失败: ${e.message.split('\n')[0]}`);
|
| 792 |
+
aiResponseText = null;
|
| 793 |
+
if (attempts >= maxRetries) {
|
| 794 |
+
console.error(`[${reqId}] - 多次尝试获取并解析 JSON 失败。`);
|
| 795 |
+
if (!e.message?.includes('snapshot')) await saveErrorSnapshot(`get_parse_json_failed_final_${reqId}`);
|
| 796 |
+
aiResponseText = ""; // Fallback to empty string
|
| 797 |
+
} else {
|
| 798 |
+
await new Promise(resolve => setTimeout(resolve, 1500 + attempts * 500));
|
| 799 |
+
}
|
| 800 |
+
}
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
if (aiResponseText === null) {
|
| 804 |
+
console.log(`[${reqId}] - JSON 解析失败,再次检查页面错误...`);
|
| 805 |
+
const finalCheckError = await detectAndExtractPageError(page, reqId);
|
| 806 |
+
if (finalCheckError) {
|
| 807 |
+
console.error(`[${reqId}] ❌ 检测到 AI Studio 页面错误 (在 JSON 解析失败后): ${finalCheckError}`);
|
| 808 |
+
await saveErrorSnapshot(`page_error_post_json_fail_${reqId}`);
|
| 809 |
+
throw new Error(`[${reqId}] AI Studio Error after JSON parse failed: ${finalCheckError}`);
|
| 810 |
+
}
|
| 811 |
+
console.warn(`[${reqId}] 警告:所有尝试均未能获取并解析出有效的 JSON 回复。返回空回复。`);
|
| 812 |
+
aiResponseText = "";
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
// Handle potential nested JSON
|
| 816 |
+
let cleanedResponse = aiResponseText;
|
| 817 |
+
try {
|
| 818 |
+
// Attempt to parse the potential stringified JSON again for nested 'response' check
|
| 819 |
+
// Only attempt if aiResponseText is likely a stringified JSON object/array
|
| 820 |
+
if (aiResponseText && aiResponseText.startsWith('{') || aiResponseText.startsWith('[')) {
|
| 821 |
+
const outerParsed = JSON.parse(aiResponseText); // Use JSON.parse directly here
|
| 822 |
+
const innerParsed = tryParseJson(outerParsed.response, reqId); // Try parsing the inner 'response' field if it exists
|
| 823 |
+
if (innerParsed && typeof innerParsed.response === 'string') {
|
| 824 |
+
console.log(`[${reqId}] (非流式) 检测到嵌套 JSON,使用内层 response 内容。`);
|
| 825 |
+
cleanedResponse = innerParsed.response;
|
| 826 |
+
} else if (typeof outerParsed.response === 'string') {
|
| 827 |
+
// If the *outer* 'response' was already a string (not nested JSON), use it directly
|
| 828 |
+
console.log(`[${reqId}] (非流式) 使用外层 'response' 字段内容。`);
|
| 829 |
+
cleanedResponse = outerParsed.response;
|
| 830 |
+
}
|
| 831 |
+
// If neither inner nor outer 'response' fields are relevant strings, keep the stringified JSON as cleanedResponse
|
| 832 |
+
}
|
| 833 |
+
} catch (e) {
|
| 834 |
+
// If parsing aiResponseText fails, it means it wasn't a stringified JSON in the first place,
|
| 835 |
+
// or it was malformed. Keep the original aiResponseText.
|
| 836 |
+
// console.warn(`[${reqId}] (Info) Post-processing check: aiResponseText ('${aiResponseText.substring(0,50)}...') is not a parseable JSON or lacks 'response'. Keeping original value. Error: ${e.message}`);
|
| 837 |
+
cleanedResponse = aiResponseText; // Keep original if parsing fails
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
console.log(`[${reqId}] ✅ 获取到解析后的 AI 回复 (来自JSON, 长度: ${cleanedResponse?.length ?? 0}): \"${cleanedResponse?.substring(0, 100)}...\"`);
|
| 841 |
+
|
| 842 |
+
// --- 新增步骤:在非流式响应中移除标记 ---
|
| 843 |
+
const startMarker = '<<<START_RESPONSE>>>';
|
| 844 |
+
|
| 845 |
+
let finalContentForUser = cleanedResponse; // 默认使用清理后的响应
|
| 846 |
+
|
| 847 |
+
// Check for and remove the starting marker if present
|
| 848 |
+
if (finalContentForUser?.startsWith(startMarker)) {
|
| 849 |
+
finalContentForUser = finalContentForUser.substring(startMarker.length);
|
| 850 |
+
console.log(`[${reqId}] (非流式 JSON) 移除前缀 ${startMarker},最终内容长度: ${finalContentForUser.length}`);
|
| 851 |
+
} else if (aiResponseText !== null && aiResponseText !== "") { // 仅在获取到非空文本但无标记时警告
|
| 852 |
+
console.warn(`[${reqId}] (非流式 JSON) 警告: 未在 response 字段中找到预期的 ${startMarker} 前缀。内容: \"${aiResponseText.substring(0,50)}...\"`);
|
| 853 |
+
}
|
| 854 |
+
// --- 结束新增步骤 ---
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
// 使用移除标记后的内容构建最终响应
|
| 858 |
+
const responsePayload = {
|
| 859 |
+
id: `${CHAT_COMPLETION_ID_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
|
| 860 |
+
object: 'chat.completion',
|
| 861 |
+
created: Math.floor(Date.now() / 1000),
|
| 862 |
+
model: MODEL_NAME,
|
| 863 |
+
choices: [{
|
| 864 |
+
index: 0,
|
| 865 |
+
message: { role: 'assistant', content: finalContentForUser }, // Use cleaned content
|
| 866 |
+
finish_reason: 'stop',
|
| 867 |
+
}],
|
| 868 |
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
| 869 |
+
};
|
| 870 |
+
console.log(`[${reqId}] ✅ 返回 JSON 响应 (来自解析后的JSON)。`);
|
| 871 |
+
clearTimeout(operationTimer); // Clear the specific timer for THIS request
|
| 872 |
+
res.json(responsePayload);
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
// --- 新增:处理 /v1/models 请求以满足 Open WebUI 验证 ---
|
| 876 |
+
app.get('/v1/models', (req, res) => {
|
| 877 |
+
const modelId = 'aistudio-proxy'; // 您计划在 Open WebUI 中使用的模型名称
|
| 878 |
+
// 使用简短的日志ID或时间戳
|
| 879 |
+
const logPrefix = `[${Date.now().toString(36).slice(-5)}]`;
|
| 880 |
+
console.log(`${logPrefix} --- 收到 /v1/models 请求,返回模拟模型列表 ---`);
|
| 881 |
+
res.json({
|
| 882 |
+
object: "list",
|
| 883 |
+
data: [
|
| 884 |
+
{
|
| 885 |
+
id: modelId, // 返回您要用的那个名字
|
| 886 |
+
object: "model",
|
| 887 |
+
created: Math.floor(Date.now() / 1000),
|
| 888 |
+
owned_by: "openai-proxy", // 可以随便写
|
| 889 |
+
permission: [],
|
| 890 |
+
root: modelId,
|
| 891 |
+
parent: null
|
| 892 |
+
}
|
| 893 |
+
// 如果需要添加更多名称指向同一个代理,可以在此添加
|
| 894 |
+
// ,{
|
| 895 |
+
// id: "gemini-pro-proxy",
|
| 896 |
+
// object: "model",
|
| 897 |
+
// created: Math.floor(Date.now() / 1000),
|
| 898 |
+
// owned_by: "openai-proxy",
|
| 899 |
+
// permission: [],
|
| 900 |
+
// root: "gemini-pro-proxy",
|
| 901 |
+
// parent: null
|
| 902 |
+
// }
|
| 903 |
+
]
|
| 904 |
+
});
|
| 905 |
+
});
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
// --- v2.18: 新增队列处理函数 ---
|
| 909 |
+
async function processQueue() {
|
| 910 |
+
if (isProcessing || requestQueue.length === 0) {
|
| 911 |
+
return;
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
isProcessing = true;
|
| 915 |
+
// 从队列头部取出包含状态的请求项
|
| 916 |
+
const queueItem = requestQueue.shift();
|
| 917 |
+
// 解构所需变量,包括取消标记和临时处理器
|
| 918 |
+
const { req, res, reqId, isCancelledByClient, preliminaryCloseHandler } = queueItem;
|
| 919 |
+
|
| 920 |
+
// --- 重要:立即移除临时监听器(如果存在且未被触发移除)---
|
| 921 |
+
// 因为我们要么跳过处理,要么添加新的主监听器
|
| 922 |
+
if (preliminaryCloseHandler) {
|
| 923 |
+
// 使用 removeListener 以防万一它已被触发并自我移除
|
| 924 |
+
res.removeListener('close', preliminaryCloseHandler);
|
| 925 |
+
}
|
| 926 |
+
// --- 结束移除临时监听器 ---
|
| 927 |
+
|
| 928 |
+
// --- 新增:检查请求是否在处理前已被取消 ---
|
| 929 |
+
if (isCancelledByClient) {
|
| 930 |
+
console.log(`[${reqId}] Request was cancelled by client before processing began. Skipping.`);
|
| 931 |
+
// 清理可能由其他地方(如主 close 事件处理器)设置的定时器,以防万一
|
| 932 |
+
if (operationTimer) clearTimeout(operationTimer);
|
| 933 |
+
// 标记处理结束(跳过),然后处理下一个
|
| 934 |
+
isProcessing = false;
|
| 935 |
+
processQueue(); // 尝试处理下一个请求
|
| 936 |
+
return; // 退出当前 processQueue 调用
|
| 937 |
+
}
|
| 938 |
+
// --- 结束新增检查 ---
|
| 939 |
+
|
| 940 |
+
console.log(`\n[${reqId}] ---开始处理队列中的请求 (剩余 ${requestQueue.length} 个)---`);
|
| 941 |
+
|
| 942 |
+
let operationTimer; // 主操作定时器
|
| 943 |
+
// *** 修改:将 isCancelledByClient 的状态传递给处理期间的 isCancelled 标志 ***
|
| 944 |
+
let isCancelled = isCancelledByClient;
|
| 945 |
+
// 如果在开始处理时就已经被取消,添加一条日志
|
| 946 |
+
if (isCancelled) {
|
| 947 |
+
console.log(`[${reqId}] Warning: Request was cancelled very shortly before processing logic started.`);
|
| 948 |
+
// 虽然上面的检查理论上会处理,但这里多一层保险
|
| 949 |
+
}
|
| 950 |
+
// *** 结束修改 ***
|
| 951 |
+
let closeEventHandler = null; // 主 close 事件处理器引用
|
| 952 |
+
|
| 953 |
+
try {
|
| 954 |
+
// 1. 检查 Playwright 状态 (现在可以安全地继续,因为请求未被提前取消)
|
| 955 |
+
// *** 新增:如果此时 isCancelled 已经是 true,则直接跳到 finally ***
|
| 956 |
+
if (isCancelled) {
|
| 957 |
+
console.log(`[${reqId}] Skipping Playwright interaction as request is already marked cancelled.`);
|
| 958 |
+
throw new Error(`[${reqId}] Request pre-cancelled`); // 抛出错误以跳到 catch/finally
|
| 959 |
+
}
|
| 960 |
+
// *** 结束新增检查 ***
|
| 961 |
+
|
| 962 |
+
if (!isPlaywrightReady && !isInitializing) {
|
| 963 |
+
console.warn(`[${reqId}] Playwright 未就绪,尝试重新初始化...`);
|
| 964 |
+
await initializePlaywright();
|
| 965 |
+
}
|
| 966 |
+
if (!isPlaywrightReady || !page || page.isClosed() || !browser?.isConnected()) {
|
| 967 |
+
console.error(`[${reqId}] API 请求失败:Playwright 未就绪、页面关闭或连接断开。`);
|
| 968 |
+
let detail = 'Unknown issue.';
|
| 969 |
+
if (!browser?.isConnected()) detail = "Browser connection lost.";
|
| 970 |
+
else if (!page || page.isClosed()) detail = "Target AI Studio page is not available or closed.";
|
| 971 |
+
else if (!isPlaywrightReady) detail = "Playwright initialization failed or incomplete.";
|
| 972 |
+
console.error(`[${reqId}] Playwright 连接不可用详情: ${detail}`);
|
| 973 |
+
// 直接为当前请求返回错误,不需要抛出,因为要继续处理队列
|
| 974 |
+
if (!res.headersSent) {
|
| 975 |
+
res.status(503).json({
|
| 976 |
+
error: { message: `[${reqId}] Playwright connection is not active. ${detail} Please ensure Chrome is running correctly, the AI Studio tab is open, and potentially restart the server.`, type: 'server_error' }
|
| 977 |
+
});
|
| 978 |
+
}
|
| 979 |
+
throw new Error("Playwright not ready for this request."); // Throw to skip further processing in try block
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
const { messages, stream, ...otherParams } = req.body;
|
| 983 |
+
const isStreaming = stream === true;
|
| 984 |
+
|
| 985 |
+
// --- 修改:基于消息数量启发式判断并执行清空操作 + 验证 ---
|
| 986 |
+
const isLikelyNewChat = Array.isArray(messages) && (messages.length === 1 || (messages.length === 2 && messages.some(m => m.role === 'system')));
|
| 987 |
+
|
| 988 |
+
if (isLikelyNewChat && CLEAR_CHAT_BUTTON_SELECTOR && CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR) {
|
| 989 |
+
console.log(`[${reqId}] 检测到可能是新对话 (消息数: ${messages.length}),尝试清空聊天记录...`);
|
| 990 |
+
try {
|
| 991 |
+
const clearButton = page.locator(CLEAR_CHAT_BUTTON_SELECTOR);
|
| 992 |
+
console.log(`[${reqId}] - 查找并点击"Clear chat"按钮...`);
|
| 993 |
+
await clearButton.waitFor({ state: 'visible', timeout: 7000 });
|
| 994 |
+
await clearButton.click({ timeout: 5000 });
|
| 995 |
+
console.log(`[${reqId}] - "Clear chat"按钮已点击。`);
|
| 996 |
+
|
| 997 |
+
console.log(`[${reqId}] - 等待确认对话框及"Continue"按钮出现...`);
|
| 998 |
+
const confirmButton = page.locator(CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR);
|
| 999 |
+
await confirmButton.waitFor({ state: 'visible', timeout: 5000 });
|
| 1000 |
+
|
| 1001 |
+
console.log(`[${reqId}] - 点击"Continue"按钮...`);
|
| 1002 |
+
await confirmButton.click({ timeout: 5000 });
|
| 1003 |
+
console.log(`[${reqId}] - "Continue"按钮已点击。开始验证清空效果...`);
|
| 1004 |
+
|
| 1005 |
+
// --- 新增:验证清空效果 ---
|
| 1006 |
+
const checkStartTime = Date.now();
|
| 1007 |
+
let cleared = false;
|
| 1008 |
+
while (Date.now() - checkStartTime < CLEAR_CHAT_VERIFY_TIMEOUT_MS) {
|
| 1009 |
+
// 定位所有 AI 回复容器
|
| 1010 |
+
const modelTurns = page.locator(RESPONSE_CONTAINER_SELECTOR);
|
| 1011 |
+
const count = await modelTurns.count();
|
| 1012 |
+
if (count === 0) {
|
| 1013 |
+
console.log(`[${reqId}] ✅ 验证成功: 页面上未找到之前的 AI 回复元素 (耗时 ${Date.now() - checkStartTime}ms)。`);
|
| 1014 |
+
cleared = true;
|
| 1015 |
+
break; // 验证成功,退出循环
|
| 1016 |
+
}
|
| 1017 |
+
// 稍微等待后再次检查
|
| 1018 |
+
await page.waitForTimeout(CLEAR_CHAT_VERIFY_INTERVAL_MS);
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
if (!cleared) {
|
| 1022 |
+
// 如果超时后仍然找到 AI 回复元素
|
| 1023 |
+
console.warn(`[${reqId}] ⚠️ 验证超时: 在 ${CLEAR_CHAT_VERIFY_TIMEOUT_MS}ms 内仍能检测到之前的 AI 回复元素。上下文可能未完全清空。`);
|
| 1024 |
+
// 保存快照以供调试
|
| 1025 |
+
await saveErrorSnapshot(`clear_chat_verify_fail_${reqId}`);
|
| 1026 |
+
}
|
| 1027 |
+
// --- 结束:验证清空效果 ---
|
| 1028 |
+
|
| 1029 |
+
} catch (clearChatError) {
|
| 1030 |
+
console.warn(`[${reqId}] ⚠️ 清空聊天记录或验证时出错: ${clearChatError.message.split('\n')[0]}. 将继续执行请求,但上下文可能未被清除。`);
|
| 1031 |
+
if (clearChatError.message.includes('selector')) {
|
| 1032 |
+
console.warn(` (请仔细检查选择器是否仍然有效: CLEAR_CHAT_BUTTON_SELECTOR='${CLEAR_CHAT_BUTTON_SELECTOR}', CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR='${CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR}')`);
|
| 1033 |
+
}
|
| 1034 |
+
await saveErrorSnapshot(`clear_chat_fail_or_verify_${reqId}`);
|
| 1035 |
+
}
|
| 1036 |
+
} else if (isLikelyNewChat && (!CLEAR_CHAT_BUTTON_SELECTOR || !CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR)) {
|
| 1037 |
+
console.warn(`[${reqId}] 检测到可能是新对话,但未完整配置清空聊天相关的选择器常量,无法自动重置上下文。`);
|
| 1038 |
+
}
|
| 1039 |
+
// --- 结束:启发式新对话处理 ---
|
| 1040 |
+
|
| 1041 |
+
console.log(`[${reqId}] 请求模式: ${isStreaming ? '流式 (SSE)' : '非流式 (JSON)'}`);
|
| 1042 |
+
|
| 1043 |
+
// 2. 设置此请求的总操作超时
|
| 1044 |
+
operationTimer = setTimeout(async () => {
|
| 1045 |
+
await saveErrorSnapshot(`operation_timeout_${reqId}`);
|
| 1046 |
+
console.error(`[${reqId}] Operation timed out after ${RESPONSE_COMPLETION_TIMEOUT / 1000} seconds.`);
|
| 1047 |
+
if (!res.headersSent) {
|
| 1048 |
+
res.status(504).json({ error: { message: `[${reqId}] Operation timed out`, type: 'timeout_error' } });
|
| 1049 |
+
} else if (isStreaming && !res.writableEnded) {
|
| 1050 |
+
sendStreamError(res, "Operation timed out on server.", reqId);
|
| 1051 |
+
}
|
| 1052 |
+
// Note: Timeout error now managed within processQueue, allowing next item to proceed
|
| 1053 |
+
}, RESPONSE_COMPLETION_TIMEOUT);
|
| 1054 |
+
|
| 1055 |
+
// 3. 验证请求 (使用更新后的函数)
|
| 1056 |
+
// Pass reqId to validation for better logging context
|
| 1057 |
+
const validationMessages = messages.map(m => ({ ...m, reqId })); // Add reqId temporarily
|
| 1058 |
+
const { userPrompt, systemPrompt: extractedSystemPrompt } = validateChatRequest(validationMessages);
|
| 1059 |
+
// Combine system prompts if provided in multiple ways
|
| 1060 |
+
const systemPrompt = extractedSystemPrompt || otherParams?.system_prompt;
|
| 1061 |
+
|
| 1062 |
+
// --- Logging (Now userPrompt is guaranteed to be a string) ---
|
| 1063 |
+
const userPromptPreview = userPrompt.substring(0, 80);
|
| 1064 |
+
console.log(`[${reqId}] 处理后的 User Prompt (用于提交, start): \"${userPromptPreview}...\" (Total length: ${userPrompt.length})`);
|
| 1065 |
+
|
| 1066 |
+
if (systemPrompt) {
|
| 1067 |
+
// systemPrompt from validateChatRequest is also guaranteed string or null
|
| 1068 |
+
const systemPromptPreview = systemPrompt.substring(0, 80);
|
| 1069 |
+
console.log(`[${reqId}] 处理后的 System Prompt (用于提交, start): \"${systemPromptPreview}...\"`);
|
| 1070 |
+
} else {
|
| 1071 |
+
console.log(`[${reqId}] 无 System Prompt。`);
|
| 1072 |
+
}
|
| 1073 |
+
if (Object.keys(otherParams).length > 0) {
|
| 1074 |
+
console.log(`[${reqId}] 记录到的额外参数: ${JSON.stringify(otherParams)}`);
|
| 1075 |
+
}
|
| 1076 |
+
// --- End Logging ---
|
| 1077 |
+
|
| 1078 |
+
// 4. 准备 Prompt (使用处理后的 userPrompt 和 systemPrompt)
|
| 1079 |
+
let prompt;
|
| 1080 |
+
if (isStreaming) {
|
| 1081 |
+
prompt = prepareAIStudioPromptStream(userPrompt, systemPrompt); // Assumes prepare functions handle null systemPrompt
|
| 1082 |
+
console.log(`[${reqId}] 构建的流式 Prompt (Raw): \"${prompt.substring(0, 200)}...\"`);
|
| 1083 |
+
} else {
|
| 1084 |
+
prompt = prepareAIStudioPrompt(userPrompt, systemPrompt); // Assumes prepare functions handle null systemPrompt
|
| 1085 |
+
console.log(`[${reqId}] 构建的非流式 Prompt (JSON): \"${prompt.substring(0, 200)}...\"`);
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
// 5. 与页面交互并提交
|
| 1089 |
+
const locators = await interactAndSubmitPrompt(page, prompt, reqId);
|
| 1090 |
+
|
| 1091 |
+
// --- 添加 'close' 事件监听器 ---
|
| 1092 |
+
closeEventHandler = async () => {
|
| 1093 |
+
console.log(`[${reqId}] 'close' event handler triggered.`); // <-- 新增日志
|
| 1094 |
+
if (isCancelled) {
|
| 1095 |
+
console.log(`[${reqId}] 'close' event handler: Already cancelled, doing nothing.`); // <-- 新增日志
|
| 1096 |
+
return; // 防止重复执行
|
| 1097 |
+
}
|
| 1098 |
+
isCancelled = true;
|
| 1099 |
+
console.log(`[${reqId}] Client disconnected ('close' event). Attempting to stop generation by clicking the run/stop button.`);
|
| 1100 |
+
clearTimeout(operationTimer); // 清除主超时定时器
|
| 1101 |
+
|
| 1102 |
+
// 尝试点击运行/停止按钮 (因为它是同一个按钮)
|
| 1103 |
+
try {
|
| 1104 |
+
// 确保 locators, submitButton, inputField 存在
|
| 1105 |
+
if (!locators || !locators.submitButton || !locators.inputField) {
|
| 1106 |
+
console.warn(`[${reqId}] closeEventHandler: Cannot attempt to click stop button: locators (button or input) not available.`); // <-- 修改日志
|
| 1107 |
+
return;
|
| 1108 |
+
}
|
| 1109 |
+
// 检查按钮是否仍然可用 (增加超时)
|
| 1110 |
+
console.log(`[${reqId}] closeEventHandler: Checking button state (timeout: 2000ms)...`); // <-- 修改日志
|
| 1111 |
+
const isEnabled = await locators.submitButton.isEnabled({ timeout: 2000 }); // <-- 增加超时
|
| 1112 |
+
console.log(`[${reqId}] closeEventHandler: Button isEnabled result: ${isEnabled}`); // <-- 新增日志
|
| 1113 |
+
|
| 1114 |
+
if (isEnabled) {
|
| 1115 |
+
// *** 新增:检查输入框是否为空 (增加超时) ***
|
| 1116 |
+
console.log(`[${reqId}] closeEventHandler: Button enabled, checking input value (timeout: 2000ms)...`); // <-- 修改日志
|
| 1117 |
+
const inputValue = await locators.inputField.inputValue({ timeout: 2000 }); // <-- 增加超时
|
| 1118 |
+
console.log(`[${reqId}] closeEventHandler: Input value: "${inputValue}"`); // <-- 新增日志
|
| 1119 |
+
if (inputValue === '') {
|
| 1120 |
+
console.log(`[${reqId}] closeEventHandler: Run/Stop button is enabled AND input is empty. Clicking it to stop generation...`); // <-- 修改日志
|
| 1121 |
+
// 使用 click({ force: true }) 可能更可靠
|
| 1122 |
+
await locators.submitButton.click({ timeout: 5000, force: true });
|
| 1123 |
+
console.log(`[${reqId}] closeEventHandler: Run/Stop button click attempted.`); // <-- 修改日志
|
| 1124 |
+
} else {
|
| 1125 |
+
console.log(`[${reqId}] closeEventHandler: Run/Stop button is enabled BUT input is NOT empty. Assuming user typed new input, not clicking stop.`); // <-- 修改日志
|
| 1126 |
+
}
|
| 1127 |
+
// *** 结束新增检查 ***
|
| 1128 |
+
} else {
|
| 1129 |
+
console.log(`[${reqId}] closeEventHandler: Run/Stop button is already disabled (generation likely finished or close event was late). No click needed.`); // <-- 修改日志
|
| 1130 |
+
}
|
| 1131 |
+
} catch (clickError) {
|
| 1132 |
+
// 捕获检查或点击过程中的错误
|
| 1133 |
+
console.warn(`[${reqId}] closeEventHandler: Error during stop button check/click: ${clickError.message.split('\n')[0]}`); // <-- 修改日志
|
| 1134 |
+
// 添加更详细日志并尝试保存快照
|
| 1135 |
+
console.error(`[${reqId}] closeEventHandler: Detailed error during check/click:`, clickError);
|
| 1136 |
+
await saveErrorSnapshot(`close_handler_click_error_${reqId}`);
|
| 1137 |
+
}
|
| 1138 |
+
};
|
| 1139 |
+
res.on('close', closeEventHandler);
|
| 1140 |
+
// --- 结束添加监听器 ---
|
| 1141 |
+
|
| 1142 |
+
// 6. 定位响应元素
|
| 1143 |
+
const { responseElement } = await locateResponseElements(page, locators, reqId);
|
| 1144 |
+
|
| 1145 |
+
// 7. 处理响应 (流式或非流式)
|
| 1146 |
+
console.log(`[${reqId}] 处理 AI 回复...`);
|
| 1147 |
+
if (isStreaming) {
|
| 1148 |
+
// --- 设置流式响应头 ---
|
| 1149 |
+
res.setHeader('Content-Type', 'text/event-stream');
|
| 1150 |
+
res.setHeader('Cache-Control', 'no-cache');
|
| 1151 |
+
res.setHeader('Connection', 'keep-alive');
|
| 1152 |
+
res.flushHeaders();
|
| 1153 |
+
|
| 1154 |
+
// 调用流式处理函数
|
| 1155 |
+
// 传递检查函数 () => isCancelled
|
| 1156 |
+
await handleStreamingResponse(res, responseElement, page, locators, operationTimer, reqId, () => isCancelled);
|
| 1157 |
+
|
| 1158 |
+
} else {
|
| 1159 |
+
// 调用非流式处理函数
|
| 1160 |
+
// 传递检查函数 () => isCancelled
|
| 1161 |
+
await handleNonStreamingResponse(res, page, locators, operationTimer, reqId, () => isCancelled);
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
// --- 修改:仅在未被取消时记录成功 ---
|
| 1165 |
+
if (!isCancelled) {
|
| 1166 |
+
console.log(`[${reqId}] ✅ 请求处理成功完成。`);
|
| 1167 |
+
clearTimeout(operationTimer); // 只有真正成功完成才清除计时器
|
| 1168 |
+
} else {
|
| 1169 |
+
console.log(`[${reqId}] ℹ️ 请求处理因客户端断开连接而被中止。`);
|
| 1170 |
+
// operationTimer 应该已经在 closeEventHandler 中被清除了
|
| 1171 |
+
}
|
| 1172 |
+
// --- 结束修改 ---
|
| 1173 |
+
|
| 1174 |
+
} catch (error) {
|
| 1175 |
+
// 确保在任何错误情况下都清除此请求的定时器 (如果 close 事件未触发)
|
| 1176 |
+
if (!isCancelled) {
|
| 1177 |
+
clearTimeout(operationTimer);
|
| 1178 |
+
}
|
| 1179 |
+
console.error(`[${reqId}] ❌ 处理队列中的请求时出错: ${error.message}\n${error.stack}`);
|
| 1180 |
+
|
| 1181 |
+
// --- 恢复:添加条件判断是否需要保存快照 ---
|
| 1182 |
+
const shouldSaveSnapshot = !(
|
| 1183 |
+
error.message?.includes('Invalid request') || // 跳过请求验证错误
|
| 1184 |
+
error.message?.includes('Playwright not ready') // 跳过 Playwright 初始化/连接错误
|
| 1185 |
+
// 未来可以根据需要添加其他不需要快照的错误类型
|
| 1186 |
+
);
|
| 1187 |
+
|
| 1188 |
+
if (shouldSaveSnapshot && !error.message?.includes('snapshot') && !error.stack?.includes('saveErrorSnapshot')) {
|
| 1189 |
+
// 避免在保存快照本身失败或已知Playwright问题时再次尝试保存
|
| 1190 |
+
await saveErrorSnapshot(`general_api_error_${reqId}`);
|
| 1191 |
+
} else if (!shouldSaveSnapshot) {
|
| 1192 |
+
console.log(`[${reqId}] (Info) Skipping error snapshot for this type of error: ${error.message.split('\n')[0]}`);
|
| 1193 |
+
}
|
| 1194 |
+
// --- 结束恢复 ---
|
| 1195 |
+
|
| 1196 |
+
// 发送错误响应,如果尚未发送
|
| 1197 |
+
if (!res.headersSent) {
|
| 1198 |
+
let statusCode = 500;
|
| 1199 |
+
let errorType = 'server_error';
|
| 1200 |
+
if (error.message?.includes('timed out') || error.message?.includes('timeout')) {
|
| 1201 |
+
statusCode = 504; // Gateway Timeout
|
| 1202 |
+
errorType = 'timeout_error';
|
| 1203 |
+
} else if (error.message?.includes('AI Studio Error')) {
|
| 1204 |
+
statusCode = 502; // Bad Gateway (error from upstream)
|
| 1205 |
+
errorType = 'upstream_error';
|
| 1206 |
+
} else if (error.message?.includes('Invalid request')) {
|
| 1207 |
+
statusCode = 400; // Bad Request
|
| 1208 |
+
errorType = 'invalid_request_error';
|
| 1209 |
+
} else if (error.message?.includes('Playwright not ready')) { // Specific handling for PW not ready here
|
| 1210 |
+
statusCode = 503;
|
| 1211 |
+
errorType = 'server_error';
|
| 1212 |
+
}
|
| 1213 |
+
res.status(statusCode).json({ error: { message: `[${reqId}] ${error.message}`, type: errorType } });
|
| 1214 |
+
} else if (req.body.stream === true && !res.writableEnded) { // Check if it WAS a streaming request
|
| 1215 |
+
// 如果是流式响应且头部已发送,则发送流式错误
|
| 1216 |
+
sendStreamError(res, error.message, reqId);
|
| 1217 |
+
}
|
| 1218 |
+
else if (!res.writableEnded) {
|
| 1219 |
+
// 对于非流式但已发送部分内容的罕见情况,或流式错误发送后的清理
|
| 1220 |
+
res.end();
|
| 1221 |
+
}
|
| 1222 |
+
} finally {
|
| 1223 |
+
// --- 添加清理逻辑 ---
|
| 1224 |
+
if (closeEventHandler) {
|
| 1225 |
+
res.removeListener('close', closeEventHandler);
|
| 1226 |
+
// console.log(`[${reqId}] Removed 'close' event listener.`); // Optional debug log
|
| 1227 |
+
}
|
| 1228 |
+
// --- 结束清理逻辑 ---
|
| 1229 |
+
isProcessing = false; // 标记处理已结束
|
| 1230 |
+
console.log(`[${reqId}] ---结束处理队列中的请求---`);
|
| 1231 |
+
// 触发处理下一个请求(如果队列中有)
|
| 1232 |
+
processQueue();
|
| 1233 |
+
}
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
// --- API 端点 (v2.18: 使用队列) ---
|
| 1237 |
+
app.post('/v1/chat/completions', async (req, res) => {
|
| 1238 |
+
const reqId = Math.random().toString(36).substring(2, 9); // 生成简短的请求 ID
|
| 1239 |
+
console.log(`\n[${reqId}] === 收到 /v1/chat/completions 请求 ===`);
|
| 1240 |
+
|
| 1241 |
+
// 创建请求队列项,并添加取消标记和临时监听器引用
|
| 1242 |
+
const queueItem = {
|
| 1243 |
+
req,
|
| 1244 |
+
res,
|
| 1245 |
+
reqId,
|
| 1246 |
+
isCancelledByClient: false,
|
| 1247 |
+
preliminaryCloseHandler: null
|
| 1248 |
+
};
|
| 1249 |
+
|
| 1250 |
+
// --- 添加临时的 'close' 事件监听器 ---
|
| 1251 |
+
queueItem.preliminaryCloseHandler = () => {
|
| 1252 |
+
if (!queueItem.isCancelledByClient) { // 避免重复标记
|
| 1253 |
+
console.log(`[${reqId}] Client disconnected before processing started.`);
|
| 1254 |
+
queueItem.isCancelledByClient = true;
|
| 1255 |
+
// 从 res 对象移除自身,防止后续冲突
|
| 1256 |
+
res.removeListener('close', queueItem.preliminaryCloseHandler);
|
| 1257 |
+
}
|
| 1258 |
+
};
|
| 1259 |
+
res.once('close', queueItem.preliminaryCloseHandler); // 使用 once 确保最多触发一次
|
| 1260 |
+
// --- 结束添加临时监听器 ---
|
| 1261 |
+
|
| 1262 |
+
// 将请求加入队列
|
| 1263 |
+
requestQueue.push(queueItem); // <-- 推入包含标记的对象
|
| 1264 |
+
console.log(`[${reqId}] 请求已加入队列 (当前队列长度: ${requestQueue.length})`);
|
| 1265 |
+
|
| 1266 |
+
// 尝试处理队列 (如果当前未在处理)
|
| 1267 |
+
if (!isProcessing) {
|
| 1268 |
+
console.log(`[Queue] 触发队列处理 (收到新请求 ${reqId} 时处于空闲状态)`);
|
| 1269 |
+
processQueue();
|
| 1270 |
+
} else {
|
| 1271 |
+
console.log(`[Queue] 当前正在处理其他请求,请求 ${reqId} 已排队等待。`);
|
| 1272 |
+
}
|
| 1273 |
+
});
|
| 1274 |
+
|
| 1275 |
+
|
| 1276 |
+
// --- Helper: 获取当前文本 (v2.14 - 获取原始文本) -> vNEXT: Try innerText
|
| 1277 |
+
async function getRawTextContent(responseElement, previousText, reqId) {
|
| 1278 |
+
try {
|
| 1279 |
+
await responseElement.waitFor({ state: 'attached', timeout: 1500 });
|
| 1280 |
+
const preElement = responseElement.locator('pre').last();
|
| 1281 |
+
let rawText = null;
|
| 1282 |
+
try {
|
| 1283 |
+
await preElement.waitFor({ state: 'attached', timeout: 500 });
|
| 1284 |
+
// 尝试使用 innerText 获取渲染后的文本,可能更好地保留换行
|
| 1285 |
+
rawText = await preElement.innerText({ timeout: 1000 });
|
| 1286 |
+
} catch {
|
| 1287 |
+
// 如果 pre 元素获取失败,回退到 responseElement 的 innerText
|
| 1288 |
+
console.warn(`[${reqId}] (Warn) Failed to get innerText from <pre>, falling back to parent.`);
|
| 1289 |
+
rawText = await responseElement.innerText({ timeout: 2000 });
|
| 1290 |
+
}
|
| 1291 |
+
// 移除 trim(),直接返回获取到的文本
|
| 1292 |
+
return rawText !== null ? rawText : previousText;
|
| 1293 |
+
} catch (e) {
|
| 1294 |
+
console.warn(`[${reqId}] (Warn) getRawTextContent (innerText) failed: ${e.message.split('\n')[0]}. Returning previous.`);
|
| 1295 |
+
return previousText;
|
| 1296 |
+
}
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
// --- Helper: 发送流式块 ---
|
| 1300 |
+
function sendStreamChunk(res, delta, reqId) {
|
| 1301 |
+
if (delta && !res.writableEnded) {
|
| 1302 |
+
const chunk = {
|
| 1303 |
+
id: `${CHAT_COMPLETION_ID_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
|
| 1304 |
+
object: "chat.completion.chunk",
|
| 1305 |
+
created: Math.floor(Date.now() / 1000),
|
| 1306 |
+
model: MODEL_NAME,
|
| 1307 |
+
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
| 1308 |
+
};
|
| 1309 |
+
try {
|
| 1310 |
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
| 1311 |
+
} catch (writeError) {
|
| 1312 |
+
console.error(`[${reqId}] Error writing stream chunk:`, writeError.message);
|
| 1313 |
+
if (!res.writableEnded) res.end(); // End stream on write error
|
| 1314 |
+
}
|
| 1315 |
+
}
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
// --- Helper: 发送流式错误块 ---
|
| 1319 |
+
function sendStreamError(res, errorMessage, reqId) {
|
| 1320 |
+
if (!res.writableEnded) {
|
| 1321 |
+
const errorPayload = { error: { message: `[${reqId}] Server error during streaming: ${errorMessage}`, type: 'server_error' } };
|
| 1322 |
+
try {
|
| 1323 |
+
// Avoid writing multiple DONE messages if error occurs after normal DONE
|
| 1324 |
+
if (!res.writableEnded) res.write(`data: ${JSON.stringify(errorPayload)}\n\n`);
|
| 1325 |
+
if (!res.writableEnded) res.write('data: [DONE]\n\n');
|
| 1326 |
+
} catch (e) {
|
| 1327 |
+
console.error(`[${reqId}] Error writing stream error chunk:`, e.message);
|
| 1328 |
+
} finally {
|
| 1329 |
+
if (!res.writableEnded) res.end(); // Ensure stream ends
|
| 1330 |
+
}
|
| 1331 |
+
}
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
// --- Helper: 保存错误快照 ---
|
| 1335 |
+
async function saveErrorSnapshot(errorName = 'error') {
|
| 1336 |
+
// Extract reqId if present in the name
|
| 1337 |
+
const nameParts = errorName.split('_');
|
| 1338 |
+
const reqId = nameParts[nameParts.length - 1].length === 7 ? nameParts.pop() : null; // Simple check for likely reqId
|
| 1339 |
+
const baseErrorName = nameParts.join('_');
|
| 1340 |
+
const logPrefix = reqId ? `[${reqId}]` : '[No ReqId]';
|
| 1341 |
+
|
| 1342 |
+
if (!browser?.isConnected() || !page || page.isClosed()) {
|
| 1343 |
+
console.log(`${logPrefix} 无法保存错误快照 (${baseErrorName}),浏览器或页面不可用。`);
|
| 1344 |
+
return;
|
| 1345 |
+
}
|
| 1346 |
+
console.log(`${logPrefix} 尝试保存错误快照 (${baseErrorName})...`);
|
| 1347 |
+
const timestamp = Date.now();
|
| 1348 |
+
const errorDir = path.join(__dirname, 'errors');
|
| 1349 |
+
try {
|
| 1350 |
+
if (!fs.existsSync(errorDir)) fs.mkdirSync(errorDir, { recursive: true });
|
| 1351 |
+
// Include reqId in filename if available
|
| 1352 |
+
const filenameSuffix = reqId ? `${reqId}_${timestamp}` : `${timestamp}`;
|
| 1353 |
+
const screenshotPath = path.join(errorDir, `${baseErrorName}_screenshot_${filenameSuffix}.png`);
|
| 1354 |
+
const htmlPath = path.join(errorDir, `${baseErrorName}_page_${filenameSuffix}.html`);
|
| 1355 |
+
|
| 1356 |
+
try {
|
| 1357 |
+
await page.screenshot({ path: screenshotPath, fullPage: true, timeout: 15000 });
|
| 1358 |
+
console.log(`${logPrefix} 错误快照已保存到: ${screenshotPath}`);
|
| 1359 |
+
} catch (screenshotError) {
|
| 1360 |
+
console.error(`${logPrefix} 保存屏幕截图失败 (${baseErrorName}): ${screenshotError.message}`);
|
| 1361 |
+
}
|
| 1362 |
+
try {
|
| 1363 |
+
const content = await page.content({timeout: 15000});
|
| 1364 |
+
fs.writeFileSync(htmlPath, content);
|
| 1365 |
+
console.log(`${logPrefix} 错误页面HTML已保存到: ${htmlPath}`);
|
| 1366 |
+
} catch (htmlError) {
|
| 1367 |
+
console.error(`${logPrefix} 保存页面HTML失败 (${baseErrorName}): ${htmlError.message}`);
|
| 1368 |
+
}
|
| 1369 |
+
} catch (dirError) {
|
| 1370 |
+
console.error(`${logPrefix} 创建错误目录或保存快照时出错: ${dirError.message}`);
|
| 1371 |
+
}
|
| 1372 |
+
}
|
| 1373 |
+
|
| 1374 |
+
// v2.14: Helper to safely parse JSON, attempting to find the outermost object/array
|
| 1375 |
+
function tryParseJson(text, reqId) {
|
| 1376 |
+
if (!text || typeof text !== 'string') return null;
|
| 1377 |
+
text = text.trim();
|
| 1378 |
+
|
| 1379 |
+
let startIndex = -1;
|
| 1380 |
+
let endIndex = -1;
|
| 1381 |
+
|
| 1382 |
+
const firstBrace = text.indexOf('{');
|
| 1383 |
+
const firstBracket = text.indexOf('[');
|
| 1384 |
+
|
| 1385 |
+
if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
|
| 1386 |
+
startIndex = firstBrace;
|
| 1387 |
+
endIndex = text.lastIndexOf('}');
|
| 1388 |
+
} else if (firstBracket !== -1) {
|
| 1389 |
+
startIndex = firstBracket;
|
| 1390 |
+
endIndex = text.lastIndexOf(']');
|
| 1391 |
+
}
|
| 1392 |
+
|
| 1393 |
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
| 1394 |
+
// console.warn(`[${reqId}] (Warn) Could not find valid start/end braces/brackets for JSON parsing.`);
|
| 1395 |
+
return null;
|
| 1396 |
+
}
|
| 1397 |
+
|
| 1398 |
+
const jsonText = text.substring(startIndex, endIndex + 1);
|
| 1399 |
+
|
| 1400 |
+
try {
|
| 1401 |
+
return JSON.parse(jsonText);
|
| 1402 |
+
} catch (e) {
|
| 1403 |
+
// console.warn(`[${reqId}] (Warn) JSON parse failed for extracted text: ${e.message}`);
|
| 1404 |
+
return null;
|
| 1405 |
+
}
|
| 1406 |
+
}
|
| 1407 |
+
|
| 1408 |
+
// --- Helper: 检测并提取页面错误提示 ---
|
| 1409 |
+
async function detectAndExtractPageError(page, reqId) {
|
| 1410 |
+
const errorToastLocator = page.locator(ERROR_TOAST_SELECTOR).last();
|
| 1411 |
+
try {
|
| 1412 |
+
const isVisible = await errorToastLocator.isVisible({ timeout: 1000 });
|
| 1413 |
+
if (isVisible) {
|
| 1414 |
+
console.log(`[${reqId}] 检测到错误 Toast 元素。`);
|
| 1415 |
+
const messageLocator = errorToastLocator.locator('span.content-text');
|
| 1416 |
+
const errorMessage = await messageLocator.textContent({ timeout: 500 });
|
| 1417 |
+
return errorMessage || "Detected error toast, but couldn't extract specific message.";
|
| 1418 |
+
} else {
|
| 1419 |
+
return null;
|
| 1420 |
+
}
|
| 1421 |
+
} catch (e) {
|
| 1422 |
+
// console.warn(`[${reqId}] (Warn) Checking for error toast failed or timed out: ${e.message.split('\n')[0]}`);
|
| 1423 |
+
return null;
|
| 1424 |
+
}
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
// --- Helper: 快速检查结束条件 ---
|
| 1428 |
+
async function checkEndConditionQuickly(page, spinnerLocator, inputLocator, buttonLocator, timeoutMs = 250, reqId) {
|
| 1429 |
+
try {
|
| 1430 |
+
const results = await Promise.allSettled([
|
| 1431 |
+
expect(spinnerLocator).toBeHidden({ timeout: timeoutMs }),
|
| 1432 |
+
expect(inputLocator).toHaveValue('', { timeout: timeoutMs }),
|
| 1433 |
+
expect(buttonLocator).toBeDisabled({ timeout: timeoutMs })
|
| 1434 |
+
]);
|
| 1435 |
+
const allMet = results.every(result => result.status === 'fulfilled');
|
| 1436 |
+
// console.log(`[${reqId}] (Quick Check) All met: ${allMet}`);
|
| 1437 |
+
return allMet;
|
| 1438 |
+
} catch (error) {
|
| 1439 |
+
// console.warn(`[${reqId}] (Quick Check) Error during checkEndConditionQuickly: ${error.message}`);
|
| 1440 |
+
return false;
|
| 1441 |
+
}
|
| 1442 |
+
}
|
| 1443 |
+
|
| 1444 |
+
// --- 启动服务器 ---
|
| 1445 |
+
let serverInstance = null;
|
| 1446 |
+
(async () => {
|
| 1447 |
+
await initializePlaywright();
|
| 1448 |
+
|
| 1449 |
+
serverInstance = app.listen(SERVER_PORT, () => {
|
| 1450 |
+
console.log("\n=============================================================");
|
| 1451 |
+
// v2.18: Updated version marker
|
| 1452 |
+
console.log(" 🚀 AI Studio Proxy Server (v2.18 - Queue) 🚀");
|
| 1453 |
+
console.log("=============================================================");
|
| 1454 |
+
console.log(`🔗 监听地址: http://localhost:${SERVER_PORT}`);
|
| 1455 |
+
console.log(` - Web UI (测试): http://localhost:${SERVER_PORT}/`);
|
| 1456 |
+
console.log(` - API 端点: http://localhost:${SERVER_PORT}/v1/chat/completions`);
|
| 1457 |
+
console.log(` - 模型接口: http://localhost:${SERVER_PORT}/v1/models`);
|
| 1458 |
+
console.log(` - 健康检查: http://localhost:${SERVER_PORT}/health`);
|
| 1459 |
+
console.log("-------------------------------------------------------------");
|
| 1460 |
+
if (isPlaywrightReady) {
|
| 1461 |
+
console.log('✅ Playwright 连接成功,服务已准备就绪!');
|
| 1462 |
+
} else {
|
| 1463 |
+
console.warn('⚠️ Playwright 未就绪。请检查下方日志并确保 Chrome/AI Studio 正常运行。');
|
| 1464 |
+
console.warn(' API 请求将失败,直到 Playwright 连接成功。');
|
| 1465 |
+
}
|
| 1466 |
+
console.log("-------------------------------------------------------------");
|
| 1467 |
+
console.log(`⏳ 等待 Chrome 实例 (调试端口: ${CHROME_DEBUGGING_PORT})...`);
|
| 1468 |
+
console.log(" 请确保已运行 auto_connect_aistudio.js 脚本,");
|
| 1469 |
+
console.log(" 并且 Google AI Studio 页面已在浏览器中打开。 ");
|
| 1470 |
+
console.log("=============================================================\n");
|
| 1471 |
+
});
|
| 1472 |
+
|
| 1473 |
+
serverInstance.on('error', (error) => {
|
| 1474 |
+
if (error.code === 'EADDRINUSE') {
|
| 1475 |
+
console.error("\n=============================================================");
|
| 1476 |
+
console.error(`❌ 致命错误:端口 ${SERVER_PORT} 已被占用!`);
|
| 1477 |
+
console.error(" 请关闭占用该端口的其他程序,或在 server.cjs 中修改 SERVER_PORT。 ");
|
| 1478 |
+
console.error("=============================================================\n");
|
| 1479 |
+
} else {
|
| 1480 |
+
console.error('❌ 服务器启动失败:', error);
|
| 1481 |
+
}
|
| 1482 |
+
process.exit(1);
|
| 1483 |
+
});
|
| 1484 |
+
|
| 1485 |
+
})();
|
| 1486 |
+
|
| 1487 |
+
// --- 优雅关闭处理 ---
|
| 1488 |
+
let isShuttingDown = false;
|
| 1489 |
+
async function shutdown(signal) {
|
| 1490 |
+
if (isShuttingDown) return;
|
| 1491 |
+
isShuttingDown = true;
|
| 1492 |
+
console.log(`\n收到 ${signal} 信号,正在关闭服务器...`);
|
| 1493 |
+
console.log(`当前队列中有 ${requestQueue.length} 个请求等待处理。将不再接受新请求。`);
|
| 1494 |
+
// Option: Wait for the current request to finish?
|
| 1495 |
+
// For now, we'll just close the server, potentially interrupting the current request.
|
| 1496 |
+
|
| 1497 |
+
if (serverInstance) {
|
| 1498 |
+
serverInstance.close(async (err) => {
|
| 1499 |
+
if (err) console.error("关闭 HTTP 服务器时出错:", err);
|
| 1500 |
+
else console.log("HTTP 服务器已关闭。");
|
| 1501 |
+
|
| 1502 |
+
console.log("Playwright connectOverCDP 将自动断开。");
|
| 1503 |
+
// No need to explicitly disconnect browser in connectOverCDP mode
|
| 1504 |
+
console.log('服务器优雅关闭完成。');
|
| 1505 |
+
process.exit(err ? 1 : 0);
|
| 1506 |
+
});
|
| 1507 |
+
|
| 1508 |
+
// Force exit after timeout
|
| 1509 |
+
setTimeout(() => {
|
| 1510 |
+
console.error("优雅关闭超时,强制退出进程。");
|
| 1511 |
+
process.exit(1);
|
| 1512 |
+
}, 10000); // 10 seconds timeout
|
| 1513 |
+
} else {
|
| 1514 |
+
console.log("服务器实例未找到,直接退出。");
|
| 1515 |
+
process.exit(0);
|
| 1516 |
+
}
|
| 1517 |
+
}
|
| 1518 |
+
|
| 1519 |
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
| 1520 |
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
deprecated_javascript_version/test.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// index.js (修改后 - 用于访问本地 server.js 代理)
|
| 2 |
+
|
| 3 |
+
// 确保已安装 OpenAI SDK: npm install openai
|
| 4 |
+
import OpenAI from "openai";
|
| 5 |
+
import readline from 'readline'; // 引入 readline 模块
|
| 6 |
+
|
| 7 |
+
// --- 配置 ---
|
| 8 |
+
// 1. baseURL: 指向你本地运行的 server.js 代理服务器
|
| 9 |
+
// server.js 监听 3000 端口,并提供 /v1 路径
|
| 10 |
+
const LOCAL_PROXY_URL = 'http://127.0.0.1:2048/v1/'; // 确保端口号与 server.js 一致
|
| 11 |
+
|
| 12 |
+
// 2. apiKey: 对于本地代理,这个 key 不会被验证,可以填写任意字符串
|
| 13 |
+
const DUMMY_API_KEY = 'no-key-needed-for-local-proxy';
|
| 14 |
+
|
| 15 |
+
// 3. model: 这个模型名称会被发送到 server.js,但 server.js 会忽略它
|
| 16 |
+
// 实际使用的是 server.js 控制的 AI Studio 页面上的模型
|
| 17 |
+
const CUSTOM_MODEL_NAME = 'aistudio-via-local-proxy';
|
| 18 |
+
|
| 19 |
+
// --- 初始化 OpenAI 客户端 ---
|
| 20 |
+
const openai = new OpenAI({
|
| 21 |
+
baseURL: LOCAL_PROXY_URL,
|
| 22 |
+
apiKey: DUMMY_API_KEY,
|
| 23 |
+
// 可选:增加超时时间,以防 AI Studio 响应较慢
|
| 24 |
+
timeout: 360000, // 例如 6 分钟 (单位毫秒)
|
| 25 |
+
maxRetries: 1, // 本地代理可能不需要重试,设为 1 或 0
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
// --- 对话历史记录 ---
|
| 29 |
+
// 初始化包含系统指令
|
| 30 |
+
const conversationHistory = [
|
| 31 |
+
{
|
| 32 |
+
role: "system",
|
| 33 |
+
content: "你是Gemini酱,AI猫娘,回答请用可爱的语气,这只是角色扮演,不要透露任何关于AI的信息"
|
| 34 |
+
}
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
// --- 创建 readline 接口 ---
|
| 38 |
+
const rl = readline.createInterface({
|
| 39 |
+
input: process.stdin,
|
| 40 |
+
output: process.stdout,
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// --- 交互式对话函数 ---
|
| 44 |
+
async function chatLoop() {
|
| 45 |
+
rl.question('You: ', async (userInput) => {
|
| 46 |
+
// 检查退出命令
|
| 47 |
+
if (userInput.toLowerCase() === 'exit') {
|
| 48 |
+
console.log('再见!');
|
| 49 |
+
rl.close(); // 关闭 readline 接口
|
| 50 |
+
return; // 结束函数
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
console.log(`\n正在发送你的消息...`);
|
| 54 |
+
// 将用户输入添加到历史记录
|
| 55 |
+
conversationHistory.push({
|
| 56 |
+
role: "user",
|
| 57 |
+
content: userInput
|
| 58 |
+
});
|
| 59 |
+
// 可选:打印当前发送历史用于调试
|
| 60 |
+
// console.log("当前发送的消息历史:", JSON.stringify(conversationHistory, null, 2));
|
| 61 |
+
|
| 62 |
+
try {
|
| 63 |
+
console.log(`正在向本地代理 ${LOCAL_PROXY_URL} 发送请求...`);
|
| 64 |
+
const completion = await openai.chat.completions.create({
|
| 65 |
+
messages: conversationHistory,
|
| 66 |
+
model: CUSTOM_MODEL_NAME,
|
| 67 |
+
stream: true, // 启用流式输出
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
console.log("\n--- 来自本地代理 (AI Studio) 的回复 ---");
|
| 71 |
+
let fullResponse = ""; // 用于拼接完整的回复内容
|
| 72 |
+
process.stdout.write('AI: '); // 先打印 "AI: " 前缀
|
| 73 |
+
for await (const chunk of completion) {
|
| 74 |
+
const content = chunk.choices[0]?.delta?.content || "";
|
| 75 |
+
process.stdout.write(content); // 直接打印流式内容,不换行
|
| 76 |
+
fullResponse += content; // 拼接内容
|
| 77 |
+
}
|
| 78 |
+
console.log(); // 在流结束后换行
|
| 79 |
+
|
| 80 |
+
// 将完整的 AI 回复添加到历史记录
|
| 81 |
+
if (fullResponse) {
|
| 82 |
+
conversationHistory.push({ role: "assistant", content: fullResponse });
|
| 83 |
+
} else {
|
| 84 |
+
console.log("未能从代理获取有效的流式内容。");
|
| 85 |
+
// 如果回复无效,可以选择从历史中移除刚才的用户输入
|
| 86 |
+
conversationHistory.pop();
|
| 87 |
+
}
|
| 88 |
+
console.log("----------------------------------------------\n");
|
| 89 |
+
|
| 90 |
+
} catch (error) {
|
| 91 |
+
console.error("\n--- 请求出错 ---");
|
| 92 |
+
// 保持之前的错误处理逻辑
|
| 93 |
+
if (error instanceof OpenAI.APIError) {
|
| 94 |
+
console.error(` 错误类型: OpenAI APIError (可能是代理返回的错误)`);
|
| 95 |
+
console.error(` 状态码: ${error.status}`);
|
| 96 |
+
console.error(` 错误消息: ${error.message}`);
|
| 97 |
+
console.error(` 错误代码: ${error.code}`);
|
| 98 |
+
console.error(` 错误参数: ${error.param}`);
|
| 99 |
+
} else if (error.code === 'ECONNREFUSED') {
|
| 100 |
+
console.error(` 错误类型: 连接被拒绝 (ECONNREFUSED)`);
|
| 101 |
+
console.error(` 无法连接到服务器 ${LOCAL_PROXY_URL}。请检查 server.js 是否运行。`);
|
| 102 |
+
} else if (error.name === 'TimeoutError' || (error.cause && error.cause.code === 'UND_ERR_CONNECT_TIMEOUT')) {
|
| 103 |
+
console.error(` 错误类型: 连接超时`);
|
| 104 |
+
console.error(` 连接到 ${LOCAL_PROXY_URL} 超时。请检查 server.js 或 AI Studio 响应。`);
|
| 105 |
+
} else {
|
| 106 |
+
console.error(' 发生了未知错误:', error.message);
|
| 107 |
+
}
|
| 108 |
+
console.error("----------------------------------------------\n");
|
| 109 |
+
// 出错时,从历史中移除刚才的用户输入,避免影响下次对话
|
| 110 |
+
conversationHistory.pop();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// 不论成功或失败,都继续下一次循环
|
| 114 |
+
chatLoop();
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// --- 启动交互式对话 ---
|
| 119 |
+
console.log('你好! 我是Gemini酱。有什么事可以帮你哒,输入 "exit" 退出。');
|
| 120 |
+
console.log(' (请确保 server.js 和 auto_connect_aistudio.js 正在运行)');
|
| 121 |
+
chatLoop(); // 开始第一次提问
|
| 122 |
+
|
| 123 |
+
// --- 不再需要文件末尾的 main 调用和 setTimeout 示例 ---
|
| 124 |
+
// // 运行第一次对话
|
| 125 |
+
// main("你好!简单介绍一下你自己以及你的能力。");
|
| 126 |
+
// ... (移除 setTimeout 示例)
|