Maynor996 commited on
Commit
75c689e
·
verified ·
1 Parent(s): b85e7fc

Upload 7 files

Browse files
Files changed (7) hide show
  1. README_hf.md +92 -0
  2. app.py +15 -0
  3. business_gemini_session.json.example +24 -0
  4. gemini.py +2498 -0
  5. hf_manual_deploy.md +97 -0
  6. index.html +2025 -0
  7. requirements-hf.txt +5 -0
README_hf.md ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Business Gemini Pool
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Business Gemini Pool 管理系统
14
+
15
+ 一个基于 Flask 的 Google Gemini Enterprise API 代理服务,现已部署到 Hugging Face Spaces!
16
+
17
+ ## 🌟 特性
18
+
19
+ - **多账号轮询**: 支持配置多个 Gemini 账号,自动轮询使用
20
+ - **OpenAI 兼容接口**: 提供与 OpenAI API 兼容的接口格式
21
+ - **流式响应**: 支持 SSE 流式输出
22
+ - **Web 管理控制台**: 美观的 Web 管理界面
23
+ - **图片处理**: 支持图片输入输出
24
+ - **代理支持**: 支持 HTTP/HTTPS 代理配置
25
+
26
+ ## 🚀 快速开始
27
+
28
+ 1. 访问此 Space:[Business Gemini Pool](https://huggingface.co/spaces/your-username/business-gemini-pool)
29
+
30
+ 2. 在 Web 界面中配置你的 Gemini 账号信息
31
+
32
+ 3. 开始使用 API 服务!
33
+
34
+ ## 📝 配置说明
35
+
36
+ ### 环境变量(可选)
37
+ 在 Space 设置中可以配置以下环境变量:
38
+ - `PROXY_URL`: 代理服务器地址
39
+ - `PROXY_ENABLED`: 是否启用代理 (true/false)
40
+
41
+ ### 账号配置
42
+ 在 Web 界面的"账号管理"页面��加你的 Gemini 账号:
43
+ - Team ID
44
+ - Secure Cookie
45
+ - Host Cookie
46
+ - Session Index
47
+ - User Agent
48
+
49
+ ## 🔧 API 使用
50
+
51
+ ### 获取模型列表
52
+ ```bash
53
+ curl https://your-space.hf.space/v1/models
54
+ ```
55
+
56
+ ### 聊天对话
57
+ ```bash
58
+ curl -X POST https://your-space.hf.space/v1/chat/completions \
59
+ -H "Content-Type: application/json" \
60
+ -d '{
61
+ "model": "gemini-enterprise",
62
+ "messages": [
63
+ {"role": "user", "content": "Hello!"}
64
+ ]
65
+ }'
66
+ ```
67
+
68
+ ## 🛠️ 本地部署
69
+
70
+ ```bash
71
+ # 克隆仓库
72
+ git clone https://huggingface.co/spaces/your-username/business-gemini-pool
73
+ cd business-gemini-pool
74
+
75
+ # 安装依赖
76
+ pip install -r requirements-hf.txt
77
+
78
+ # 运行
79
+ python app.py
80
+ ```
81
+
82
+ ## 📄 许可证
83
+
84
+ MIT License
85
+
86
+ ## 🤝 贡献
87
+
88
+ 欢迎提交 Issue 和 Pull Request!
89
+
90
+ ## ⚠️ 免责声明
91
+
92
+ 本工具仅供学习和研究使用,请遵守 Google 的使用条款。
app.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Spaces兼容的应用入口文件"""
2
+
3
+ import os
4
+ import sys
5
+
6
+ # 将当前目录添加到Python路径
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ # 导入主应用
10
+ from gemini import app
11
+
12
+ if __name__ == "__main__":
13
+ # Hugging Face Spaces通常需要运行在特定端口
14
+ port = int(os.environ.get("PORT", 7860))
15
+ app.run(host="0.0.0.0", port=port)
business_gemini_session.json.example ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "proxy": "http://127.0.0.1:7890",
3
+ "proxy_enabled": false,
4
+ "image_base_url": "http://127.0.0.1:8000/",
5
+ "image_output_mode": "url",
6
+ "log_level": "INFO",
7
+ "admin_password_hash": "",
8
+ "admin_secret_key": "",
9
+ "api_tokens": [
10
+ "please_set_api_token_here"
11
+ ],
12
+ "accounts": [
13
+ ],
14
+ "models": [
15
+ {
16
+ "id": "gemini-3-pro-preview",
17
+ "name": "gemini-3-pro-preview",
18
+ "description": "gemini-3-pro-preview 模型",
19
+ "context_length": 32768,
20
+ "max_tokens": 8192,
21
+ "price_per_1k_tokens": 0.0015
22
+ }
23
+ ]
24
+ }
gemini.py ADDED
@@ -0,0 +1,2498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Business Gemini OpenAPI 兼容服务
2
+ 整合JWT获取和聊天功能,提供OpenAPI接口
3
+ 支持多账号轮训
4
+ 支持图片输出(OpenAI格式)
5
+ """
6
+
7
+ import json
8
+ import time
9
+ import hmac
10
+ import hashlib
11
+ import base64
12
+ import uuid
13
+ import threading
14
+ import os
15
+ import re
16
+ import shutil
17
+ import mimetypes
18
+ import requests
19
+ from pathlib import Path
20
+ from datetime import datetime, timedelta, timezone
21
+ from dataclasses import dataclass, field
22
+ from typing import List, Optional, Dict, Any, Tuple
23
+ import builtins
24
+ import secrets
25
+ from flask import Flask, request, Response, jsonify, send_from_directory, abort
26
+ from flask_cors import CORS
27
+ from functools import wraps
28
+ from werkzeug.security import generate_password_hash, check_password_hash
29
+
30
+ # 禁用SSL警告
31
+ import urllib3
32
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
33
+
34
+ # 配置
35
+ CONFIG_FILE = Path(__file__).parent / "business_gemini_session.json"
36
+
37
+ # 图片缓存配置
38
+ IMAGE_CACHE_DIR = Path(__file__).parent / "image"
39
+ IMAGE_CACHE_HOURS = 1 # 图片缓存时间(小时)
40
+ IMAGE_CACHE_DIR.mkdir(exist_ok=True)
41
+
42
+ # API endpoints
43
+ BASE_URL = "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global"
44
+ CREATE_SESSION_URL = f"{BASE_URL}/widgetCreateSession"
45
+ STREAM_ASSIST_URL = f"{BASE_URL}/widgetStreamAssist"
46
+ LIST_FILE_METADATA_URL = f"{BASE_URL}/widgetListSessionFileMetadata"
47
+ ADD_CONTEXT_FILE_URL = f"{BASE_URL}/widgetAddContextFile"
48
+ GETOXSRF_URL = "https://business.gemini.google/auth/getoxsrf"
49
+
50
+ # 账号错误冷却时间(秒)
51
+ AUTH_ERROR_COOLDOWN_SECONDS = 900 # 凭证错误,15分钟
52
+ RATE_LIMIT_COOLDOWN_SECONDS = 300 # 触发限额,5分钟
53
+ GENERIC_ERROR_COOLDOWN_SECONDS = 120 # 其他错误的短暂冷却
54
+ LOG_LEVELS = {"DEBUG": 10, "INFO": 20, "ERROR": 40}
55
+ DEFAULT_LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
56
+ CURRENT_LOG_LEVEL_NAME = DEFAULT_LOG_LEVEL if DEFAULT_LOG_LEVEL in LOG_LEVELS else "INFO"
57
+ CURRENT_LOG_LEVEL = LOG_LEVELS[CURRENT_LOG_LEVEL_NAME]
58
+ ADMIN_SECRET_KEY = None
59
+ API_TOKENS = set()
60
+
61
+ try:
62
+ from zoneinfo import ZoneInfo
63
+ except ImportError:
64
+ ZoneInfo = None
65
+
66
+ # Flask应用
67
+ app = Flask(__name__, static_folder='.')
68
+ CORS(app)
69
+
70
+
71
+ def _infer_log_level(text: str) -> str:
72
+ t = text.strip()
73
+ if t.startswith("[DEBUG]"):
74
+ return "DEBUG"
75
+ if t.startswith("[ERROR]") or t.startswith("[!]"):
76
+ return "ERROR"
77
+ return "INFO"
78
+
79
+
80
+ _original_print = builtins.print
81
+
82
+
83
+ def filtered_print(*args, **kwargs):
84
+ """简单的日志过滤,根据全局日志级别屏蔽低级别输出"""
85
+ level = kwargs.pop("_level", None)
86
+ sep = kwargs.get("sep", " ")
87
+ text = sep.join(str(a) for a in args)
88
+ level_name = (level or _infer_log_level(text)).upper()
89
+ if LOG_LEVELS.get(level_name, LOG_LEVELS["INFO"]) >= CURRENT_LOG_LEVEL:
90
+ _original_print(*args, **kwargs)
91
+
92
+
93
+ builtins.print = filtered_print
94
+
95
+
96
+ def set_log_level(level: str, persist: bool = False):
97
+ """设置全局日志级别"""
98
+ global CURRENT_LOG_LEVEL_NAME, CURRENT_LOG_LEVEL
99
+ lvl = (level or "").upper()
100
+ if lvl not in LOG_LEVELS:
101
+ raise ValueError(f"无效日志级别: {level}")
102
+ CURRENT_LOG_LEVEL_NAME = lvl
103
+ CURRENT_LOG_LEVEL = LOG_LEVELS[lvl]
104
+ if persist and globals().get("account_manager") and account_manager.config is not None:
105
+ account_manager.config["log_level"] = lvl
106
+ account_manager.save_config()
107
+ _original_print(f"[LOG] 当前日志级别: {CURRENT_LOG_LEVEL_NAME}")
108
+
109
+
110
+ class AccountError(Exception):
111
+ """基础账号异常"""
112
+
113
+ def __init__(self, message: str, status_code: Optional[int] = None):
114
+ super().__init__(message)
115
+ self.status_code = status_code
116
+
117
+
118
+ class AccountAuthError(AccountError):
119
+ """凭证/权限相关异常"""
120
+
121
+
122
+ class AccountRateLimitError(AccountError):
123
+ """配额或限流异常"""
124
+
125
+
126
+ class AccountRequestError(AccountError):
127
+ """其他请求异常"""
128
+
129
+
130
+ class NoAvailableAccount(AccountError):
131
+ """无可用账号异常"""
132
+
133
+
134
+ def get_admin_secret_key() -> str:
135
+ """获取/初始化后台密钥"""
136
+ global ADMIN_SECRET_KEY
137
+ if ADMIN_SECRET_KEY:
138
+ return ADMIN_SECRET_KEY
139
+ if account_manager.config is None:
140
+ ADMIN_SECRET_KEY = os.getenv("ADMIN_SECRET_KEY", "change_me_secret")
141
+ return ADMIN_SECRET_KEY
142
+ secret = account_manager.config.get("admin_secret_key") or os.getenv("ADMIN_SECRET_KEY")
143
+ if not secret:
144
+ secret = secrets.token_urlsafe(32)
145
+ account_manager.config["admin_secret_key"] = secret
146
+ account_manager.save_config()
147
+ ADMIN_SECRET_KEY = secret
148
+ return ADMIN_SECRET_KEY
149
+
150
+
151
+ def load_api_tokens():
152
+ """从配置加载用户访问token"""
153
+ global API_TOKENS
154
+ API_TOKENS = set()
155
+ if not account_manager.config:
156
+ return
157
+ tokens = account_manager.config.get("api_tokens") or account_manager.config.get("api_token")
158
+ if isinstance(tokens, str):
159
+ API_TOKENS.add(tokens)
160
+ elif isinstance(tokens, list):
161
+ for t in tokens:
162
+ if isinstance(t, str):
163
+ API_TOKENS.add(t)
164
+
165
+
166
+ def persist_api_tokens():
167
+ if account_manager.config is None:
168
+ account_manager.config = {}
169
+ account_manager.config["api_tokens"] = list(API_TOKENS)
170
+ account_manager.save_config()
171
+
172
+
173
+ def get_admin_password_hash() -> Optional[str]:
174
+ if account_manager.config:
175
+ return account_manager.config.get("admin_password_hash")
176
+ return None
177
+
178
+
179
+ def set_admin_password(password: str):
180
+ if not password:
181
+ raise ValueError("密码不能为空")
182
+ if account_manager.config is None:
183
+ account_manager.config = {}
184
+ account_manager.config["admin_password_hash"] = generate_password_hash(password)
185
+ account_manager.save_config()
186
+
187
+
188
+ def is_valid_api_token(token: str) -> bool:
189
+ if not token:
190
+ return False
191
+ if verify_admin_token(token):
192
+ return True
193
+ return token in API_TOKENS
194
+
195
+
196
+ def require_api_auth(func):
197
+ """开放接口需要 api_token 或 admin token"""
198
+ @wraps(func)
199
+ def wrapper(*args, **kwargs):
200
+ token = (
201
+ request.headers.get("X-API-Token")
202
+ or request.headers.get("Authorization", "").replace("Bearer ", "")
203
+ or request.cookies.get("admin_token")
204
+ )
205
+ if not is_valid_api_token(token):
206
+ return jsonify({"error": "未授权"}), 401
207
+ return func(*args, **kwargs)
208
+ return wrapper
209
+
210
+
211
+ def create_admin_token(exp_seconds: int = 86400) -> str:
212
+ payload = {
213
+ "exp": time.time() + exp_seconds,
214
+ "ts": int(time.time())
215
+ }
216
+ payload_b = json.dumps(payload, separators=(",", ":")).encode()
217
+ b64 = base64.urlsafe_b64encode(payload_b).decode().rstrip("=")
218
+ secret = get_admin_secret_key().encode()
219
+ signature = hmac.new(secret, b64.encode(), hashlib.sha256).hexdigest()
220
+ return f"{b64}.{signature}"
221
+
222
+
223
+ def verify_admin_token(token: str) -> bool:
224
+ if not token:
225
+ return False
226
+ try:
227
+ b64, sig = token.split(".", 1)
228
+ except ValueError:
229
+ return False
230
+ expected_sig = hmac.new(get_admin_secret_key().encode(), b64.encode(), hashlib.sha256).hexdigest()
231
+ if not hmac.compare_digest(expected_sig, sig):
232
+ return False
233
+ padding = '=' * (-len(b64) % 4)
234
+ try:
235
+ payload = json.loads(base64.urlsafe_b64decode(b64 + padding).decode())
236
+ except Exception:
237
+ return False
238
+ if payload.get("exp", 0) < time.time():
239
+ return False
240
+ return True
241
+
242
+
243
+ def require_admin(func):
244
+ @wraps(func)
245
+ def wrapper(*args, **kwargs):
246
+ token = (
247
+ request.headers.get("X-Admin-Token")
248
+ or request.headers.get("Authorization", "").replace("Bearer ", "")
249
+ or request.cookies.get("admin_token")
250
+ )
251
+ if not verify_admin_token(token):
252
+ return jsonify({"error": "未授权"}), 401
253
+ return func(*args, **kwargs)
254
+ return wrapper
255
+
256
+
257
+ def seconds_until_next_pt_midnight(now_ts: Optional[float] = None) -> int:
258
+ """计算距离下一个 PT 午夜的秒数,用于配额冷却"""
259
+ now_utc = datetime.now(timezone.utc) if now_ts is None else datetime.fromtimestamp(now_ts, tz=timezone.utc)
260
+ if ZoneInfo:
261
+ pt_tz = ZoneInfo("America/Los_Angeles")
262
+ now_pt = now_utc.astimezone(pt_tz)
263
+ else:
264
+ # 兼容旧版本 Python 的简易回退(不考虑夏令时)
265
+ now_pt = now_utc - timedelta(hours=8)
266
+
267
+ tomorrow = (now_pt + timedelta(days=1)).date()
268
+ midnight_pt = datetime.combine(tomorrow, datetime.min.time(), tzinfo=now_pt.tzinfo)
269
+ delta = (midnight_pt - now_pt).total_seconds()
270
+ return max(0, int(delta))
271
+
272
+
273
+ class AccountManager:
274
+ """多账号管理器,支持轮训策略"""
275
+
276
+ def __init__(self):
277
+ self.config = None
278
+ self.accounts = [] # 账号列表
279
+ self.current_index = 0 # 当前轮训索引
280
+ self.account_states = {} # 账号状态: {index: {jwt, jwt_time, session, available, cooldown_until, cooldown_reason}}
281
+ self.lock = threading.Lock()
282
+ self.auth_error_cooldown = AUTH_ERROR_COOLDOWN_SECONDS
283
+ self.rate_limit_cooldown = RATE_LIMIT_COOLDOWN_SECONDS
284
+ self.generic_error_cooldown = GENERIC_ERROR_COOLDOWN_SECONDS
285
+
286
+ def load_config(self):
287
+ """加载配置"""
288
+ if CONFIG_FILE.exists():
289
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
290
+ self.config = json.load(f)
291
+ if "log_level" in self.config:
292
+ try:
293
+ set_log_level(self.config.get("log_level"), persist=False)
294
+ except Exception:
295
+ pass
296
+ if "admin_secret_key" in self.config:
297
+ global ADMIN_SECRET_KEY
298
+ ADMIN_SECRET_KEY = self.config.get("admin_secret_key")
299
+ load_api_tokens()
300
+ self.accounts = self.config.get("accounts", [])
301
+ # 初始化账号状态
302
+ for i, acc in enumerate(self.accounts):
303
+ available = acc.get("available", True) # 默认可用
304
+ self.account_states[i] = {
305
+ "jwt": None,
306
+ "jwt_time": 0,
307
+ "session": None,
308
+ "available": available,
309
+ "cooldown_until": acc.get("cooldown_until"),
310
+ "cooldown_reason": acc.get("unavailable_reason") or acc.get("cooldown_reason") or ""
311
+ }
312
+ return self.config
313
+
314
+ def save_config(self):
315
+ """保存配置到文件"""
316
+ if self.config and CONFIG_FILE.exists():
317
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
318
+ json.dump(self.config, f, indent=4, ensure_ascii=False)
319
+
320
+ def mark_account_unavailable(self, index: int, reason: str = ""):
321
+ """标记账号不可用"""
322
+ with self.lock:
323
+ if 0 <= index < len(self.accounts):
324
+ self.accounts[index]["available"] = False
325
+ self.accounts[index]["unavailable_reason"] = reason
326
+ self.accounts[index]["unavailable_time"] = datetime.now().isoformat()
327
+ self.account_states[index]["available"] = False
328
+ self.save_config()
329
+ print(f"[!] 账号 {index} 已标记为不可用: {reason}")
330
+
331
+ def mark_account_cooldown(self, index: int, reason: str = "", cooldown_seconds: Optional[int] = None):
332
+ """临时拉黑账号(冷却),在冷却时间内不会被选择"""
333
+ if cooldown_seconds is None:
334
+ cooldown_seconds = self.generic_error_cooldown
335
+
336
+ with self.lock:
337
+ if 0 <= index < len(self.accounts):
338
+ now_ts = time.time()
339
+ new_until = now_ts + cooldown_seconds
340
+ state = self.account_states.setdefault(index, {})
341
+ current_until = state.get("cooldown_until") or 0
342
+ # 如果已有更长的冷却,则不重复更新
343
+ if current_until > now_ts and current_until >= new_until:
344
+ return
345
+
346
+ until = max(new_until, current_until)
347
+ state["cooldown_until"] = until
348
+ state["cooldown_reason"] = reason
349
+ state["jwt"] = None
350
+ state["jwt_time"] = 0
351
+ state["session"] = None
352
+
353
+ # 在配置中记录冷却信息,便于前端展示
354
+ self.accounts[index]["cooldown_until"] = until
355
+ self.accounts[index]["unavailable_reason"] = reason
356
+ self.accounts[index]["unavailable_time"] = datetime.now().isoformat()
357
+
358
+ self.save_config()
359
+ print(f"[!] 账号 {index} 进入冷却 {cooldown_seconds} 秒: {reason}")
360
+
361
+ def _is_in_cooldown(self, index: int, now_ts: Optional[float] = None) -> bool:
362
+ """检查账号是否处于冷却期"""
363
+ now_ts = now_ts or time.time()
364
+ state = self.account_states.get(index, {})
365
+ cooldown_until = state.get("cooldown_until")
366
+ if not cooldown_until:
367
+ return False
368
+ return now_ts < cooldown_until
369
+
370
+ def get_next_cooldown_info(self) -> Optional[dict]:
371
+ """获取最近即将结束冷却的账号信息"""
372
+ now_ts = time.time()
373
+ candidates = []
374
+ for idx, state in self.account_states.items():
375
+ cooldown_until = state.get("cooldown_until")
376
+ if cooldown_until and cooldown_until > now_ts and state.get("available", True):
377
+ candidates.append((cooldown_until, idx))
378
+ if not candidates:
379
+ return None
380
+ cooldown_until, idx = min(candidates, key=lambda x: x[0])
381
+ return {"index": idx, "cooldown_until": cooldown_until}
382
+
383
+ def is_account_available(self, index: int) -> bool:
384
+ """计算账号当前是否可用(考虑冷却和手动禁用)"""
385
+ state = self.account_states.get(index, {})
386
+ if not state.get("available", True):
387
+ return False
388
+ return not self._is_in_cooldown(index)
389
+
390
+ def get_available_accounts(self):
391
+ """获取可用账号列表"""
392
+ now_ts = time.time()
393
+ available_accounts = []
394
+ for i, acc in enumerate(self.accounts):
395
+ state = self.account_states.get(i, {})
396
+ if not state.get("available", True):
397
+ continue
398
+ if self._is_in_cooldown(i, now_ts):
399
+ continue
400
+ available_accounts.append((i, acc))
401
+ return available_accounts
402
+
403
+ def get_next_account(self):
404
+ """轮训获取下一个可用账号"""
405
+ with self.lock:
406
+ available = self.get_available_accounts()
407
+ if not available:
408
+ cooldown_info = self.get_next_cooldown_info()
409
+ if cooldown_info:
410
+ remaining = int(max(0, cooldown_info["cooldown_until"] - time.time()))
411
+ raise NoAvailableAccount(f"没有可用的账号(最近冷却账号 {cooldown_info['index']}��约 {remaining} 秒后可重试)")
412
+ raise NoAvailableAccount("没有可用的账号")
413
+
414
+ # 轮训选择
415
+ self.current_index = self.current_index % len(available)
416
+ idx, account = available[self.current_index]
417
+ self.current_index = (self.current_index + 1) % len(available)
418
+ return idx, account
419
+
420
+ def get_account_count(self):
421
+ """获取账号数量统计"""
422
+ total = len(self.accounts)
423
+ available = len(self.get_available_accounts())
424
+ return total, available
425
+
426
+
427
+ # 全局账号管理器
428
+ account_manager = AccountManager()
429
+
430
+
431
+ class FileManager:
432
+ """文件管理器 - 管理上传文件的映射关系(OpenAI file_id <-> Gemini fileId)"""
433
+
434
+ def __init__(self):
435
+ self.files: Dict[str, Dict] = {} # openai_file_id -> {gemini_file_id, session_name, filename, mime_type, size, created_at}
436
+
437
+ def add_file(self, openai_file_id: str, gemini_file_id: str, session_name: str,
438
+ filename: str, mime_type: str, size: int) -> Dict:
439
+ """添加文件映射"""
440
+ file_info = {
441
+ "id": openai_file_id,
442
+ "gemini_file_id": gemini_file_id,
443
+ "session_name": session_name,
444
+ "filename": filename,
445
+ "mime_type": mime_type,
446
+ "bytes": size,
447
+ "created_at": int(time.time()),
448
+ "purpose": "assistants",
449
+ "object": "file"
450
+ }
451
+ self.files[openai_file_id] = file_info
452
+ return file_info
453
+
454
+ def get_file(self, openai_file_id: str) -> Optional[Dict]:
455
+ """获取文件信息"""
456
+ return self.files.get(openai_file_id)
457
+
458
+ def get_gemini_file_id(self, openai_file_id: str) -> Optional[str]:
459
+ """获取 Gemini 文件ID"""
460
+ file_info = self.files.get(openai_file_id)
461
+ return file_info.get("gemini_file_id") if file_info else None
462
+
463
+ def delete_file(self, openai_file_id: str) -> bool:
464
+ """删除文件映射"""
465
+ if openai_file_id in self.files:
466
+ del self.files[openai_file_id]
467
+ return True
468
+ return False
469
+
470
+ def list_files(self) -> List[Dict]:
471
+ """列出所有文件"""
472
+ return list(self.files.values())
473
+
474
+ def get_session_for_file(self, openai_file_id: str) -> Optional[str]:
475
+ """获取文件关联的会话名称"""
476
+ file_info = self.files.get(openai_file_id)
477
+ return file_info.get("session_name") if file_info else None
478
+
479
+
480
+ # 全局文件管理器
481
+ file_manager = FileManager()
482
+
483
+
484
+ def check_proxy(proxy: str) -> bool:
485
+ """检测代理是否可用"""
486
+ if not proxy:
487
+ return False
488
+ try:
489
+ proxies = {"http": proxy, "https": proxy}
490
+ resp = requests.get("https://www.google.com", proxies=proxies,
491
+ verify=False, timeout=10)
492
+ return resp.status_code == 200
493
+ except:
494
+ return False
495
+
496
+
497
+ def get_proxy() -> Optional[str]:
498
+ """获取代理配置,根据proxy_enabled开关决定是否返回代理地址
499
+
500
+ Returns:
501
+ 代理地址字符串,如果禁用代理则返回None
502
+ """
503
+ if account_manager.config is None:
504
+ return None
505
+ if not account_manager.config.get("proxy_enabled", False):
506
+ return None
507
+ return account_manager.config.get("proxy")
508
+
509
+
510
+ def url_safe_b64encode(data: bytes) -> str:
511
+ """URL安全的Base64编码,不带padding"""
512
+ return base64.urlsafe_b64encode(data).decode('utf-8').rstrip('=')
513
+
514
+
515
+ def kq_encode(s: str) -> str:
516
+ """模拟JS的kQ函数"""
517
+ byte_arr = bytearray()
518
+ for char in s:
519
+ val = ord(char)
520
+ if val > 255:
521
+ byte_arr.append(val & 255)
522
+ byte_arr.append(val >> 8)
523
+ else:
524
+ byte_arr.append(val)
525
+ return url_safe_b64encode(bytes(byte_arr))
526
+
527
+
528
+ def decode_xsrf_token(xsrf_token: str) -> bytes:
529
+ """将 xsrfToken 解码为字节数组(用于HMAC签名)"""
530
+ padding = 4 - len(xsrf_token) % 4
531
+ if padding != 4:
532
+ xsrf_token += '=' * padding
533
+ return base64.urlsafe_b64decode(xsrf_token)
534
+
535
+
536
+ def create_jwt(key_bytes: bytes, key_id: str, csesidx: str) -> str:
537
+ """创建JWT token"""
538
+ now = int(time.time())
539
+
540
+ header = {
541
+ "alg": "HS256",
542
+ "typ": "JWT",
543
+ "kid": key_id
544
+ }
545
+
546
+ payload = {
547
+ "iss": "https://business.gemini.google",
548
+ "aud": "https://biz-discoveryengine.googleapis.com",
549
+ "sub": f"csesidx/{csesidx}",
550
+ "iat": now,
551
+ "exp": now + 300,
552
+ "nbf": now
553
+ }
554
+
555
+ header_b64 = kq_encode(json.dumps(header, separators=(',', ':')))
556
+ payload_b64 = kq_encode(json.dumps(payload, separators=(',', ':')))
557
+ message = f"{header_b64}.{payload_b64}"
558
+
559
+ signature = hmac.new(key_bytes, message.encode('utf-8'), hashlib.sha256).digest()
560
+ signature_b64 = url_safe_b64encode(signature)
561
+
562
+ return f"{message}.{signature_b64}"
563
+
564
+
565
+ def get_jwt_for_account(account: dict, proxy: str) -> str:
566
+ """为指定账号获取JWT"""
567
+ secure_c_ses = account.get("secure_c_ses")
568
+ host_c_oses = account.get("host_c_oses")
569
+ csesidx = account.get("csesidx")
570
+
571
+ if not secure_c_ses or not csesidx:
572
+ raise ValueError("缺少 secure_c_ses 或 csesidx")
573
+
574
+ url = f"{GETOXSRF_URL}?csesidx={csesidx}"
575
+ proxies = {"http": proxy, "https": proxy} if proxy else None
576
+
577
+ headers = {
578
+ "accept": "*/*",
579
+ "user-agent": account.get('user_agent', 'Mozilla/5.0'),
580
+ "cookie": f'__Secure-C_SES={secure_c_ses}; __Host-C_OSES={host_c_oses}',
581
+ }
582
+
583
+ try:
584
+ resp = requests.get(url, headers=headers, proxies=proxies, verify=False, timeout=30)
585
+ except requests.RequestException as e:
586
+ raise AccountRequestError(f"获取JWT 请求失败: {e}") from e
587
+
588
+ if resp.status_code != 200:
589
+ raise_for_account_response(resp, "获取JWT")
590
+
591
+ # 处理Google安全前缀
592
+ text = resp.text
593
+ if text.startswith(")]}'\n") or text.startswith(")]}'"):
594
+ text = text[4:].strip()
595
+
596
+ try:
597
+ data = json.loads(text)
598
+ except json.JSONDecodeError as e:
599
+ raise AccountAuthError(f"解析JWT响应失败: {e}") from e
600
+
601
+ key_id = data.get("keyId")
602
+ xsrf_token = data.get("xsrfToken")
603
+ if not key_id or not xsrf_token:
604
+ raise AccountAuthError(f"JWT 响应缺少 keyId/xsrfToken: {data}")
605
+
606
+ print(f"账号: {account.get('csesidx')} 账号可用! key_id: {key_id}")
607
+
608
+ key_bytes = decode_xsrf_token(xsrf_token)
609
+
610
+ return create_jwt(key_bytes, key_id, csesidx)
611
+
612
+
613
+ def get_headers(jwt: str) -> dict:
614
+ """获取请求头"""
615
+ return {
616
+ "accept": "*/*",
617
+ "accept-encoding": "gzip, deflate, br, zstd",
618
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
619
+ "authorization": f"Bearer {jwt}",
620
+ "content-type": "application/json",
621
+ "origin": "https://business.gemini.google",
622
+ "referer": "https://business.gemini.google/",
623
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
624
+ "x-server-timeout": "1800",
625
+ }
626
+
627
+
628
+ def raise_for_account_response(resp: requests.Response, action: str):
629
+ """根据响应状态抛出对应的账号异常"""
630
+ status = resp.status_code
631
+ body_preview = resp.text[:500] if resp.text else ""
632
+ lower_body = body_preview.lower()
633
+
634
+ if status in (401, 403):
635
+ raise AccountAuthError(f"{action} 认证失败: {status} - {body_preview}", status)
636
+ if status == 429 or "quota" in lower_body or "exceed" in lower_body or "limit" in lower_body:
637
+ raise AccountRateLimitError(f"{action} 触发限额: {status} - {body_preview}", status)
638
+
639
+ raise AccountRequestError(f"{action} 请求失败: {status} - {body_preview}", status)
640
+
641
+
642
+ def ensure_jwt_for_account(account_idx: int, account: dict):
643
+ """确保指定账号的JWT有效,必要时刷新"""
644
+ print(f"[DEBUG][ensure_jwt_for_account] 开始 - 账号索引: {account_idx}, CSESIDX: {account.get('csesidx')}")
645
+ start_time = time.time()
646
+ with account_manager.lock:
647
+ state = account_manager.account_states[account_idx]
648
+ jwt_age = time.time() - state["jwt_time"] if state["jwt"] else float('inf')
649
+ print(f"[DEBUG][ensure_jwt_for_account] JWT状态 - 存在: {state['jwt'] is not None}, 年龄: {jwt_age:.2f}秒")
650
+ if state["jwt"] is None or jwt_age > 240:
651
+ print(f"[DEBUG][ensure_jwt_for_account] 需要刷新JWT...")
652
+ proxy = get_proxy()
653
+ try:
654
+ refresh_start = time.time()
655
+ state["jwt"] = get_jwt_for_account(account, proxy)
656
+ state["jwt_time"] = time.time()
657
+ print(f"[DEBUG][ensure_jwt_for_account] JWT刷新成功 - 耗时: {time.time() - refresh_start:.2f}秒")
658
+ except Exception as e:
659
+ print(f"[DEBUG][ensure_jwt_for_account] JWT刷新失败: {e}")
660
+ raise
661
+ else:
662
+ print(f"[DEBUG][ensure_jwt_for_account] 使用缓存JWT")
663
+ print(f"[DEBUG][ensure_jwt_for_account] 完成 - 总耗时: {time.time() - start_time:.2f}秒")
664
+ return state["jwt"]
665
+
666
+
667
+ def create_chat_session(jwt: str, team_id: str, proxy: str) -> str:
668
+ """创建会话,返回session ID"""
669
+ print(f"[DEBUG][create_chat_session] 开始 - team_id: {team_id}")
670
+ start_time = time.time()
671
+ session_id = uuid.uuid4().hex[:12]
672
+ print(f"[DEBUG][create_chat_session] 生成session_id: {session_id}")
673
+ body = {
674
+ "configId": team_id,
675
+ "additionalParams": {"token": "-"},
676
+ "createSessionRequest": {
677
+ "session": {"name": session_id, "displayName": session_id}
678
+ }
679
+ }
680
+
681
+ proxies = {"http": proxy, "https": proxy} if proxy else None
682
+ print(f"[DEBUG][create_chat_session] 发送请求到: {CREATE_SESSION_URL}")
683
+ print(f"[DEBUG][create_chat_session] 使用代理: {proxy}")
684
+
685
+ request_start = time.time()
686
+ try:
687
+ resp = requests.post(
688
+ CREATE_SESSION_URL,
689
+ headers=get_headers(jwt),
690
+ json=body,
691
+ proxies=proxies,
692
+ verify=False,
693
+ timeout=30
694
+ )
695
+ except requests.RequestException as e:
696
+ raise AccountRequestError(f"创建会话请求失败: {e}") from e
697
+ print(f"[DEBUG][create_chat_session] 请求完成 - 状态码: {resp.status_code}, 耗时: {time.time() - request_start:.2f}秒")
698
+
699
+ if resp.status_code != 200:
700
+ print(f"[DEBUG][create_chat_session] 请求失败 - 响应: {resp.text[:500]}")
701
+ if resp.status_code == 401:
702
+ print(f"[DEBUG][create_chat_session] 401错误 - 可能是team_id填错了")
703
+ raise_for_account_response(resp, "创建会话")
704
+
705
+ data = resp.json()
706
+ session_name = data.get("session", {}).get("name")
707
+ print(f"[DEBUG][create_chat_session] 完成 - session_name: {session_name}, 总耗时: {time.time() - start_time:.2f}秒")
708
+ return session_name
709
+
710
+
711
+ def ensure_session_for_account(account_idx: int, account: dict):
712
+ """确保指定账号的会话有效"""
713
+ print(f"[DEBUG][ensure_session_for_account] 开始 - 账号索引: {account_idx}")
714
+ start_time = time.time()
715
+
716
+ jwt_start = time.time()
717
+ jwt = ensure_jwt_for_account(account_idx, account)
718
+ print(f"[DEBUG][ensure_session_for_account] JWT获取完成 - 耗时: {time.time() - jwt_start:.2f}秒")
719
+
720
+ with account_manager.lock:
721
+ state = account_manager.account_states[account_idx]
722
+ print(f"[DEBUG][ensure_session_for_account] 当前session状态: {state['session'] is not None}")
723
+ if state["session"] is None:
724
+ print(f"[DEBUG][ensure_session_for_account] 需要创建新session...")
725
+ proxy = get_proxy()
726
+ team_id = account.get("team_id")
727
+ session_start = time.time()
728
+ state["session"] = create_chat_session(jwt, team_id, proxy)
729
+ print(f"[DEBUG][ensure_session_for_account] Session创建完成 - 耗时: {time.time() - session_start:.2f}秒")
730
+ else:
731
+ print(f"[DEBUG][ensure_session_for_account] 使用缓存session: {state['session']}")
732
+
733
+ print(f"[DEBUG][ensure_session_for_account] 完成 - 总耗时: {time.time() - start_time:.2f}秒")
734
+ return state["session"], jwt, account.get("team_id")
735
+
736
+
737
+ # ==================== 文件上传功能 ====================
738
+
739
+ def upload_file_to_gemini(jwt: str, session_name: str, team_id: str,
740
+ file_content: bytes, filename: str, mime_type: str,
741
+ proxy: str = None) -> str:
742
+ """
743
+ 上传文件到 Gemini,返回 Gemini 的 fileId
744
+
745
+ Args:
746
+ jwt: JWT 认证令牌
747
+ session_name: 会话名称
748
+ team_id: 团队ID
749
+ file_content: 文件内容(字节)
750
+ filename: 文件名
751
+ mime_type: MIME 类型
752
+ proxy: 代理地址
753
+
754
+ Returns:
755
+ str: Gemini 返回的 fileId
756
+ """
757
+ start_time = time.time()
758
+ print(f"[DEBUG][upload_file_to_gemini] 开始上传文件: {filename}, MIME类型: {mime_type}, 文件大小: {len(file_content)} bytes")
759
+
760
+ encode_start = time.time()
761
+ file_contents_b64 = base64.b64encode(file_content).decode('utf-8')
762
+ print(f"[DEBUG][upload_file_to_gemini] Base64编码完成 - 耗时: {time.time() - encode_start:.2f}秒, 编码后大小: {len(file_contents_b64)} chars")
763
+
764
+ body = {
765
+ "addContextFileRequest": {
766
+ "fileContents": file_contents_b64,
767
+ "fileName": filename,
768
+ "mimeType": mime_type,
769
+ "name": session_name
770
+ },
771
+ "additionalParams": {"token": "-"},
772
+ "configId": team_id
773
+ }
774
+
775
+ proxies = {"http": proxy, "https": proxy} if proxy else None
776
+ print(f"[DEBUG][upload_file_to_gemini] 准备发送请求到: {ADD_CONTEXT_FILE_URL}")
777
+ print(f"[DEBUG][upload_file_to_gemini] 使用代理: {proxy if proxy else '无'}")
778
+
779
+ request_start = time.time()
780
+ try:
781
+ resp = requests.post(
782
+ ADD_CONTEXT_FILE_URL,
783
+ headers=get_headers(jwt),
784
+ json=body,
785
+ proxies=proxies,
786
+ verify=False,
787
+ timeout=60
788
+ )
789
+ except requests.RequestException as e:
790
+ raise AccountRequestError(f"文件上传请求失败: {e}") from e
791
+ print(f"[DEBUG][upload_file_to_gemini] 请求完成 - 耗时: {time.time() - request_start:.2f}秒, 状态码: {resp.status_code}")
792
+
793
+ if resp.status_code != 200:
794
+ print(f"[DEBUG][upload_file_to_gemini] 上传失败 - 响应内容: {resp.text[:500]}")
795
+ raise_for_account_response(resp, "文件上传")
796
+
797
+ parse_start = time.time()
798
+ data = resp.json()
799
+ file_id = data.get("addContextFileResponse", {}).get("fileId")
800
+ print(f"[DEBUG][upload_file_to_gemini] 解析响应完成 - 耗时: {time.time() - parse_start:.2f}秒")
801
+
802
+ if not file_id:
803
+ print(f"[DEBUG][upload_file_to_gemini] 响应中未找到fileId - 响应数据: {data}")
804
+ raise ValueError(f"响应中未找到 fileId: {data}")
805
+
806
+ print(f"[DEBUG][upload_file_to_gemini] 上传成功 - fileId: {file_id}, 总耗时: {time.time() - start_time:.2f}秒")
807
+ return file_id
808
+
809
+
810
+ # ==================== 图片处理功能 ====================
811
+
812
+ @dataclass
813
+ class ChatImage:
814
+ """表示生成的图片"""
815
+ url: Optional[str] = None
816
+ base64_data: Optional[str] = None
817
+ mime_type: str = "image/png"
818
+ local_path: Optional[str] = None
819
+ file_id: Optional[str] = None
820
+ file_name: Optional[str] = None
821
+
822
+
823
+ @dataclass
824
+ class ChatResponse:
825
+ """聊天响应,包含文本和图片"""
826
+ text: str = ""
827
+ images: List[ChatImage] = field(default_factory=list)
828
+ thoughts: List[str] = field(default_factory=list)
829
+
830
+
831
+ def cleanup_expired_images():
832
+ """清理过期的缓存图片"""
833
+ if not IMAGE_CACHE_DIR.exists():
834
+ return
835
+
836
+ now = time.time()
837
+ max_age_seconds = IMAGE_CACHE_HOURS * 3600
838
+
839
+ for filepath in IMAGE_CACHE_DIR.iterdir():
840
+ if filepath.is_file():
841
+ try:
842
+ file_age = now - filepath.stat().st_mtime
843
+ if file_age > max_age_seconds:
844
+ filepath.unlink()
845
+ print(f"[图片缓存] 已删除过期图片: {filepath.name}")
846
+ except Exception as e:
847
+ print(f"[图片缓存] 删除失败: {filepath.name}, 错误: {e}")
848
+
849
+
850
+ def save_image_to_cache(image_data: bytes, mime_type: str = "image/png", filename: Optional[str] = None) -> str:
851
+ """保存图片到缓存目录,返回文件名"""
852
+ IMAGE_CACHE_DIR.mkdir(exist_ok=True)
853
+
854
+ # 确定文件扩展名
855
+ ext_map = {
856
+ "image/png": ".png",
857
+ "image/jpeg": ".jpg",
858
+ "image/gif": ".gif",
859
+ "image/webp": ".webp",
860
+ }
861
+ ext = ext_map.get(mime_type, ".png")
862
+
863
+ if filename:
864
+ # 确保有正确的扩展名
865
+ if not any(filename.endswith(e) for e in ext_map.values()):
866
+ filename = f"{filename}{ext}"
867
+ else:
868
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
869
+ filename = f"gemini_{timestamp}_{uuid.uuid4().hex[:8]}{ext}"
870
+
871
+ filepath = IMAGE_CACHE_DIR / filename
872
+ with open(filepath, "wb") as f:
873
+ f.write(image_data)
874
+
875
+ return filename
876
+
877
+
878
+ def parse_base64_data_url(data_url: str) -> Optional[Dict]:
879
+ """解析 base64 data URL,返回 {type, mime_type, data} 或 None"""
880
+ if not data_url or not data_url.startswith("data:"):
881
+ return None
882
+
883
+ # base64格式: data:image/png;base64,xxxxx
884
+ match = re.match(r"data:([^;]+);base64,(.+)", data_url)
885
+ if match:
886
+ return {
887
+ "type": "base64",
888
+ "mime_type": match.group(1),
889
+ "data": match.group(2)
890
+ }
891
+ return None
892
+
893
+
894
+ def extract_images_from_files_array(files: List[Dict]) -> List[Dict]:
895
+ """从 files 数组中提取图片(支持内联 base64 格式)
896
+
897
+ 支持格式:
898
+ {
899
+ "data": "data:image/png;base64,xxxxx",
900
+ "type": "image",
901
+ "detail": "high" # 可选
902
+ }
903
+
904
+ 返回: 图片列表 [{type: 'base64', mime_type: ..., data: ...}]
905
+ """
906
+ images = []
907
+ for file_item in files:
908
+ if not isinstance(file_item, dict):
909
+ continue
910
+
911
+ file_type = file_item.get("type", "")
912
+
913
+ # 只处理图片类型
914
+ if file_type != "image":
915
+ continue
916
+
917
+ data = file_item.get("data", "")
918
+ if data:
919
+ parsed = parse_base64_data_url(data)
920
+ if parsed:
921
+ images.append(parsed)
922
+
923
+ return images
924
+
925
+
926
+ def extract_images_from_openai_content(content: Any) -> Tuple[str, List[Dict]]:
927
+ """从OpenAI格式的content中提取文本和图片
928
+
929
+ 返回: (文本内容, 图片列表[{type: 'base64'|'url', data: ...}])
930
+ """
931
+ if isinstance(content, str):
932
+ return content, []
933
+
934
+ if not isinstance(content, list):
935
+ return str(content), []
936
+
937
+ text_parts = []
938
+ images = []
939
+
940
+ for item in content:
941
+ if not isinstance(item, dict):
942
+ continue
943
+
944
+ item_type = item.get("type", "")
945
+
946
+ if item_type == "text":
947
+ text_parts.append(item.get("text", ""))
948
+
949
+ elif item_type == "image_url":
950
+ image_url_obj = item.get("image_url", {})
951
+ if isinstance(image_url_obj, str):
952
+ url = image_url_obj
953
+ else:
954
+ url = image_url_obj.get("url", "")
955
+
956
+ parsed = parse_base64_data_url(url)
957
+ if parsed:
958
+ images.append(parsed)
959
+ elif url:
960
+ # 普通URL
961
+ images.append({
962
+ "type": "url",
963
+ "url": url
964
+ })
965
+
966
+ # 支持直接的 image 类型(带 data 字段)
967
+ elif item_type == "image" and item.get("data"):
968
+ parsed = parse_base64_data_url(item.get("data"))
969
+ if parsed:
970
+ images.append(parsed)
971
+
972
+ return "\n".join(text_parts), images
973
+
974
+
975
+ def download_image_from_url(url: str, proxy: Optional[str] = None) -> Tuple[bytes, str]:
976
+ """从URL下载图片,返回(图片数据, mime_type)"""
977
+ proxies = {"http": proxy, "https": proxy} if proxy else None
978
+ resp = requests.get(url, proxies=proxies, verify=False, timeout=60)
979
+ resp.raise_for_status()
980
+
981
+ content_type = resp.headers.get("Content-Type", "image/png")
982
+ # 提取主mime类型
983
+ mime_type = content_type.split(";")[0].strip()
984
+
985
+ return resp.content, mime_type
986
+
987
+
988
+ def get_session_file_metadata(jwt: str, session_name: str, team_id: str, proxy: Optional[str] = None) -> Dict:
989
+ """获取会话中的文件元数据(AI生成的图片)"""
990
+ body = {
991
+ "configId": team_id,
992
+ "additionalParams": {"token": "-"},
993
+ "listSessionFileMetadataRequest": {
994
+ "name": session_name,
995
+ "filter": "file_origin_type = AI_GENERATED"
996
+ }
997
+ }
998
+
999
+ proxies = {"http": proxy, "https": proxy} if proxy else None
1000
+ resp = requests.post(
1001
+ LIST_FILE_METADATA_URL,
1002
+ headers=get_headers(jwt),
1003
+ json=body,
1004
+ proxies=proxies,
1005
+ verify=False,
1006
+ timeout=30
1007
+ )
1008
+
1009
+ if resp.status_code != 200:
1010
+ print(f"[图片] 获取文件元数据失败: {resp.status_code}")
1011
+ return {}
1012
+
1013
+ data = resp.json()
1014
+ # 返回 fileId -> metadata 的映射
1015
+ result = {}
1016
+ file_metadata_list = data.get("listSessionFileMetadataResponse", {}).get("fileMetadata", [])
1017
+ for meta in file_metadata_list:
1018
+ file_id = meta.get("fileId")
1019
+ if file_id:
1020
+ result[file_id] = meta
1021
+ return result
1022
+
1023
+
1024
+ def build_download_url(session_name: str, file_id: str) -> str:
1025
+ """构造正确的下载URL"""
1026
+ return f"https://biz-discoveryengine.googleapis.com/v1alpha/{session_name}:downloadFile?fileId={file_id}&alt=media"
1027
+
1028
+
1029
+ def download_file_with_jwt(jwt: str, session_name: str, file_id: str, proxy: Optional[str] = None) -> bytes:
1030
+ """使用JWT认证下载文件"""
1031
+ url = build_download_url(session_name, file_id)
1032
+ proxies = {"http": proxy, "https": proxy} if proxy else None
1033
+
1034
+ resp = requests.get(
1035
+ url,
1036
+ headers=get_headers(jwt),
1037
+ proxies=proxies,
1038
+ verify=False,
1039
+ timeout=120,
1040
+ allow_redirects=True
1041
+ )
1042
+
1043
+ resp.raise_for_status()
1044
+ content = resp.content
1045
+
1046
+ # 检测是否为base64编码的内容
1047
+ try:
1048
+ text_content = content.decode("utf-8", errors="ignore").strip()
1049
+ if text_content.startswith("iVBORw0KGgo") or text_content.startswith("/9j/"):
1050
+ # 是base64编码,需要解码
1051
+ return base64.b64decode(text_content)
1052
+ except Exception:
1053
+ pass
1054
+
1055
+ return content
1056
+
1057
+
1058
+ def upload_inline_image_to_gemini(jwt: str, session_name: str, team_id: str,
1059
+ image_data: Dict, proxy: str = None) -> Optional[str]:
1060
+ """上传内联图片到 Gemini,返回 fileId"""
1061
+ try:
1062
+ ext_map = {"image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp"}
1063
+
1064
+ if image_data.get("type") == "base64":
1065
+ mime_type = image_data.get("mime_type", "image/png")
1066
+ file_content = base64.b64decode(image_data.get("data", ""))
1067
+ ext = ext_map.get(mime_type, ".png")
1068
+ filename = f"inline_{uuid.uuid4().hex[:8]}{ext}"
1069
+ elif image_data.get("type") == "url":
1070
+ file_content, mime_type = download_image_from_url(image_data.get("url"), proxy)
1071
+ ext = ext_map.get(mime_type, ".png")
1072
+ filename = f"url_{uuid.uuid4().hex[:8]}{ext}"
1073
+ else:
1074
+ return None
1075
+
1076
+ return upload_file_to_gemini(jwt, session_name, team_id, file_content, filename, mime_type, proxy)
1077
+ except AccountError:
1078
+ # 让账号相关错误向上抛出,以便触发冷却
1079
+ raise
1080
+ except Exception:
1081
+ return None
1082
+
1083
+
1084
+ def stream_chat_with_images(jwt: str, sess_name: str, message: str,
1085
+ proxy: str, team_id: str, file_ids: List[str] = None) -> ChatResponse:
1086
+ """发送消息并流式接收响应"""
1087
+ query_parts = [{"text": message}]
1088
+ request_file_ids = file_ids if file_ids else []
1089
+
1090
+ body = {
1091
+ "configId": team_id,
1092
+ "additionalParams": {"token": "-"},
1093
+ "streamAssistRequest": {
1094
+ "session": sess_name,
1095
+ "query": {"parts": query_parts},
1096
+ "filter": "",
1097
+ "fileIds": request_file_ids,
1098
+ "answerGenerationMode": "NORMAL",
1099
+ "assistGenerationConfig":{
1100
+ "modelId":"gemini-3-pro-preview"
1101
+ },
1102
+ "toolsSpec": {
1103
+ "webGroundingSpec": {},
1104
+ "toolRegistry": "default_tool_registry",
1105
+ "imageGenerationSpec": {},
1106
+ "videoGenerationSpec": {}
1107
+ },
1108
+ "languageCode": "zh-CN",
1109
+ "userMetadata": {"timeZone": "Etc/GMT-8"},
1110
+ "assistSkippingMode": "REQUEST_ASSIST"
1111
+ }
1112
+ }
1113
+
1114
+ proxies = {"http": proxy, "https": proxy} if proxy else None
1115
+ try:
1116
+ resp = requests.post(
1117
+ STREAM_ASSIST_URL,
1118
+ headers=get_headers(jwt),
1119
+ json=body,
1120
+ proxies=proxies,
1121
+ verify=False,
1122
+ timeout=120,
1123
+ stream=True
1124
+ )
1125
+ except requests.RequestException as e:
1126
+ raise AccountRequestError(f"聊天请求失败: {e}") from e
1127
+
1128
+ if resp.status_code != 200:
1129
+ raise_for_account_response(resp, "聊天请求")
1130
+
1131
+ # 收集完整响应
1132
+ full_response = ""
1133
+ for line in resp.iter_lines():
1134
+ if line:
1135
+ full_response += line.decode('utf-8') + "\n"
1136
+
1137
+ # 解析响应
1138
+ result = ChatResponse()
1139
+ texts = []
1140
+ file_ids = [] # 收集需要下载的文件 {fileId, mimeType}
1141
+ current_session = None
1142
+
1143
+ try:
1144
+ data_list = json.loads(full_response)
1145
+ for data in data_list:
1146
+ sar = data.get("streamAssistResponse")
1147
+ if not sar:
1148
+ continue
1149
+
1150
+ # 获取session信息
1151
+ session_info = sar.get("sessionInfo", {})
1152
+ if session_info.get("session"):
1153
+ current_session = session_info["session"]
1154
+
1155
+ # 检查顶层的generatedImages
1156
+ for gen_img in sar.get("generatedImages", []):
1157
+ parse_generated_image(gen_img, result, proxy)
1158
+
1159
+ answer = sar.get("answer") or {}
1160
+
1161
+ # 检查answer级别的generatedImages
1162
+ for gen_img in answer.get("generatedImages", []):
1163
+ parse_generated_image(gen_img, result, proxy)
1164
+
1165
+ for reply in answer.get("replies", []):
1166
+ # 检查reply级别的generatedImages
1167
+ for gen_img in reply.get("generatedImages", []):
1168
+ parse_generated_image(gen_img, result, proxy)
1169
+
1170
+ gc = reply.get("groundedContent", {})
1171
+ content = gc.get("content", {})
1172
+ text = content.get("text", "")
1173
+ thought = content.get("thought", False)
1174
+
1175
+ # 检查file字段(图片生成的关键)
1176
+ file_info = content.get("file")
1177
+ if file_info and file_info.get("fileId"):
1178
+ file_ids.append({
1179
+ "fileId": file_info["fileId"],
1180
+ "mimeType": file_info.get("mimeType", "image/png"),
1181
+ "fileName": file_info.get("name")
1182
+ })
1183
+
1184
+ # 解析图片数据
1185
+ parse_image_from_content(content, result, proxy)
1186
+ parse_image_from_content(gc, result, proxy)
1187
+
1188
+ # 检查attachments
1189
+ for att in reply.get("attachments", []) + gc.get("attachments", []) + content.get("attachments", []):
1190
+ parse_attachment(att, result, proxy)
1191
+
1192
+ if text and not thought:
1193
+ texts.append(text)
1194
+
1195
+ # 处理通过fileId引用的图片
1196
+ if file_ids and current_session:
1197
+ try:
1198
+ file_metadata = get_session_file_metadata(jwt, current_session, team_id, proxy)
1199
+ for finfo in file_ids:
1200
+ fid = finfo["fileId"]
1201
+ mime = finfo["mimeType"]
1202
+ fname = finfo.get("fileName")
1203
+ meta = file_metadata.get(fid)
1204
+
1205
+ if meta:
1206
+ fname = fname or meta.get("name")
1207
+ session_path = meta.get("session") or current_session
1208
+ else:
1209
+ session_path = current_session
1210
+
1211
+ try:
1212
+ image_data = download_file_with_jwt(jwt, session_path, fid, proxy)
1213
+ filename = None
1214
+ local_path = None
1215
+ b64_data = base64.b64encode(image_data).decode("utf-8")
1216
+
1217
+ # 仅在 URL 模式下缓存到本地以便通过 /image/ 访问
1218
+ if not is_base64_output_mode():
1219
+ filename = save_image_to_cache(image_data, mime, fname)
1220
+ local_path = str(IMAGE_CACHE_DIR / filename)
1221
+
1222
+ img = ChatImage(
1223
+ file_id=fid,
1224
+ file_name=filename,
1225
+ mime_type=mime,
1226
+ local_path=local_path,
1227
+ base64_data=b64_data,
1228
+ )
1229
+ result.images.append(img)
1230
+ if filename:
1231
+ print(f"[图片] 已保存: {filename}")
1232
+ except Exception as e:
1233
+ print(f"[图片] 下载失败 (fileId={fid}): {e}")
1234
+ except Exception as e:
1235
+ print(f"[图片] 获取文件元数据失败: {e}")
1236
+
1237
+ except json.JSONDecodeError:
1238
+ pass
1239
+
1240
+ result.text = "".join(texts)
1241
+ return result
1242
+
1243
+
1244
+ def parse_generated_image(gen_img: Dict, result: ChatResponse, proxy: Optional[str] = None):
1245
+ """解析generatedImages中的图片"""
1246
+ image_data = gen_img.get("image")
1247
+ if not image_data:
1248
+ return
1249
+
1250
+ # 检查base64数据
1251
+ b64_data = image_data.get("bytesBase64Encoded")
1252
+ if b64_data:
1253
+ try:
1254
+ mime_type = image_data.get("mimeType", "image/png")
1255
+ filename = None
1256
+ local_path = None
1257
+
1258
+ # 仅在 URL 模式下落盘缓存
1259
+ if not is_base64_output_mode():
1260
+ decoded = base64.b64decode(b64_data)
1261
+ filename = save_image_to_cache(decoded, mime_type)
1262
+ local_path = str(IMAGE_CACHE_DIR / filename)
1263
+
1264
+ img = ChatImage(
1265
+ base64_data=b64_data,
1266
+ mime_type=mime_type,
1267
+ file_name=filename,
1268
+ local_path=local_path,
1269
+ )
1270
+ result.images.append(img)
1271
+ if filename:
1272
+ print(f"[图片] 已保存: {filename}")
1273
+ except Exception as e:
1274
+ print(f"[图片] 解析base64失败: {e}")
1275
+
1276
+
1277
+ def parse_image_from_content(content: Dict, result: ChatResponse, proxy: Optional[str] = None):
1278
+ """从content中解析图片"""
1279
+ # 检查inlineData
1280
+ inline_data = content.get("inlineData")
1281
+ if inline_data:
1282
+ b64_data = inline_data.get("data")
1283
+ if b64_data:
1284
+ try:
1285
+ mime_type = inline_data.get("mimeType", "image/png")
1286
+ filename = None
1287
+ local_path = None
1288
+
1289
+ if not is_base64_output_mode():
1290
+ decoded = base64.b64decode(b64_data)
1291
+ filename = save_image_to_cache(decoded, mime_type)
1292
+ local_path = str(IMAGE_CACHE_DIR / filename)
1293
+
1294
+ img = ChatImage(
1295
+ base64_data=b64_data,
1296
+ mime_type=mime_type,
1297
+ file_name=filename,
1298
+ local_path=local_path,
1299
+ )
1300
+ result.images.append(img)
1301
+ if filename:
1302
+ print(f"[图片] 已保存: {filename}")
1303
+ except Exception as e:
1304
+ print(f"[图片] 解析inlineData失败: {e}")
1305
+
1306
+
1307
+ def parse_attachment(att: Dict, result: ChatResponse, proxy: Optional[str] = None):
1308
+ """解析attachment中的图片"""
1309
+ # 检查是否是图片类型
1310
+ mime_type = att.get("mimeType", "")
1311
+ if not mime_type.startswith("image/"):
1312
+ return
1313
+
1314
+ # 检查base64数据
1315
+ b64_data = att.get("data") or att.get("bytesBase64Encoded")
1316
+ if b64_data:
1317
+ try:
1318
+ filename = None
1319
+ local_path = None
1320
+
1321
+ if not is_base64_output_mode():
1322
+ decoded = base64.b64decode(b64_data)
1323
+ filename = att.get("name") or None
1324
+ filename = save_image_to_cache(decoded, mime_type, filename)
1325
+ local_path = str(IMAGE_CACHE_DIR / filename)
1326
+
1327
+ img = ChatImage(
1328
+ base64_data=b64_data,
1329
+ mime_type=mime_type,
1330
+ file_name=filename,
1331
+ local_path=local_path,
1332
+ )
1333
+ result.images.append(img)
1334
+ if filename:
1335
+ print(f"[图片] 已保存: {filename}")
1336
+ except Exception as e:
1337
+ print(f"[图片] 解析attachment失败: {e}")
1338
+
1339
+
1340
+ # ==================== OpenAPI 接口 ====================
1341
+
1342
+ @app.route('/v1/models', methods=['GET'])
1343
+ @require_api_auth
1344
+ def list_models():
1345
+ """获取模型列表"""
1346
+ models_config = account_manager.config.get("models", [])
1347
+ models_data = []
1348
+
1349
+ for model in models_config:
1350
+ models_data.append({
1351
+ "id": model.get("id", "gemini-enterprise"),
1352
+ "object": "model",
1353
+ "created": int(time.time()),
1354
+ "owned_by": "google",
1355
+ "permission": [],
1356
+ "root": model.get("id", "gemini-enterprise"),
1357
+ "parent": None
1358
+ })
1359
+
1360
+ # 如果没有配置模型,返回默认模型
1361
+ if not models_data:
1362
+ models_data.append({
1363
+ "id": "gemini-enterprise",
1364
+ "object": "model",
1365
+ "created": int(time.time()),
1366
+ "owned_by": "google",
1367
+ "permission": [],
1368
+ "root": "gemini-enterprise",
1369
+ "parent": None
1370
+ })
1371
+
1372
+ return jsonify({"object": "list", "data": models_data})
1373
+
1374
+
1375
+ @app.route('/v1/files', methods=['POST'])
1376
+ @require_api_auth
1377
+ def upload_file():
1378
+ """OpenAI 兼容的文件上传接口"""
1379
+ import traceback
1380
+ request_start_time = time.time()
1381
+ print(f"\n{'='*60}")
1382
+ print(f"[文件上传] ===== 接口调用开始 =====")
1383
+ print(f"[文件上传] 请求时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
1384
+
1385
+ try:
1386
+ # 检查是否有文件
1387
+ step_start = time.time()
1388
+ print(f"[文件上传] 步骤1: 检查请求中的文件...")
1389
+ if 'file' not in request.files:
1390
+ print(f"[文件上传] 错误: 请求中没有文件")
1391
+ return jsonify({"error": {"message": "No file provided", "type": "invalid_request_error"}}), 400
1392
+
1393
+ file = request.files['file']
1394
+ if file.filename == '':
1395
+ print(f"[文件上传] 错误: 文件名为空")
1396
+ return jsonify({"error": {"message": "No file selected", "type": "invalid_request_error"}}), 400
1397
+ print(f"[文件上传] 步骤1完成: 文件名={file.filename}, 耗时={time.time()-step_start:.3f}秒")
1398
+
1399
+ # 获取文件内容和MIME类型
1400
+ step_start = time.time()
1401
+ print(f"[文件上传] 步骤2: 读取文件内容...")
1402
+ file_content = file.read()
1403
+ mime_type = file.content_type or mimetypes.guess_type(file.filename)[0] or 'application/octet-stream'
1404
+ print(f"[文件上传] 步骤2完成: 文件大小={len(file_content)}字节, MIME类型={mime_type}, 耗时={time.time()-step_start:.3f}秒")
1405
+
1406
+ # 获取账号信息
1407
+ available_accounts = account_manager.get_available_accounts()
1408
+ if not available_accounts:
1409
+ next_cd = account_manager.get_next_cooldown_info()
1410
+ wait_msg = ""
1411
+ if next_cd:
1412
+ wait_msg = f"(最近冷却账号 {next_cd['index']},约 {int(next_cd['cooldown_until']-time.time())} 秒后可重试)"
1413
+ return jsonify({"error": {"message": f"没有可用的账号{wait_msg}", "type": "rate_limit"}}), 429
1414
+
1415
+ max_retries = len(available_accounts)
1416
+ last_error = None
1417
+ gemini_file_id = None
1418
+ print(f"[文件上传] 步骤3: 开始尝试上传, 最大重试次数={max_retries}")
1419
+
1420
+ for retry_idx in range(max_retries):
1421
+ retry_start = time.time()
1422
+ print(f"\n[文件上传] --- 第{retry_idx+1}次尝试 ---")
1423
+ account_idx = None
1424
+ try:
1425
+ # 获取账号
1426
+ step_start = time.time()
1427
+ print(f"[文件上传] 步骤3.{retry_idx+1}.1: 获取下一个可用账号...")
1428
+ account_idx, account = account_manager.get_next_account()
1429
+ print(f"[文件上传] 步骤3.{retry_idx+1}.1完成: 账号索引={account_idx}, CSESIDX={account.get('csesidx')}, 耗时={time.time()-step_start:.3f}秒")
1430
+
1431
+ # 确保会话有效
1432
+ step_start = time.time()
1433
+ print(f"[文件上传] 步骤3.{retry_idx+1}.2: 确保会话有效(JWT+Session)...")
1434
+ session, jwt, team_id = ensure_session_for_account(account_idx, account)
1435
+ print(f"[文件上传] 步骤3.{retry_idx+1}.2完成: session={session}, team_id={team_id}, 耗时={time.time()-step_start:.3f}秒")
1436
+
1437
+ proxy = get_proxy()
1438
+ print(f"[文件上传] 代理设置: {proxy}")
1439
+
1440
+ # 上传文件到 Gemini
1441
+ step_start = time.time()
1442
+ print(f"[文件上传] 步骤3.{retry_idx+1}.3: 上传文件到Gemini...")
1443
+ gemini_file_id = upload_file_to_gemini(jwt, session, team_id, file_content, file.filename, mime_type, proxy)
1444
+ print(f"[文件上传] 步骤3.{retry_idx+1}.3完成: gemini_file_id={gemini_file_id}, 耗时={time.time()-step_start:.3f}秒")
1445
+
1446
+ if gemini_file_id:
1447
+ # 生成 OpenAI 格式的 file_id
1448
+ step_start = time.time()
1449
+ print(f"[文件上传] 步骤4: 生成OpenAI格式响应...")
1450
+ openai_file_id = f"file-{uuid.uuid4().hex[:24]}"
1451
+
1452
+ # 保存映射关系
1453
+ file_manager.add_file(
1454
+ openai_file_id=openai_file_id,
1455
+ gemini_file_id=gemini_file_id,
1456
+ session_name=session,
1457
+ filename=file.filename,
1458
+ mime_type=mime_type,
1459
+ size=len(file_content)
1460
+ )
1461
+ print(f"[文件上传] 步骤4完成: openai_file_id={openai_file_id}, 耗时={time.time()-step_start:.3f}秒")
1462
+
1463
+ total_time = time.time() - request_start_time
1464
+ print(f"\n[文件上传] ===== 上传成功 =====")
1465
+ print(f"[文件上传] 总耗时: {total_time:.3f}秒")
1466
+ print(f"{'='*60}\n")
1467
+
1468
+ # 返回 OpenAI 格式响应
1469
+ return jsonify({
1470
+ "id": openai_file_id,
1471
+ "object": "file",
1472
+ "bytes": len(file_content),
1473
+ "created_at": int(time.time()),
1474
+ "filename": file.filename,
1475
+ "purpose": request.form.get('purpose', 'assistants')
1476
+ })
1477
+ else:
1478
+ print(f"[文件上传] 警告: gemini_file_id为空")
1479
+
1480
+ except AccountRateLimitError as e:
1481
+ last_error = e
1482
+ if account_idx is not None:
1483
+ pt_wait = seconds_until_next_pt_midnight()
1484
+ cooldown_seconds = max(account_manager.rate_limit_cooldown, pt_wait)
1485
+ account_manager.mark_account_cooldown(account_idx, str(e), cooldown_seconds)
1486
+ print(f"[文件上传] 第{retry_idx+1}次尝试失败(限额): {e}")
1487
+ print(f"[文件上传] 本次尝试耗时: {time.time()-retry_start:.3f}秒")
1488
+ continue
1489
+ except AccountAuthError as e:
1490
+ last_error = e
1491
+ if account_idx is not None:
1492
+ account_manager.mark_account_cooldown(account_idx, str(e), account_manager.auth_error_cooldown)
1493
+ print(f"[文件上传] 第{retry_idx+1}次尝试失败(凭证): {e}")
1494
+ print(f"[文件上传] 本次尝试耗时: {time.time()-retry_start:.3f}秒")
1495
+ continue
1496
+ except AccountRequestError as e:
1497
+ last_error = e
1498
+ if account_idx is not None:
1499
+ account_manager.mark_account_cooldown(account_idx, str(e), account_manager.generic_error_cooldown)
1500
+ print(f"[文件上传] 第{retry_idx+1}次尝试失败(请求异常): {e}")
1501
+ print(f"[文件上传] 本次尝试耗时: {time.time()-retry_start:.3f}秒")
1502
+ continue
1503
+ except NoAvailableAccount as e:
1504
+ last_error = e
1505
+ print(f"[文件上传] 无可用账号: {e}")
1506
+ break
1507
+ except Exception as e:
1508
+ last_error = e
1509
+ print(f"[文件上传] 第{retry_idx+1}次尝试失败: {type(e).__name__}: {e}")
1510
+ print(f"[文件上传] 堆栈跟踪:\n{traceback.format_exc()}")
1511
+ print(f"[文件上传] 本次尝试耗时: {time.time()-retry_start:.3f}秒")
1512
+ if account_idx is None:
1513
+ break
1514
+ continue
1515
+
1516
+ total_time = time.time() - request_start_time
1517
+ print(f"\n[文件上传] ===== 所有重试均失败 =====")
1518
+ error_message = last_error or "没有可用的账号"
1519
+ print(f"[文件上传] 最后错误: {error_message}")
1520
+ print(f"[文件上传] 总耗时: {total_time:.3f}秒")
1521
+ print(f"{'='*60}\n")
1522
+ status_code = 429 if isinstance(last_error, (AccountRateLimitError, NoAvailableAccount)) else 500
1523
+ err_type = "rate_limit" if status_code == 429 else "api_error"
1524
+ return jsonify({"error": {"message": f"文件上传失败: {error_message}", "type": err_type}}), status_code
1525
+
1526
+ except Exception as e:
1527
+ total_time = time.time() - request_start_time
1528
+ print(f"\n[文件上传] ===== 发生异常 =====")
1529
+ print(f"[文件上传] 错误类型: {type(e).__name__}")
1530
+ print(f"[文件上传] 错误信息: {e}")
1531
+ print(f"[文件上传] 堆栈跟踪:\n{traceback.format_exc()}")
1532
+ print(f"[文件上传] 总耗时: {total_time:.3f}秒")
1533
+ print(f"{'='*60}\n")
1534
+ return jsonify({"error": {"message": str(e), "type": "api_error"}}), 500
1535
+
1536
+
1537
+ @app.route('/v1/files', methods=['GET'])
1538
+ @require_api_auth
1539
+ def list_files():
1540
+ """获取已上传文件列表"""
1541
+ files = file_manager.list_files()
1542
+ return jsonify({
1543
+ "object": "list",
1544
+ "data": [{
1545
+ "id": f["openai_file_id"],
1546
+ "object": "file",
1547
+ "bytes": f.get("size", 0),
1548
+ "created_at": f.get("created_at", int(time.time())),
1549
+ "filename": f.get("filename", ""),
1550
+ "purpose": "assistants"
1551
+ } for f in files]
1552
+ })
1553
+
1554
+
1555
+ @app.route('/v1/files/<file_id>', methods=['GET'])
1556
+ @require_api_auth
1557
+ def get_file(file_id):
1558
+ """获取文件信息"""
1559
+ file_info = file_manager.get_file(file_id)
1560
+ if not file_info:
1561
+ return jsonify({"error": {"message": "File not found", "type": "invalid_request_error"}}), 404
1562
+
1563
+ return jsonify({
1564
+ "id": file_info["openai_file_id"],
1565
+ "object": "file",
1566
+ "bytes": file_info.get("size", 0),
1567
+ "created_at": file_info.get("created_at", int(time.time())),
1568
+ "filename": file_info.get("filename", ""),
1569
+ "purpose": "assistants"
1570
+ })
1571
+
1572
+
1573
+ @app.route('/v1/files/<file_id>', methods=['DELETE'])
1574
+ @require_api_auth
1575
+ def delete_file(file_id):
1576
+ """删除文件"""
1577
+ if file_manager.delete_file(file_id):
1578
+ return jsonify({
1579
+ "id": file_id,
1580
+ "object": "file",
1581
+ "deleted": True
1582
+ })
1583
+ return jsonify({"error": {"message": "File not found", "type": "invalid_request_error"}}), 404
1584
+
1585
+
1586
+ @app.route('/v1/chat/completions', methods=['POST'])
1587
+ @require_api_auth
1588
+ def chat_completions():
1589
+ """聊天对话接口(支持图��输入输出)"""
1590
+ try:
1591
+ # 每次请求时清理过期图片
1592
+ cleanup_expired_images()
1593
+
1594
+ data = request.json
1595
+ messages = data.get('messages', [])
1596
+ prompts = data.get('prompts', []) # 支持替代格式
1597
+ stream = data.get('stream', False)
1598
+
1599
+ # 提取用户消息、图片和文件ID
1600
+ user_message = ""
1601
+ input_images = []
1602
+ input_file_ids = [] # OpenAI file_id 列表
1603
+
1604
+ # 处理标准 OpenAI messages 格式
1605
+ for msg in messages:
1606
+ if msg.get('role') == 'user':
1607
+ content = msg.get('content', '')
1608
+ text, images = extract_images_from_openai_content(content)
1609
+ if text:
1610
+ user_message = text
1611
+ input_images.extend(images)
1612
+
1613
+ # 提取文件ID(支持多种格式)
1614
+ if isinstance(content, list):
1615
+ for item in content:
1616
+ if isinstance(item, dict):
1617
+ # 格式1: {"type": "file", "file_id": "xxx"}
1618
+ if item.get('type') == 'file' and item.get('file_id'):
1619
+ input_file_ids.append(item['file_id'])
1620
+ # 格式2: {"type": "file", "file": {"file_id": "xxx"}}
1621
+ elif item.get('type') == 'file' and isinstance(item.get('file'), dict):
1622
+ file_obj = item['file']
1623
+ # 支持 file_id 或 id 两种字段名
1624
+ fid = file_obj.get('file_id') or file_obj.get('id')
1625
+ if fid:
1626
+ input_file_ids.append(fid)
1627
+
1628
+ # 处理替代 prompts 格式(支持内联 base64 图片)
1629
+ # 格式: {"prompts": [{"role": "user", "text": "...", "files": [{"data": "data:image...", "type": "image"}]}]}
1630
+ for prompt in prompts:
1631
+ if prompt.get('role') == 'user':
1632
+ # 提取文本
1633
+ prompt_text = prompt.get('text', '')
1634
+ if prompt_text and not user_message:
1635
+ user_message = prompt_text
1636
+ elif prompt_text:
1637
+ user_message = prompt_text # 使用最新的用户消息
1638
+
1639
+ # 提取内联 files 数组中的图片
1640
+ files_array = prompt.get('files', [])
1641
+ if files_array:
1642
+ images_from_files = extract_images_from_files_array(files_array)
1643
+ input_images.extend(images_from_files)
1644
+
1645
+ # 将 OpenAI file_id 转换为 Gemini fileId
1646
+ gemini_file_ids = []
1647
+ for fid in input_file_ids:
1648
+ gemini_fid = file_manager.get_gemini_file_id(fid)
1649
+ if gemini_fid:
1650
+ gemini_file_ids.append(gemini_fid)
1651
+
1652
+ if not user_message and not input_images and not gemini_file_ids:
1653
+ return jsonify({"error": "No user message found"}), 400
1654
+
1655
+ # 检查是否指定了特定账号
1656
+ specified_account_id = data.get('account_id')
1657
+
1658
+ if specified_account_id is not None:
1659
+ # 使用指定的账号
1660
+ accounts = account_manager.accounts
1661
+ if specified_account_id < 0 or specified_account_id >= len(accounts):
1662
+ return jsonify({"error": f"无效的账号ID: {specified_account_id}"}), 400
1663
+ account = accounts[specified_account_id]
1664
+ if not account.get('enabled', True):
1665
+ return jsonify({"error": f"账号 {specified_account_id} 已禁用"}), 400
1666
+ # 检查是否在冷却中
1667
+ cooldown_until = account.get('cooldown_until', 0)
1668
+ if cooldown_until > time.time():
1669
+ return jsonify({"error": f"账号 {specified_account_id} 正在冷却中,请稍后重试"}), 429
1670
+
1671
+ max_retries = 1
1672
+ last_error = None
1673
+ chat_response = None
1674
+ account_idx = specified_account_id
1675
+ try:
1676
+ session, jwt, team_id = ensure_session_for_account(account_idx, account)
1677
+ proxy = get_proxy()
1678
+
1679
+ for img in input_images:
1680
+ uploaded_file_id = upload_inline_image_to_gemini(jwt, session, team_id, img, proxy)
1681
+ if uploaded_file_id:
1682
+ gemini_file_ids.append(uploaded_file_id)
1683
+
1684
+ chat_response = stream_chat_with_images(jwt, session, user_message, proxy, team_id, gemini_file_ids)
1685
+ except (AccountRateLimitError, AccountAuthError, AccountRequestError) as e:
1686
+ last_error = e
1687
+ account_manager.mark_account_cooldown(account_idx, str(e), account_manager.generic_error_cooldown)
1688
+ except Exception as e:
1689
+ last_error = e
1690
+ else:
1691
+ # 轮训获取账号
1692
+ available_accounts = account_manager.get_available_accounts()
1693
+ if not available_accounts:
1694
+ next_cd = account_manager.get_next_cooldown_info()
1695
+ wait_msg = ""
1696
+ if next_cd:
1697
+ wait_msg = f"(最近冷却账号 {next_cd['index']},约 {int(next_cd['cooldown_until']-time.time())} 秒后可重试)"
1698
+ return jsonify({"error": f"没有可用的账号{wait_msg}"}), 429
1699
+
1700
+ max_retries = len(available_accounts)
1701
+ last_error = None
1702
+ chat_response = None
1703
+
1704
+ for retry_idx in range(max_retries):
1705
+ account_idx = None
1706
+ try:
1707
+ account_idx, account = account_manager.get_next_account()
1708
+ session, jwt, team_id = ensure_session_for_account(account_idx, account)
1709
+ proxy = get_proxy()
1710
+
1711
+ # 上传内联图片获取 fileId
1712
+ for img in input_images:
1713
+ uploaded_file_id = upload_inline_image_to_gemini(jwt, session, team_id, img, proxy)
1714
+ if uploaded_file_id:
1715
+ gemini_file_ids.append(uploaded_file_id)
1716
+
1717
+ chat_response = stream_chat_with_images(jwt, session, user_message, proxy, team_id, gemini_file_ids)
1718
+ break
1719
+ except AccountRateLimitError as e:
1720
+ last_error = e
1721
+ if account_idx is not None:
1722
+ pt_wait = seconds_until_next_pt_midnight()
1723
+ cooldown_seconds = max(account_manager.rate_limit_cooldown, pt_wait)
1724
+ account_manager.mark_account_cooldown(account_idx, str(e), cooldown_seconds)
1725
+ print(f"[聊天] 第{retry_idx+1}次尝试失败(限额): {e}")
1726
+ continue
1727
+ except AccountAuthError as e:
1728
+ last_error = e
1729
+ if account_idx is not None:
1730
+ account_manager.mark_account_cooldown(account_idx, str(e), account_manager.auth_error_cooldown)
1731
+ print(f"[聊天] 第{retry_idx+1}次尝试失败(凭证): {e}")
1732
+ continue
1733
+ except AccountRequestError as e:
1734
+ last_error = e
1735
+ if account_idx is not None:
1736
+ account_manager.mark_account_cooldown(account_idx, str(e), account_manager.generic_error_cooldown)
1737
+ print(f"[聊天] 第{retry_idx+1}次尝试失败(请求异常): {e}")
1738
+ continue
1739
+ except Exception as e:
1740
+ last_error = e
1741
+ print(f"[聊天] 第{retry_idx+1}次尝试失败: {type(e).__name__}: {e}")
1742
+ if account_idx is None:
1743
+ break
1744
+ continue
1745
+
1746
+ if chat_response is None:
1747
+ error_message = last_error or "没有可用的账号"
1748
+ status_code = 429 if isinstance(last_error, (AccountRateLimitError, NoAvailableAccount)) else 500
1749
+ return jsonify({"error": f"所有账号请求失败: {error_message}"}), status_code
1750
+
1751
+ # 获取使用的账号csesidx
1752
+ used_account_csesidx = None
1753
+ if account_idx is not None and account_idx < len(account_manager.accounts):
1754
+ used_account = account_manager.accounts[account_idx]
1755
+ used_account_csesidx = used_account.get('csesidx', f'账号{account_idx}')
1756
+
1757
+ # 构建响应内容(包含图片)
1758
+ response_content = build_openai_response_content(chat_response, request.host_url)
1759
+
1760
+ if stream:
1761
+ # 流式响应
1762
+ def generate():
1763
+ chunk_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
1764
+ chunk = {
1765
+ "id": chunk_id,
1766
+ "object": "chat.completion.chunk",
1767
+ "created": int(time.time()),
1768
+ "model": "gemini-enterprise",
1769
+ "account_csesidx": used_account_csesidx,
1770
+ "choices": [{
1771
+ "index": 0,
1772
+ "delta": {"content": response_content},
1773
+ "finish_reason": None
1774
+ }]
1775
+ }
1776
+ yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
1777
+
1778
+ # 结束标记
1779
+ end_chunk = {
1780
+ "id": chunk_id,
1781
+ "object": "chat.completion.chunk",
1782
+ "created": int(time.time()),
1783
+ "model": "gemini-enterprise",
1784
+ "choices": [{
1785
+ "index": 0,
1786
+ "delta": {},
1787
+ "finish_reason": "stop"
1788
+ }]
1789
+ }
1790
+ yield f"data: {json.dumps(end_chunk, ensure_ascii=False)}\n\n"
1791
+ yield "data: [DONE]\n\n"
1792
+
1793
+ return Response(generate(), mimetype='text/event-stream')
1794
+ else:
1795
+ # 非流式响应
1796
+ response = {
1797
+ "id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
1798
+ "object": "chat.completion",
1799
+ "created": int(time.time()),
1800
+ "model": "gemini-enterprise",
1801
+ "account_csesidx": used_account_csesidx,
1802
+ "choices": [{
1803
+ "index": 0,
1804
+ "message": {
1805
+ "role": "assistant",
1806
+ "content": response_content
1807
+ },
1808
+ "finish_reason": "stop"
1809
+ }],
1810
+ "usage": {
1811
+ "prompt_tokens": len(user_message),
1812
+ "completion_tokens": len(chat_response.text),
1813
+ "total_tokens": len(user_message) + len(chat_response.text)
1814
+ }
1815
+ }
1816
+ return jsonify(response)
1817
+
1818
+ except Exception as e:
1819
+ import traceback
1820
+ traceback.print_exc()
1821
+ return jsonify({"error": str(e)}), 500
1822
+
1823
+
1824
+ def get_image_base_url(fallback_host_url: str) -> str:
1825
+ """获取图片基础URL
1826
+
1827
+ 优先使用配置文件中的 image_base_url,否则使用请求的 host_url
1828
+ """
1829
+ configured_url = account_manager.config.get("image_base_url", "").strip()
1830
+ if configured_url:
1831
+ # 确保以 / 结尾
1832
+ if not configured_url.endswith("/"):
1833
+ configured_url += "/"
1834
+ return configured_url
1835
+ return fallback_host_url
1836
+
1837
+
1838
+ def is_base64_output_mode() -> bool:
1839
+ try:
1840
+ if account_manager.config:
1841
+ mode = account_manager.config.get("image_output_mode") or "url"
1842
+ if isinstance(mode, str) and mode.lower() == "base64":
1843
+ return True
1844
+ except Exception:
1845
+ pass
1846
+ return False
1847
+
1848
+
1849
+ def build_openai_response_content(chat_response: ChatResponse, host_url: str) -> str:
1850
+ """构建OpenAI格式的响应内容
1851
+
1852
+ 返回纯文本,如果有图片可根据配置选择:
1853
+ - url: 在文本末尾追加图片URL(默认行为)
1854
+ - base64: 在文本末尾追加 data:image/...;base64,...
1855
+ """
1856
+ result_text = chat_response.text
1857
+
1858
+ if not chat_response.images:
1859
+ return result_text
1860
+
1861
+ # 从配置读取图片输出模式,默认 url
1862
+ image_mode = "base64" if is_base64_output_mode() else "url"
1863
+
1864
+ image_lines = []
1865
+
1866
+ if image_mode == "base64":
1867
+ # 优先使用已有的base64数据(使用 Markdown 图片语法,方便前端渲染)
1868
+ for img in chat_response.images:
1869
+ if img.base64_data:
1870
+ mime = img.mime_type or "image/png"
1871
+ image_lines.append(f"![image](data:{mime};base64,{img.base64_data})")
1872
+
1873
+ # 若部分图片没有base64数据,降级为URL形式,同样用 Markdown 图片语法
1874
+ base_url = get_image_base_url(host_url)
1875
+ for img in chat_response.images:
1876
+ if not img.base64_data and img.file_name:
1877
+ image_lines.append(f"![image]({base_url}image/{img.file_name})")
1878
+ else:
1879
+ # 传统URL模式
1880
+ base_url = get_image_base_url(host_url)
1881
+ for img in chat_response.images:
1882
+ if img.file_name:
1883
+ image_lines.append(f"{base_url}image/{img.file_name}")
1884
+
1885
+ if image_lines:
1886
+ if result_text:
1887
+ result_text += "\n\n"
1888
+ result_text += "\n".join(image_lines)
1889
+
1890
+ return result_text
1891
+
1892
+
1893
+ # ==================== 图片服务接口 ====================
1894
+
1895
+ @app.route('/image/<path:filename>')
1896
+ def serve_image(filename):
1897
+ """提供缓存图片的访问"""
1898
+ # 安全检查:防止路径遍历
1899
+ if '..' in filename or filename.startswith('/'):
1900
+ abort(404)
1901
+
1902
+ filepath = IMAGE_CACHE_DIR / filename
1903
+ if not filepath.exists():
1904
+ abort(404)
1905
+
1906
+ # 确定Content-Type
1907
+ ext = filepath.suffix.lower()
1908
+ mime_types = {
1909
+ '.png': 'image/png',
1910
+ '.jpg': 'image/jpeg',
1911
+ '.jpeg': 'image/jpeg',
1912
+ '.gif': 'image/gif',
1913
+ '.webp': 'image/webp',
1914
+ }
1915
+ mime_type = mime_types.get(ext, 'application/octet-stream')
1916
+
1917
+ return send_from_directory(IMAGE_CACHE_DIR, filename, mimetype=mime_type)
1918
+
1919
+
1920
+ @app.route('/health', methods=['GET'])
1921
+ def health_check():
1922
+ """健康检查"""
1923
+ return jsonify({"status": "ok", "timestamp": datetime.now().isoformat()})
1924
+
1925
+
1926
+ @app.route('/api/status', methods=['GET'])
1927
+ @require_admin
1928
+ def system_status():
1929
+ """获取系统状态"""
1930
+ total, available = account_manager.get_account_count()
1931
+ proxy_url = account_manager.config.get("proxy")
1932
+ proxy_enabled = account_manager.config.get("proxy_enabled", False)
1933
+ effective_proxy = get_proxy() # 实际使用的代理(考虑开关状态)
1934
+
1935
+ return jsonify({
1936
+ "status": "ok",
1937
+ "timestamp": datetime.now().isoformat(),
1938
+ "accounts": {
1939
+ "total": total,
1940
+ "available": available
1941
+ },
1942
+ "proxy": {
1943
+ "url": proxy_url,
1944
+ "enabled": proxy_enabled,
1945
+ "effective": effective_proxy, # 实际生效的代理地址
1946
+ "available": check_proxy(effective_proxy) if effective_proxy else False
1947
+ },
1948
+ "models": account_manager.config.get("models", [])
1949
+ })
1950
+
1951
+
1952
+ # ==================== 管理接口 ====================
1953
+
1954
+ @app.route('/')
1955
+ def index():
1956
+ """返回管理页面"""
1957
+ return send_from_directory('.', 'index.html')
1958
+
1959
+ @app.route('/chat_history.html')
1960
+ @require_admin
1961
+ def chat_history():
1962
+ """返回聊天记录页面"""
1963
+ return send_from_directory('.', 'chat_history.html')
1964
+
1965
+ @app.route('/api/accounts', methods=['GET'])
1966
+ @require_admin
1967
+ def get_accounts():
1968
+ """获取账号列表"""
1969
+ accounts_data = []
1970
+ now_ts = time.time()
1971
+ for i, acc in enumerate(account_manager.accounts):
1972
+ state = account_manager.account_states.get(i, {})
1973
+ cooldown_until = state.get("cooldown_until")
1974
+ cooldown_active = bool(cooldown_until and cooldown_until > now_ts)
1975
+ effective_available = state.get("available", True) and not cooldown_active
1976
+
1977
+ # 返回完整值用于编辑,前端显示时再截断
1978
+ accounts_data.append({
1979
+ "id": i,
1980
+ "team_id": acc.get("team_id", ""),
1981
+ "secure_c_ses": acc.get("secure_c_ses", ""),
1982
+ "host_c_oses": acc.get("host_c_oses", ""),
1983
+ "csesidx": acc.get("csesidx", ""),
1984
+ "user_agent": acc.get("user_agent", ""),
1985
+ "available": effective_available,
1986
+ "unavailable_reason": acc.get("unavailable_reason", ""),
1987
+ "cooldown_until": cooldown_until if cooldown_active else None,
1988
+ "cooldown_reason": state.get("cooldown_reason", ""),
1989
+ "has_jwt": state.get("jwt") is not None
1990
+ })
1991
+ return jsonify({"accounts": accounts_data})
1992
+
1993
+
1994
+ @app.route('/api/accounts', methods=['POST'])
1995
+ @require_admin
1996
+ def add_account():
1997
+ """添加账号"""
1998
+ data = request.json
1999
+ # 去重:基于 csesidx 或 team_id 检查
2000
+ new_csesidx = data.get("csesidx", "")
2001
+ new_team_id = data.get("team_id", "")
2002
+ for acc in account_manager.accounts:
2003
+ if new_csesidx and acc.get("csesidx") == new_csesidx:
2004
+ return jsonify({"error": "账号已存在(同 csesidx)"}), 400
2005
+ if new_team_id and acc.get("team_id") == new_team_id and new_csesidx == acc.get("csesidx"):
2006
+ return jsonify({"error": "账号已存在(同 team_id + csesidx)"}), 400
2007
+
2008
+ new_account = {
2009
+ "team_id": data.get("team_id", ""),
2010
+ "secure_c_ses": data.get("secure_c_ses", ""),
2011
+ "host_c_oses": data.get("host_c_oses", ""),
2012
+ "csesidx": data.get("csesidx", ""),
2013
+ "user_agent": data.get("user_agent", "Mozilla/5.0"),
2014
+ "available": True
2015
+ }
2016
+
2017
+ account_manager.accounts.append(new_account)
2018
+ idx = len(account_manager.accounts) - 1
2019
+ account_manager.account_states[idx] = {
2020
+ "jwt": None,
2021
+ "jwt_time": 0,
2022
+ "session": None,
2023
+ "available": True,
2024
+ "cooldown_until": None,
2025
+ "cooldown_reason": ""
2026
+ }
2027
+ account_manager.config["accounts"] = account_manager.accounts
2028
+ account_manager.save_config()
2029
+
2030
+ return jsonify({"success": True, "id": idx})
2031
+
2032
+
2033
+ @app.route('/api/accounts/<int:account_id>', methods=['PUT'])
2034
+ @require_admin
2035
+ def update_account(account_id):
2036
+ """更新账号"""
2037
+ if account_id < 0 or account_id >= len(account_manager.accounts):
2038
+ return jsonify({"error": "账号不存在"}), 404
2039
+
2040
+ data = request.json
2041
+ acc = account_manager.accounts[account_id]
2042
+
2043
+ if "team_id" in data:
2044
+ acc["team_id"] = data["team_id"]
2045
+ if "secure_c_ses" in data:
2046
+ acc["secure_c_ses"] = data["secure_c_ses"]
2047
+ if "host_c_oses" in data:
2048
+ acc["host_c_oses"] = data["host_c_oses"]
2049
+ if "csesidx" in data:
2050
+ acc["csesidx"] = data["csesidx"]
2051
+ if "user_agent" in data:
2052
+ acc["user_agent"] = data["user_agent"]
2053
+
2054
+ # 同步更新config中的accounts
2055
+ account_manager.config["accounts"] = account_manager.accounts
2056
+ account_manager.save_config()
2057
+ return jsonify({"success": True})
2058
+
2059
+
2060
+ @app.route('/api/accounts/<int:account_id>', methods=['DELETE'])
2061
+ @require_admin
2062
+ def delete_account(account_id):
2063
+ """删除账号"""
2064
+ if account_id < 0 or account_id >= len(account_manager.accounts):
2065
+ return jsonify({"error": "账号不存在"}), 404
2066
+
2067
+ account_manager.accounts.pop(account_id)
2068
+ # 重建状态映射
2069
+ new_states = {}
2070
+ for i in range(len(account_manager.accounts)):
2071
+ if i < account_id:
2072
+ new_states[i] = account_manager.account_states.get(i, {})
2073
+ else:
2074
+ new_states[i] = account_manager.account_states.get(i + 1, {})
2075
+ account_manager.account_states = new_states
2076
+ account_manager.config["accounts"] = account_manager.accounts
2077
+ account_manager.save_config()
2078
+
2079
+ return jsonify({"success": True})
2080
+
2081
+
2082
+ @app.route('/api/accounts/<int:account_id>/toggle', methods=['POST'])
2083
+ @require_admin
2084
+ def toggle_account(account_id):
2085
+ """��换账号状态"""
2086
+ if account_id < 0 or account_id >= len(account_manager.accounts):
2087
+ return jsonify({"error": "账号不存在"}), 404
2088
+
2089
+ state = account_manager.account_states.get(account_id, {})
2090
+ current = state.get("available", True)
2091
+ state["available"] = not current
2092
+ account_manager.accounts[account_id]["available"] = not current
2093
+
2094
+ if not current:
2095
+ # 重新启用时清除错误信息
2096
+ account_manager.accounts[account_id].pop("unavailable_reason", None)
2097
+ account_manager.accounts[account_id].pop("unavailable_time", None)
2098
+ state.pop("cooldown_until", None)
2099
+ state.pop("cooldown_reason", None)
2100
+ account_manager.accounts[account_id].pop("cooldown_until", None)
2101
+
2102
+ account_manager.save_config()
2103
+ return jsonify({"success": True, "available": not current})
2104
+
2105
+
2106
+ @app.route('/api/accounts/<int:account_id>/refresh-cookie', methods=['POST'])
2107
+ @require_admin
2108
+ def refresh_account_cookies(account_id):
2109
+ """刷新账号的secure_c_ses、host_c_oses和csesidx"""
2110
+ if account_id < 0 or account_id >= len(account_manager.accounts):
2111
+ return jsonify({"error": "账号不存在"}), 404
2112
+
2113
+ data = request.json
2114
+ acc = account_manager.accounts[account_id]
2115
+
2116
+ # 更新Cookie字段
2117
+ if "secure_c_ses" in data:
2118
+ acc["secure_c_ses"] = data["secure_c_ses"]
2119
+ if "host_c_oses" in data:
2120
+ acc["host_c_oses"] = data["host_c_oses"]
2121
+ if "csesidx" in data and data["csesidx"]:
2122
+ acc["csesidx"] = data["csesidx"]
2123
+
2124
+ # 清除JWT缓存,强制重新获取
2125
+ state = account_manager.account_states.get(account_id, {})
2126
+ state["jwt"] = None
2127
+ state["jwt_time"] = 0
2128
+ account_manager.account_states[account_id] = state
2129
+
2130
+ account_manager.config["accounts"] = account_manager.accounts
2131
+ account_manager.save_config()
2132
+
2133
+ return jsonify({"success": True, "message": "Cookie已刷新"})
2134
+
2135
+
2136
+ @app.route('/api/accounts/<int:account_id>/test', methods=['GET'])
2137
+ @require_admin
2138
+ def test_account(account_id):
2139
+ """测试账号JWT获取"""
2140
+ if account_id < 0 or account_id >= len(account_manager.accounts):
2141
+ return jsonify({"error": "账号不存在"}), 404
2142
+
2143
+ account = account_manager.accounts[account_id]
2144
+ proxy = account_manager.config.get("proxy")
2145
+
2146
+ try:
2147
+ jwt = get_jwt_for_account(account, proxy)
2148
+ return jsonify({"success": True, "message": "JWT获取成功"})
2149
+ except AccountRateLimitError as e:
2150
+ pt_wait = seconds_until_next_pt_midnight()
2151
+ cooldown_seconds = max(account_manager.rate_limit_cooldown, pt_wait)
2152
+ account_manager.mark_account_cooldown(account_id, str(e), cooldown_seconds)
2153
+ return jsonify({"success": False, "message": str(e), "cooldown": cooldown_seconds})
2154
+ except AccountAuthError as e:
2155
+ account_manager.mark_account_cooldown(account_id, str(e), account_manager.auth_error_cooldown)
2156
+ return jsonify({"success": False, "message": str(e), "cooldown": account_manager.auth_error_cooldown})
2157
+ except AccountRequestError as e:
2158
+ account_manager.mark_account_cooldown(account_id, str(e), account_manager.generic_error_cooldown)
2159
+ return jsonify({"success": False, "message": str(e), "cooldown": account_manager.generic_error_cooldown})
2160
+ except Exception as e:
2161
+ return jsonify({"success": False, "message": str(e)})
2162
+
2163
+
2164
+ @app.route('/api/models', methods=['GET'])
2165
+ @require_admin
2166
+ def get_models_config():
2167
+ """获取模型配置"""
2168
+ models = account_manager.config.get("models", [])
2169
+ return jsonify({"models": models})
2170
+
2171
+
2172
+ @app.route('/api/models', methods=['POST'])
2173
+ @require_admin
2174
+ def add_model():
2175
+ """添加模型"""
2176
+ data = request.json
2177
+ new_model = {
2178
+ "id": data.get("id", ""),
2179
+ "name": data.get("name", ""),
2180
+ "description": data.get("description", ""),
2181
+ "context_length": data.get("context_length", 32768),
2182
+ "max_tokens": data.get("max_tokens", 8192),
2183
+ "enabled": data.get("enabled", True)
2184
+ }
2185
+
2186
+ if "models" not in account_manager.config:
2187
+ account_manager.config["models"] = []
2188
+
2189
+ account_manager.config["models"].append(new_model)
2190
+ account_manager.save_config()
2191
+
2192
+ return jsonify({"success": True})
2193
+
2194
+
2195
+ @app.route('/api/models/<model_id>', methods=['PUT'])
2196
+ @require_admin
2197
+ def update_model(model_id):
2198
+ """更新模型"""
2199
+ models = account_manager.config.get("models", [])
2200
+ for model in models:
2201
+ if model.get("id") == model_id:
2202
+ data = request.json
2203
+ if "name" in data:
2204
+ model["name"] = data["name"]
2205
+ if "description" in data:
2206
+ model["description"] = data["description"]
2207
+ if "context_length" in data:
2208
+ model["context_length"] = data["context_length"]
2209
+ if "max_tokens" in data:
2210
+ model["max_tokens"] = data["max_tokens"]
2211
+ if "enabled" in data:
2212
+ model["enabled"] = data["enabled"]
2213
+ account_manager.save_config()
2214
+ return jsonify({"success": True})
2215
+
2216
+ return jsonify({"error": "模型不存在"}), 404
2217
+
2218
+
2219
+ @app.route('/api/models/<model_id>', methods=['DELETE'])
2220
+ @require_admin
2221
+ def delete_model(model_id):
2222
+ """删除模型"""
2223
+ models = account_manager.config.get("models", [])
2224
+ for i, model in enumerate(models):
2225
+ if model.get("id") == model_id:
2226
+ models.pop(i)
2227
+ account_manager.save_config()
2228
+ return jsonify({"success": True})
2229
+
2230
+ return jsonify({"error": "模型不存在"}), 404
2231
+
2232
+
2233
+ @app.route('/api/config', methods=['GET'])
2234
+ @require_admin
2235
+ def get_config():
2236
+ """获取完整配置"""
2237
+ return jsonify(account_manager.config)
2238
+
2239
+
2240
+ @app.route('/api/config', methods=['PUT'])
2241
+ @require_admin
2242
+ def update_config():
2243
+ """更新配置"""
2244
+ data = request.json or {}
2245
+ if "proxy" in data:
2246
+ account_manager.config["proxy"] = data["proxy"]
2247
+ if "log_level" in data:
2248
+ try:
2249
+ set_log_level(data["log_level"], persist=True)
2250
+ except Exception as e:
2251
+ return jsonify({"error": str(e)}), 400
2252
+ if "image_output_mode" in data:
2253
+ mode = data["image_output_mode"]
2254
+ if isinstance(mode, str) and mode.lower() in ("url", "base64"):
2255
+ account_manager.config["image_output_mode"] = mode.lower()
2256
+ account_manager.save_config()
2257
+ return jsonify({"success": True})
2258
+
2259
+
2260
+ @app.route('/api/logging', methods=['GET', 'POST'])
2261
+ @require_admin
2262
+ def logging_config():
2263
+ """获取或设置日志级别"""
2264
+ if request.method == 'GET':
2265
+ return jsonify({
2266
+ "level": CURRENT_LOG_LEVEL_NAME,
2267
+ "levels": list(LOG_LEVELS.keys())
2268
+ })
2269
+
2270
+ data = request.json or {}
2271
+ level = data.get("level", "").upper()
2272
+ if level not in LOG_LEVELS:
2273
+ return jsonify({"error": "无效日志级别"}), 400
2274
+
2275
+ set_log_level(level, persist=True)
2276
+ return jsonify({"success": True, "level": CURRENT_LOG_LEVEL_NAME})
2277
+
2278
+
2279
+ @app.route('/api/auth/login', methods=['POST'])
2280
+ def admin_login():
2281
+ """后台登录,返回 token。若尚未设置密码,则首次设置。"""
2282
+ data = request.json or {}
2283
+ password = data.get("password", "")
2284
+ if not password:
2285
+ return jsonify({"error": "密码不能为空"}), 400
2286
+
2287
+ stored_hash = get_admin_password_hash()
2288
+ if stored_hash:
2289
+ if not check_password_hash(stored_hash, password):
2290
+ return jsonify({"error": "密码错误"}), 401
2291
+ else:
2292
+ # 首次设置密码
2293
+ set_admin_password(password)
2294
+
2295
+ token = create_admin_token()
2296
+ resp = jsonify({"token": token, "level": CURRENT_LOG_LEVEL_NAME})
2297
+ resp.set_cookie(
2298
+ "admin_token",
2299
+ token,
2300
+ max_age=86400,
2301
+ httponly=True,
2302
+ secure=False,
2303
+ samesite="Lax",
2304
+ path="/"
2305
+ )
2306
+ return resp
2307
+
2308
+
2309
+ @app.route('/api/tokens', methods=['GET', 'POST'])
2310
+ @require_admin
2311
+ def manage_tokens():
2312
+ """获取或创建API访问Token"""
2313
+ if request.method == 'GET':
2314
+ return jsonify({"tokens": list(API_TOKENS)})
2315
+
2316
+ data = request.json or {}
2317
+ token = data.get("token")
2318
+ if not token:
2319
+ token = secrets.token_urlsafe(32)
2320
+ if not isinstance(token, str) or len(token) < 8:
2321
+ return jsonify({"error": "Token格式不合法"}), 400
2322
+ if token in API_TOKENS:
2323
+ return jsonify({"error": "Token已存在"}), 400
2324
+
2325
+ API_TOKENS.add(token)
2326
+ persist_api_tokens()
2327
+ return jsonify({"success": True, "token": token})
2328
+
2329
+
2330
+ @app.route('/api/tokens/<token>', methods=['DELETE'])
2331
+ @require_admin
2332
+ def delete_token(token):
2333
+ """删除指定API Token"""
2334
+ if token in API_TOKENS:
2335
+ API_TOKENS.remove(token)
2336
+ persist_api_tokens()
2337
+ return jsonify({"success": True})
2338
+ return jsonify({"error": "Token不存在"}), 404
2339
+
2340
+
2341
+ @app.route('/api/config/import', methods=['POST'])
2342
+ @require_admin
2343
+ def import_config():
2344
+ """导入配置"""
2345
+ try:
2346
+ data = request.json
2347
+ account_manager.config = data
2348
+ if data.get("log_level"):
2349
+ try:
2350
+ set_log_level(data.get("log_level"), persist=False)
2351
+ except Exception:
2352
+ pass
2353
+ if data.get("admin_secret_key"):
2354
+ global ADMIN_SECRET_KEY
2355
+ ADMIN_SECRET_KEY = data.get("admin_secret_key")
2356
+ else:
2357
+ get_admin_secret_key()
2358
+ load_api_tokens()
2359
+ account_manager.accounts = data.get("accounts", [])
2360
+ # 重建账号状态
2361
+ account_manager.account_states = {}
2362
+ for i, acc in enumerate(account_manager.accounts):
2363
+ available = acc.get("available", True)
2364
+ account_manager.account_states[i] = {
2365
+ "jwt": None,
2366
+ "jwt_time": 0,
2367
+ "session": None,
2368
+ "available": available,
2369
+ "cooldown_until": acc.get("cooldown_until"),
2370
+ "cooldown_reason": acc.get("unavailable_reason") or acc.get("cooldown_reason") or ""
2371
+ }
2372
+ account_manager.save_config()
2373
+ return jsonify({"success": True})
2374
+ except Exception as e:
2375
+ return jsonify({"error": str(e)}), 400
2376
+
2377
+
2378
+ @app.route('/api/proxy/test', methods=['POST'])
2379
+ @require_admin
2380
+ def test_proxy():
2381
+ """测试代理"""
2382
+ data = request.json
2383
+ proxy_url = data.get("proxy") or account_manager.config.get("proxy")
2384
+
2385
+ if not proxy_url:
2386
+ return jsonify({"success": False, "message": "未配置代理地址"})
2387
+
2388
+ available = check_proxy(proxy_url)
2389
+ return jsonify({
2390
+ "success": available,
2391
+ "message": "代理可用" if available else "代理不可用或连接超时"
2392
+ })
2393
+
2394
+
2395
+ @app.route('/api/proxy/status', methods=['GET'])
2396
+ @require_admin
2397
+ def get_proxy_status():
2398
+ """获取代理状态"""
2399
+ proxy = account_manager.config.get("proxy")
2400
+ if not proxy:
2401
+ return jsonify({"enabled": False, "url": None, "available": False})
2402
+
2403
+ available = check_proxy(proxy)
2404
+ return jsonify({
2405
+ "enabled": True,
2406
+ "url": proxy,
2407
+ "available": available
2408
+ })
2409
+
2410
+
2411
+ @app.route('/api/config/export', methods=['GET'])
2412
+ @require_admin
2413
+ def export_config():
2414
+ """导出配置"""
2415
+ return jsonify(account_manager.config)
2416
+
2417
+
2418
+ def print_startup_info():
2419
+ """打印启动信息"""
2420
+ print("="*60)
2421
+ print("Business Gemini OpenAPI 服务 (多账号轮训版)")
2422
+ print("支持图片输入输出 (OpenAI格式)")
2423
+ print("="*60)
2424
+
2425
+ # 检测配置文件是否存在,不存在则从 .example 复制初始化
2426
+ example_file = Path(__file__).parent / "business_gemini_session.json.example"
2427
+ if not CONFIG_FILE.exists():
2428
+ if example_file.exists():
2429
+ shutil.copy(example_file, CONFIG_FILE)
2430
+ print(f"\n[初始化] 配置文件不存在,已从 {example_file.name} 复制创建")
2431
+ else:
2432
+ print(f"\n[警告] 配置文件和示例文件都不存在,请创建 {CONFIG_FILE.name}")
2433
+
2434
+ # 加载配置
2435
+ account_manager.load_config()
2436
+ get_admin_secret_key()
2437
+
2438
+ # 代理信息
2439
+ proxy = account_manager.config.get("proxy")
2440
+ print(f"\n[代理配置]")
2441
+ print(f" 地址: {proxy or '未配置'}")
2442
+ if proxy:
2443
+ proxy_available = check_proxy(proxy)
2444
+ print(f" 状态: {'✓ 可用' if proxy_available else '✗ 不可用'}")
2445
+
2446
+ # 图片缓存信息
2447
+ print(f"\n[图片缓存]")
2448
+ print(f" 目录: {IMAGE_CACHE_DIR}")
2449
+ print(f" 缓存时间: {IMAGE_CACHE_HOURS} 小时")
2450
+
2451
+ # 账号信息
2452
+ total, available = account_manager.get_account_count()
2453
+ print(f"\n[账号配置]")
2454
+ print(f" 总数量: {total}")
2455
+ print(f" 可用数量: {available}")
2456
+
2457
+ for i, acc in enumerate(account_manager.accounts):
2458
+ state = account_manager.account_states.get(i, {})
2459
+ is_available = account_manager.is_account_available(i)
2460
+ status = "✓" if is_available else "✗"
2461
+ team_id = acc.get("team_id", "未知") + "..."
2462
+ cooldown_until = state.get("cooldown_until")
2463
+ extra = ""
2464
+ if cooldown_until and cooldown_until > time.time():
2465
+ remaining = int(cooldown_until - time.time())
2466
+ extra = f" (冷却中 ~{remaining}s)"
2467
+ print(f" [{i}] {status} team_id: {team_id}{extra}")
2468
+
2469
+ # 模型信息
2470
+ models = account_manager.config.get("models", [])
2471
+ print(f"\n[模型配置]")
2472
+ if models:
2473
+ for model in models:
2474
+ print(f" - {model.get('id')}: {model.get('name', '')}")
2475
+ else:
2476
+ print(" - gemini-enterprise (默认)")
2477
+
2478
+ print(f"\n[接口列表]")
2479
+ print(" GET /v1/models - 获取模型列表")
2480
+ print(" POST /v1/chat/completions - 聊天对话 (支持图片)")
2481
+ print(" GET /v1/status - 系统状态")
2482
+ print(" GET /health - 健康检查")
2483
+ print(" GET /image/<filename> - 获取缓存图片")
2484
+ print("\n" + "="*60)
2485
+ print("启动服务...")
2486
+
2487
+
2488
+ if __name__ == '__main__':
2489
+ print_startup_info()
2490
+
2491
+ if not account_manager.accounts:
2492
+ print("[!] 警告: 没有配置任何账号")
2493
+
2494
+ # 支持Hugging Face Spaces的端口配置
2495
+ port = int(os.environ.get("PORT", 8000))
2496
+ host = os.environ.get("HOST", "0.0.0.0")
2497
+
2498
+ app.run(host=host, port=port, debug=False)
hf_manual_deploy.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face 手动部署指南
2
+
3
+ 由于网络连接问题,请按照以下步骤手动部署到 Hugging Face Spaces:
4
+
5
+ ## 方法 1: 通过 Web 界面上传文件(推荐)
6
+
7
+ 1. **访问你的 Space**
8
+ - 打开浏览器访问: https://huggingface.co/spaces/Maynor996/gg2
9
+
10
+ 2. **上传文件**
11
+ - 点击页面上的 "Files" 标签
12
+ - 点击 "Upload file" 按钮
13
+ - 上传以下文件(从 `/Users/chinamanor/Downloads/cursor编程/gg2/` 目录):
14
+ - `app.py`
15
+ - `gemini.py`
16
+ - `index.html`
17
+ - `requirements.txt`(使用 requirements-hf.txt 的内容)
18
+ - `README.md`(使用 README_hf.md 的内容)
19
+ - `business_gemini_session.json.example`
20
+
21
+ 3. **文件内容参考**
22
+ - `requirements.txt` 内容:
23
+ ```
24
+ flask>=2.0.0
25
+ flask-cors>=3.0.0
26
+ requests>=2.25.0
27
+ urllib3>=1.26.0
28
+ ```
29
+
30
+ - `README.md` 开头需要添加:
31
+ ```yaml
32
+ ---
33
+ title: Business Gemini Pool
34
+ emoji: 🚀
35
+ colorFrom: blue
36
+ colorTo: green
37
+ sdk: gradio
38
+ sdk_version: 4.44.0
39
+ app_file: app.py
40
+ pinned: false
41
+ license: mit
42
+ ---
43
+ ```
44
+
45
+ ## 方法 2: 使用 Git 命令(如果网络允许)
46
+
47
+ ```bash
48
+ # 1. 克隆你的 Space
49
+ git clone https://huggingface.co/spaces/Maynor996/gg2
50
+ cd gg2
51
+
52
+ # 2. 复制文件(从项目目录)
53
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/app.py ./
54
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/gemini.py ./
55
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/index.html ./
56
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/requirements-hf.txt ./requirements.txt
57
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/README_hf.md ./README.md
58
+ cp /Users/chinamanor/Downloads/cursor编程/gg2/business_gemini_session.json.example ./
59
+
60
+ # 3. 提交并推送
61
+ git add .
62
+ git commit -m "Deploy Business Gemini Pool"
63
+ git push
64
+ ```
65
+
66
+ ## 部署后配置
67
+
68
+ 部署成功后:
69
+
70
+ 1. **访问应用**
71
+ - URL: https://Maynor996-gg2.hf.space
72
+
73
+ 2. **配置 Gemini 账号**
74
+ - 在 Web 界面点击"账号管理"
75
+ - 添加你的 Gemini 账号信息:
76
+ - Team ID
77
+ - Secure Cookie
78
+ - Host Cookie
79
+ - Session Index
80
+ - User Agent
81
+
82
+ 3. **测试 API**
83
+ ```bash
84
+ curl -X POST https://Maynor996-gg2.hf.space/v1/chat/completions \
85
+ -H "Content-Type: application/json" \
86
+ -d '{
87
+ "model": "gemini-enterprise",
88
+ "messages": [{"role": "user", "content": "Hello!"}]
89
+ }'
90
+ ```
91
+
92
+ ## 注意事项
93
+
94
+ - Hugging Face 会自动构建和部署你的应用
95
+ - 构建过程通常需要 2-5 分钟
96
+ - 可以在 Space 页面查看构建日志
97
+ - 免费的 CPU Space 有一些限制,但足够基本使用
index.html ADDED
@@ -0,0 +1,2025 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Business Gemini Pool 管理控制台</title>
7
+ <style>
8
+ /* [OPTIMIZATION] 1. 全局样式优化与变量调整 */
9
+ :root {
10
+ /* 核心颜色保持不变 */
11
+ --primary: #4285f4;
12
+ --primary-hover: #3367d6;
13
+ --primary-light: rgba(66, 133, 244, 0.1);
14
+ --success: #34a853;
15
+ --success-light: rgba(52, 168, 83, 0.1);
16
+ --danger: #ea4335;
17
+ --danger-light: rgba(234, 67, 53, 0.1);
18
+ --warning: #fbbc04;
19
+ --warning-light: rgba(251, 188, 4, 0.1);
20
+
21
+ /* [NEW] 引入更精细的变量控制 */
22
+ --radius-sm: 6px;
23
+ --radius-md: 12px; /* 增大圆角,更柔和 */
24
+ --radius-lg: 16px;
25
+ --transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); /* [NEW] 现代化的缓动函数 */
26
+
27
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; /* [NEW] 引入更适合UI的字体 */
28
+ }
29
+
30
+ /* [OPTIMIZATION] 2. Light & Dark Theme 优化,增强对比度和质感 */
31
+ [data-theme="light"] {
32
+ --bg-color: #f7f8fc; /* 更柔和的背景色 */
33
+ --card-bg: #ffffff;
34
+ --text-main: #1f2328;
35
+ --text-muted: #656d76;
36
+ --border: #e4e7eb; /* 更浅的边框色 */
37
+ --hover-bg: #f2f3f5;
38
+ --input-bg: #ffffff;
39
+ --shadow-sm: 0 1px 2px 0 rgba(27, 31, 35, 0.04);
40
+ --shadow-md: 0 4px 8px 0 rgba(27, 31, 35, 0.06), 0 1px 2px 0 rgba(27, 31, 35, 0.05); /* 更柔和的阴影 */
41
+ --shadow-lg: 0 10px 20px 0 rgba(27, 31, 35, 0.07), 0 3px 6px 0 rgba(27, 31, 35, 0.05);
42
+ }
43
+
44
+ [data-theme="dark"] {
45
+ --bg-color: #1a1b1e;
46
+ --card-bg: #242528;
47
+ --text-main: #e8eaed;
48
+ --text-muted: #9aa0a6;
49
+ --border: #3a3c40;
50
+ --hover-bg: #303134;
51
+ --input-bg: #2f3033;
52
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
53
+ --shadow-md: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 1px 2px 0 rgba(0, 0, 0, 0.1);
54
+ --shadow-lg: 0 10px 20px 0 rgba(0, 0, 0, 0.2), 0 3px 6px 0 rgba(0, 0, 0, 0.15);
55
+ }
56
+
57
+ * {
58
+ margin: 0;
59
+ padding: 0;
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ body {
64
+ font-family: var(--font-main);
65
+ background-color: var(--bg-color);
66
+ color: var(--text-main);
67
+ min-height: 100vh;
68
+ transition: background-color 0.3s, color 0.3s;
69
+ -webkit-font-smoothing: antialiased;
70
+ -moz-osx-font-smoothing: grayscale;
71
+ }
72
+
73
+ .container {
74
+ max-width: 1400px;
75
+ margin: 0 auto;
76
+ padding: 32px; /* 增加页面内边距 */
77
+ }
78
+
79
+ /* [OPTIMIZATION] 3. Header 重新设计,更简洁大气 */
80
+ .header {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ margin-bottom: 32px;
85
+ /* 移除背景和阴影,使其融入页面 */
86
+ }
87
+
88
+ .header-left { display: flex; align-items: center; gap: 16px; }
89
+
90
+ .logo {
91
+ width: 44px;
92
+ height: 44px;
93
+ background: linear-gradient(135deg, #4285f4, #34a853, #fbbc04, #ea4335);
94
+ border-radius: var(--radius-md);
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ color: white;
99
+ font-weight: 600;
100
+ font-size: 22px;
101
+ transform: rotate(-10deg); /* [NEW] 增加一点趣味性 */
102
+ transition: var(--transition-ease);
103
+ }
104
+ .logo:hover { transform: rotate(0deg) scale(1.05); }
105
+
106
+ .header h1 {
107
+ font-size: 26px; /* 增大标题字号 */
108
+ font-weight: 600;
109
+ color: var(--text-main);
110
+ }
111
+
112
+ .header h1 span {
113
+ color: var(--text-muted);
114
+ font-weight: 400;
115
+ font-size: 16px;
116
+ margin-left: 10px;
117
+ }
118
+
119
+ .header-right { display: flex; align-items: center; gap: 16px; }
120
+
121
+ .status-indicator {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 8px 16px;
126
+ background: var(--success-light);
127
+ border: 1px solid rgba(52, 168, 83, 0.2);
128
+ border-radius: 50px; /* 改为胶囊形状 */
129
+ font-size: 14px;
130
+ color: var(--success);
131
+ font-weight: 500;
132
+ }
133
+ .status-indicator::before {
134
+ content: ''; width: 8px; height: 8px;
135
+ background: var(--success); border-radius: 50%;
136
+ animation: pulse 2s infinite;
137
+ }
138
+ @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.9); } }
139
+
140
+ .theme-toggle {
141
+ width: 44px; height: 44px; border: 1px solid var(--border);
142
+ background: var(--card-bg); border-radius: var(--radius-md);
143
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
144
+ font-size: 20px; transition: var(--transition-ease);
145
+ }
146
+ .theme-toggle:hover { background: var(--hover-bg); border-color: var(--primary); transform: translateY(-2px); }
147
+
148
+ /* [OPTIMIZATION] 4. Tabs 重新设计,更现代、更 subtle */
149
+ .tabs {
150
+ display: flex;
151
+ gap: 16px;
152
+ border-bottom: 1px solid var(--border); /* 底部线条导航 */
153
+ margin-bottom: 32px;
154
+ }
155
+ .tab {
156
+ padding: 14px 4px; /* 减少水平padding,通过gap控制间距 */
157
+ border: none; border-bottom: 2px solid transparent;
158
+ background: transparent; color: var(--text-muted);
159
+ font-size: 15px; font-weight: 500;
160
+ cursor: pointer; border-radius: 0;
161
+ transition: var(--transition-ease);
162
+ display: flex; align-items: center; justify-content: center;
163
+ gap: 8px;
164
+ }
165
+ .tab:hover { color: var(--primary); }
166
+ .tab.active { color: var(--primary); border-bottom-color: var(--primary); }
167
+ .tab-icon { font-size: 20px; }
168
+
169
+
170
+ /* Status Badge */
171
+ .badge {
172
+ display: inline-flex;
173
+ align-items: center;
174
+ gap: 6px;
175
+ padding: 6px 12px;
176
+ border-radius: 20px;
177
+ font-size: 12px;
178
+ font-weight: 500;
179
+ }
180
+
181
+ .badge::before {
182
+ content: '';
183
+ width: 6px;
184
+ height: 6px;
185
+ border-radius: 50%;
186
+ }
187
+
188
+ .badge-success {
189
+ background: var(--success-light);
190
+ color: var(--success);
191
+ }
192
+
193
+ .badge-success::before {
194
+ background: var(--success);
195
+ }
196
+
197
+ .badge-danger {
198
+ background: var(--danger-light);
199
+ color: var(--danger);
200
+ }
201
+
202
+ .badge-danger::before {
203
+ background: var(--danger);
204
+ }
205
+
206
+ .cooldown-hint {
207
+ display: block;
208
+ color: var(--text-muted);
209
+ font-size: 12px;
210
+ margin-top: 4px;
211
+ }
212
+
213
+ .log-level-control {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ background: var(--card-bg);
218
+ border: 1px solid var(--border);
219
+ border-radius: var(--radius-md);
220
+ padding: 6px 10px;
221
+ }
222
+ .log-level-control label {
223
+ font-size: 12px;
224
+ color: var(--text-muted);
225
+ }
226
+ .log-level-select {
227
+ border: 1px solid var(--border);
228
+ background: var(--input-bg);
229
+ color: var(--text-main);
230
+ border-radius: var(--radius-sm);
231
+ padding: 6px 8px;
232
+ }
233
+
234
+ .token-actions {
235
+ display: flex;
236
+ gap: 8px;
237
+ flex-wrap: wrap;
238
+ margin-bottom: 12px;
239
+ }
240
+ .token-input {
241
+ flex: 1;
242
+ min-width: 240px;
243
+ }
244
+
245
+ .badge-warning {
246
+ background: var(--warning-light);
247
+ color: #b06000;
248
+ }
249
+
250
+ .badge-warning::before {
251
+ background: var(--warning);
252
+ }
253
+
254
+ /* [OPTIMIZATION] 5. 动画效果增强 */
255
+ .tab-content { display: none; }
256
+ .tab-content.active { display: block; animation: contentFadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
257
+ @keyframes contentFadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
258
+
259
+ /* [OPTIMIZATION] 6. Card 样式优化 */
260
+ .card {
261
+ background: var(--card-bg);
262
+ border-radius: var(--radius-lg);
263
+ box-shadow: var(--shadow-md);
264
+ border: 1px solid var(--border);
265
+ margin-bottom: 32px;
266
+ overflow: hidden;
267
+ transition: var(--transition-ease);
268
+ }
269
+ .card:hover { border-color: var(--primary-light); box-shadow: var(--shadow-lg); }
270
+
271
+ .card-header {
272
+ display: flex; justify-content: space-between; align-items: center;
273
+ padding: 20px 24px; border-bottom: 1px solid var(--border);
274
+ }
275
+ .card-title {
276
+ font-size: 18px; font-weight: 600; color: var(--text-main);
277
+ display: flex; align-items: center; gap: 12px;
278
+ }
279
+ .card-title-icon { font-size: 22px; color: var(--text-muted); }
280
+ .card-body { padding: 24px; }
281
+
282
+ /* [OPTIMIZATION] 7. Button 样式优化 */
283
+ .btn {
284
+ padding: 10px 20px; border: none; border-radius: var(--radius-md);
285
+ cursor: pointer; font-size: 14px; font-weight: 500;
286
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
287
+ transition: var(--transition-ease); text-decoration: none;
288
+ }
289
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
290
+ .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-md); }
291
+ .btn-primary { background: var(--primary); color: white; }
292
+ .btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
293
+
294
+ .btn-outline {
295
+ background: transparent; color: var(--text-muted);
296
+ border: 1px solid var(--border);
297
+ }
298
+ .btn-outline:hover:not(:disabled) { border-color: var(--text-main); color: var(--text-main); }
299
+ /* 其他按钮颜色保持 */
300
+ .btn-success { background: var(--success-light); color: var(--success); border: 1px solid rgba(52, 168, 83, 0.2); }
301
+ .btn-success:hover:not(:disabled) { background: var(--success); color: white; border-color: var(--success); }
302
+ .btn-danger { background: var(--danger-light); color: var(--danger); border: 1px solid rgba(234, 67, 53, 0.2); }
303
+ .btn-danger:hover:not(:disabled) { background: var(--danger); color: white; border-color: var(--danger); }
304
+
305
+ .btn-sm { padding: 6px 14px; font-size: 13px; border-radius: var(--radius-sm); }
306
+ .btn-icon { width: 32px; height: 32px; padding: 0; border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }
307
+ .btn-warning { background: #fff3cd; color: #856404; border: 1px solid rgba(133, 100, 4, 0.2); }
308
+ .btn-warning:hover:not(:disabled) { background: #ffc107; color: #212529; border-color: #ffc107; }
309
+
310
+ /* [OPTIMIZATION] 8. Table 样式优化,增强可读性 */
311
+ .table-container { overflow-x: auto; }
312
+ table { width: 100%; border-collapse: collapse; }
313
+ th {
314
+ text-align: left; padding: 16px 24px; font-size: 13px;
315
+ font-weight: 500; color: var(--text-muted); text-transform: uppercase;
316
+ letter-spacing: 0.5px; background: transparent; /* 移除背景色 */
317
+ border-bottom: 2px solid var(--border); /* 加粗底部边框 */
318
+ }
319
+ td {
320
+ padding: 18px 24px; border-bottom: 1px solid var(--border);
321
+ font-size: 14px; color: var(--text-main);
322
+ transition: background-color 0.2s;
323
+ }
324
+ tr:last-child td { border-bottom: none; }
325
+ tr:hover td { background: var(--hover-bg); }
326
+
327
+ /* [OPTIMIZATION] 9. Form 样式优化 */
328
+ .form-group {
329
+ display: flex;
330
+ flex-direction: column;
331
+ margin-bottom: 20px;
332
+ }
333
+ .form-group label,
334
+ .form-label {
335
+ display: block;
336
+ margin-bottom: 12px;
337
+ font-size: 14px;
338
+ font-weight: 600;
339
+ color: var(--text-main);
340
+ letter-spacing: 0.2px;
341
+ }
342
+ .form-group input, .form-group textarea, .form-group select,
343
+ .form-input,
344
+ .form-textarea {
345
+ width: 100%;
346
+ padding: 14px 16px;
347
+ border-radius: var(--radius-md);
348
+ border: 1px solid var(--border);
349
+ background: var(--bg);
350
+ color: var(--text-main);
351
+ font-size: 14px;
352
+ transition: var(--transition-ease);
353
+ box-sizing: border-box;
354
+ line-height: 1.5;
355
+ }
356
+ .form-textarea {
357
+ min-height: 90px;
358
+ resize: vertical;
359
+ font-family: inherit;
360
+ }
361
+ .form-group input:focus, .form-group textarea:focus, .form-group select:focus {
362
+ outline: none;
363
+ border-color: var(--primary);
364
+ box-shadow: 0 0 0 3px var(--primary-light), 0 1px 2px rgba(0,0,0,0.05) inset;
365
+ }
366
+ .form-group input:disabled {
367
+ background: var(--hover-bg);
368
+ color: var(--text-muted);
369
+ cursor: not-allowed;
370
+ }
371
+ .form-group small {
372
+ display: block;
373
+ margin-top: 6px;
374
+ font-size: 13px;
375
+ color: var(--text-muted);
376
+ }
377
+ .form-row {
378
+ display: grid;
379
+ grid-template-columns: 1fr 1fr;
380
+ gap: 24px;
381
+ }
382
+
383
+ /* Settings Section 样式 */
384
+ .settings-section {
385
+ background: var(--bg);
386
+ border: 1px solid var(--border);
387
+ border-radius: var(--radius-lg);
388
+ padding: 28px;
389
+ margin-bottom: 28px;
390
+ }
391
+ .settings-section:last-child {
392
+ margin-bottom: 0;
393
+ }
394
+ .settings-section h3 {
395
+ font-size: 17px;
396
+ font-weight: 600;
397
+ color: var(--text-main);
398
+ margin-bottom: 24px;
399
+ padding-bottom: 16px;
400
+ border-bottom: 1px solid var(--border);
401
+ display: flex;
402
+ align-items: center;
403
+ }
404
+ .settings-section .form-group {
405
+ margin-bottom: 24px;
406
+ }
407
+ .settings-section .form-group:last-of-type {
408
+ margin-bottom: 20px;
409
+ }
410
+
411
+ /* [OPTIMIZATION] 10. Modal 动画与样式优化 */
412
+ .modal {
413
+ display: flex; /* 改为flex,便于控制 */
414
+ align-items: center; justify-content: center;
415
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
416
+ background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px);
417
+ z-index: 1000; opacity: 0; visibility: hidden;
418
+ transition: opacity 0.3s, visibility 0.3s;
419
+ }
420
+ .modal.show { opacity: 1; visibility: visible; }
421
+ .modal-content {
422
+ background: var(--card-bg); border-radius: var(--radius-lg);
423
+ width: 600px; max-width: 90vw; max-height: 90vh;
424
+ overflow-y: auto; box-shadow: var(--shadow-lg);
425
+ transform: translateY(20px) scale(0.98);
426
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
427
+ }
428
+ .modal.show .modal-content { transform: translateY(0) scale(1); }
429
+ .modal-header { padding: 24px; border-bottom: 1px solid var(--border); }
430
+ .modal-header h3 { font-size: 20px; font-weight: 600; display: inline-block; }
431
+ .modal-close {
432
+ width: 36px; height: 36px; border: none; background: transparent;
433
+ color: var(--text-muted); cursor: pointer; border-radius: 50%;
434
+ display: flex; align-items: center; justify-content: center;
435
+ font-size: 22px; transition: var(--transition-ease);
436
+ float: right;
437
+ }
438
+ .modal-close:hover { background: var(--hover-bg); color: var(--text-main); transform: rotate(90deg); }
439
+ .modal-body { padding: 24px; }
440
+ .modal-footer {
441
+ display: flex; justify-content: flex-end; gap: 12px;
442
+ padding: 20px 24px; border-top: 1px solid var(--border);
443
+ background: var(--hover-bg);
444
+ border-bottom-left-radius: var(--radius-lg);
445
+ border-bottom-right-radius: var(--radius-lg);
446
+ }
447
+
448
+
449
+ /* [OPTIMIZATION] 11. Stats Card 优化 */
450
+ .stats-grid {
451
+ display: grid;
452
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
453
+ gap: 24px;
454
+ margin-bottom: 32px;
455
+ }
456
+ .stat-card {
457
+ background: var(--card-bg); border: 1px solid var(--border);
458
+ border-radius: var(--radius-lg); padding: 24px;
459
+ display: flex; flex-direction: column; /* 垂直布局 */
460
+ align-items: flex-start; gap: 16px;
461
+ transition: var(--transition-ease);
462
+ /* [NEW] 入场动画 */
463
+ opacity: 0;
464
+ transform: translateY(20px);
465
+ animation: fadeIn-up 0.5s ease-out forwards;
466
+ }
467
+ /* [NEW] Staggered Animation for Stats Cards */
468
+ .stat-card:nth-child(1) { animation-delay: 0.1s; }
469
+ .stat-card:nth-child(2) { animation-delay: 0.2s; }
470
+ .stat-card:nth-child(3) { animation-delay: 0.3s; }
471
+ .stat-card:nth-child(4) { animation-delay: 0.4s; }
472
+
473
+ @keyframes fadeIn-up {
474
+ to {
475
+ opacity: 1;
476
+ transform: translateY(0);
477
+ }
478
+ }
479
+ .stat-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); border-color: var(--primary); }
480
+
481
+ .stat-info-top { display: flex; justify-content: space-between; align-items: center; width: 100%; }
482
+ .stat-info-top p { font-size: 14px; font-weight: 500; color: var(--text-muted); }
483
+
484
+ .stat-icon {
485
+ width: 40px; height: 40px; border-radius: var(--radius-md);
486
+ display: flex; align-items: center; justify-content: center; font-size: 20px;
487
+ }
488
+
489
+ .stat-info-bottom h3 { font-size: 32px; font-weight: 600; color: var(--text-main); }
490
+ .stat-icon.blue { background: var(--primary-light); color: var(--primary); }
491
+ .stat-icon.green { background: var(--success-light); color: var(--success); }
492
+ .stat-icon.red { background: var(--danger-light); color: var(--danger); }
493
+ .stat-icon.yellow { background: var(--warning-light); color: #b06000; }
494
+
495
+ /* 其他样式保持或微调 */
496
+ .badge {
497
+ padding: 5px 12px; border-radius: 50px;
498
+ font-size: 12px; font-weight: 500;
499
+ }
500
+ .empty-state { text-align: center; padding: 80px 20px; color: var(--text-muted); }
501
+ .empty-state-icon { font-size: 56px; margin-bottom: 20px; opacity: 0.4; }
502
+
503
+ .toast {
504
+ position: fixed; bottom: 32px; left: 50%;
505
+ transform: translateX(-50%) translateY(100px);
506
+ background: var(--card-bg); border: 1px solid var(--border);
507
+ border-radius: var(--radius-md); padding: 16px 24px;
508
+ box-shadow: var(--shadow-lg); min-width: 320px;
509
+ z-index: 2000; opacity: 0; visibility: hidden;
510
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
511
+ display: flex; align-items: center; gap: 12px;
512
+ }
513
+ .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; visibility: visible; }
514
+
515
+ /* [NEW] SVG Icon Styles */
516
+ .icon {
517
+ width: 1em;
518
+ height: 1em;
519
+ stroke-width: 2;
520
+ fill: none;
521
+ stroke: currentColor;
522
+ stroke-linecap: round;
523
+ stroke-linejoin: round;
524
+ }
525
+
526
+ /* Responsive */
527
+ @media (max-width: 768px) {
528
+ .container { padding: 24px 16px; }
529
+ .header { flex-direction: column; gap: 24px; text-align: center; }
530
+ .tabs {
531
+ gap: 8px;
532
+ /* [NEW] 允许在移动端横向滚动 */
533
+ overflow-x: auto;
534
+ white-space: nowrap;
535
+ -ms-overflow-style: none; /* IE and Edge */
536
+ scrollbar-width: none; /* Firefox */
537
+ }
538
+ .tabs::-webkit-scrollbar { display: none; } /* Chrome, Safari, and Opera */
539
+ .tab { flex-shrink: 0; }
540
+ .form-row { grid-template-columns: 1fr; }
541
+ .stats-grid { gap: 16px; }
542
+ }
543
+ </style>
544
+ </head>
545
+ <body>
546
+ <!-- [NEW] SVG Icon Definitions -->
547
+ <svg width="0" height="0" style="display: none;">
548
+ <symbol id="icon-users" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></symbol>
549
+ <symbol id="icon-robot" viewBox="0 0 24 24"><path d="M12 8V4H8"></path><rect x="4" y="12" width="16" height="8" rx="2"></rect><path d="M2 12h20"></path><path d="M12 12V8a4 4 0 0 0-4-4"></path></symbol>
550
+ <symbol id="icon-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 0 2l-.15.08a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l-.22-.38a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1 0-2l.15-.08a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
551
+ <symbol id="icon-server" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></symbol>
552
+ <symbol id="icon-list" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></symbol>
553
+ <symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol>
554
+ <symbol id="icon-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></symbol>
555
+ <symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
556
+ <symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></symbol>
557
+ <symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></symbol>
558
+ <symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></symbol>
559
+ <symbol id="icon-message" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></symbol>
560
+ <symbol id="icon-play" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></symbol>
561
+ <symbol id="icon-pause" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></symbol>
562
+ <symbol id="icon-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></symbol>
563
+ <symbol id="icon-key" viewBox="0 0 24 24"><path d="M21 2l-2 2"></path><path d="M9 6l-2 2"></path><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="M21 2l-9.6 9.6"></path><path d="M15.5 7.5l3 3"></path><path d="M16 13l-3-3"></path></symbol>
564
+ </svg>
565
+
566
+ <div class="container">
567
+ <!-- Header -->
568
+ <header class="header">
569
+ <div class="header-left">
570
+ <div class="logo">G</div>
571
+ <h1>Business Gemini Pool <span>管理控制台</span></h1>
572
+ </div>
573
+ <div class="header-right">
574
+ <div class="status-indicator" id="serviceStatus">服务运行中</div>
575
+ <div class="log-level-control">
576
+ <label for="logLevelSelect">日志</label>
577
+ <select id="logLevelSelect" class="log-level-select" onchange="updateLogLevel(this.value)">
578
+ <option value="DEBUG">DEBUG</option>
579
+ <option value="INFO" selected>INFO</option>
580
+ <option value="ERROR">ERROR</option>
581
+ </select>
582
+ </div>
583
+ <button class="btn btn-outline" id="loginButton" style="padding: 8px 12px;" onclick="showLoginModal()">登录</button>
584
+ <a href="chat_history.html" class="btn btn-primary" style="padding: 8px 16px; font-size: 14px; text-decoration: none; display: flex; align-items: center; gap: 6px;" title="进入在线对话">
585
+ <svg class="icon" style="width: 16px; height: 16px;"><use xlink:href="#icon-message"></use></svg>
586
+ 在线对话
587
+ </a>
588
+ <button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
589
+ <span id="themeIconContainer">
590
+ <svg class="icon"><use xlink:href="#icon-sun"></use></svg>
591
+ </span>
592
+ </button>
593
+ </div>
594
+ </header>
595
+
596
+ <!-- Tabs -->
597
+ <div class="tabs">
598
+ <button class="tab active" onclick="switchTab('accounts')">
599
+ <svg class="icon tab-icon"><use xlink:href="#icon-users"></use></svg>
600
+ 账号管理
601
+ </button>
602
+ <button class="tab" onclick="switchTab('models')">
603
+ <svg class="icon tab-icon"><use xlink:href="#icon-robot"></use></svg>
604
+ 模型管理
605
+ </button>
606
+ <button class="tab" onclick="switchTab('settings')">
607
+ <svg class="icon tab-icon"><use xlink:href="#icon-settings"></use></svg>
608
+ 系统设置
609
+ </button>
610
+ <button class="tab" onclick="switchTab('tokens')">
611
+ <svg class="icon tab-icon"><use xlink:href="#icon-key"></use></svg>
612
+ Token 管理
613
+ </button>
614
+ </div>
615
+
616
+ <!-- 账号管理 -->
617
+ <div id="accounts" class="tab-content active">
618
+ <!-- Stats -->
619
+ <div class="stats-grid">
620
+ <div class="stat-card">
621
+ <div class="stat-info-top">
622
+ <p>总账号数</p>
623
+ <div class="stat-icon blue"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
624
+ </div>
625
+ <div class="stat-info-bottom">
626
+ <h3 id="totalAccounts">0</h3>
627
+ </div>
628
+ </div>
629
+ <div class="stat-card">
630
+ <div class="stat-info-top">
631
+ <p>可用账号</p>
632
+ <div class="stat-icon green"><svg class="icon"><use xlink:href="#icon-check"></use></svg></div>
633
+ </div>
634
+ <div class="stat-info-bottom">
635
+ <h3 id="availableAccounts">0</h3>
636
+ </div>
637
+ </div>
638
+ <div class="stat-card">
639
+ <div class="stat-info-top">
640
+ <p>不可用账号</p>
641
+ <div class="stat-icon red"><svg class="icon"><use xlink:href="#icon-x"></use></svg></div>
642
+ </div>
643
+ <div class="stat-info-bottom">
644
+ <h3 id="unavailableAccounts">0</h3>
645
+ </div>
646
+ </div>
647
+ <div class="stat-card">
648
+ <div class="stat-info-top">
649
+ <p>当前轮训索引</p>
650
+ <div class="stat-icon yellow"><svg class="icon"><use xlink:href="#icon-refresh"></use></svg></div>
651
+ </div>
652
+ <div class="stat-info-bottom">
653
+ <h3 id="currentIndex">0</h3>
654
+ </div>
655
+ </div>
656
+ </div>
657
+
658
+ <div class="card">
659
+ <div class="card-header">
660
+ <div class="card-title">
661
+ <svg class="icon card-title-icon"><use xlink:href="#icon-list"></use></svg>
662
+ 账号列表
663
+ </div>
664
+ <button class="btn btn-primary" onclick="showAddAccountModal()">
665
+ <svg class="icon"><use xlink:href="#icon-plus"></use></svg>
666
+ 添加账号
667
+ </button>
668
+ </div>
669
+ <div class="table-container">
670
+ <table id="accountsTable">
671
+ <thead>
672
+ <tr>
673
+ <th>序号</th>
674
+ <th>Team ID</th>
675
+ <th>csesidx</th>
676
+ <th>User Agent</th>
677
+ <th>状态</th>
678
+ <th>操作</th>
679
+ </tr>
680
+ </thead>
681
+ <tbody id="accountsTableBody"></tbody>
682
+ </table>
683
+ </div>
684
+ </div>
685
+ </div>
686
+
687
+ <!-- 模型管理 (HTML结构类似,图标已替换) -->
688
+ <div id="models" class="tab-content">
689
+ <div class="card">
690
+ <div class="card-header">
691
+ <div class="card-title">
692
+ <svg class="icon card-title-icon"><use xlink:href="#icon-robot"></use></svg>
693
+ 模型列表
694
+ </div>
695
+ <button class="btn btn-primary" onclick="showAddModelModal()">
696
+ <svg class="icon"><use xlink:href="#icon-plus"></use></svg>
697
+ 添加模型
698
+ </button>
699
+ </div>
700
+ <div class="table-container">
701
+ <table id="modelsTable">
702
+ <thead>
703
+ <tr>
704
+ <th>模型ID</th>
705
+ <th>名称</th>
706
+ <th>描述</th>
707
+ <th>上下文长度</th>
708
+ <th>最大Token</th>
709
+ <th>状态</th>
710
+ <th>操作</th>
711
+ </tr>
712
+ </thead>
713
+ <tbody id="modelsTableBody"></tbody>
714
+ </table>
715
+ </div>
716
+ </div>
717
+ </div>
718
+
719
+ <!-- 系统设置 (HTML结构类似,图标已替换) -->
720
+ <div id="settings" class="tab-content">
721
+ <div class="card">
722
+ <div class="card-header">
723
+ <div class="card-title">
724
+ <svg class="icon card-title-icon"><use xlink:href="#icon-settings"></use></svg>
725
+ 系统配置
726
+ </div>
727
+ </div>
728
+ <div class="card-body">
729
+ <form id="settingsForm">
730
+ <div class="settings-section">
731
+ <h3>代理设置</h3>
732
+ <div class="form-group">
733
+ <label class="form-label" for="proxyUrl">代理地址</label>
734
+ <input type="text" class="form-input" id="proxyUrl" placeholder="http://127.0.0.1:7890">
735
+ <small>用于访问Google API的代理服务器地址</small>
736
+ <div class="proxy-status" id="proxyStatus"></div>
737
+ </div>
738
+ <div class="form-group">
739
+ <label class="form-label" for="imageOutputMode">图片输出模式</label>
740
+ <select class="form-input" id="imageOutputMode">
741
+ <option value="url">图片URL(默认)</option>
742
+ <option value="base64">Base64 Data URL</option>
743
+ </select>
744
+ <small>控制聊天接口返回的图片是以URL形式还是以 data:image/...;base64,... 形式输出</small>
745
+ </div>
746
+ <div style="display: flex; gap: 12px;">
747
+ <button type="button" class="btn btn-outline" onclick="testProxy()">
748
+ 测试代理
749
+ </button>
750
+ <button type="button" class="btn btn-primary" onclick="saveSettings()">
751
+ 保存设置
752
+ </button>
753
+ </div>
754
+ </div>
755
+
756
+ <div class="settings-section">
757
+ <h3><svg class="icon" style="width: 1em; height: 1em; vertical-align: -2px; margin-right: 8px;"><use xlink:href="#icon-server"></use></svg>服务信息</h3>
758
+ <div class="form-row">
759
+ <div class="form-group">
760
+ <label class="form-label">服务端口</label>
761
+ <input type="text" class="form-input" value="8000" disabled>
762
+ </div>
763
+ <div class="form-group">
764
+ <label class="form-label">API地址</label>
765
+ <input type="text" class="form-input" value="http://localhost:8000/v1" disabled>
766
+ </div>
767
+ </div>
768
+ </div>
769
+
770
+ <div class="settings-section">
771
+ <h3>配置文件</h3>
772
+ <div class="form-group">
773
+ <label class="form-label" for="configJson">当前配置 (JSON)</label>
774
+ <textarea class="form-textarea" id="configJson" rows="15" readonly></textarea>
775
+ <small>配置文件路径: business_gemini_session.json</small>
776
+ </div>
777
+ <div style="display: flex; gap: 12px; flex-wrap: wrap;">
778
+ <button type="button" class="btn btn-outline" onclick="refreshConfig()">
779
+ 刷新配置
780
+ </button>
781
+ <button type="button" class="btn btn-outline" onclick="downloadConfig()">
782
+ 下载配置
783
+ </button>
784
+ <button type="button" class="btn btn-primary" onclick="uploadConfig()">
785
+ 导入配置
786
+ </button>
787
+ <input type="file" id="configFileInput" accept=".json" style="display: none;" onchange="handleConfigUpload(event)">
788
+ </div>
789
+ </div>
790
+ </form>
791
+ </div>
792
+ </div>
793
+ </div>
794
+
795
+ <!-- Token 管理 -->
796
+ <div id="tokens" class="tab-content">
797
+ <div class="card">
798
+ <div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
799
+ <div class="card-title" style="display:flex; align-items:center; gap:8px;">
800
+ <svg class="icon card-title-icon"><use xlink:href="#icon-key"></use></svg>
801
+ Token 管理
802
+ </div>
803
+ <div class="token-actions">
804
+ <input id="manualToken" class="form-input token-input" placeholder="手动输入 Token(留空自动生成)">
805
+ <button class="btn btn-outline" type="button" onclick="generateToken()">生成 Token</button>
806
+ <button class="btn btn-primary" type="button" onclick="addToken()">添加 Token</button>
807
+ </div>
808
+ </div>
809
+ <table class="table">
810
+ <thead>
811
+ <tr>
812
+ <th style="width:70%;">Token</th>
813
+ <th>操作</th>
814
+ </tr>
815
+ </thead>
816
+ <tbody id="tokensTableBody">
817
+ <tr><td colspan="2" class="empty-state">加载中...</td></tr>
818
+ </tbody>
819
+ </table>
820
+ </div>
821
+ </div>
822
+ </div>
823
+
824
+ <!-- 模态框 (已优化关闭按钮) -->
825
+ <div class="modal" id="addAccountModal">
826
+ <div class="modal-content">
827
+ <div class="modal-header">
828
+ <h3>添加账号</h3>
829
+ <button class="modal-close" onclick="closeModal('addAccountModal')" title="关闭">&times;</button>
830
+ </div>
831
+ <!-- Modal Body and Footer ... (No functional changes needed) -->
832
+ <div class="modal-body">
833
+ <div class="form-group">
834
+ <label class="form-label" for="newAccountJson">粘贴账号JSON(可直接复制工具输出)</label>
835
+ <textarea class="form-textarea" id="newAccountJson" placeholder='{"team_id":"...","secure_c_ses":"...","host_c_oses":"...","csesidx":"...","user_agent":"..."}' rows="4"></textarea>
836
+ <div style="display:flex; gap:8px; margin-top:8px;">
837
+ <button class="btn btn-outline btn-sm" type="button" onclick="parseAccountJson()">解析填充</button>
838
+ <button class="btn btn-outline btn-sm" type="button" onclick="pasteAccountJson()">从剪贴板读取并填充</button>
839
+ </div>
840
+ </div>
841
+ <div class="form-group">
842
+ <label class="form-label" for="newTeamId">Team ID</label>
843
+ <input type="text" class="form-input" id="newTeamId" placeholder="输入Team ID">
844
+ </div>
845
+ <div class="form-group">
846
+ <label class="form-label" for="newSecureCses">Cookie中的__Secure-C_SES</label>
847
+ <textarea class="form-textarea" id="newSecureCses" placeholder="输入Cookie中的__Secure-C_SES" rows="3"></textarea>
848
+ </div>
849
+ <div class="form-group">
850
+ <label class="form-label" for="newHostCoses">Cookie中的__Host-C_OSES</label>
851
+ <textarea class="form-textarea" id="newHostCoses" placeholder="输入Cookie中的__Host-C_OSES" rows="3"></textarea>
852
+ </div>
853
+ <div class="form-group">
854
+ <label class="form-label" for="newCsesidx">CSESIDX</label>
855
+ <input type="text" class="form-input" id="newCsesidx" placeholder="输入CSESIDX">
856
+ </div>
857
+ <div class="form-group">
858
+ <label class="form-label" for="newUserAgent">User Agent</label>
859
+ <input type="text" class="form-input" id="newUserAgent" placeholder="输入User Agent">
860
+ </div>
861
+ </div>
862
+ <div class="modal-footer">
863
+ <button class="btn btn-outline" onclick="closeModal('addAccountModal')">取消</button>
864
+ <button class="btn btn-primary" onclick="saveNewAccount()">保存</button>
865
+ </div>
866
+ </div>
867
+ </div>
868
+ <!-- 编辑账号模态框 -->
869
+ <div class="modal" id="editAccountModal">
870
+ <div class="modal-content">
871
+ <div class="modal-header">
872
+ <h3>编辑账号</h3>
873
+ <button class="modal-close" onclick="closeModal('editAccountModal')" title="关闭">&times;</button>
874
+ </div>
875
+ <div class="modal-body">
876
+ <input type="hidden" id="editAccountId">
877
+ <div class="form-group">
878
+ <label class="form-label" for="editTeamId">Team ID</label>
879
+ <input type="text" class="form-input" id="editTeamId" placeholder="输入Team ID">
880
+ </div>
881
+ <div class="form-group">
882
+ <label class="form-label" for="editSecureCses">Cookie中的__Secure-C_SES</label>
883
+ <textarea class="form-textarea" id="editSecureCses" placeholder="输入Secure C Ses" rows="3"></textarea>
884
+ </div>
885
+ <div class="form-group">
886
+ <label class="form-label" for="editHostCoses">Cookie中的__Host-C_OSES</label>
887
+ <textarea class="form-textarea" id="editHostCoses" placeholder="输入Host C Oses" rows="3"></textarea>
888
+ </div>
889
+ <div class="form-group">
890
+ <label class="form-label" for="editCsesidx">CSESIDX</label>
891
+ <input type="text" class="form-input" id="editCsesidx" placeholder="输入CSESIDX">
892
+ </div>
893
+ <div class="form-group">
894
+ <label class="form-label" for="editUserAgent">User Agent</label>
895
+ <input type="text" class="form-input" id="editUserAgent" placeholder="输入User Agent">
896
+ </div>
897
+ </div>
898
+ <div class="modal-footer">
899
+ <button class="btn btn-outline" onclick="closeModal('editAccountModal')">取消</button>
900
+ <button class="btn btn-primary" onclick="updateAccount()">保存</button>
901
+ </div>
902
+ </div>
903
+ </div>
904
+
905
+ <!-- 刷新Cookie模态框 -->
906
+ <div class="modal" id="refreshCookieModal">
907
+ <div class="modal-content">
908
+ <div class="modal-header">
909
+ <h3>刷新账号Cookie</h3>
910
+ <button class="modal-close" onclick="closeModal('refreshCookieModal')" title="关闭">&times;</button>
911
+ </div>
912
+ <div class="modal-body">
913
+ <input type="hidden" id="refreshAccountId">
914
+ <p class="text-muted" style="margin-bottom: 16px;">请输入新的Cookie值来刷新账号认证信息。刷新后将清除JWT缓存。</p>
915
+ <div class="form-group">
916
+ <label class="form-label" for="refreshSecureCses">Cookie中的__Secure-C_SES <span style="color: var(--danger);">*</span></label>
917
+ <textarea class="form-textarea" id="refreshSecureCses" placeholder="输入新的__Secure-C_SES值" rows="3"></textarea>
918
+ </div>
919
+ <div class="form-group">
920
+ <label class="form-label" for="refreshHostCoses">Cookie中的__Host-C_OSES <span style="color: var(--danger);">*</span></label>
921
+ <textarea class="form-textarea" id="refreshHostCoses" placeholder="输入新的__Host-C_OSES值" rows="3"></textarea>
922
+ </div>
923
+ <div class="form-group">
924
+ <label class="form-label" for="refreshCsesidx">CSESIDX (可选)</label>
925
+ <input type="text" class="form-input" id="refreshCsesidx" placeholder="输入CSESIDX值">
926
+ </div>
927
+ <div class="form-group">
928
+ <label class="form-label">从JSON粘贴 (可选)</label>
929
+ <textarea class="form-textarea" id="refreshCookieJson" placeholder="粘贴Cookie JSON数据" rows="3"></textarea>
930
+ <div style="display:flex; gap:8px; margin-top:8px;">
931
+ <button class="btn btn-outline btn-sm" type="button" onclick="parseRefreshCookieJson()">解析填充</button>
932
+ <button class="btn btn-outline btn-sm" type="button" onclick="pasteRefreshCookieJson()">📋 粘贴并解析</button>
933
+ </div>
934
+ </div>
935
+ </div>
936
+ <div class="modal-footer">
937
+ <button class="btn btn-outline" onclick="closeModal('refreshCookieModal')">取消</button>
938
+ <button class="btn btn-primary" onclick="refreshAccountCookie()">刷新Cookie</button>
939
+ </div>
940
+ </div>
941
+ </div>
942
+
943
+ <!-- 添加模型模态框 -->
944
+ <div class="modal" id="addModelModal">
945
+ <div class="modal-content">
946
+ <div class="modal-header">
947
+ <h3>添加模型</h3>
948
+ <button class="modal-close" onclick="closeModal('addModelModal')" title="关闭">&times;</button>
949
+ </div>
950
+ <div class="modal-body">
951
+ <div class="form-row">
952
+ <div class="form-group">
953
+ <label class="form-label" for="newModelId">模型ID</label>
954
+ <input type="text" class="form-input" id="newModelId" placeholder="如: gemini-pro">
955
+ </div>
956
+ <div class="form-group">
957
+ <label class="form-label" for="newModelName">模型名称</label>
958
+ <input type="text" class="form-input" id="newModelName" placeholder="如: Gemini Pro">
959
+ </div>
960
+ </div>
961
+ <div class="form-group">
962
+ <label class="form-label" for="newModelDesc">描述</label>
963
+ <input type="text" class="form-input" id="newModelDesc" placeholder="模型描述">
964
+ </div>
965
+ <div class="form-row">
966
+ <div class="form-group">
967
+ <label class="form-label" for="newContextLength">上下文长度</label>
968
+ <input type="number" class="form-input" id="newContextLength" value="32768">
969
+ </div>
970
+ <div class="form-group">
971
+ <label class="form-label" for="newMaxTokens">最大Token</label>
972
+ <input type="number" class="form-input" id="newMaxTokens" value="8192">
973
+ </div>
974
+ </div>
975
+ </div>
976
+ <div class="modal-footer">
977
+ <button class="btn btn-outline" onclick="closeModal('addModelModal')">取消</button>
978
+ <button class="btn btn-primary" onclick="saveNewModel()">保存</button>
979
+ </div>
980
+ </div>
981
+ </div>
982
+
983
+ <!-- 登录模态框 -->
984
+ <div class="modal" id="loginModal">
985
+ <div class="modal-content">
986
+ <div class="modal-header">
987
+ <h3>管理员登录</h3>
988
+ <button class="modal-close" onclick="closeModal('loginModal')" title="关闭">&times;</button>
989
+ </div>
990
+ <div class="modal-body">
991
+ <div class="form-group">
992
+ <label class="form-label" for="loginPassword">后台密码</label>
993
+ <input type="password" class="form-input" id="loginPassword" placeholder="输入后台密码">
994
+ </div>
995
+ <p class="text-muted" style="font-size: 12px;">首次登录将设置当前密码为后台密码。</p>
996
+ </div>
997
+ <div class="modal-footer">
998
+ <button class="btn btn-outline" onclick="closeModal('loginModal')">取消</button>
999
+ <button class="btn btn-primary" onclick="submitLogin()">登录</button>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <!-- 编辑模型模态框 -->
1005
+ <div class="modal" id="editModelModal">
1006
+ <div class="modal-content">
1007
+ <div class="modal-header">
1008
+ <h3>编辑模型</h3>
1009
+ <button class="modal-close" onclick="closeModal('editModelModal')" title="关闭">&times;</button>
1010
+ </div>
1011
+ <div class="modal-body">
1012
+ <input type="hidden" id="editModelOriginalId">
1013
+ <div class="form-row">
1014
+ <div class="form-group">
1015
+ <label class="form-label" for="editModelId">模型ID</label>
1016
+ <input type="text" class="form-input" id="editModelId" placeholder="如: gemini-pro" readonly style="background-color: var(--bg-tertiary); cursor: not-allowed;">
1017
+ </div>
1018
+ <div class="form-group">
1019
+ <label class="form-label" for="editModelName">模型名称</label>
1020
+ <input type="text" class="form-input" id="editModelName" placeholder="如: Gemini Pro">
1021
+ </div>
1022
+ </div>
1023
+ <div class="form-group">
1024
+ <label class="form-label" for="editModelDesc">描述</label>
1025
+ <input type="text" class="form-input" id="editModelDesc" placeholder="模型描述">
1026
+ </div>
1027
+ <div class="form-row">
1028
+ <div class="form-group">
1029
+ <label class="form-label" for="editContextLength">上下文长度</label>
1030
+ <input type="number" class="form-input" id="editContextLength">
1031
+ </div>
1032
+ <div class="form-group">
1033
+ <label class="form-label" for="editMaxTokens">最大Token</label>
1034
+ <input type="number" class="form-input" id="editMaxTokens">
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+ <div class="modal-footer">
1039
+ <button class="btn btn-outline" onclick="closeModal('editModelModal')">取消</button>
1040
+ <button class="btn btn-primary" onclick="updateModel()">保存</button>
1041
+ </div>
1042
+ </div>
1043
+ </div>
1044
+
1045
+
1046
+ <!-- Toast通知 -->
1047
+ <div id="toastContainer" class="toast-container">
1048
+ <!-- Toasts will be injected here by JS -->
1049
+ </div>
1050
+ <div class="toast" id="toast"></div>
1051
+
1052
+ <script>
1053
+ // [OPTIMIZATION] 1. 脚本微调以适应新的图标
1054
+ function updateThemeIcon(theme) {
1055
+ const iconContainer = document.getElementById('themeIconContainer');
1056
+ if (iconContainer) {
1057
+ const iconId = theme === 'dark' ? 'icon-sun' : 'icon-moon';
1058
+ iconContainer.innerHTML = `<svg class="icon"><use xlink:href="#${iconId}"></use></svg>`;
1059
+ }
1060
+ }
1061
+
1062
+ // [OPTIMIZATION] 2. 改进Toast通知
1063
+ let toastTimeout;
1064
+ function showToast(message, type = 'info') {
1065
+ const toast = document.getElementById('toast');
1066
+ if (!toast) return;
1067
+
1068
+ let icon = '';
1069
+ let borderType = type; // 'success', 'error', 'info'
1070
+ switch(type) {
1071
+ case 'success':
1072
+ icon = '<svg class="icon" style="color: var(--success);"><use xlink:href="#icon-check"></use></svg>';
1073
+ break;
1074
+ case 'error':
1075
+ icon = '<svg class="icon" style="color: var(--danger);"><use xlink:href="#icon-x"></use></svg>';
1076
+ break;
1077
+ default:
1078
+ icon = '<svg class="icon" style="color: var(--primary);"><use xlink:href="#icon-server"></use></svg>';
1079
+ borderType = 'primary';
1080
+ break;
1081
+ }
1082
+
1083
+ toast.innerHTML = `${icon} <span class="toast-message">${message}</span>`;
1084
+ toast.className = `toast show`;
1085
+ toast.style.borderLeft = `4px solid var(--${borderType})`;
1086
+
1087
+ clearTimeout(toastTimeout);
1088
+ toastTimeout = setTimeout(() => {
1089
+ toast.classList.remove('show');
1090
+ }, 3500);
1091
+ }
1092
+
1093
+ // =======================================================
1094
+ // [FULL SCRIPT] 以下是完整的、未删减的功能性 JavaScript 代码
1095
+ // =======================================================
1096
+
1097
+ // API 基础 URL
1098
+ const API_BASE = '.';
1099
+
1100
+ // 全局数据缓存
1101
+ let accountsData = [];
1102
+ let modelsData = [];
1103
+ let configData = {};
1104
+ let currentEditAccountId = null;
1105
+ let currentEditModelId = null;
1106
+ const ADMIN_TOKEN_KEY = 'admin_token';
1107
+ let tokensData = [];
1108
+
1109
+ // --- 初始化 ---
1110
+ document.addEventListener('DOMContentLoaded', () => {
1111
+ initTheme();
1112
+ loadAllData();
1113
+ setInterval(checkServerStatus, 30000); // 每30秒检查一次服务状态
1114
+ updateLoginButton();
1115
+ });
1116
+
1117
+ // --- 核心加载与渲染 ---
1118
+ async function loadAllData() {
1119
+ await Promise.all([
1120
+ loadAccounts(),
1121
+ loadModels(),
1122
+ loadConfig(),
1123
+ checkServerStatus(),
1124
+ loadLogLevel(),
1125
+ loadTokens()
1126
+ ]);
1127
+ }
1128
+
1129
+ function getAuthHeaders() {
1130
+ const token = localStorage.getItem(ADMIN_TOKEN_KEY);
1131
+ return token ? { 'X-Admin-Token': token } : {};
1132
+ }
1133
+
1134
+ function updateLoginButton() {
1135
+ const token = localStorage.getItem(ADMIN_TOKEN_KEY);
1136
+ const btn = document.getElementById('loginButton');
1137
+ if (!btn) return;
1138
+ if (token) {
1139
+ btn.textContent = '注销';
1140
+ btn.disabled = false;
1141
+ btn.classList.remove('btn-disabled');
1142
+ btn.title = '注销登录';
1143
+ btn.onclick = logoutAdmin;
1144
+ } else {
1145
+ btn.textContent = '登录';
1146
+ btn.disabled = false;
1147
+ btn.classList.remove('btn-disabled');
1148
+ btn.title = '管理员登录';
1149
+ btn.onclick = showLoginModal;
1150
+ }
1151
+ }
1152
+
1153
+ async function apiFetch(url, options = {}) {
1154
+ const headers = Object.assign({}, options.headers || {}, getAuthHeaders());
1155
+ const res = await fetch(url, { ...options, headers });
1156
+ if (res.status === 401 || res.status === 403) {
1157
+ showLoginModal();
1158
+ updateLoginButton();
1159
+ throw new Error('需要登录');
1160
+ }
1161
+ return res;
1162
+ }
1163
+
1164
+ // --- 主题控制 ---
1165
+ function initTheme() {
1166
+ const savedTheme = localStorage.getItem('theme') || 'light';
1167
+ document.documentElement.setAttribute('data-theme', savedTheme);
1168
+ updateThemeIcon(savedTheme);
1169
+ }
1170
+
1171
+ function toggleTheme() {
1172
+ const current = document.documentElement.getAttribute('data-theme');
1173
+ const newTheme = current === 'dark' ? 'light' : 'dark';
1174
+ document.documentElement.setAttribute('data-theme', newTheme);
1175
+ localStorage.setItem('theme', newTheme);
1176
+ updateThemeIcon(newTheme);
1177
+ }
1178
+
1179
+ // --- 标签页控制 ---
1180
+ function switchTab(tabName) {
1181
+ document.querySelectorAll('.tab').forEach(btn => btn.classList.remove('active'));
1182
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
1183
+
1184
+ const tabBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`);
1185
+ const tabContent = document.getElementById(tabName);
1186
+
1187
+ if (tabBtn) tabBtn.classList.add('active');
1188
+ if (tabContent) tabContent.classList.add('active');
1189
+ }
1190
+
1191
+ // --- 状态检查 ---
1192
+ async function checkServerStatus() {
1193
+ const indicator = document.getElementById('serviceStatus');
1194
+ if (!indicator) return;
1195
+ try {
1196
+ const res = await apiFetch(`${API_BASE}/api/status`);
1197
+ console.log('Server Status Response:', res);
1198
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
1199
+ const data = await res.json();
1200
+ indicator.textContent = '服务运行中';
1201
+ indicator.classList.remove('offline');
1202
+ indicator.title = '服务连接正常 - ' + new Date().toLocaleString();
1203
+ } catch (e) {
1204
+ indicator.textContent = '服务离线';
1205
+ indicator.classList.add('offline');
1206
+ indicator.title = '无法连接到后端服务';
1207
+ }
1208
+ }
1209
+
1210
+ // --- 账号管理 (Accounts) ---
1211
+ async function loadAccounts() {
1212
+ try {
1213
+ const res = await apiFetch(`${API_BASE}/api/accounts`);
1214
+ const data = await res.json();
1215
+ accountsData = data.accounts || [];
1216
+ document.getElementById('currentIndex').textContent = data.current_index || 0;
1217
+ renderAccounts();
1218
+ updateAccountStats();
1219
+ } catch (e) {
1220
+ showToast('加载账号列表失败: ' + e.message, 'error');
1221
+ }
1222
+ }
1223
+
1224
+ function renderAccounts() {
1225
+ const tbody = document.getElementById('accountsTableBody');
1226
+ if (!tbody) return;
1227
+
1228
+ if (accountsData.length === 0) {
1229
+ tbody.innerHTML = `<tr><td colspan="6" class="empty-state">
1230
+ <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-users"></use></svg></div>
1231
+ <h3>暂无账号</h3><p>点击 "添加账号" 按钮来创建一个。</p>
1232
+ </td></tr>`;
1233
+ return;
1234
+ }
1235
+
1236
+ tbody.innerHTML = accountsData.map((acc, index) => `
1237
+ <tr>
1238
+ <td>${index + 1}</td>
1239
+ <td><code>${acc.team_id || '-'}</code></td>
1240
+ <td><code>${acc.csesidx || '-'}</code></td>
1241
+ <td title="${acc.user_agent}">${acc.user_agent ? acc.user_agent.substring(0, 30) + '...' : '-'}</td>
1242
+ <td>
1243
+ <span class="badge ${acc.available ? 'badge-success' : 'badge-danger'}">${acc.available ? '可用' : '不可用'}</span>
1244
+ ${renderNextRefresh(acc)}
1245
+ </td>
1246
+ <td style="white-space: nowrap;">
1247
+ <button class="btn btn-sm ${acc.enabled !== false ? 'btn-warning' : 'btn-success'} btn-icon" onclick="toggleAccount(${acc.id})" title="${acc.enabled !== false ? '停用' : '启用'}"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-${acc.enabled !== false ? 'pause' : 'play'}"></use></svg></button>
1248
+ <button class="btn btn-sm btn-outline btn-icon" onclick="testAccount(${acc.id})" title="测试连接"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-zap"></use></svg></button>
1249
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showRefreshCookieModal(${acc.id})" title="刷新Cookie"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-refresh"></use></svg></button>
1250
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showEditAccountModal(${acc.id})" title="编辑"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-settings"></use></svg></button>
1251
+ <button class="btn btn-sm btn-danger btn-icon" onclick="deleteAccount(${acc.id})" title="删除"><svg class="icon" style="width:16px; height:16px;"><use xlink:href="#icon-x"></use></svg></button>
1252
+ </td>
1253
+ </tr>
1254
+ `).join('');
1255
+ }
1256
+
1257
+ function updateAccountStats() {
1258
+ document.getElementById('totalAccounts').textContent = accountsData.length;
1259
+ document.getElementById('availableAccounts').textContent = accountsData.filter(a => a.available).length;
1260
+ document.getElementById('unavailableAccounts').textContent = accountsData.length - accountsData.filter(a => a.available).length;
1261
+ }
1262
+
1263
+ function renderNextRefresh(acc) {
1264
+ if (!acc || !acc.cooldown_until) return '';
1265
+ const now = Date.now();
1266
+ const ts = acc.cooldown_until * 1000;
1267
+ if (ts <= now) return '';
1268
+ const next = new Date(ts);
1269
+ const remaining = Math.max(0, ts - now);
1270
+ const minutes = Math.floor(remaining / 60000);
1271
+ const label = minutes >= 60
1272
+ ? `${Math.floor(minutes / 60)}小时${minutes % 60}分`
1273
+ : `${minutes}分`;
1274
+ return `<span class="cooldown-hint">下次恢复: ${next.toLocaleString()}(约${label})</span>`;
1275
+ }
1276
+
1277
+ function showAddAccountModal() {
1278
+ // 清空表单字段
1279
+ document.getElementById('newAccountJson').value = '';
1280
+ document.getElementById('newTeamId').value = '';
1281
+ document.getElementById('newSecureCses').value = '';
1282
+ document.getElementById('newHostCoses').value = '';
1283
+ document.getElementById('newCsesidx').value = '';
1284
+ document.getElementById('newUserAgent').value = '';
1285
+ openModal('addAccountModal');
1286
+ }
1287
+
1288
+ function showEditAccountModal(id) {
1289
+ const acc = accountsData.find(a => a.id === id);
1290
+ if (!acc) return;
1291
+
1292
+ document.getElementById('editAccountId').value = id;
1293
+ document.getElementById('editTeamId').value = acc.team_id || '';
1294
+ document.getElementById('editSecureCses').value = acc.secure_c_ses || '';
1295
+ document.getElementById('editHostCoses').value = acc.host_c_oses || '';
1296
+ document.getElementById('editCsesidx').value = acc.csesidx || '';
1297
+ document.getElementById('editUserAgent').value = acc.user_agent ? acc.user_agent.replace('...', '') : '';
1298
+
1299
+ openModal('editAccountModal');
1300
+ }
1301
+
1302
+ async function updateAccount() {
1303
+ const id = document.getElementById('editAccountId').value;
1304
+ const account = {};
1305
+
1306
+ const teamId = document.getElementById('editTeamId').value;
1307
+ const secureCses = document.getElementById('editSecureCses').value;
1308
+ const hostCoses = document.getElementById('editHostCoses').value;
1309
+ const csesidx = document.getElementById('editCsesidx').value;
1310
+ const userAgent = document.getElementById('editUserAgent').value;
1311
+
1312
+ if (teamId) account.team_id = teamId;
1313
+ if (secureCses) account.secure_c_ses = secureCses;
1314
+ if (hostCoses) account.host_c_oses = hostCoses;
1315
+ if (csesidx) account.csesidx = csesidx;
1316
+ if (userAgent) account.user_agent = userAgent;
1317
+
1318
+ try {
1319
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, {
1320
+ method: 'PUT',
1321
+ headers: { 'Content-Type': 'application/json' },
1322
+ body: JSON.stringify(account)
1323
+ });
1324
+ const data = await res.json();
1325
+
1326
+ if (data.success) {
1327
+ showToast('账号更新成功', 'success');
1328
+ closeModal('editAccountModal');
1329
+ loadAccounts();
1330
+ } else {
1331
+ showToast('更新失败: ' + (data.error || '未知错误'), 'error');
1332
+ }
1333
+ } catch (e) {
1334
+ showToast('更新失败: ' + e.message, 'error');
1335
+ }
1336
+ }
1337
+
1338
+ async function saveNewAccount() {
1339
+ const teamId = document.getElementById('newTeamId').value;
1340
+ const secureCses = document.getElementById('newSecureCses').value;
1341
+ const hostCoses = document.getElementById('newHostCoses').value;
1342
+ const csesidx = document.getElementById('newCsesidx').value;
1343
+ const userAgent = document.getElementById('newUserAgent').value;
1344
+
1345
+ try {
1346
+ const res = await apiFetch(`${API_BASE}/api/accounts`, {
1347
+ method: 'POST',
1348
+ headers: { 'Content-Type': 'application/json' },
1349
+ body: JSON.stringify({
1350
+ team_id: teamId,
1351
+ "secure_c_ses": secureCses,
1352
+ "host_c_oses": hostCoses,
1353
+ "csesidx": csesidx,
1354
+ "user_agent": userAgent })
1355
+ });
1356
+ const data = await res.json();
1357
+ if (!res.ok || data.error) throw new Error(data.error || data.detail || '添加失败');
1358
+ showToast('账号添加成功!', 'success');
1359
+ closeModal('addAccountModal');
1360
+ loadAccounts();
1361
+ } catch (e) {
1362
+ showToast('添加失败: ' + e.message, 'error');
1363
+ }
1364
+ }
1365
+
1366
+ function parseAccountJson(text) {
1367
+ const textarea = document.getElementById('newAccountJson');
1368
+ const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
1369
+ if (!raw) {
1370
+ showToast('请先粘贴账号JSON', 'warning');
1371
+ return;
1372
+ }
1373
+ let acc;
1374
+ try {
1375
+ const parsed = JSON.parse(raw);
1376
+ acc = Array.isArray(parsed) ? parsed[0] : parsed;
1377
+ if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
1378
+ } catch (err) {
1379
+ showToast('解析失败: ' + err.message, 'error');
1380
+ return;
1381
+ }
1382
+
1383
+ document.getElementById('newTeamId').value = acc.team_id || '';
1384
+ document.getElementById('newSecureCses').value = acc.secure_c_ses || '';
1385
+ document.getElementById('newHostCoses').value = acc.host_c_oses || '';
1386
+ document.getElementById('newCsesidx').value = acc.csesidx || '';
1387
+ document.getElementById('newUserAgent').value = acc.user_agent || '';
1388
+ showToast('已填充账号信息', 'success');
1389
+ }
1390
+
1391
+ async function pasteAccountJson() {
1392
+ try {
1393
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
1394
+ showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
1395
+ return;
1396
+ }
1397
+ const text = await navigator.clipboard.readText();
1398
+ document.getElementById('newAccountJson').value = text;
1399
+ parseAccountJson(text);
1400
+ } catch (e) {
1401
+ showToast('无法读取剪贴板: ' + e.message, 'error');
1402
+ }
1403
+ }
1404
+
1405
+ async function deleteAccount(id) {
1406
+ if (!confirm('确定要删除这个账号吗?')) return;
1407
+ try {
1408
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}`, { method: 'DELETE' });
1409
+ if (!res.ok) throw new Error((await res.json()).detail);
1410
+ showToast('账号删除成功!', 'success');
1411
+ loadAccounts();
1412
+ } catch (e) {
1413
+ showToast('删除失败: ' + e.message, 'error');
1414
+ }
1415
+ }
1416
+
1417
+ async function testAccount(id) {
1418
+ showToast(`正在测试账号ID: ${id}...`, 'info');
1419
+ try {
1420
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/test`);
1421
+ const data = await res.json();
1422
+ if (res.ok && data.success) {
1423
+ showToast(`账号 ${id} 测试成功!`, 'success');
1424
+ } else {
1425
+ throw new Error(data.detail || '未知错误');
1426
+ }
1427
+ loadAccounts();
1428
+ } catch (e) {
1429
+ showToast(`账号 ${id} 测试失败: ${e.message}`, 'error');
1430
+ }
1431
+ }
1432
+
1433
+ async function toggleAccount(id) {
1434
+ const acc = accountsData.find(a => a.id === id);
1435
+ const action = acc && acc.enabled !== false ? '停用' : '启用';
1436
+ try {
1437
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/toggle`, {
1438
+ method: 'POST',
1439
+ headers: { 'Content-Type': 'application/json' }
1440
+ });
1441
+ const data = await res.json();
1442
+ if (res.ok && data.success) {
1443
+ showToast(`账号 ${id} ${action}成功!`, 'success');
1444
+ loadAccounts();
1445
+ } else {
1446
+ throw new Error(data.error || data.detail || '未知错误');
1447
+ }
1448
+ } catch (e) {
1449
+ showToast(`账号 ${id} ${action}失败: ${e.message}`, 'error');
1450
+ }
1451
+ }
1452
+
1453
+ /**
1454
+ * 显示刷新Cookie的模态框
1455
+ * @param {number} id - 账号ID
1456
+ */
1457
+ function showRefreshCookieModal(id) {
1458
+ const acc = accountsData.find(a => a.id === id);
1459
+ if (!acc) {
1460
+ showToast('账号不存在', 'error');
1461
+ return;
1462
+ }
1463
+
1464
+ document.getElementById('refreshAccountId').value = id;
1465
+ document.getElementById('refreshSecureCses').value = '';
1466
+ document.getElementById('refreshHostCoses').value = '';
1467
+ document.getElementById('refreshCsesidx').value = '';
1468
+ document.getElementById('refreshCookieJson').value = '';
1469
+
1470
+ openModal('refreshCookieModal');
1471
+ }
1472
+
1473
+ /**
1474
+ * 从JSON解析并填充刷新Cookie表单
1475
+ * @param {string} text - JSON字符串
1476
+ */
1477
+ function parseRefreshCookieJson(text) {
1478
+ const textarea = document.getElementById('refreshCookieJson');
1479
+ const raw = (typeof text === 'string' ? text : textarea.value || '').trim();
1480
+ if (!raw) {
1481
+ showToast('请先粘贴Cookie JSON', 'warning');
1482
+ return;
1483
+ }
1484
+ let acc;
1485
+ try {
1486
+ const parsed = JSON.parse(raw);
1487
+ acc = Array.isArray(parsed) ? parsed[0] : parsed;
1488
+ if (!acc || typeof acc !== 'object') throw new Error('格式不正确');
1489
+ } catch (err) {
1490
+ showToast('解析失败: ' + err.message, 'error');
1491
+ return;
1492
+ }
1493
+
1494
+ document.getElementById('refreshSecureCses').value = acc.secure_c_ses || '';
1495
+ document.getElementById('refreshHostCoses').value = acc.host_c_oses || '';
1496
+ document.getElementById('refreshCsesidx').value = acc.csesidx || '';
1497
+ showToast('已填充Cookie信息', 'success');
1498
+ }
1499
+
1500
+ /**
1501
+ * 从剪贴板粘贴并解析刷新Cookie JSON
1502
+ */
1503
+ async function pasteRefreshCookieJson() {
1504
+ try {
1505
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
1506
+ showToast('当前环境不支持剪贴板API,请使用HTTPS或手动粘贴', 'warning');
1507
+ return;
1508
+ }
1509
+ const text = await navigator.clipboard.readText();
1510
+ document.getElementById('refreshCookieJson').value = text;
1511
+ parseRefreshCookieJson(text);
1512
+ } catch (e) {
1513
+ showToast('无法读取剪贴板: ' + e.message, 'error');
1514
+ }
1515
+ }
1516
+
1517
+ /**
1518
+ * 刷新账号Cookie
1519
+ * 调用后端API更新账号的Cookie信息
1520
+ */
1521
+ async function refreshAccountCookie() {
1522
+ const id = document.getElementById('refreshAccountId').value;
1523
+ const secureCses = document.getElementById('refreshSecureCses').value.trim();
1524
+ const hostCoses = document.getElementById('refreshHostCoses').value.trim();
1525
+ const csesidx = document.getElementById('refreshCsesidx').value.trim();
1526
+
1527
+ // 验证必填字段
1528
+ if (!secureCses || !hostCoses) {
1529
+ showToast('secure_c_ses 和 host_c_oses 为必填项', 'warning');
1530
+ return;
1531
+ }
1532
+
1533
+ try {
1534
+ const res = await apiFetch(`${API_BASE}/api/accounts/${id}/refresh-cookie`, {
1535
+ method: 'POST',
1536
+ headers: { 'Content-Type': 'application/json' },
1537
+ body: JSON.stringify({
1538
+ secure_c_ses: secureCses,
1539
+ host_c_oses: hostCoses,
1540
+ csesidx: csesidx || undefined
1541
+ })
1542
+ });
1543
+ const data = await res.json();
1544
+
1545
+ if (res.ok && data.success) {
1546
+ showToast('Cookie刷新成功!', 'success');
1547
+ closeModal('refreshCookieModal');
1548
+ loadAccounts();
1549
+ } else {
1550
+ throw new Error(data.error || data.detail || '未知错误');
1551
+ }
1552
+ } catch (e) {
1553
+ showToast('Cookie刷新失败: ' + e.message, 'error');
1554
+ }
1555
+ }
1556
+
1557
+ // --- 模型管理 (Models) ---
1558
+ async function loadModels() {
1559
+ try {
1560
+ const res = await apiFetch(`${API_BASE}/api/models`);
1561
+ const data = await res.json();
1562
+ modelsData = data.models || [];
1563
+ renderModels();
1564
+ } catch (e) {
1565
+ showToast('加载模型列表失败: ' + e.message, 'error');
1566
+ }
1567
+ }
1568
+
1569
+ function escapeHtml(str) {
1570
+ if (!str) return '';
1571
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
1572
+ }
1573
+
1574
+ function renderModels() {
1575
+ const tbody = document.getElementById('modelsTableBody');
1576
+ if (!tbody) return;
1577
+ if (modelsData.length === 0) {
1578
+ tbody.innerHTML = `<tr><td colspan="7" class="empty-state">
1579
+ <div class="empty-state-icon"><svg class="icon"><use xlink:href="#icon-robot"></use></svg></div>
1580
+ <h3>暂无模型</h3><p>点击 "添加模型" 按钮来创建一个。</p>
1581
+ </td></tr>`;
1582
+ return;
1583
+ }
1584
+ tbody.innerHTML = modelsData.map((model, index) => {
1585
+ const safeId = escapeHtml(model.id);
1586
+ const safeName = escapeHtml(model.name);
1587
+ const safeDesc = escapeHtml(model.description);
1588
+ return `
1589
+ <tr>
1590
+ <td><code>${safeId}</code></td>
1591
+ <td>${safeName}</td>
1592
+ <td title="${safeDesc}">${model.description ? safeDesc.substring(0, 40) + '...' : ''}</td>
1593
+ <td>${model.context_length}</td>
1594
+ <td>${model.max_tokens}</td>
1595
+ <td><span class="badge ${model.is_public ? 'badge-success' : 'badge-warning'}">${model.is_public ? '公共' : '私有'}</span></td>
1596
+ <td>
1597
+ <button class="btn btn-sm btn-outline btn-icon" onclick="showEditModelModalByIndex(${index})" title="编辑">✏️</button>
1598
+ <button class="btn btn-sm btn-danger btn-icon" onclick="deleteModelByIndex(${index})" title="删除">🗑️</button>
1599
+ </td>
1600
+ </tr>
1601
+ `;
1602
+ }).join('');
1603
+ }
1604
+
1605
+ function showAddModelModal() {
1606
+ openModal('addModelModal');
1607
+ }
1608
+
1609
+ function showEditModelModalByIndex(index) {
1610
+ const model = modelsData[index];
1611
+ if (!model) return;
1612
+
1613
+ document.getElementById('editModelOriginalId').value = model.id;
1614
+ document.getElementById('editModelId').value = model.id;
1615
+ document.getElementById('editModelName').value = model.name || '';
1616
+ document.getElementById('editModelDesc').value = model.description || '';
1617
+ document.getElementById('editContextLength').value = model.context_length || '';
1618
+ document.getElementById('editMaxTokens').value = model.max_tokens || '';
1619
+
1620
+ openModal('editModelModal');
1621
+ }
1622
+
1623
+ async function updateModel() {
1624
+ const originalId = document.getElementById('editModelOriginalId').value;
1625
+ const model = {
1626
+ name: document.getElementById('editModelName').value,
1627
+ description: document.getElementById('editModelDesc').value,
1628
+ context_length: parseInt(document.getElementById('editContextLength').value) || 32000,
1629
+ max_tokens: parseInt(document.getElementById('editMaxTokens').value) || 8096
1630
+ };
1631
+
1632
+ try {
1633
+ const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(originalId)}`, {
1634
+ method: 'PUT',
1635
+ headers: { 'Content-Type': 'application/json' },
1636
+ body: JSON.stringify(model)
1637
+ });
1638
+ const data = await res.json();
1639
+
1640
+ if (data.success) {
1641
+ showToast('模型更新成功', 'success');
1642
+ closeModal('editModelModal');
1643
+ loadModels();
1644
+ } else {
1645
+ showToast('更新失败: ' + (data.error || '未知错误'), 'error');
1646
+ }
1647
+ } catch (e) {
1648
+ showToast('更新失败: ' + e.message, 'error');
1649
+ }
1650
+ }
1651
+
1652
+ /**
1653
+ * 保存新模型
1654
+ * 从添加模型模态框获取数据并调用API创建新模型
1655
+ */
1656
+ async function saveNewModel() {
1657
+ const modelId = document.getElementById('newModelId').value.trim();
1658
+ const modelName = document.getElementById('newModelName').value.trim();
1659
+ const modelDesc = document.getElementById('newModelDesc').value.trim();
1660
+ const contextLength = parseInt(document.getElementById('newContextLength').value) || 32000;
1661
+ const maxTokens = parseInt(document.getElementById('newMaxTokens').value) || 8096;
1662
+
1663
+ // 验证必填字段
1664
+ if (!modelId) {
1665
+ showToast('请输入模型ID', 'warning');
1666
+ return;
1667
+ }
1668
+ if (!modelName) {
1669
+ showToast('请输入模型名称', 'warning');
1670
+ return;
1671
+ }
1672
+
1673
+ const model = {
1674
+ id: modelId,
1675
+ name: modelName,
1676
+ description: modelDesc,
1677
+ context_length: contextLength,
1678
+ max_tokens: maxTokens
1679
+ };
1680
+
1681
+ try {
1682
+ const res = await apiFetch(`${API_BASE}/api/models`, {
1683
+ method: 'POST',
1684
+ headers: { 'Content-Type': 'application/json' },
1685
+ body: JSON.stringify(model)
1686
+ });
1687
+ const data = await res.json();
1688
+
1689
+ if (res.ok && (data.success || !data.error)) {
1690
+ showToast('模型添加成功', 'success');
1691
+ closeModal('addModelModal');
1692
+ // 清空表单
1693
+ document.getElementById('newModelId').value = '';
1694
+ document.getElementById('newModelName').value = '';
1695
+ document.getElementById('newModelDesc').value = '';
1696
+ document.getElementById('newContextLength').value = '';
1697
+ document.getElementById('newMaxTokens').value = '';
1698
+ loadModels();
1699
+ } else {
1700
+ throw new Error(data.error || '添加失败');
1701
+ }
1702
+ } catch (e) {
1703
+ showToast('添加模型失败: ' + e.message, 'error');
1704
+ }
1705
+ }
1706
+
1707
+ /**
1708
+ * 删除模型
1709
+ * @param {string} id - 模型ID
1710
+ */
1711
+ async function deleteModelByIndex(index) {
1712
+ const model = modelsData[index];
1713
+ if (!model) return;
1714
+ const id = model.id;
1715
+ if (!confirm(`确定要删除模型 "${id}" 吗?此操作不可恢复。`)) {
1716
+ return;
1717
+ }
1718
+
1719
+ try {
1720
+ const res = await apiFetch(`${API_BASE}/api/models/${encodeURIComponent(id)}`, {
1721
+ method: 'DELETE'
1722
+ });
1723
+ const data = await res.json();
1724
+
1725
+ if (res.ok && (data.success || !data.error)) {
1726
+ showToast('模型删除成功', 'success');
1727
+ loadModels();
1728
+ } else {
1729
+ throw new Error(data.error || '删除失败');
1730
+ }
1731
+ } catch (e) {
1732
+ showToast('删除模型失败: ' + e.message, 'error');
1733
+ }
1734
+ }
1735
+
1736
+ // --- 系统设置 (Settings) ---
1737
+ async function loadConfig() {
1738
+ try {
1739
+ const res = await apiFetch(`${API_BASE}/api/config`);
1740
+ configData = await res.json();
1741
+ document.getElementById('proxyUrl').value = configData.proxy || '';
1742
+ const imageModeSelect = document.getElementById('imageOutputMode');
1743
+ if (imageModeSelect) {
1744
+ const mode = (configData.image_output_mode || 'url');
1745
+ imageModeSelect.value = mode === 'base64' ? 'base64' : 'url';
1746
+ }
1747
+ document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
1748
+ } catch (e) {
1749
+ showToast('加载配置失败: ' + e.message, 'error');
1750
+ }
1751
+ }
1752
+
1753
+ async function loadLogLevel() {
1754
+ try {
1755
+ const res = await apiFetch(`${API_BASE}/api/logging`);
1756
+ const data = await res.json();
1757
+ const select = document.getElementById('logLevelSelect');
1758
+ if (select && data.level) {
1759
+ select.value = data.level;
1760
+ }
1761
+ } catch (e) {
1762
+ console.warn('日志级别加载失败', e);
1763
+ }
1764
+ }
1765
+
1766
+ async function updateLogLevel(level) {
1767
+ try {
1768
+ const res = await apiFetch(`${API_BASE}/api/logging`, {
1769
+ method: 'POST',
1770
+ headers: { 'Content-Type': 'application/json' },
1771
+ body: JSON.stringify({ level })
1772
+ });
1773
+ const data = await res.json();
1774
+ if (!res.ok || data.error) {
1775
+ throw new Error(data.error || '设置失败');
1776
+ }
1777
+ showToast(`日志级别已切换为 ${data.level}`, 'success');
1778
+ } catch (e) {
1779
+ showToast('日志级别设置失败: ' + e.message, 'error');
1780
+ }
1781
+ }
1782
+
1783
+ // --- Token 管理 ---
1784
+ async function loadTokens() {
1785
+ try {
1786
+ const res = await apiFetch(`${API_BASE}/api/tokens`);
1787
+ const data = await res.json();
1788
+ tokensData = data.tokens || [];
1789
+ renderTokens();
1790
+ } catch (e) {
1791
+ showToast('加载 Token 失败: ' + e.message, 'error');
1792
+ }
1793
+ }
1794
+
1795
+ function renderTokens() {
1796
+ const tbody = document.getElementById('tokensTableBody');
1797
+ if (!tbody) return;
1798
+ if (!tokensData.length) {
1799
+ tbody.innerHTML = `<tr><td colspan="2" class="empty-state">暂无 Token</td></tr>`;
1800
+ return;
1801
+ }
1802
+ tbody.innerHTML = tokensData.map(token => `
1803
+ <tr>
1804
+ <td><code>${token}</code></td>
1805
+ <td style="white-space: nowrap;">
1806
+ <button class="btn btn-outline btn-sm" data-token="${token}" onclick="copyToken(this.dataset.token)" title="复制Token">复制</button>
1807
+ <button class="btn btn-danger btn-sm" data-token="${token}" onclick="deleteToken(this.dataset.token)" title="删除Token">删除</button>
1808
+ </td>
1809
+ </tr>
1810
+ `).join('');
1811
+ }
1812
+
1813
+ async function addToken() {
1814
+ const manual = document.getElementById('manualToken').value.trim();
1815
+ try {
1816
+ const res = await apiFetch(`${API_BASE}/api/tokens`, {
1817
+ method: 'POST',
1818
+ headers: { 'Content-Type': 'application/json' },
1819
+ body: JSON.stringify(manual ? { token: manual } : {})
1820
+ });
1821
+ const data = await res.json();
1822
+ if (!res.ok || data.error) throw new Error(data.error || '创建失败');
1823
+ document.getElementById('manualToken').value = data.token;
1824
+ showToast('Token 创建成功', 'success');
1825
+ loadTokens();
1826
+ } catch (e) {
1827
+ showToast('创建 Token 失败: ' + e.message, 'error');
1828
+ }
1829
+ }
1830
+
1831
+ function generateToken() {
1832
+ if (window.crypto && crypto.randomUUID) {
1833
+ document.getElementById('manualToken').value = crypto.randomUUID().replace(/-/g, '');
1834
+ } else {
1835
+ document.getElementById('manualToken').value = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
1836
+ }
1837
+ }
1838
+
1839
+ async function deleteToken(token) {
1840
+ if (!confirm('确定删除该 Token 吗?')) return;
1841
+ try {
1842
+ const res = await apiFetch(`${API_BASE}/api/tokens/${token}`, { method: 'DELETE' });
1843
+ const data = await res.json();
1844
+ if (!res.ok || data.error) throw new Error(data.error || '删除失败');
1845
+ showToast('Token 删除成功', 'success');
1846
+ loadTokens();
1847
+ } catch (e) {
1848
+ showToast('删除 Token 失败: ' + e.message, 'error');
1849
+ }
1850
+ }
1851
+
1852
+ function copyToken(token) {
1853
+ if (!token) {
1854
+ showToast('无效的 Token', 'warning');
1855
+ return;
1856
+ }
1857
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1858
+ navigator.clipboard.writeText(token).then(() => {
1859
+ showToast('已复制', 'success');
1860
+ }).catch(() => {
1861
+ fallbackCopy(token);
1862
+ });
1863
+ } else {
1864
+ fallbackCopy(token);
1865
+ }
1866
+ }
1867
+
1868
+ function fallbackCopy(text) {
1869
+ try {
1870
+ const textarea = document.createElement('textarea');
1871
+ textarea.value = text;
1872
+ document.body.appendChild(textarea);
1873
+ textarea.select();
1874
+ document.execCommand('copy');
1875
+ document.body.removeChild(textarea);
1876
+ showToast('已复制', 'success');
1877
+ } catch (err) {
1878
+ showToast('复制失败', 'error');
1879
+ }
1880
+ }
1881
+
1882
+ function logoutAdmin() {
1883
+ localStorage.removeItem(ADMIN_TOKEN_KEY);
1884
+ document.cookie = 'admin_token=; Max-Age=0; path=/';
1885
+ showToast('已注销', 'success');
1886
+ updateLoginButton();
1887
+ }
1888
+
1889
+ function showLoginModal() {
1890
+ document.getElementById('loginPassword').value = '';
1891
+ openModal('loginModal');
1892
+ }
1893
+
1894
+ async function submitLogin() {
1895
+ const pwd = document.getElementById('loginPassword').value;
1896
+ if (!pwd) {
1897
+ showToast('请输入密码', 'warning');
1898
+ return;
1899
+ }
1900
+ try {
1901
+ const res = await fetch(`${API_BASE}/api/auth/login`, {
1902
+ method: 'POST',
1903
+ headers: { 'Content-Type': 'application/json' },
1904
+ body: JSON.stringify({ password: pwd })
1905
+ });
1906
+ const data = await res.json();
1907
+ if (!res.ok || data.error) {
1908
+ throw new Error(data.error || '登录失败');
1909
+ }
1910
+ localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
1911
+ showToast('登录成功', 'success');
1912
+ closeModal('loginModal');
1913
+ loadAllData();
1914
+ updateLoginButton();
1915
+ } catch (e) {
1916
+ showToast('登录失败: ' + e.message, 'error');
1917
+ }
1918
+ }
1919
+
1920
+ async function saveSettings() {
1921
+ const proxyUrl = document.getElementById('proxyUrl').value;
1922
+ const imageModeSelect = document.getElementById('imageOutputMode');
1923
+ const imageOutputMode = imageModeSelect ? imageModeSelect.value : 'url';
1924
+ try {
1925
+ const res = await apiFetch(`${API_BASE}/api/config`, {
1926
+ method: 'PUT',
1927
+ headers: { 'Content-Type': 'application/json' },
1928
+ body: JSON.stringify({ proxy: proxyUrl, image_output_mode: imageOutputMode })
1929
+ });
1930
+ if (!res.ok) throw new Error((await res.json()).detail);
1931
+ showToast('设置保存成功!', 'success');
1932
+ loadConfig();
1933
+ } catch (e) {
1934
+ showToast('保存失败: ' + e.message, 'error');
1935
+ }
1936
+ }
1937
+
1938
+ async function testProxy() {
1939
+ const proxyUrl = document.getElementById('proxyUrl').value;
1940
+ const proxyStatus = document.getElementById('proxyStatus');
1941
+ proxyStatus.textContent = '测试中...';
1942
+ proxyStatus.style.color = 'var(--text-muted)';
1943
+ try {
1944
+ const res = await apiFetch(`${API_BASE}/api/proxy/test`, {
1945
+ method: 'POST',
1946
+ headers: { 'Content-Type': 'application/json' },
1947
+ body: JSON.stringify({ proxy: proxyUrl })
1948
+ });
1949
+ const data = await res.json();
1950
+ if (res.ok && data.success) {
1951
+ proxyStatus.textContent = `测试成功! (${data.delay_ms}ms)`;
1952
+ proxyStatus.style.color = 'var(--success)';
1953
+ } else {
1954
+ throw new Error(data.detail);
1955
+ }
1956
+ } catch (e) {
1957
+ proxyStatus.textContent = `测试失败: ${e.message}`;
1958
+ proxyStatus.style.color = 'var(--danger)';
1959
+ }
1960
+ }
1961
+
1962
+ function refreshConfig() {
1963
+ loadConfig();
1964
+ showToast('配置已刷新', 'info');
1965
+ }
1966
+
1967
+ function downloadConfig() {
1968
+ const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(configData, null, 2));
1969
+ const downloadAnchorNode = document.createElement('a');
1970
+ downloadAnchorNode.setAttribute("href", dataStr);
1971
+ downloadAnchorNode.setAttribute("download", "business_gemini_session.json");
1972
+ document.body.appendChild(downloadAnchorNode);
1973
+ downloadAnchorNode.click();
1974
+ downloadAnchorNode.remove();
1975
+ showToast('配置文件已开始下载', 'success');
1976
+ }
1977
+
1978
+ function uploadConfig() {
1979
+ document.getElementById('configFileInput').click();
1980
+ }
1981
+
1982
+ function handleConfigUpload(event) {
1983
+ const file = event.target.files[0];
1984
+ if (!file) return;
1985
+ const reader = new FileReader();
1986
+ reader.onload = async (e) => {
1987
+ try {
1988
+ const newConfig = JSON.parse(e.target.result);
1989
+ const res = await apiFetch(`${API_BASE}/api/config/import`, {
1990
+ method: 'POST',
1991
+ headers: { 'Content-Type': 'application/json' },
1992
+ body: JSON.stringify(newConfig)
1993
+ });
1994
+ if (!res.ok) throw new Error((await res.json()).detail);
1995
+ showToast('配置导入成功!', 'success');
1996
+ loadAllData();
1997
+ } catch (err) {
1998
+ showToast('导入失败: ' + err.message, 'error');
1999
+ }
2000
+ };
2001
+ reader.readAsText(file);
2002
+ }
2003
+
2004
+ // --- 模态框控制 ---
2005
+ function openModal(modalId) {
2006
+ const modal = document.getElementById(modalId);
2007
+ if (modal) modal.classList.add('show');
2008
+ }
2009
+
2010
+ function closeModal(modalId) {
2011
+ const modal = document.getElementById(modalId);
2012
+ if (modal) modal.classList.remove('show');
2013
+ }
2014
+
2015
+ document.querySelectorAll('.modal').forEach(modal => {
2016
+ modal.addEventListener('click', (e) => {
2017
+ if (e.target.classList.contains('modal')) {
2018
+ closeModal(modal.id);
2019
+ }
2020
+ });
2021
+ });
2022
+ </script>
2023
+
2024
+ </body>
2025
+ </html>
requirements-hf.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Hugging Face Spaces requirements
2
+ flask>=2.0.0
3
+ flask-cors>=3.0.0
4
+ requests>=2.25.0
5
+ urllib3>=1.26.0