malt666 commited on
Commit
3d71190
·
verified ·
1 Parent(s): c1f2b1a

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +24 -0
  2. README.md +115 -5
  3. app.py +1792 -0
  4. requirements.txt +0 -0
  5. templates/dashboard.html +1032 -0
  6. templates/login.html +462 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # 设置用户为root
4
+ USER root
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ # 设置环境变量
14
+ ENV HOST=0.0.0.0
15
+ ENV PORT=7860
16
+
17
+ # 删除敏感文件
18
+ RUN rm -f config.json password.txt
19
+
20
+ # 暴露端口(Hugging Face默认使用7860端口)
21
+ EXPOSE 7860
22
+
23
+ # 启动命令
24
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,11 +1,121 @@
1
  ---
2
- title: Abacus Procy
3
- emoji: 🦀
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: docker
 
 
7
  pinned: false
8
- license: mit
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Abacus Chat Proxy
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: "3.9"
8
+ app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ # Abacus Chat Proxy
15
+
16
+ > 📢 本项目基于 [orbitoo/abacus_chat_proxy](https://github.com/orbitoo/abacus_chat_proxy) 改进
17
+ >
18
+ > 特别感谢 orbitoo 大佬提供的原始项目!
19
+ >
20
+ > 本项目增加了:Docker部署支持、Hugging Face一键部署、自动保活功能等
21
+
22
+ 一个用于中转API请求的代理服务器。
23
+
24
+ #### 注意创建space的时候把private改为public,因为不改为public导致的错误别来找我
25
+
26
+ [![Deploy to Hugging Face Spaces](https://huggingface.co/datasets/huggingface/badges/raw/main/deploy-to-spaces-lg.svg)](https://huggingface.co/spaces/malt666/abacus_chat_proxy?duplicate=true)
27
+
28
+ ## ⚠️ 警告
29
+
30
+ **本地部署方式已失效!**为了适配hugging face,本项目的本地部署方式已不再可用。目前只能通过Hugging Face Spaces部署来使用本代理服务。请使用下方的Hugging Face一键部署方法。
31
+
32
+ ## 🚀 快速开始
33
+
34
+ ### Hugging Face一键部署
35
+
36
+ #### 注意创建space的时候把private改为public,因为不改为public导致的错误别来找我
37
+
38
+ 1. 点击上方的"Deploy to Hugging Face Spaces"按钮
39
+ 2. 登录你的Hugging Face账号(如果还没有,需要注册一个)
40
+ 3. 在弹出的页面中设置你的Space名称,注意创建space的时候把private改为public,因为不改为为public导致的错误别来找我
41
+ 4. 创建完Space后,在Space的Settings -> Repository Secrets中添加以下配置:
42
+ - `cookie_1`: 第一个cookie
43
+ - `cookie_2`: 第二个cookie(如果有)
44
+ - 以此类推...
45
+ - `password`: (必填,最好超过8位数,不然被盗用api导致点数用光,损失自负)api调用密码,也是dashboard登录密码
46
+ 5. 等待自动部署完成即可
47
+ 6. 登录dashboard查看使用情况,space里面是没办法登录的,点击弹出的登录页面的小窗口提示“请点击 https://xyz-abacus-chat-proxy.hf.space 来登录并查看使用情况”的链接来登录,登录密码是你设置的password
48
+ 7. **获取API链接**:部署成功后,网络的登录页面会显示api接口链接;或者你点击右上角的三个点按钮,在弹出的选项卡里面点击"Embed this Space",然后在弹出的"Embed this Space"界面里的"Direct URL"就是你的访问链接,你可以用这个链接调用API和查看使用情况
49
+ 8. api调用:api调用的时候注意密钥填写你的password,不然没办法调用
50
+ ### 本地运行(已失效)
51
+
52
+ > ⚠️ 以下本地运行方法已失效,仅作参考。请使用Hugging Face部署方式。
53
+
54
+ #### Windows用户
55
+
56
+ 1. 双击运行 `start.bat`
57
+ 2. 首次运行选择 `0` 进行配置
58
+ 3. 配置完成后选择 `Y` 直接启动,或 `N` 返回菜单
59
+ 4. 之后可直接选择 `1` 启动代理
60
+ 5. 代理服务器默认运行在 `http://127.0.0.1:9876/`
61
+
62
+ #### Linux/macOS用户
63
+
64
+ ```bash
65
+ # 赋予脚本执行权限
66
+ chmod +x start.sh
67
+
68
+ # 运行脚本
69
+ ./start.sh
70
+ ```
71
+
72
+ 选项说明同Windows。
73
+
74
+ ### 🌐 Hugging Face部署
75
+
76
+ 1. Fork本仓库到你的GitHub账号
77
+ 2. 在Hugging Face上创建新的Space(选择Docker类型)
78
+ 3. 在Space的设置中连接你的GitHub仓库
79
+ 4. 在Space的设置中添加以下Secrets:
80
+ - 第1组配置:
81
+ - `cookie_1`: 第1个cookies字符串
82
+ - 第2组配置(如果需要):
83
+ - `cookie_2`: 第2个cookies字符串
84
+ - 更多配置以此类推(`cookie_3`...)
85
+ - `password`: (可选)访问密码
86
+ 5. Space会自动部署,服务将在 `https://你的空间名-你的用户名.hf.space` 上运行
87
+
88
+ ## ⚙️ 环境要求
89
+
90
+ - Python 3.8+
91
+ - pip
92
+
93
+ ## 📦 依赖
94
+
95
+ ```bash
96
+ Flask==3.1.0
97
+ requests==2.32.3
98
+ PyJWT==2.8.0
99
+ ```
100
+
101
+ ## 📝 配置说明
102
+
103
+ ### 本地配置
104
+
105
+ 首次运行时,请选择 `0` 进行配置,按照提示填写相关信息。配置文件将保存在 `config.json` 中。
106
+
107
+ ### 环境变量配置
108
+
109
+ 在Docker或云平台部署时,需要配置以下环境变量:
110
+
111
+ - 必需的配置(至少需要一组):
112
+ - `cookie_1`: 第1组配置
113
+ - `cookie_2`: 第2组配置(可选)
114
+ - 以此类推...
115
+ - 可选配置:
116
+ - `password`: 访问密码
117
+
118
+ ## 🔒 安全说明
119
+
120
+ - 建议在部署到Hugging Face时设置访问密码
121
+ - 在Hugging Face上配置时,请使用Secrets来存储敏感信息
app.py ADDED
@@ -0,0 +1,1792 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, Response, render_template_string, render_template, redirect, url_for, session as flask_session
2
+ import requests
3
+ import time
4
+ import json
5
+ import uuid
6
+ import random
7
+ import io
8
+ import re
9
+ from functools import wraps
10
+ import hashlib
11
+ import jwt
12
+ import os
13
+ import threading
14
+ from datetime import datetime, timedelta
15
+
16
+ app = Flask(__name__, template_folder='templates')
17
+ app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
18
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
19
+
20
+ # 添加tokenizer服务URL
21
+ TOKENIZER_SERVICE_URL = "https://malt666-tokenizer.hf.space/count_tokens"
22
+
23
+ API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
24
+ MODEL_LIST_URL = "https://abacus.ai/api/v0/listExternalApplications"
25
+ CHAT_URL = "https://apps.abacus.ai/api/_chatLLMSendMessageSSE"
26
+ USER_INFO_URL = "https://abacus.ai/api/v0/_getUserInfo"
27
+ COMPUTE_POINTS_URL = "https://apps.abacus.ai/api/_getOrganizationComputePoints"
28
+ COMPUTE_POINTS_LOG_URL = "https://abacus.ai/api/v0/_getOrganizationComputePointLog"
29
+ CREATE_CONVERSATION_URL = "https://apps.abacus.ai/api/createDeploymentConversation"
30
+ DELETE_CONVERSATION_URL = "https://apps.abacus.ai/api/deleteDeploymentConversation"
31
+ GET_CONVERSATION_URL = "https://apps.abacus.ai/api/getDeploymentConversation"
32
+ COMPUTE_POINT_TOGGLE_URL = "https://abacus.ai/api/v0/_updateOrganizationComputePointToggle"
33
+
34
+ # 添加编辑聊天消息的URL
35
+ EDIT_CHAT_RESPONSE_URL = "https://apps.abacus.ai/api/_editChatResponse"
36
+
37
+ USER_AGENTS = [
38
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
39
+ ]
40
+
41
+
42
+ PASSWORD = None
43
+ USER_NUM = 0
44
+ USER_DATA = []
45
+ CURRENT_USER = -1
46
+ MODELS = set()
47
+
48
+ TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
49
+ SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
50
+
51
+
52
+ # 添加一个计数器记录健康检查次数
53
+ health_check_counter = 0
54
+
55
+
56
+ # 添加统计变量
57
+ model_usage_stats = {} # 模型使用次数统计
58
+ total_tokens = {
59
+ "prompt": 0, # 输入token统计
60
+ "completion": 0, # 输出token统计
61
+ "total": 0 # 总token统计
62
+ }
63
+
64
+ # 模型调用记录
65
+ model_usage_records = [] # 每次调用详细记录
66
+ MODEL_USAGE_RECORDS_FILE = "model_usage_records.json" # 调用记录保存文件
67
+
68
+ # 计算点信息
69
+ compute_points = {
70
+ "left": 0, # 剩余计算点
71
+ "total": 0, # 总计算点
72
+ "used": 0, # 已使用计算点
73
+ "percentage": 0, # 使用百分比
74
+ "last_update": None # 最后更新时间
75
+ }
76
+
77
+ # 计算点使用日志
78
+ compute_points_log = {
79
+ "columns": {}, # 列名
80
+ "log": [] # 日志数据
81
+ }
82
+
83
+ # 多用户计算点信息
84
+ users_compute_points = []
85
+
86
+ # 记录启动时间
87
+ START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
88
+
89
+
90
+ # 自定义JSON编码器,处理datetime对象
91
+ class DateTimeEncoder(json.JSONEncoder):
92
+ def default(self, obj):
93
+ if isinstance(obj, datetime):
94
+ return obj.strftime('%Y-%m-%d %H:%M:%S')
95
+ return super(DateTimeEncoder, self).default(obj)
96
+
97
+
98
+ # 加载模型调用记录
99
+ def load_model_usage_records():
100
+ global model_usage_records
101
+ try:
102
+ if os.path.exists(MODEL_USAGE_RECORDS_FILE):
103
+ with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
104
+ records = json.load(f)
105
+ if isinstance(records, list):
106
+ model_usage_records = records
107
+ print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
108
+ else:
109
+ print("调用记录文件格式不正确,初始化为空列表")
110
+ except Exception as e:
111
+ print(f"加载模型调用记录失败: {e}")
112
+ model_usage_records = []
113
+
114
+ # 保存模型调用记录
115
+ def save_model_usage_records():
116
+ try:
117
+ with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
118
+ json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
119
+ print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
120
+ except Exception as e:
121
+ print(f"保存模型调用记录失败: {e}")
122
+
123
+
124
+ def update_conversation_id(user_index, conversation_id):
125
+ """更新用户的conversation_id并保存到配置文件"""
126
+ try:
127
+ with open("config.json", "r") as f:
128
+ config = json.load(f)
129
+
130
+ if "config" in config and user_index < len(config["config"]):
131
+ config["config"][user_index]["conversation_id"] = conversation_id
132
+
133
+ # 保存到配置文件
134
+ with open("config.json", "w") as f:
135
+ json.dump(config, f, indent=4)
136
+
137
+ print(f"已将用户 {user_index+1} 的conversation_id更新为: {conversation_id}")
138
+ else:
139
+ print(f"更新conversation_id失败: 配置文件格式错误或用户索引越界")
140
+ except Exception as e:
141
+ print(f"更新conversation_id失败: {e}")
142
+
143
+
144
+ def resolve_config():
145
+ # 从环境变量读取多组配置
146
+ config_list = []
147
+ i = 1
148
+ while True:
149
+ cookie = os.environ.get(f"cookie_{i}")
150
+ if not cookie:
151
+ break
152
+
153
+ # 为每个cookie创建一个配置项,conversation_id初始为空
154
+ config_list.append({
155
+ "conversation_id": "", # 初始为空,将通过get_or_create_conversation自动创建
156
+ "cookies": cookie
157
+ })
158
+ i += 1
159
+
160
+ # 如果环境变量存在配置,使用环境变量的配置
161
+ if config_list:
162
+ print(f"从环境变量加载了 {len(config_list)} 个配置")
163
+ return config_list
164
+
165
+ # 如果环境变量不存在,从文件读取
166
+ try:
167
+ with open("config.json", "r") as f:
168
+ config = json.load(f)
169
+ config_list = config.get("config")
170
+ return config_list
171
+ except FileNotFoundError:
172
+ print("未找到config.json文件")
173
+ return []
174
+ except json.JSONDecodeError:
175
+ print("config.json格式错误")
176
+ return []
177
+
178
+
179
+ def get_password():
180
+ global PASSWORD
181
+ # 从环境变量读取密码
182
+ env_password = os.environ.get("password")
183
+ if env_password:
184
+ PASSWORD = hashlib.sha256(env_password.encode()).hexdigest()
185
+ return
186
+
187
+ # 如果环境变量不存在,从文件读取
188
+ try:
189
+ with open("password.txt", "r") as f:
190
+ PASSWORD = f.read().strip()
191
+ except FileNotFoundError:
192
+ with open("password.txt", "w") as f:
193
+ PASSWORD = None
194
+
195
+
196
+ def require_auth(f):
197
+ @wraps(f)
198
+ def decorated(*args, **kwargs):
199
+ if not PASSWORD:
200
+ return f(*args, **kwargs)
201
+
202
+ # 检查Flask会话是否已登录
203
+ if flask_session.get('logged_in'):
204
+ return f(*args, **kwargs)
205
+
206
+ # 如果是API请求,检查Authorization头
207
+ auth = request.authorization
208
+ if not auth or not check_auth(auth.token):
209
+ # 如果是浏览器请求,重定向到登录页面
210
+ if request.headers.get('Accept', '').find('text/html') >= 0:
211
+ return redirect(url_for('login'))
212
+ return jsonify({"error": "Unauthorized access"}), 401
213
+ return f(*args, **kwargs)
214
+
215
+ return decorated
216
+
217
+
218
+ def check_auth(token):
219
+ return hashlib.sha256(token.encode()).hexdigest() == PASSWORD
220
+
221
+
222
+ def is_token_expired(token):
223
+ if not token:
224
+ return True
225
+
226
+ try:
227
+ # Malkodi tokenon sen validigo de subskribo
228
+ payload = jwt.decode(token, options={"verify_signature": False})
229
+ # Akiru eksvalidiĝan tempon, konsideru eksvalidiĝinta 5 minutojn antaŭe
230
+ return payload.get('exp', 0) - time.time() < 300
231
+ except:
232
+ return True
233
+
234
+
235
+ def refresh_token(session, cookies):
236
+ """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
237
+ headers = {
238
+ "accept": "application/json, text/plain, */*",
239
+ "accept-language": "zh-CN,zh;q=0.9",
240
+ "content-type": "application/json",
241
+ "reai-ui": "1",
242
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
243
+ "sec-ch-ua-mobile": "?0",
244
+ "sec-ch-ua-platform": "\"Windows\"",
245
+ "sec-fetch-dest": "empty",
246
+ "sec-fetch-mode": "cors",
247
+ "sec-fetch-site": "same-site",
248
+ "x-abacus-org-host": "apps",
249
+ "user-agent": random.choice(USER_AGENTS),
250
+ "origin": "https://apps.abacus.ai",
251
+ "referer": "https://apps.abacus.ai/",
252
+ "cookie": cookies
253
+ }
254
+
255
+ try:
256
+ response = session.post(
257
+ USER_INFO_URL,
258
+ headers=headers,
259
+ json={},
260
+ cookies=None
261
+ )
262
+
263
+ if response.status_code == 200:
264
+ response_data = response.json()
265
+ if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
266
+ return response_data['result']['sessionToken']
267
+ else:
268
+ print(f"刷新token失败: {response_data.get('error', '未知错误')}")
269
+ return None
270
+ else:
271
+ print(f"刷新token失败,状态码: {response.status_code}")
272
+ return None
273
+ except Exception as e:
274
+ print(f"刷新token异常: {e}")
275
+ return None
276
+
277
+
278
+ def get_model_map(session, cookies, session_token):
279
+ """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
280
+ headers = {
281
+ "accept": "application/json, text/plain, */*",
282
+ "accept-language": "zh-CN,zh;q=0.9",
283
+ "content-type": "application/json",
284
+ "reai-ui": "1",
285
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
286
+ "sec-ch-ua-mobile": "?0",
287
+ "sec-ch-ua-platform": "\"Windows\"",
288
+ "sec-fetch-dest": "empty",
289
+ "sec-fetch-mode": "cors",
290
+ "sec-fetch-site": "same-site",
291
+ "x-abacus-org-host": "apps",
292
+ "user-agent": random.choice(USER_AGENTS),
293
+ "origin": "https://apps.abacus.ai",
294
+ "referer": "https://apps.abacus.ai/",
295
+ "cookie": cookies
296
+ }
297
+
298
+ if session_token:
299
+ headers["session-token"] = session_token
300
+
301
+ model_map = {}
302
+ models_set = set()
303
+
304
+ try:
305
+ response = session.post(
306
+ MODEL_LIST_URL,
307
+ headers=headers,
308
+ json={},
309
+ cookies=None
310
+ )
311
+
312
+ if response.status_code != 200:
313
+ print(f"获取模型列表失败,状态码: {response.status_code}")
314
+ raise Exception("API请求失败")
315
+
316
+ data = response.json()
317
+ if not data.get('success'):
318
+ print(f"获取模型列表失败: {data.get('error', '未知错误')}")
319
+ raise Exception("API返回错误")
320
+
321
+ applications = []
322
+ if isinstance(data.get('result'), dict):
323
+ applications = data.get('result', {}).get('externalApplications', [])
324
+ elif isinstance(data.get('result'), list):
325
+ applications = data.get('result', [])
326
+
327
+ for app in applications:
328
+ app_name = app.get('name', '')
329
+ app_id = app.get('externalApplicationId', '')
330
+ prediction_overrides = app.get('predictionOverrides', {})
331
+ llm_name = prediction_overrides.get('llmName', '') if prediction_overrides else ''
332
+
333
+ if not (app_name and app_id and llm_name):
334
+ continue
335
+
336
+ model_name = app_name
337
+ model_map[model_name] = (app_id, llm_name)
338
+ models_set.add(model_name)
339
+
340
+ if not model_map:
341
+ raise Exception("未找到任何可用模型")
342
+
343
+ return model_map, models_set
344
+
345
+ except Exception as e:
346
+ print(f"获取模型列表异常: {e}")
347
+ raise
348
+
349
+
350
+ def init_session():
351
+ get_password()
352
+ global USER_NUM, MODELS, USER_DATA
353
+
354
+ config_list = resolve_config()
355
+ user_num = len(config_list)
356
+ all_models = set()
357
+
358
+ for i in range(user_num):
359
+ user = config_list[i]
360
+ cookies = user.get("cookies")
361
+ conversation_id = user.get("conversation_id")
362
+ session = requests.Session()
363
+
364
+ session_token = refresh_token(session, cookies)
365
+ if not session_token:
366
+ print(f"无法获取cookie {i+1}的token")
367
+ continue
368
+
369
+ try:
370
+ model_map, models_set = get_model_map(session, cookies, session_token)
371
+ all_models.update(models_set)
372
+ USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
373
+
374
+ # 对第一个成功配置的用户,初始化计算点数记录功能
375
+ if i == 0:
376
+ try:
377
+ headers = {
378
+ "accept": "application/json, text/plain, */*",
379
+ "accept-language": "zh-CN,zh;q=0.9",
380
+ "content-type": "application/json",
381
+ "reai-ui": "1",
382
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
383
+ "sec-ch-ua-mobile": "?0",
384
+ "sec-ch-ua-platform": "\"Windows\"",
385
+ "sec-fetch-dest": "empty",
386
+ "sec-fetch-mode": "cors",
387
+ "sec-fetch-site": "same-site",
388
+ "x-abacus-org-host": "apps",
389
+ "session-token": session_token
390
+ }
391
+
392
+ response = session.post(
393
+ COMPUTE_POINT_TOGGLE_URL,
394
+ headers=headers,
395
+ json={"alwaysDisplay": True},
396
+ cookies=None
397
+ )
398
+
399
+ if response.status_code == 200:
400
+ result = response.json()
401
+ if result.get("success"):
402
+ print("成功初始化计算点数记录功能为开启状态")
403
+ else:
404
+ print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
405
+ else:
406
+ print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
407
+ except Exception as e:
408
+ print(f"初始化计算点数记录功能时出错: {e}")
409
+ except Exception as e:
410
+ print(f"配置用户 {i+1} 失败: {e}")
411
+ continue
412
+
413
+ USER_NUM = len(USER_DATA)
414
+ if USER_NUM == 0:
415
+ print("No user available, exiting...")
416
+ exit(1)
417
+
418
+ MODELS = all_models
419
+ print(f"启动完成,共配置 {USER_NUM} 个用户")
420
+
421
+
422
+ def update_cookie(session, cookies):
423
+ cookie_jar = {}
424
+ for key, value in session.cookies.items():
425
+ cookie_jar[key] = value
426
+ cookie_dict = {}
427
+ for item in cookies.split(";"):
428
+ key, value = item.strip().split("=", 1)
429
+ cookie_dict[key] = value
430
+ cookie_dict.update(cookie_jar)
431
+ cookies = "; ".join([f"{key}={value}" for key, value in cookie_dict.items()])
432
+ return cookies
433
+
434
+
435
+ user_data = init_session()
436
+
437
+
438
+ @app.route("/v1/models", methods=["GET"])
439
+ @require_auth
440
+ def get_models():
441
+ if len(MODELS) == 0:
442
+ return jsonify({"error": "No models available"}), 500
443
+ model_list = []
444
+ for model in MODELS:
445
+ model_list.append(
446
+ {
447
+ "id": model,
448
+ "object": "model",
449
+ "created": int(time.time()),
450
+ "owned_by": "Elbert",
451
+ "name": model,
452
+ }
453
+ )
454
+ return jsonify({"object": "list", "data": model_list})
455
+
456
+
457
+ @app.route("/v1/chat/completions", methods=["POST"])
458
+ @require_auth
459
+ def chat_completions():
460
+ openai_request = request.get_json()
461
+ stream = openai_request.get("stream", False)
462
+ messages = openai_request.get("messages")
463
+ if messages is None:
464
+ return jsonify({"error": "Messages is required", "status": 400}), 400
465
+ model = openai_request.get("model")
466
+ if model not in MODELS:
467
+ return (
468
+ jsonify(
469
+ {
470
+ "error": "Model not available, check if it is configured properly",
471
+ "status": 404,
472
+ }
473
+ ),
474
+ 404,
475
+ )
476
+ message = format_message(messages)
477
+ think = (
478
+ openai_request.get("think", False) if model == "Claude Sonnet 3.7" else False
479
+ )
480
+ return (
481
+ send_message(message, model, think)
482
+ if stream
483
+ else send_message_non_stream(message, model, think)
484
+ )
485
+
486
+
487
+ def get_user_data():
488
+ global CURRENT_USER
489
+ CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
490
+ print(f"使用配置 {CURRENT_USER+1}")
491
+
492
+ # Akiru uzantajn datumojn
493
+ session, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[CURRENT_USER]
494
+
495
+ # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
496
+ if is_token_expired(session_token):
497
+ print(f"Cookie {CURRENT_USER+1}的token已过期或即将过期,正在刷新...")
498
+ new_token = refresh_token(session, cookies)
499
+ if new_token:
500
+ # Ĝisdatigu la globale konservitan tokenon
501
+ USER_DATA[CURRENT_USER] = (session, cookies, new_token, conversation_id, model_map, user_index)
502
+ session_token = new_token
503
+ print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
504
+ else:
505
+ print(f"警告:无法刷新Cookie {CURRENT_USER+1}的token,继续使用当前token")
506
+
507
+ return (session, cookies, session_token, conversation_id, model_map, user_index)
508
+
509
+
510
+ def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
511
+ """创建新的会话"""
512
+ if not (external_application_id and deployment_id):
513
+ print("无法创建新会话: 缺少必要参数")
514
+ return None
515
+
516
+ headers = {
517
+ "accept": "application/json, text/plain, */*",
518
+ "accept-language": "zh-CN,zh;q=0.9",
519
+ "content-type": "application/json",
520
+ "cookie": cookies,
521
+ "user-agent": random.choice(USER_AGENTS),
522
+ "x-abacus-org-host": "apps"
523
+ }
524
+
525
+ if session_token:
526
+ headers["session-token"] = session_token
527
+
528
+ create_payload = {
529
+ "deploymentId": deployment_id,
530
+ "name": "New Chat",
531
+ "externalApplicationId": external_application_id
532
+ }
533
+
534
+ try:
535
+ response = session.post(
536
+ CREATE_CONVERSATION_URL,
537
+ headers=headers,
538
+ json=create_payload
539
+ )
540
+
541
+ if response.status_code == 200:
542
+ data = response.json()
543
+ if data.get("success", False):
544
+ new_conversation_id = data.get("result", {}).get("deploymentConversationId")
545
+ if new_conversation_id:
546
+ print(f"成功创建新的conversation: {new_conversation_id}")
547
+ return new_conversation_id
548
+
549
+ print(f"创建会话失败: {response.status_code} - {response.text[:100]}")
550
+ return None
551
+ except Exception as e:
552
+ print(f"创建会话时出错: {e}")
553
+ return None
554
+
555
+
556
+ def delete_conversation(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
557
+ """删除指定的对话"""
558
+ if not conversation_id:
559
+ print("无法删除对话: 缺少conversation_id")
560
+ return False
561
+
562
+ headers = {
563
+ "accept": "application/json, text/plain, */*",
564
+ "accept-language": "zh-CN,zh;q=0.9",
565
+ "content-type": "application/json",
566
+ "cookie": cookies,
567
+ "user-agent": random.choice(USER_AGENTS),
568
+ "x-abacus-org-host": "apps"
569
+ }
570
+
571
+ if session_token:
572
+ headers["session-token"] = session_token
573
+
574
+ delete_payload = {
575
+ "deploymentId": deployment_id,
576
+ "deploymentConversationId": conversation_id
577
+ }
578
+
579
+ try:
580
+ response = session.post(
581
+ DELETE_CONVERSATION_URL,
582
+ headers=headers,
583
+ json=delete_payload
584
+ )
585
+
586
+ if response.status_code == 200:
587
+ data = response.json()
588
+ if data.get("success", False):
589
+ print(f"成功删除对话: {conversation_id}")
590
+ return True
591
+
592
+ print(f"删除对话失败: {response.status_code} - {response.text[:100]}")
593
+ return False
594
+ except Exception as e:
595
+ print(f"删除对话时出错: {e}")
596
+ return False
597
+
598
+
599
+ def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
600
+ """获取对话ID,如果不存在则创建并初始化"""
601
+ print(f"\n----- 获取会话ID (用户 {user_index+1}) -----")
602
+
603
+ # 如果有现有的会话ID,直接使用
604
+ if conversation_id:
605
+ print(f"使用现有会话ID: {conversation_id}")
606
+ return conversation_id
607
+
608
+ # 如果没有会话ID,创建新的并初始化
609
+ print("没有会话ID,创建并初始化新会话...")
610
+
611
+ new_conversation_id = create_and_initialize_conversation(
612
+ session, cookies, session_token, model_map, model, user_index
613
+ )
614
+
615
+ if not new_conversation_id:
616
+ print("创建并初始化新会话失败")
617
+ return None
618
+
619
+ return new_conversation_id
620
+
621
+
622
+ def generate_trace_id():
623
+ """Generu novan trace_id kaj sentry_trace"""
624
+ trace_id = str(uuid.uuid4()).replace('-', '')
625
+ sentry_trace = f"{trace_id}-{str(uuid.uuid4())[:16]}"
626
+ return trace_id, sentry_trace
627
+
628
+
629
+ def send_message(message, model, think=False):
630
+ """发送消息,流式响应
631
+
632
+ 流程:
633
+ 1. 获取对话ID(若不存在则创建并初始化)
634
+ 2. 编辑索引为1的助手消息为处理好的message
635
+ 3. 发送占位符消息"Now, the internal test begins."并获取响应
636
+ 4. 如果失败,尝试重新创建对话并重试一次
637
+ """
638
+ print("\n===== 开始处理消息 =====")
639
+ print(f"模型: {model}")
640
+ print(f"思考模式: {think}")
641
+
642
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
643
+ print(f"使用用户配置: {user_index + 1}")
644
+
645
+ # 计算输入token
646
+ print("\n----- 计算输入token -----")
647
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
648
+ print(f"输入token数: {prompt_tokens}")
649
+ print(f"计算方法: {calculation_method}")
650
+
651
+ # 使用新的流程
652
+ def process_with_conversation(current_conversation_id, is_retry=False):
653
+ # 如果是重试,需要先初始化对话
654
+ if is_retry:
655
+ current_conversation_id = create_and_initialize_conversation(
656
+ session, cookies, session_token, model_map, model, user_index
657
+ )
658
+ if not current_conversation_id:
659
+ return jsonify({"error": "Failed to create a new conversation for retry"}), 500
660
+
661
+ # 编辑索引为1的助手消息
662
+ edit_success = edit_assistant_message(
663
+ session, cookies, session_token,
664
+ current_conversation_id, message, model_map, model
665
+ )
666
+
667
+ if not edit_success:
668
+ if is_retry:
669
+ return jsonify({"error": "Failed to edit assistant message even after retry"}), 500
670
+ else:
671
+ print("编辑助手消息失败,尝试重新创建对话...")
672
+ return process_with_conversation(None, True)
673
+
674
+ # 发送占位符消息并获取响应
675
+ trace_id, sentry_trace = generate_trace_id()
676
+ completion_buffer = io.StringIO() # 收集所有输出用于计算token
677
+
678
+ headers = {
679
+ "accept": "text/event-stream",
680
+ "accept-language": "zh-CN,zh;q=0.9",
681
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
682
+ "content-type": "text/plain;charset=UTF-8",
683
+ "cookie": cookies,
684
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
685
+ "sec-ch-ua-mobile": "?0",
686
+ "sec-ch-ua-platform": "\"Windows\"",
687
+ "sec-fetch-dest": "empty",
688
+ "sec-fetch-mode": "cors",
689
+ "sec-fetch-site": "same-origin",
690
+ "sentry-trace": sentry_trace,
691
+ "user-agent": random.choice(USER_AGENTS),
692
+ "x-abacus-org-host": "apps"
693
+ }
694
+
695
+ if session_token:
696
+ headers["session-token"] = session_token
697
+
698
+ # 构建请求
699
+ payload = {
700
+ "requestId": str(uuid.uuid4()),
701
+ "deploymentConversationId": current_conversation_id,
702
+ "message": "Now, the internal test begins.", # 占位符消息
703
+ "isDesktop": False,
704
+ "chatConfig": {
705
+ "timezone": "Asia/Shanghai",
706
+ "language": "zh-CN"
707
+ },
708
+ "llmName": model_map[model][1],
709
+ "externalApplicationId": model_map[model][0],
710
+ "regenerate": True,
711
+ "editPrompt": True
712
+ }
713
+
714
+ if think:
715
+ payload["useThinking"] = think
716
+
717
+ try:
718
+ print(f"发送占位符消息: \"Now, the internal test begins.\"")
719
+ response = session.post(
720
+ CHAT_URL,
721
+ headers=headers,
722
+ data=json.dumps(payload),
723
+ stream=True,
724
+ cookies=None
725
+ )
726
+
727
+ response.raise_for_status()
728
+
729
+ def extract_segment(line_data):
730
+ try:
731
+ data = json.loads(line_data)
732
+ if "segment" in data:
733
+ if isinstance(data["segment"], str):
734
+ return data["segment"]
735
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
736
+ return data["segment"]["segment"]
737
+ return ""
738
+ except:
739
+ return ""
740
+
741
+ def generate():
742
+ id = ""
743
+ think_state = 2
744
+
745
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
746
+
747
+ for line in response.iter_lines():
748
+ if line:
749
+ decoded_line = line.decode("utf-8")
750
+ try:
751
+ if think:
752
+ data = json.loads(decoded_line)
753
+ if data.get("type") != "text":
754
+ continue
755
+ elif think_state == 2:
756
+ id = data.get("messageId")
757
+ segment = "<think>\n" + data.get("segment", "")
758
+ completion_buffer.write(segment) # 收集输出
759
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
760
+ think_state = 1
761
+ elif think_state == 1:
762
+ if data.get("messageId") != id:
763
+ segment = data.get("segment", "")
764
+ completion_buffer.write(segment) # 收集输出
765
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
766
+ else:
767
+ segment = "\n</think>\n" + data.get("segment", "")
768
+ completion_buffer.write(segment) # 收集输出
769
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
770
+ think_state = 0
771
+ else:
772
+ segment = data.get("segment", "")
773
+ completion_buffer.write(segment) # 收集输出
774
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
775
+ else:
776
+ segment = extract_segment(decoded_line)
777
+ if segment:
778
+ completion_buffer.write(segment) # 收集输出
779
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
780
+ except Exception as e:
781
+ print(f"处理响应出错: {e}")
782
+
783
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
784
+ yield "data: [DONE]\n\n"
785
+
786
+ # 在流式传输完成后计算token并更新统计
787
+ completion_result, _ = num_tokens_from_string(completion_buffer.getvalue(), model)
788
+
789
+ # 保存对话历史并获取计算点数
790
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, current_conversation_id)
791
+
792
+ # 更新统计信息
793
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
794
+
795
+ return Response(generate(), mimetype="text/event-stream")
796
+
797
+ except requests.exceptions.RequestException as e:
798
+ error_details = str(e)
799
+ if hasattr(e, 'response') and e.response is not None:
800
+ if hasattr(e.response, 'text'):
801
+ error_details += f" - Response: {e.response.text[:200]}"
802
+ print(f"发送消息失败: {error_details}")
803
+
804
+ # 如果不是重试,尝试重新创建对话重试一次
805
+ if not is_retry:
806
+ print("尝试重新创建对话...")
807
+ return process_with_conversation(None, True)
808
+
809
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
810
+
811
+ # 获取会话ID
812
+ conversation_id = get_or_create_conversation(
813
+ session, cookies, session_token, conversation_id, model_map, model, user_index
814
+ )
815
+
816
+ # 如果没有有效的会话ID,返回错误
817
+ if not conversation_id:
818
+ return jsonify({"error": "Failed to get a valid conversation ID"}), 500
819
+
820
+ print(f"会话ID: {conversation_id}")
821
+
822
+ # 使用上面定义的流程处理消息
823
+ return process_with_conversation(conversation_id)
824
+
825
+
826
+ def send_message_non_stream(message, model, think=False):
827
+ """发送消息,非流式响应
828
+
829
+ 流程:
830
+ 1. 获取对话ID(若不存在则创建并初始化)
831
+ 2. 编辑索引为1的助手消息为处理好的message
832
+ 3. 发送占位符消息"Now, the internal test begins."并获取响应
833
+ 4. 如果失败,尝试重新创建对话并重试一次
834
+ """
835
+ print("\n===== 开始处理消息(非流式) =====")
836
+ print(f"模型: {model}")
837
+ print(f"思考模式: {think}")
838
+
839
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
840
+ print(f"使用用户配置: {user_index + 1}")
841
+
842
+ # 计算输入token
843
+ print("\n----- 计算输入token -----")
844
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
845
+ print(f"输入token数: {prompt_tokens}")
846
+ print(f"计算方法: {calculation_method}")
847
+
848
+ # 使用新的流程
849
+ def process_with_conversation(current_conversation_id, is_retry=False):
850
+ # 如果是重试,需要先初始化对话
851
+ if is_retry:
852
+ current_conversation_id = create_and_initialize_conversation(
853
+ session, cookies, session_token, model_map, model, user_index
854
+ )
855
+ if not current_conversation_id:
856
+ return jsonify({"error": "Failed to create a new conversation for retry"}), 500
857
+
858
+ # 编辑索引为1的助手消息
859
+ edit_success = edit_assistant_message(
860
+ session, cookies, session_token,
861
+ current_conversation_id, message, model_map, model
862
+ )
863
+
864
+ if not edit_success:
865
+ if is_retry:
866
+ return jsonify({"error": "Failed to edit assistant message even after retry"}), 500
867
+ else:
868
+ print("编辑助手消息失败,尝试重新创建对话...")
869
+ return process_with_conversation(None, True)
870
+
871
+ # 发送占位符消息并获取响应
872
+ trace_id, sentry_trace = generate_trace_id()
873
+
874
+ headers = {
875
+ "accept": "text/event-stream",
876
+ "accept-language": "zh-CN,zh;q=0.9",
877
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
878
+ "content-type": "text/plain;charset=UTF-8",
879
+ "cookie": cookies,
880
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
881
+ "sec-ch-ua-mobile": "?0",
882
+ "sec-ch-ua-platform": "\"Windows\"",
883
+ "sec-fetch-dest": "empty",
884
+ "sec-fetch-mode": "cors",
885
+ "sec-fetch-site": "same-origin",
886
+ "sentry-trace": sentry_trace,
887
+ "user-agent": random.choice(USER_AGENTS),
888
+ "x-abacus-org-host": "apps"
889
+ }
890
+
891
+ if session_token:
892
+ headers["session-token"] = session_token
893
+
894
+ # 构建请求
895
+ payload = {
896
+ "requestId": str(uuid.uuid4()),
897
+ "deploymentConversationId": current_conversation_id,
898
+ "message": "Now, the internal test begins.", # 占位符消息
899
+ "isDesktop": False,
900
+ "chatConfig": {
901
+ "timezone": "Asia/Shanghai",
902
+ "language": "zh-CN"
903
+ },
904
+ "llmName": model_map[model][1],
905
+ "externalApplicationId": model_map[model][0],
906
+ "regenerate": True,
907
+ "editPrompt": True
908
+ }
909
+
910
+ if think:
911
+ payload["useThinking"] = think
912
+
913
+ try:
914
+ print(f"发送占位符消息: \"Now, the internal test begins.\"")
915
+ response = session.post(
916
+ CHAT_URL,
917
+ headers=headers,
918
+ data=json.dumps(payload),
919
+ stream=True,
920
+ cookies=None
921
+ )
922
+
923
+ response.raise_for_status()
924
+
925
+ def extract_segment(line_data):
926
+ try:
927
+ data = json.loads(line_data)
928
+ if "segment" in data:
929
+ if isinstance(data["segment"], str):
930
+ return data["segment"]
931
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
932
+ return data["segment"]["segment"]
933
+ return ""
934
+ except:
935
+ return ""
936
+
937
+ if think:
938
+ id = ""
939
+ think_state = 2
940
+ think_buffer = io.StringIO()
941
+ content_buffer = io.StringIO()
942
+
943
+ for line in response.iter_lines():
944
+ if line:
945
+ decoded_line = line.decode("utf-8")
946
+ try:
947
+ data = json.loads(decoded_line)
948
+ if data.get("type") != "text":
949
+ continue
950
+ elif think_state == 2:
951
+ id = data.get("messageId")
952
+ segment = data.get("segment", "")
953
+ think_buffer.write(segment)
954
+ think_state = 1
955
+ elif think_state == 1:
956
+ if data.get("messageId") != id:
957
+ segment = data.get("segment", "")
958
+ content_buffer.write(segment)
959
+ else:
960
+ segment = data.get("segment", "")
961
+ think_buffer.write(segment)
962
+ think_state = 0
963
+ else:
964
+ segment = data.get("segment", "")
965
+ content_buffer.write(segment)
966
+ except Exception as e:
967
+ print(f"处理响应出错: {e}")
968
+
969
+ think_content = think_buffer.getvalue()
970
+ response_content = content_buffer.getvalue()
971
+
972
+ # 计算输出token并更新统计信息
973
+ completion_result, _ = num_tokens_from_string(think_content + response_content, model)
974
+
975
+ # 保存对话历史并获取计算点数
976
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, current_conversation_id)
977
+
978
+ # 更新统计信息
979
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
980
+
981
+ return jsonify({
982
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
983
+ "object": "chat.completion",
984
+ "created": int(time.time()),
985
+ "model": model,
986
+ "choices": [{
987
+ "index": 0,
988
+ "message": {
989
+ "role": "assistant",
990
+ "content": f"<think>\n{think_content}\n</think>\n{response_content}"
991
+ },
992
+ "finish_reason": "stop"
993
+ }],
994
+ "usage": {
995
+ "prompt_tokens": prompt_tokens,
996
+ "completion_tokens": completion_result,
997
+ "total_tokens": prompt_tokens + completion_result
998
+ }
999
+ })
1000
+ else:
1001
+ buffer = io.StringIO()
1002
+ for line in response.iter_lines():
1003
+ if line:
1004
+ decoded_line = line.decode("utf-8")
1005
+ segment = extract_segment(decoded_line)
1006
+ if segment:
1007
+ buffer.write(segment)
1008
+
1009
+ response_content = buffer.getvalue()
1010
+
1011
+ # 计算输出token并更新统计信息
1012
+ completion_result, _ = num_tokens_from_string(response_content, model)
1013
+
1014
+ # 保存对话历史并获取计算点数
1015
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, current_conversation_id)
1016
+
1017
+ # 更新统计信息
1018
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
1019
+
1020
+ return jsonify({
1021
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
1022
+ "object": "chat.completion",
1023
+ "created": int(time.time()),
1024
+ "model": model,
1025
+ "choices": [{
1026
+ "index": 0,
1027
+ "message": {
1028
+ "role": "assistant",
1029
+ "content": response_content
1030
+ },
1031
+ "finish_reason": "stop"
1032
+ }],
1033
+ "usage": {
1034
+ "prompt_tokens": prompt_tokens,
1035
+ "completion_tokens": completion_result,
1036
+ "total_tokens": prompt_tokens + completion_result
1037
+ }
1038
+ })
1039
+
1040
+ except requests.exceptions.RequestException as e:
1041
+ error_details = str(e)
1042
+ if hasattr(e, 'response') and e.response is not None:
1043
+ if hasattr(e.response, 'text'):
1044
+ error_details += f" - Response: {e.response.text[:200]}"
1045
+ print(f"发送消息失败: {error_details}")
1046
+
1047
+ # 如果不是重试,尝试重新创建对话重试一次
1048
+ if not is_retry:
1049
+ print("尝试重新创建对话...")
1050
+ return process_with_conversation(None, True)
1051
+
1052
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
1053
+
1054
+ # 获取会话ID
1055
+ conversation_id = get_or_create_conversation(
1056
+ session, cookies, session_token, conversation_id, model_map, model, user_index
1057
+ )
1058
+
1059
+ # 如果没有有效的会话ID,返回错误
1060
+ if not conversation_id:
1061
+ return jsonify({"error": "Failed to get a valid conversation ID"}), 500
1062
+
1063
+ print(f"会话ID: {conversation_id}")
1064
+
1065
+ # 使用上面定义的流程处理消息
1066
+ return process_with_conversation(conversation_id)
1067
+
1068
+
1069
+ def format_message(messages):
1070
+ buffer = io.StringIO()
1071
+ role_map, prefix, messages = extract_role(messages)
1072
+ for message in messages:
1073
+ role = message.get("role")
1074
+ role = "\b" + role_map[role] if prefix else role_map[role]
1075
+ content = message.get("content").replace("\\n", "\n")
1076
+ pattern = re.compile(r"<\|removeRole\|>\n")
1077
+ if pattern.match(content):
1078
+ content = pattern.sub("", content)
1079
+ buffer.write(f"{content}\n")
1080
+ else:
1081
+ buffer.write(f"{role}: {content}\n\n")
1082
+ formatted_message = buffer.getvalue()
1083
+ return formatted_message
1084
+
1085
+
1086
+ def extract_role(messages):
1087
+ role_map = {"user": "Human", "assistant": "Assistant", "system": "System"}
1088
+ prefix = True # 默认添加前缀
1089
+ first_message = messages[0]["content"]
1090
+ pattern = re.compile(
1091
+ r"""
1092
+ <roleInfo>\s*
1093
+ (?:user:\s*(?P<user>[^\n]*)\s*)? # Make user optional
1094
+ (?:assistant:\s*(?P<assistant>[^\n]*)\s*)? # Make assistant optional
1095
+ (?:system:\s*(?P<system>[^\n]*)\s*)? # Make system optional
1096
+ (?:prefix:\s*(?P<prefix>[^\n]*)\s*)? # Make prefix optional
1097
+ </roleInfo>\n
1098
+ """,
1099
+ re.VERBOSE,
1100
+ )
1101
+ match = pattern.search(first_message)
1102
+ if match:
1103
+ # 更新 role_map 如果提供了值
1104
+ user_role = match.group("user")
1105
+ assistant_role = match.group("assistant")
1106
+ system_role = match.group("system")
1107
+ if user_role: role_map["user"] = user_role
1108
+ if assistant_role: role_map["assistant"] = assistant_role
1109
+ if system_role: role_map["system"] = system_role
1110
+
1111
+ # 检查 prefix 值:仅当显式设置为非 "1" 时才将 prefix 设为 False
1112
+ prefix_value = match.group("prefix")
1113
+ if prefix_value is not None and prefix_value != "1":
1114
+ prefix = False
1115
+ # 如果 prefix_value 是 None (标签不存在) 或 "1", prefix 保持 True
1116
+
1117
+ messages[0]["content"] = pattern.sub("", first_message)
1118
+ print(f"Extracted role map:")
1119
+ print(
1120
+ f"User: {role_map['user']}, Assistant: {role_map['assistant']}, System: {role_map['system']}"
1121
+ )
1122
+ print(f"Using prefix: {prefix}") # 打印语句保持不变,反映最终结果
1123
+ # 如果没有匹配到 <roleInfo>,prefix 保持默认值 True
1124
+ return (role_map, prefix, messages)
1125
+
1126
+
1127
+ @app.route("/health", methods=["GET"])
1128
+ def health_check():
1129
+ global health_check_counter
1130
+ health_check_counter += 1
1131
+ return jsonify({
1132
+ "status": "healthy",
1133
+ "timestamp": datetime.now().isoformat(),
1134
+ "checks": health_check_counter
1135
+ })
1136
+
1137
+
1138
+ def keep_alive():
1139
+ """每20分钟进行一次自我健康检查"""
1140
+ while True:
1141
+ try:
1142
+ requests.get("http://127.0.0.1:7860/health")
1143
+ time.sleep(1200) # 20分钟
1144
+ except:
1145
+ pass # 忽略错误,保持运行
1146
+
1147
+
1148
+ @app.route("/", methods=["GET"])
1149
+ def index():
1150
+ # 如果需要密码且用户未登录,重定向到登录页面
1151
+ if PASSWORD and not flask_session.get('logged_in'):
1152
+ return redirect(url_for('login'))
1153
+
1154
+ # 否则重定向到仪表盘
1155
+ return redirect(url_for('dashboard'))
1156
+
1157
+
1158
+ def num_tokens_from_string(string, model=""):
1159
+ try:
1160
+ print("\n===================== 开始计算token =====================")
1161
+ print(f"模型: {model}")
1162
+ print(f"输���内容长度: {len(string)} 字符")
1163
+
1164
+ request_data = {
1165
+ "model": model,
1166
+ "messages": [{"role": "user", "content": string}]
1167
+ }
1168
+ print(f"发送请求到tokenizer服务: {TOKENIZER_SERVICE_URL}")
1169
+ print(f"请求数据: {json.dumps(request_data, ensure_ascii=False)}")
1170
+
1171
+ response = requests.post(
1172
+ TOKENIZER_SERVICE_URL,
1173
+ json=request_data,
1174
+ timeout=10
1175
+ )
1176
+
1177
+ print(f"\nTokenizer响应状态码: {response.status_code}")
1178
+ print(f"Tokenizer响应内容: {response.text}")
1179
+
1180
+ if response.status_code == 200:
1181
+ result = response.json()
1182
+ input_tokens = result.get("input_tokens", 0)
1183
+ print(f"\n成功获取token数: {input_tokens}")
1184
+ print(f"使用计算方法: 精确")
1185
+ print("===================== 计算完成 =====================\n")
1186
+ return input_tokens, "精确"
1187
+ else:
1188
+ estimated_tokens = len(string) // 4
1189
+ print(f"\nTokenizer服务错误: {response.status_code}")
1190
+ print(f"错误响应: {response.text}")
1191
+ print(f"使用估算token数: {estimated_tokens}")
1192
+ print(f"使用计算方法: 估算")
1193
+ print("===================== 计算完成 =====================\n")
1194
+ return estimated_tokens, "估算"
1195
+ except Exception as e:
1196
+ estimated_tokens = len(string) // 4
1197
+ print(f"\n计算token时发生错误: {str(e)}")
1198
+ print(f"使用估算token数: {estimated_tokens}")
1199
+ print(f"使用计算方法: 估算")
1200
+ print("===================== 计算完成 =====================\n")
1201
+ return estimated_tokens, "估算"
1202
+
1203
+
1204
+ # 更新模型使用统计
1205
+ def update_model_stats(model, prompt_tokens, completion_tokens, calculation_method="estimate", compute_points=None):
1206
+ global model_usage_stats, total_tokens, model_usage_records
1207
+
1208
+ # 添加调用记录
1209
+ # 获取UTC时间
1210
+ utc_now = datetime.utcnow()
1211
+ # 转换为北京时间 (UTC+8)
1212
+ beijing_time = utc_now + timedelta(hours=8)
1213
+ call_time = beijing_time.strftime('%Y-%m-%d %H:%M:%S') # 北京时间
1214
+
1215
+ record = {
1216
+ "model": model,
1217
+ "call_time": call_time,
1218
+ "prompt_tokens": prompt_tokens,
1219
+ "completion_tokens": completion_tokens,
1220
+ "calculation_method": calculation_method, # 直接使用传入的值
1221
+ "compute_points": compute_points
1222
+ }
1223
+ model_usage_records.append(record)
1224
+
1225
+ # 限制记录数量,保留最新的500条
1226
+ if len(model_usage_records) > 500:
1227
+ model_usage_records.pop(0)
1228
+
1229
+ # 保存调用记录到本地文件
1230
+ save_model_usage_records()
1231
+
1232
+ # 更新聚合统计
1233
+ if model not in model_usage_stats:
1234
+ model_usage_stats[model] = {
1235
+ "count": 0,
1236
+ "prompt_tokens": 0,
1237
+ "completion_tokens": 0,
1238
+ "total_tokens": 0
1239
+ }
1240
+
1241
+ model_usage_stats[model]["count"] += 1
1242
+ model_usage_stats[model]["prompt_tokens"] += prompt_tokens
1243
+ model_usage_stats[model]["completion_tokens"] += completion_tokens
1244
+ model_usage_stats[model]["total_tokens"] += (prompt_tokens + completion_tokens)
1245
+
1246
+ total_tokens["prompt"] += prompt_tokens
1247
+ total_tokens["completion"] += completion_tokens
1248
+ total_tokens["total"] += (prompt_tokens + completion_tokens)
1249
+
1250
+
1251
+ # 获取计算点信息
1252
+ def get_compute_points():
1253
+ global compute_points, USER_DATA, users_compute_points
1254
+
1255
+ if USER_NUM == 0:
1256
+ return
1257
+
1258
+ # 清空用户计算点列表
1259
+ users_compute_points = []
1260
+
1261
+ # 累计总计算点
1262
+ total_left = 0
1263
+ total_points = 0
1264
+
1265
+ # 获取每个用户的计算点信息
1266
+ for i, user_data in enumerate(USER_DATA):
1267
+ try:
1268
+ session, cookies, session_token, _, _, _ = user_data
1269
+
1270
+ # 检查token是否有效
1271
+ if is_token_expired(session_token):
1272
+ session_token = refresh_token(session, cookies)
1273
+ if not session_token:
1274
+ print(f"用户{i+1}刷新token失败,无法获取计算点信息")
1275
+ continue
1276
+ USER_DATA[i] = (session, cookies, session_token, user_data[3], user_data[4], i)
1277
+
1278
+ headers = {
1279
+ "accept": "application/json, text/plain, */*",
1280
+ "accept-language": "zh-CN,zh;q=0.9",
1281
+ "baggage": f"sentry-environment=production,sentry-release=93da8385541a6ce339b1f41b0c94428c70657e22,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
1282
+ "reai-ui": "1",
1283
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1284
+ "sec-ch-ua-mobile": "?0",
1285
+ "sec-ch-ua-platform": "\"Windows\"",
1286
+ "sec-fetch-dest": "empty",
1287
+ "sec-fetch-mode": "cors",
1288
+ "sec-fetch-site": "same-origin",
1289
+ "sentry-trace": SENTRY_TRACE,
1290
+ "session-token": session_token,
1291
+ "x-abacus-org-host": "apps",
1292
+ "cookie": cookies
1293
+ }
1294
+
1295
+ response = session.get(
1296
+ COMPUTE_POINTS_URL,
1297
+ headers=headers
1298
+ )
1299
+
1300
+ if response.status_code == 200:
1301
+ result = response.json()
1302
+ if result.get("success") and "result" in result:
1303
+ data = result["result"]
1304
+ left = data.get("computePointsLeft", 0)
1305
+ total = data.get("totalComputePoints", 0)
1306
+ used = total - left
1307
+ percentage = round((used / total) * 100, 2) if total > 0 else 0
1308
+
1309
+ # 获取北京时间
1310
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
1311
+
1312
+ # 添加到用户列表
1313
+ user_points = {
1314
+ "user_id": i + 1, # 用户ID从1开始
1315
+ "left": left,
1316
+ "total": total,
1317
+ "used": used,
1318
+ "percentage": percentage,
1319
+ "last_update": beijing_now
1320
+ }
1321
+ users_compute_points.append(user_points)
1322
+
1323
+ # 累计总数
1324
+ total_left += left
1325
+ total_points += total
1326
+
1327
+ print(f"用户{i+1}计算点信息更新成功: 剩余 {left}, 总计 {total}")
1328
+
1329
+ # 对于第一个用户,获取计算点使用日志
1330
+ if i == 0:
1331
+ get_compute_points_log(session, cookies, session_token)
1332
+ else:
1333
+ print(f"获取用户{i+1}计算点信息失败: {result.get('error', '未知错误')}")
1334
+ else:
1335
+ print(f"获取用户{i+1}计算点信息失败,状态码: {response.status_code}")
1336
+ except Exception as e:
1337
+ print(f"获取用户{i+1}计算点信息异常: {e}")
1338
+
1339
+ # 更新全局计算点信息(所有用户总和)
1340
+ if users_compute_points:
1341
+ compute_points["left"] = total_left
1342
+ compute_points["total"] = total_points
1343
+ compute_points["used"] = total_points - total_left
1344
+ compute_points["percentage"] = round((compute_points["used"] / compute_points["total"]) * 100, 2) if compute_points["total"] > 0 else 0
1345
+ compute_points["last_update"] = datetime.utcnow() + timedelta(hours=8) # 北京时间
1346
+ print(f"所有用户计算点总计: 剩余 {total_left}, 总计 {total_points}")
1347
+
1348
+ # 获取计算点使用日志
1349
+ def get_compute_points_log(session, cookies, session_token):
1350
+ global compute_points_log
1351
+
1352
+ try:
1353
+ headers = {
1354
+ "accept": "application/json, text/plain, */*",
1355
+ "accept-language": "zh-CN,zh;q=0.9",
1356
+ "content-type": "application/json",
1357
+ "reai-ui": "1",
1358
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1359
+ "sec-ch-ua-mobile": "?0",
1360
+ "sec-ch-ua-platform": "\"Windows\"",
1361
+ "sec-fetch-dest": "empty",
1362
+ "sec-fetch-mode": "cors",
1363
+ "sec-fetch-site": "same-site",
1364
+ "session-token": session_token,
1365
+ "x-abacus-org-host": "apps",
1366
+ "cookie": cookies
1367
+ }
1368
+
1369
+ response = session.post(
1370
+ COMPUTE_POINTS_LOG_URL,
1371
+ headers=headers,
1372
+ json={"byLlm": True}
1373
+ )
1374
+
1375
+ if response.status_code == 200:
1376
+ result = response.json()
1377
+ if result.get("success") and "result" in result:
1378
+ data = result["result"]
1379
+ compute_points_log["columns"] = data.get("columns", {})
1380
+ compute_points_log["log"] = data.get("log", [])
1381
+ print(f"计算点使用日志更新成功,获取到 {len(compute_points_log['log'])} 条记录")
1382
+ else:
1383
+ print(f"获取计算点使用日志失败: {result.get('error', '未知错误')}")
1384
+ else:
1385
+ print(f"获取计算点使用日志失败,状态码: {response.status_code}")
1386
+ except Exception as e:
1387
+ print(f"获取计算点使用日志异常: {e}")
1388
+
1389
+
1390
+ # 添加登录相关路由
1391
+ @app.route("/login", methods=["GET", "POST"])
1392
+ def login():
1393
+ error = None
1394
+ if request.method == "POST":
1395
+ password = request.form.get("password")
1396
+ if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
1397
+ flask_session['logged_in'] = True
1398
+ flask_session.permanent = True
1399
+ return redirect(url_for('dashboard'))
1400
+ else:
1401
+ # 密码错误时提示使用环境变量密码
1402
+ error = "密码不正确。请使用设置的环境变量 password 或 password.txt 中的值作为密码和API认证密钥。"
1403
+
1404
+ # 传递空间URL给模板
1405
+ return render_template('login.html', error=error, space_url=SPACE_URL)
1406
+
1407
+
1408
+ @app.route("/logout")
1409
+ def logout():
1410
+ flask_session.clear()
1411
+ return redirect(url_for('login'))
1412
+
1413
+
1414
+ @app.route("/dashboard")
1415
+ @require_auth
1416
+ def dashboard():
1417
+ # 在每次访问仪表盘时更新计算点信息
1418
+ get_compute_points()
1419
+
1420
+ # 计算运行时间(使用北京时间)
1421
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
1422
+ uptime = beijing_now - START_TIME
1423
+ days = uptime.days
1424
+ hours, remainder = divmod(uptime.seconds, 3600)
1425
+ minutes, seconds = divmod(remainder, 60)
1426
+
1427
+ if days > 0:
1428
+ uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
1429
+ elif hours > 0:
1430
+ uptime_str = f"{hours}小时 {minutes}分钟"
1431
+ else:
1432
+ uptime_str = f"{minutes}分钟 {seconds}秒"
1433
+
1434
+ # 当前北京年份
1435
+ beijing_year = beijing_now.year
1436
+
1437
+ return render_template(
1438
+ 'dashboard.html',
1439
+ uptime=uptime_str,
1440
+ health_checks=health_check_counter,
1441
+ user_count=USER_NUM,
1442
+ models=sorted(list(MODELS)),
1443
+ year=beijing_year,
1444
+ model_stats=model_usage_stats,
1445
+ total_tokens=total_tokens,
1446
+ compute_points=compute_points,
1447
+ compute_points_log=compute_points_log,
1448
+ space_url=SPACE_URL, # 传递空间URL
1449
+ users_compute_points=users_compute_points, # 传递用户计算点信息
1450
+ model_usage_records=model_usage_records, # 传递模型使用记录
1451
+ )
1452
+
1453
+
1454
+ # 添加更新计算点数记录设置的路由
1455
+ @app.route("/update_compute_point_toggle", methods=["POST"])
1456
+ @require_auth
1457
+ def update_compute_point_toggle():
1458
+ try:
1459
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
1460
+ data = request.get_json()
1461
+ if data and "always_display" in data:
1462
+ headers = {
1463
+ "accept": "application/json, text/plain, */*",
1464
+ "accept-language": "zh-CN,zh;q=0.9",
1465
+ "content-type": "application/json",
1466
+ "reai-ui": "1",
1467
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1468
+ "sec-ch-ua-mobile": "?0",
1469
+ "sec-ch-ua-platform": "\"Windows\"",
1470
+ "sec-fetch-dest": "empty",
1471
+ "sec-fetch-mode": "cors",
1472
+ "sec-fetch-site": "same-site",
1473
+ "x-abacus-org-host": "apps"
1474
+ }
1475
+
1476
+ if session_token:
1477
+ headers["session-token"] = session_token
1478
+
1479
+ response = session.post(
1480
+ COMPUTE_POINT_TOGGLE_URL,
1481
+ headers=headers,
1482
+ json={"alwaysDisplay": data["always_display"]},
1483
+ cookies=None
1484
+ )
1485
+
1486
+ if response.status_code == 200:
1487
+ result = response.json()
1488
+ if result.get("success"):
1489
+ print(f"更新计算点数记录设置为: {data['always_display']}")
1490
+ return jsonify({"success": True})
1491
+
1492
+ return jsonify({"success": False, "error": "API调用失败"})
1493
+ else:
1494
+ return jsonify({"success": False, "error": "缺少always_display参数"})
1495
+ except Exception as e:
1496
+ print(f"更新计算点数记录设置失败: {e}")
1497
+ return jsonify({"success": False, "error": str(e)})
1498
+
1499
+
1500
+ # 获取Hugging Face Space URL
1501
+ def get_space_url():
1502
+ # 尝试从环境变量获取
1503
+ space_url = os.environ.get("SPACE_URL")
1504
+ if space_url:
1505
+ return space_url
1506
+
1507
+ # 如果SPACE_URL不存在,尝试从SPACE_ID构建
1508
+ space_id = os.environ.get("SPACE_ID")
1509
+ if space_id:
1510
+ username, space_name = space_id.split("/")
1511
+ # 将空间名称中的下划线替换为连字符
1512
+ # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
1513
+ # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
1514
+ space_name = space_name.replace("_", "-")
1515
+ return f"https://{username}-{space_name}.hf.space"
1516
+
1517
+ # 如果以上都不存在,尝试从单独的用户名和空间名构建
1518
+ username = os.environ.get("SPACE_USERNAME")
1519
+ space_name = os.environ.get("SPACE_NAME")
1520
+ if username and space_name:
1521
+ # 将空间名称中的下划线替换为连字符
1522
+ # 同上,Hugging Face会自动进行此转换
1523
+ space_name = space_name.replace("_", "-")
1524
+ return f"https://{username}-{space_name}.hf.space"
1525
+
1526
+ # 默认返回None
1527
+ return None
1528
+
1529
+ # 获取空间URL
1530
+ SPACE_URL = get_space_url()
1531
+ if SPACE_URL:
1532
+ print(f"Space URL: {SPACE_URL}")
1533
+ print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
1534
+
1535
+
1536
+ def save_conversation_history(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
1537
+ """保存对话历史,返回使用的计算点数"""
1538
+ if not conversation_id:
1539
+ return False, None
1540
+
1541
+ headers = {
1542
+ "accept": "application/json, text/plain, */*",
1543
+ "accept-language": "zh-CN,zh;q=0.9",
1544
+ "baggage": f"sentry-environment=production,sentry-release=946244517de08b08598b94f18098411f5a5352d5,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
1545
+ "reai-ui": "1",
1546
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1547
+ "sec-ch-ua-mobile": "?0",
1548
+ "sec-ch-ua-platform": "\"Windows\"",
1549
+ "sec-fetch-dest": "empty",
1550
+ "sec-fetch-mode": "cors",
1551
+ "sec-fetch-site": "same-origin",
1552
+ "sentry-trace": f"{TRACE_ID}-800cb7f4613dec52",
1553
+ "x-abacus-org-host": "apps"
1554
+ }
1555
+
1556
+ if session_token:
1557
+ headers["session-token"] = session_token
1558
+
1559
+ params = {
1560
+ "deploymentId": deployment_id,
1561
+ "deploymentConversationId": conversation_id,
1562
+ "skipDocumentBoundingBoxes": "true",
1563
+ "filterIntermediateConversationEvents": "false",
1564
+ "getUnusedDocumentUploads": "false"
1565
+ }
1566
+
1567
+ try:
1568
+ response = session.get(
1569
+ GET_CONVERSATION_URL,
1570
+ headers=headers,
1571
+ params=params,
1572
+ cookies=None
1573
+ )
1574
+
1575
+ if response.status_code == 200:
1576
+ data = response.json()
1577
+ if data.get("success"):
1578
+ # 从最后一条BOT消息中获取计算点数
1579
+ history = data.get("result", {}).get("history", [])
1580
+ compute_points = None
1581
+ for msg in reversed(history):
1582
+ if msg.get("role") == "BOT":
1583
+ compute_points = msg.get("computePointsUsed")
1584
+ break
1585
+ print(f"成功保存对话历史: {conversation_id}, 使用计算点: {compute_points}")
1586
+ return True, compute_points
1587
+ else:
1588
+ print(f"保存对话历史失败: {data.get('error', '未知错误')}")
1589
+ else:
1590
+ print(f"保存对话历史失败,状态码: {response.status_code}")
1591
+ return False, None
1592
+ except Exception as e:
1593
+ print(f"保存对话历史时出错: {e}")
1594
+ return False, None
1595
+
1596
+
1597
+ def create_and_initialize_conversation(session, cookies, session_token, model_map, model, user_index):
1598
+ """创建新的对话ID并初始化
1599
+
1600
+ 创建新的对话ID
1601
+ 使用新对话ID发送"Repeat \"123\""作为第一条消息
1602
+ 休息五秒等待系统自动生成第二条消息
1603
+
1604
+ 注意:初始化必须使用Abacus.AI Smaug模型
1605
+ """
1606
+ print("\n===== 创建并初始化新对话 =====")
1607
+
1608
+ deployment_id = "14b2a314cc"
1609
+
1610
+ # 查找Abacus.AI Smaug模型
1611
+ smaug_model_name = "Abacus.AI Smaug"
1612
+
1613
+ # 如果找不到Smaug,尝试找带Smaug的模型
1614
+ if smaug_model_name not in model_map:
1615
+ for m_name in model_map.keys():
1616
+ if "Smaug" in m_name:
1617
+ smaug_model_name = m_name
1618
+ print(f"未找到精确的Abacus.AI Smaug模型名称,使用: {smaug_model_name}")
1619
+ break
1620
+
1621
+ # 仍然找不到,使用传入的模型作为后备
1622
+ if smaug_model_name not in model_map:
1623
+ print(f"警告: 未找到任何Smaug模型,尝试使用传入的模型: {model}")
1624
+ smaug_model_name = model
1625
+
1626
+ # 确保模型信息存在
1627
+ if smaug_model_name not in model_map or len(model_map[smaug_model_name]) < 2:
1628
+ print(f"错误: 无法获取模型 {smaug_model_name} 的信息")
1629
+ return None
1630
+
1631
+ smaug_app_id = model_map[smaug_model_name][0]
1632
+ smaug_llm_name = model_map[smaug_model_name][1]
1633
+
1634
+ print(f"使用Smaug模型初始化对话: {smaug_model_name}, app_id: {smaug_app_id}, llm_name: {smaug_llm_name}")
1635
+
1636
+ # 创建新会话
1637
+ new_conversation_id = create_conversation(
1638
+ session, cookies, session_token,
1639
+ external_application_id=smaug_app_id,
1640
+ deployment_id=deployment_id
1641
+ )
1642
+
1643
+ if not new_conversation_id:
1644
+ print("创建新会话失败")
1645
+ return None
1646
+
1647
+ print(f"成功创建新会话ID: {new_conversation_id}")
1648
+
1649
+ # 发送初始消息 "Repeat \"123\""
1650
+ trace_id, sentry_trace = generate_trace_id()
1651
+
1652
+ headers = {
1653
+ "accept": "text/event-stream",
1654
+ "accept-language": "zh-CN,zh;q=0.9",
1655
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
1656
+ "content-type": "text/plain;charset=UTF-8",
1657
+ "cookie": cookies,
1658
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1659
+ "sec-ch-ua-mobile": "?0",
1660
+ "sec-ch-ua-platform": "\"Windows\"",
1661
+ "sec-fetch-dest": "empty",
1662
+ "sec-fetch-mode": "cors",
1663
+ "sec-fetch-site": "same-origin",
1664
+ "sentry-trace": sentry_trace,
1665
+ "user-agent": random.choice(USER_AGENTS),
1666
+ "x-abacus-org-host": "apps"
1667
+ }
1668
+
1669
+ if session_token:
1670
+ headers["session-token"] = session_token
1671
+
1672
+ # 构建初始化请求 - 使用Smaug模型
1673
+ init_payload = {
1674
+ "requestId": str(uuid.uuid4()),
1675
+ "deploymentConversationId": new_conversation_id,
1676
+ "message": "Repeat \"123\"",
1677
+ "isDesktop": False,
1678
+ "chatConfig": {
1679
+ "timezone": "Asia/Shanghai",
1680
+ "language": "zh-CN"
1681
+ },
1682
+ "llmName": smaug_llm_name,
1683
+ "externalApplicationId": smaug_app_id
1684
+ }
1685
+
1686
+ try:
1687
+ print(f"发送初始化消息: \"Repeat \\\"123\\\"\"")
1688
+ response = session.post(
1689
+ CHAT_URL,
1690
+ headers=headers,
1691
+ data=json.dumps(init_payload),
1692
+ stream=True,
1693
+ cookies=None
1694
+ )
1695
+
1696
+ # 不处理响应,只确认成功发送
1697
+ if response.status_code != 200:
1698
+ print(f"发送初始化消息失败: {response.status_code}")
1699
+ return None
1700
+
1701
+ # 关闭响应流
1702
+ response.close()
1703
+
1704
+ # 等待5秒,让系统生成响应
1705
+ print("等待5秒,让系统生成响应...")
1706
+ time.sleep(5)
1707
+
1708
+ # 更新全局存储的会话ID
1709
+ global USER_DATA, CURRENT_USER
1710
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
1711
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
1712
+
1713
+ # 保存到配置文件
1714
+ update_conversation_id(user_index, new_conversation_id)
1715
+
1716
+ print(f"对话初始化完成: {new_conversation_id}")
1717
+ return new_conversation_id
1718
+
1719
+ except Exception as e:
1720
+ print(f"初始化对话失败: {e}")
1721
+ return None
1722
+
1723
+ def edit_assistant_message(session, cookies, session_token, conversation_id, message, model_map, model):
1724
+ """编辑对话中索引为1的助手消息"""
1725
+ print("\n===== 编辑助手消息 =====")
1726
+
1727
+ trace_id, sentry_trace = generate_trace_id()
1728
+
1729
+ headers = {
1730
+ "accept": "application/json, text/plain, */*",
1731
+ "accept-language": "zh-CN,zh;q=0.9",
1732
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
1733
+ "content-type": "application/json",
1734
+ "cookie": cookies,
1735
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1736
+ "sec-ch-ua-mobile": "?0",
1737
+ "sec-ch-ua-platform": "\"Windows\"",
1738
+ "sec-fetch-dest": "empty",
1739
+ "sec-fetch-mode": "cors",
1740
+ "sec-fetch-site": "same-origin",
1741
+ "sentry-trace": sentry_trace,
1742
+ "user-agent": random.choice(USER_AGENTS),
1743
+ "x-abacus-org-host": "apps"
1744
+ }
1745
+
1746
+ if session_token:
1747
+ headers["session-token"] = session_token
1748
+
1749
+ # 编辑消息负载
1750
+ edit_payload = {
1751
+ "deploymentConversationId": conversation_id,
1752
+ "text": message,
1753
+ "messageIndex": 1,
1754
+ "segmentCounter": 0
1755
+ }
1756
+
1757
+ try:
1758
+ print(f"编辑助手消息,索引: 1, 对话ID: {conversation_id}")
1759
+ response = session.post(
1760
+ EDIT_CHAT_RESPONSE_URL,
1761
+ headers=headers,
1762
+ json=edit_payload
1763
+ )
1764
+
1765
+ if response.status_code == 200:
1766
+ data = response.json()
1767
+ if data.get("success", False):
1768
+ print("成功编辑助手消息")
1769
+ return True
1770
+ else:
1771
+ print(f"编辑助手消息失败: {data.get('error', '未知错误')}")
1772
+ else:
1773
+ print(f"编辑助手消息失败,状态码: {response.status_code}")
1774
+
1775
+ return False
1776
+ except Exception as e:
1777
+ print(f"编辑助手消息时出错: {e}")
1778
+ return False
1779
+
1780
+
1781
+ if __name__ == "__main__":
1782
+ # 启动保活线程
1783
+ threading.Thread(target=keep_alive, daemon=True).start()
1784
+
1785
+ # 加载历史模型调用记录
1786
+ load_model_usage_records()
1787
+
1788
+ # 获取初始计算点信息
1789
+ get_compute_points()
1790
+
1791
+ port = int(os.environ.get("PORT", 9876))
1792
+ app.run(port=port, host="0.0.0.0")
requirements.txt ADDED
Binary file (86 Bytes). View file
 
templates/dashboard.html ADDED
@@ -0,0 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 仪表盘</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6f42c1;
10
+ --secondary-color: #4a32a8;
11
+ --accent-color: #5e85f1;
12
+ --bg-color: #0a0a1a;
13
+ --text-color: #e6e6ff;
14
+ --card-bg: rgba(30, 30, 60, 0.7);
15
+ --input-bg: rgba(40, 40, 80, 0.6);
16
+ --success-color: #36d399;
17
+ --warning-color: #fbbd23;
18
+ --error-color: #f87272;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ body {
29
+ min-height: 100vh;
30
+ background-color: var(--bg-color);
31
+ background-image:
32
+ radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
33
+ radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
34
+ color: var(--text-color);
35
+ position: relative;
36
+ overflow-x: hidden;
37
+ }
38
+
39
+ /* 动态背景网格 */
40
+ .grid-background {
41
+ position: fixed;
42
+ top: 0;
43
+ left: 0;
44
+ width: 100%;
45
+ height: 100%;
46
+ background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
47
+ linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
48
+ background-size: 30px 30px;
49
+ z-index: -1;
50
+ animation: grid-move 20s linear infinite;
51
+ }
52
+
53
+ @keyframes grid-move {
54
+ 0% {
55
+ transform: translateY(0);
56
+ }
57
+ 100% {
58
+ transform: translateY(30px);
59
+ }
60
+ }
61
+
62
+ /* 顶部导航栏 */
63
+ .navbar {
64
+ padding: 1rem 2rem;
65
+ background: rgba(15, 15, 30, 0.8);
66
+ backdrop-filter: blur(10px);
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: 100;
74
+ }
75
+
76
+ .navbar-brand {
77
+ display: flex;
78
+ align-items: center;
79
+ text-decoration: none;
80
+ color: var(--text-color);
81
+ }
82
+
83
+ .navbar-logo {
84
+ font-size: 1.5rem;
85
+ margin-right: 0.75rem;
86
+ animation: pulse 2s infinite alternate;
87
+ }
88
+
89
+ @keyframes pulse {
90
+ 0% {
91
+ transform: scale(1);
92
+ text-shadow: 0 0 5px rgba(111, 66, 193, 0.5);
93
+ }
94
+ 100% {
95
+ transform: scale(1.05);
96
+ text-shadow: 0 0 15px rgba(111, 66, 193, 0.8);
97
+ }
98
+ }
99
+
100
+ .navbar-title {
101
+ font-size: 1.25rem;
102
+ font-weight: 600;
103
+ background: linear-gradient(45deg, #6f42c1, #5181f1);
104
+ -webkit-background-clip: text;
105
+ -webkit-text-fill-color: transparent;
106
+ }
107
+
108
+ .navbar-actions {
109
+ display: flex;
110
+ gap: 1rem;
111
+ }
112
+
113
+ .btn-logout {
114
+ background: rgba(255, 255, 255, 0.1);
115
+ color: var(--text-color);
116
+ border: none;
117
+ padding: 0.5rem 1rem;
118
+ border-radius: 6px;
119
+ cursor: pointer;
120
+ transition: all 0.2s;
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .btn-logout:hover {
127
+ background: rgba(255, 255, 255, 0.2);
128
+ }
129
+
130
+ /* 主内容区域 */
131
+ .container {
132
+ max-width: 1200px;
133
+ margin: 0 auto;
134
+ padding: 2rem;
135
+ }
136
+
137
+ /* 信息卡片样式 */
138
+ .card {
139
+ background: var(--card-bg);
140
+ border-radius: 12px;
141
+ padding: 1.5rem;
142
+ margin-bottom: 2rem;
143
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
144
+ backdrop-filter: blur(8px);
145
+ border: 1px solid rgba(255, 255, 255, 0.1);
146
+ animation: card-fade-in 0.6s ease-out;
147
+ }
148
+
149
+ @keyframes card-fade-in {
150
+ from {
151
+ opacity: 0;
152
+ transform: translateY(20px);
153
+ }
154
+ to {
155
+ opacity: 1;
156
+ transform: translateY(0);
157
+ }
158
+ }
159
+
160
+ .card-header {
161
+ margin-bottom: 1rem;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: space-between;
165
+ }
166
+
167
+ .card-title {
168
+ font-size: 1.25rem;
169
+ font-weight: 600;
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 0.75rem;
173
+ }
174
+
175
+ .card-icon {
176
+ width: 32px;
177
+ height: 32px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ background: linear-gradient(45deg, rgba(111, 66, 193, 0.2), rgba(94, 133, 241, 0.2));
182
+ border-radius: 8px;
183
+ font-size: 1.25rem;
184
+ }
185
+
186
+ /* 状态项样式 */
187
+ .status-item {
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
191
+ padding: 0.75rem 0;
192
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
193
+ }
194
+
195
+ .status-item:last-child {
196
+ border-bottom: none;
197
+ }
198
+
199
+ .status-label {
200
+ color: rgba(230, 230, 255, 0.7);
201
+ font-weight: 500;
202
+ }
203
+
204
+ .status-value {
205
+ color: var(--text-color);
206
+ font-weight: 600;
207
+ }
208
+
209
+ .status-value.success {
210
+ color: var(--success-color);
211
+ }
212
+
213
+ .status-value.warning {
214
+ color: var(--warning-color);
215
+ }
216
+
217
+ .status-value.danger {
218
+ color: var(--error-color);
219
+ }
220
+
221
+ /* 模型标签 */
222
+ .models-list {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0.5rem;
226
+ }
227
+
228
+ .model-tag {
229
+ background: rgba(111, 66, 193, 0.2);
230
+ padding: 0.25rem 0.75rem;
231
+ border-radius: 16px;
232
+ font-size: 0.875rem;
233
+ color: var(--text-color);
234
+ border: 1px solid rgba(111, 66, 193, 0.3);
235
+ }
236
+
237
+ /* 表格样式 */
238
+ .table-container {
239
+ overflow-x: auto;
240
+ margin-top: 1rem;
241
+ }
242
+
243
+ .data-table {
244
+ width: 100%;
245
+ border-collapse: collapse;
246
+ text-align: left;
247
+ }
248
+
249
+ .data-table th {
250
+ background-color: rgba(50, 50, 100, 0.3);
251
+ padding: 0.75rem 1rem;
252
+ font-weight: 600;
253
+ color: rgba(230, 230, 255, 0.9);
254
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
255
+ }
256
+
257
+ .data-table td {
258
+ padding: 0.75rem 1rem;
259
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
260
+ }
261
+
262
+ .data-table tbody tr {
263
+ transition: background-color 0.2s;
264
+ }
265
+
266
+ .data-table tbody tr:hover {
267
+ background-color: rgba(50, 50, 100, 0.2);
268
+ }
269
+
270
+ /* 特殊值样式 */
271
+ .token-count {
272
+ font-family: 'Consolas', monospace;
273
+ color: var(--accent-color);
274
+ font-weight: bold;
275
+ }
276
+
277
+ .call-count {
278
+ font-family: 'Consolas', monospace;
279
+ color: var(--success-color);
280
+ font-weight: bold;
281
+ }
282
+
283
+ .compute-points {
284
+ font-family: 'Consolas', monospace;
285
+ color: var(--primary-color);
286
+ font-weight: bold;
287
+ }
288
+
289
+ /* 进度条 */
290
+ .progress-container {
291
+ width: 100%;
292
+ height: 8px;
293
+ background-color: rgba(100, 100, 150, 0.2);
294
+ border-radius: 4px;
295
+ margin-top: 0.5rem;
296
+ overflow: hidden;
297
+ position: relative;
298
+ }
299
+
300
+ .progress-bar {
301
+ height: 100%;
302
+ border-radius: 4px;
303
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
304
+ position: relative;
305
+ overflow: hidden;
306
+ }
307
+
308
+ .progress-bar.warning {
309
+ background: linear-gradient(90deg, #fbbd23, #f59e0b);
310
+ }
311
+
312
+ .progress-bar.danger {
313
+ background: linear-gradient(90deg, #f87272, #ef4444);
314
+ }
315
+
316
+ /* 添加进度条闪光效果 */
317
+ .progress-bar::after {
318
+ content: '';
319
+ position: absolute;
320
+ top: 0;
321
+ left: -100%;
322
+ width: 100%;
323
+ height: 100%;
324
+ background: linear-gradient(90deg,
325
+ transparent,
326
+ rgba(255, 255, 255, 0.2),
327
+ transparent);
328
+ animation: progress-shine 3s infinite;
329
+ }
330
+
331
+ @keyframes progress-shine {
332
+ 0% {
333
+ left: -100%;
334
+ }
335
+ 50%, 100% {
336
+ left: 100%;
337
+ }
338
+ }
339
+
340
+ /* API端点卡片 */
341
+ .endpoint-item {
342
+ background: rgba(50, 50, 100, 0.2);
343
+ padding: 1rem;
344
+ border-radius: 8px;
345
+ margin-bottom: 1rem;
346
+ border-left: 3px solid var(--primary-color);
347
+ }
348
+
349
+ .endpoint-url {
350
+ font-family: 'Consolas', monospace;
351
+ background: rgba(0, 0, 0, 0.2);
352
+ padding: 0.5rem;
353
+ border-radius: 4px;
354
+ margin-top: 0.25rem;
355
+ display: inline-block;
356
+ color: var(--text-color);
357
+ text-decoration: none;
358
+ transition: all 0.2s;
359
+ }
360
+
361
+ .endpoint-url:hover {
362
+ background: rgba(111, 66, 193, 0.3);
363
+ color: var(--text-color);
364
+ }
365
+
366
+ /* 响应式布局 */
367
+ .grid {
368
+ display: grid;
369
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
370
+ gap: 1.5rem;
371
+ }
372
+
373
+ /* 页脚 */
374
+ .footer {
375
+ text-align: center;
376
+ padding: 2rem 0;
377
+ color: rgba(230, 230, 255, 0.5);
378
+ font-size: 0.9rem;
379
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
380
+ margin-top: 2rem;
381
+ }
382
+
383
+ /* 悬浮图标按钮 */
384
+ .float-btn {
385
+ position: fixed;
386
+ bottom: 2rem;
387
+ right: 2rem;
388
+ width: 50px;
389
+ height: 50px;
390
+ border-radius: 50%;
391
+ background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ color: white;
396
+ font-size: 1.5rem;
397
+ box-shadow: 0 4px 20px rgba(111, 66, 193, 0.4);
398
+ cursor: pointer;
399
+ transition: all 0.3s;
400
+ z-index: 50;
401
+ }
402
+
403
+ .float-btn:hover {
404
+ transform: translateY(-5px);
405
+ box-shadow: 0 8px 25px rgba(111, 66, 193, 0.5);
406
+ }
407
+
408
+ /* 滚动条美化 */
409
+ ::-webkit-scrollbar {
410
+ width: 8px;
411
+ height: 8px;
412
+ }
413
+
414
+ ::-webkit-scrollbar-track {
415
+ background: rgba(50, 50, 100, 0.1);
416
+ }
417
+
418
+ ::-webkit-scrollbar-thumb {
419
+ background: rgba(111, 66, 193, 0.5);
420
+ border-radius: 4px;
421
+ }
422
+
423
+ ::-webkit-scrollbar-thumb:hover {
424
+ background: rgba(111, 66, 193, 0.7);
425
+ }
426
+
427
+ /* 模型统计折叠样式 */
428
+ .hidden-model {
429
+ display: none;
430
+ }
431
+
432
+ .btn-toggle {
433
+ background: rgba(111, 66, 193, 0.2);
434
+ border: 1px solid rgba(111, 66, 193, 0.3);
435
+ border-radius: 4px;
436
+ padding: 0.3rem 0.7rem;
437
+ color: rgba(230, 230, 255, 0.9);
438
+ cursor: pointer;
439
+ transition: all 0.2s;
440
+ font-size: 0.85rem;
441
+ margin-left: auto;
442
+ }
443
+
444
+ .btn-toggle:hover {
445
+ background: rgba(111, 66, 193, 0.4);
446
+ }
447
+
448
+ /* Token注释样式 */
449
+ .token-note {
450
+ margin-top: 0.75rem;
451
+ color: rgba(230, 230, 255, 0.6);
452
+ font-style: italic;
453
+ line-height: 1.4;
454
+ padding: 0.5rem;
455
+ border-top: 1px dashed rgba(255, 255, 255, 0.1);
456
+ }
457
+
458
+ .token-model-table {
459
+ margin-top: 1rem;
460
+ }
461
+
462
+ /* Token计算方法标签样式 */
463
+ .token-method {
464
+ padding: 4px 8px;
465
+ border-radius: 4px;
466
+ font-size: 0.85rem;
467
+ font-weight: 500;
468
+ }
469
+
470
+ .token-method-exact {
471
+ background-color: rgba(54, 211, 153, 0.2);
472
+ color: #36d399;
473
+ }
474
+
475
+ .token-method-estimate {
476
+ background-color: rgba(251, 189, 35, 0.2);
477
+ color: #fbbd23;
478
+ }
479
+
480
+ /* 时间日期样式 */
481
+ .datetime {
482
+ font-family: 'Consolas', monospace;
483
+ color: rgba(230, 230, 255, 0.8);
484
+ font-size: 0.9rem;
485
+ }
486
+
487
+ /* 媒体查询 */
488
+ @media (max-width: 768px) {
489
+ .container {
490
+ padding: 1rem;
491
+ }
492
+
493
+ .navbar {
494
+ padding: 1rem;
495
+ }
496
+
497
+ .card {
498
+ padding: 1rem;
499
+ }
500
+
501
+ .grid {
502
+ grid-template-columns: 1fr;
503
+ }
504
+ }
505
+
506
+ .token-model-table td, .token-model-table th {
507
+ white-space: nowrap;
508
+ }
509
+
510
+ /* 开关按钮样式 */
511
+ .toggle-switch-container {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 10px;
515
+ }
516
+
517
+ .toggle-switch {
518
+ position: relative;
519
+ display: inline-block;
520
+ width: 50px;
521
+ height: 24px;
522
+ }
523
+
524
+ .toggle-switch input {
525
+ opacity: 0;
526
+ width: 0;
527
+ height: 0;
528
+ }
529
+
530
+ .toggle-slider {
531
+ position: absolute;
532
+ cursor: pointer;
533
+ top: 0;
534
+ left: 0;
535
+ right: 0;
536
+ bottom: 0;
537
+ background-color: rgba(100, 100, 150, 0.3);
538
+ transition: .4s;
539
+ border-radius: 24px;
540
+ }
541
+
542
+ .toggle-slider:before {
543
+ position: absolute;
544
+ content: "";
545
+ height: 18px;
546
+ width: 18px;
547
+ left: 3px;
548
+ bottom: 3px;
549
+ background-color: #e6e6ff;
550
+ transition: .4s;
551
+ border-radius: 50%;
552
+ }
553
+
554
+ input:checked + .toggle-slider {
555
+ background-color: var(--primary-color);
556
+ }
557
+
558
+ input:checked + .toggle-slider:before {
559
+ transform: translateX(26px);
560
+ }
561
+
562
+ .toggle-status {
563
+ font-weight: 600;
564
+ }
565
+
566
+ .info-text {
567
+ font-size: 0.85rem;
568
+ color: rgba(230, 230, 255, 0.7);
569
+ }
570
+
571
+ /* 通知样式 */
572
+ .notification {
573
+ position: fixed;
574
+ top: 20px;
575
+ right: 20px;
576
+ padding: 12px 20px;
577
+ border-radius: 8px;
578
+ color: white;
579
+ font-weight: 500;
580
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
581
+ z-index: 1000;
582
+ transform: translateY(-20px);
583
+ opacity: 0;
584
+ transition: all 0.3s ease;
585
+ max-width: 300px;
586
+ }
587
+
588
+ .notification.show {
589
+ transform: translateY(0);
590
+ opacity: 1;
591
+ }
592
+
593
+ .notification.success {
594
+ background-color: var(--success-color);
595
+ }
596
+
597
+ .notification.error {
598
+ background-color: var(--error-color);
599
+ }
600
+
601
+ .notification.info {
602
+ background-color: var(--accent-color);
603
+ }
604
+
605
+ /* 响应式样式 */
606
+ @media (max-width: 768px) {
607
+ .container {
608
+ padding: 1rem;
609
+ }
610
+
611
+ .navbar {
612
+ padding: 1rem;
613
+ }
614
+
615
+ .card {
616
+ padding: 1rem;
617
+ }
618
+
619
+ .grid {
620
+ grid-template-columns: 1fr;
621
+ }
622
+ }
623
+ </style>
624
+ </head>
625
+ <body>
626
+ <div class="grid-background"></div>
627
+
628
+ <nav class="navbar">
629
+ <a href="/" class="navbar-brand">
630
+ <span class="navbar-logo">🤖</span>
631
+ <span class="navbar-title">Abacus Chat Proxy</span>
632
+ </a>
633
+ <div class="navbar-actions">
634
+ <a href="/logout" class="btn-logout">
635
+ <span>退出</span>
636
+ <span>↗</span>
637
+ </a>
638
+ </div>
639
+ </nav>
640
+
641
+ <div class="container">
642
+ <div class="card">
643
+ <div class="card-header">
644
+ <h2 class="card-title">
645
+ <span class="card-icon">📊</span>
646
+ 系统状态
647
+ </h2>
648
+ </div>
649
+ <div class="status-item">
650
+ <span class="status-label">服务状态</span>
651
+ <span class="status-value success">运行中</span>
652
+ </div>
653
+ <div class="status-item">
654
+ <span class="status-label">运行时间</span>
655
+ <span class="status-value">{{ uptime }}</span>
656
+ </div>
657
+ <div class="status-item">
658
+ <span class="status-label">健康检查次数</span>
659
+ <span class="status-value">{{ health_checks }}</span>
660
+ </div>
661
+ <div class="status-item">
662
+ <span class="status-label">已配置用户数</span>
663
+ <span class="status-value">{{ user_count }}</span>
664
+ </div>
665
+ <div class="status-item">
666
+ <span class="status-label">可用模型</span>
667
+ <div class="models-list">
668
+ {% for model in models %}
669
+ <span class="model-tag">{{ model }}</span>
670
+ {% endfor %}
671
+ </div>
672
+ </div>
673
+ </div>
674
+
675
+ <div class="grid">
676
+ <div class="card">
677
+ <div class="card-header">
678
+ <h2 class="card-title">
679
+ <span class="card-icon">💰</span>
680
+ 计算点总计
681
+ </h2>
682
+ </div>
683
+ <div class="status-item">
684
+ <span class="status-label">总计算点</span>
685
+ <span class="status-value compute-points">{{ compute_points.total|int }}</span>
686
+ </div>
687
+ <div class="status-item">
688
+ <span class="status-label">已使用</span>
689
+ <span class="status-value compute-points">{{ compute_points.used|int }}</span>
690
+ </div>
691
+ <div class="status-item">
692
+ <span class="status-label">剩余</span>
693
+ <span class="status-value compute-points">{{ compute_points.left|int }}</span>
694
+ </div>
695
+ <div class="status-item">
696
+ <span class="status-label">使用比例</span>
697
+ <div style="width: 100%; text-align: right;">
698
+ <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
699
+ {{ compute_points.percentage }}%
700
+ </span>
701
+ <div class="progress-container">
702
+ <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
703
+ </div>
704
+ </div>
705
+ </div>
706
+ {% if compute_points.last_update %}
707
+ <div class="status-item">
708
+ <span class="status-label">最后更新时间</span>
709
+ <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
710
+ </div>
711
+ {% endif %}
712
+ </div>
713
+
714
+ <div class="card">
715
+ <div class="card-header">
716
+ <h2 class="card-title">
717
+ <span class="card-icon">🔍</span>
718
+ Token 使用统计
719
+ </h2>
720
+ </div>
721
+ <div class="status-item">
722
+ <span class="status-label">总输入Token</span>
723
+ <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
724
+ </div>
725
+ <div class="status-item">
726
+ <span class="status-label">总输出Token</span>
727
+ <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
728
+ </div>
729
+ <div class="token-note">
730
+ <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
731
+ </div>
732
+ <div class="table-container">
733
+ <table class="data-table token-model-table">
734
+ <thead>
735
+ <tr>
736
+ <th>模型</th>
737
+ <th>调用次数</th>
738
+ <th>输入Token</th>
739
+ <th>输出Token</th>
740
+ </tr>
741
+ </thead>
742
+ <tbody>
743
+ {% for model, stats in model_stats.items() %}
744
+ <tr>
745
+ <td>{{ model }}</td>
746
+ <td class="call-count">{{ stats.count }}</td>
747
+ <td class="token-count">{{ stats.prompt_tokens|int }}</td>
748
+ <td class="token-count">{{ stats.completion_tokens|int }}</td>
749
+ </tr>
750
+ {% endfor %}
751
+ </tbody>
752
+ </table>
753
+ </div>
754
+ </div>
755
+ </div>
756
+
757
+ {% if users_compute_points|length > 0 %}
758
+ <div class="card">
759
+ <div class="card-header">
760
+ <h2 class="card-title">
761
+ <span class="card-icon">👥</span>
762
+ 用户计算点详情
763
+ </h2>
764
+ </div>
765
+ <div class="table-container">
766
+ <table class="data-table">
767
+ <thead>
768
+ <tr>
769
+ <th>用户</th>
770
+ <th>总计算点</th>
771
+ <th>已使用</th>
772
+ <th>剩余</th>
773
+ <th>使用比例</th>
774
+ </tr>
775
+ </thead>
776
+ <tbody>
777
+ {% for user in users_compute_points %}
778
+ <tr>
779
+ <td>用户 {{ user.user_id }}</td>
780
+ <td class="compute-points">{{ user.total|int }}</td>
781
+ <td class="compute-points">{{ user.used|int }}</td>
782
+ <td class="compute-points">{{ user.left|int }}</td>
783
+ <td>
784
+ <div style="width: 100%; position: relative;">
785
+ <span class="status-value compute-points {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}">
786
+ {{ user.percentage }}%
787
+ </span>
788
+ <div class="progress-container">
789
+ <div class="progress-bar {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}" style="width: {{ user.percentage }}%"></div>
790
+ </div>
791
+ </div>
792
+ </td>
793
+ </tr>
794
+ {% endfor %}
795
+ </tbody>
796
+ </table>
797
+ </div>
798
+ </div>
799
+ {% endif %}
800
+
801
+ <div class="card">
802
+ <div class="card-header">
803
+ <h2 class="card-title">
804
+ <span class="card-icon">📊</span>
805
+ 计算点使用日志
806
+ </h2>
807
+ </div>
808
+ <div class="table-container">
809
+ <table class="data-table">
810
+ <thead>
811
+ <tr>
812
+ {% for key, value in compute_points_log.columns.items() %}
813
+ <th>{{ value }}</th>
814
+ {% endfor %}
815
+ </tr>
816
+ </thead>
817
+ <tbody>
818
+ {% for entry in compute_points_log.log %}
819
+ <tr>
820
+ {% for key, value in compute_points_log.columns.items() %}
821
+ <td class="compute-points">{{ entry.get(key, 0) }}</td>
822
+ {% endfor %}
823
+ </tr>
824
+ {% endfor %}
825
+ </tbody>
826
+ </table>
827
+ </div>
828
+ </div>
829
+
830
+ <div class="card">
831
+ <div class="card-header">
832
+ <h2 class="card-title">
833
+ <span class="card-icon">📈</span>
834
+ 模型调用记录
835
+ </h2>
836
+ <button id="toggleModelStats" class="btn-toggle">显示全部</button>
837
+ </div>
838
+ <div class="table-container">
839
+ <table class="data-table">
840
+ <thead>
841
+ <tr>
842
+ <th>调用时间 (北京时间)</th>
843
+ <th>模型</th>
844
+ <th>输入Token</th>
845
+ <th>输出Token</th>
846
+ <th>总Token</th>
847
+ <th>计算方式</th>
848
+ <th>计算点数</th>
849
+ </tr>
850
+ </thead>
851
+ <tbody>
852
+ {% for record in model_usage_records|reverse %}
853
+ <tr class="model-row {% if loop.index > 10 %}hidden-model{% endif %}">
854
+ <td class="datetime">{{ record.call_time }}</td>
855
+ <td>{{ record.model }}</td>
856
+ <td class="token-count">{{ record.prompt_tokens }}</td>
857
+ <td class="token-count">{{ record.completion_tokens }}</td>
858
+ <td>{{ record.prompt_tokens + record.completion_tokens }}</td>
859
+ <td>
860
+ {% if record.calculation_method == "精确" %}
861
+ <span class="token-method token-method-exact">精确</span>
862
+ {% else %}
863
+ <span class="token-method token-method-estimate">估算</span>
864
+ {% endif %}
865
+ </td>
866
+ <td>{{ record.compute_points if record.compute_points is not none and record.compute_points != 0 else 'null' }}</td>
867
+ </tr>
868
+ {% endfor %}
869
+ </tbody>
870
+ </table>
871
+ <div class="token-note">
872
+ <small>* Token计算方式:<span class="token-method token-method-exact">精确</span> 表示调用官方API精确计算,<span class="token-method token-method-estimate">估算</span> 表示使用gpt-4o模型估算。所有统计数据仅供参考,不代表实际计费标准。</small>
873
+ </div>
874
+ </div>
875
+ </div>
876
+
877
+ <div class="card">
878
+ <div class="card-header">
879
+ <h2 class="card-title">
880
+ <span class="card-icon">📡</span>
881
+ API 端点
882
+ </h2>
883
+ </div>
884
+ <div class="endpoint-item">
885
+ <p>获取模型列表:</p>
886
+ {% if space_url %}
887
+ <a href="{{ space_url }}/v1/models" class="endpoint-url" target="_blank">GET {{ space_url }}/v1/models</a>
888
+ {% else %}
889
+ <a href="/v1/models" class="endpoint-url" target="_blank">GET /v1/models</a>
890
+ {% endif %}
891
+ </div>
892
+ <div class="endpoint-item">
893
+ <p>聊天补全:</p>
894
+ {% if space_url %}
895
+ <code class="endpoint-url">POST {{ space_url }}/v1/chat/completions</code>
896
+ {% else %}
897
+ <code class="endpoint-url">POST /v1/chat/completions</code>
898
+ {% endif %}
899
+ </div>
900
+ <div class="endpoint-item">
901
+ <p>健康检查:</p>
902
+ {% if space_url %}
903
+ <a href="{{ space_url }}/health" class="endpoint-url" target="_blank">GET {{ space_url }}/health</a>
904
+ {% else %}
905
+ <a href="/health" class="endpoint-url" target="_blank">GET /health</a>
906
+ {% endif %}
907
+ </div>
908
+ </div>
909
+
910
+ <div class="footer">
911
+ <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
912
+ </div>
913
+ </div>
914
+
915
+ <a href="#" class="float-btn" title="回到顶部">↑</a>
916
+
917
+ <script>
918
+ // 回到顶部按钮
919
+ document.querySelector('.float-btn').addEventListener('click', (e) => {
920
+ e.preventDefault();
921
+ window.scrollTo({ top: 0, behavior: 'smooth' });
922
+ });
923
+
924
+ // 显示/隐藏回到顶部按钮
925
+ window.addEventListener('scroll', () => {
926
+ const floatBtn = document.querySelector('.float-btn');
927
+ if (window.pageYOffset > 300) {
928
+ floatBtn.style.opacity = '1';
929
+ } else {
930
+ floatBtn.style.opacity = '0';
931
+ }
932
+ });
933
+
934
+ // 初始化隐藏回到顶部按钮
935
+ document.querySelector('.float-btn').style.opacity = '0';
936
+
937
+ // 模型统计折叠功能
938
+ const toggleBtn = document.getElementById('toggleModelStats');
939
+ const hiddenModels = document.querySelectorAll('.hidden-model');
940
+ let isExpanded = false;
941
+
942
+ if (toggleBtn) {
943
+ toggleBtn.addEventListener('click', () => {
944
+ hiddenModels.forEach(model => {
945
+ model.classList.toggle('hidden-model');
946
+ });
947
+
948
+ isExpanded = !isExpanded;
949
+ toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
950
+ });
951
+ }
952
+
953
+ document.addEventListener('DOMContentLoaded', function() {
954
+ initCharts();
955
+
956
+ // 显示/隐藏更多模型使用记录
957
+ const toggleModelStats = document.getElementById('toggleModelStats');
958
+ if (toggleModelStats) {
959
+ toggleModelStats.addEventListener('click', function() {
960
+ const hiddenRows = document.querySelectorAll('.hidden-model');
961
+ hiddenRows.forEach(row => {
962
+ row.classList.toggle('show-model');
963
+ });
964
+ toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部';
965
+ });
966
+ }
967
+
968
+ // 处理计算点数记录开关
969
+ const computePointToggle = document.getElementById('compute-point-toggle');
970
+ const computePointStatus = document.getElementById('compute-point-status');
971
+
972
+ if (computePointToggle && computePointStatus) {
973
+ computePointToggle.addEventListener('change', function() {
974
+ const isChecked = this.checked;
975
+ computePointStatus.textContent = isChecked ? '开启' : '关闭';
976
+
977
+ // 发送更新请求到后端
978
+ fetch('/update_compute_point_toggle', {
979
+ method: 'POST',
980
+ headers: {
981
+ 'Content-Type': 'application/json',
982
+ },
983
+ body: JSON.stringify({ always_display: isChecked })
984
+ })
985
+ .then(response => response.json())
986
+ .then(data => {
987
+ if (data.success) {
988
+ // 显示成功提示
989
+ showNotification(isChecked ? '已开启计算点数记录功能' : '已关闭计算点数记录功能', 'success');
990
+ } else {
991
+ // 显示错误提示
992
+ showNotification('设置更新失败: ' + data.error, 'error');
993
+ // 回滚UI状态
994
+ computePointToggle.checked = !isChecked;
995
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
996
+ }
997
+ })
998
+ .catch(error => {
999
+ console.error('更新设置出错:', error);
1000
+ showNotification('更新设置失败,请重试', 'error');
1001
+ // 回滚UI状态
1002
+ computePointToggle.checked = !isChecked;
1003
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
1004
+ });
1005
+ });
1006
+ }
1007
+ });
1008
+
1009
+ // 通知函数
1010
+ function showNotification(message, type = 'info') {
1011
+ const notification = document.createElement('div');
1012
+ notification.className = `notification ${type}`;
1013
+ notification.textContent = message;
1014
+
1015
+ document.body.appendChild(notification);
1016
+
1017
+ // 显示动画
1018
+ setTimeout(() => {
1019
+ notification.classList.add('show');
1020
+ }, 10);
1021
+
1022
+ // 3秒后淡出
1023
+ setTimeout(() => {
1024
+ notification.classList.remove('show');
1025
+ setTimeout(() => {
1026
+ notification.remove();
1027
+ }, 300);
1028
+ }, 3000);
1029
+ }
1030
+ </script>
1031
+ </body>
1032
+ </html>
templates/login.html ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 登录</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6f42c1;
10
+ --secondary-color: #4a32a8;
11
+ --bg-color: #0a0a1a;
12
+ --text-color: #e6e6ff;
13
+ --card-bg: rgba(30, 30, 60, 0.7);
14
+ --input-bg: rgba(40, 40, 80, 0.6);
15
+ --success-color: #36d399;
16
+ --error-color: #f87272;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
+ }
25
+
26
+ body {
27
+ min-height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background-color: var(--bg-color);
32
+ background-image:
33
+ radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
34
+ radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
35
+ color: var(--text-color);
36
+ position: relative;
37
+ overflow: hidden;
38
+ }
39
+
40
+ /* 科幻蜘蛛网动画 */
41
+ .web-container {
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ z-index: -2;
48
+ opacity: 0.6;
49
+ }
50
+
51
+ .web {
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+
56
+ /* 动态背景网格 */
57
+ .grid-background {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
64
+ linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
65
+ background-size: 30px 30px;
66
+ z-index: -1;
67
+ animation: grid-move 20s linear infinite;
68
+ }
69
+
70
+ @keyframes grid-move {
71
+ 0% {
72
+ transform: translateY(0);
73
+ }
74
+ 100% {
75
+ transform: translateY(30px);
76
+ }
77
+ }
78
+
79
+ /* 浮动粒子效果 */
80
+ .particles {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ width: 100%;
85
+ height: 100%;
86
+ overflow: hidden;
87
+ z-index: -1;
88
+ }
89
+
90
+ .particle {
91
+ position: absolute;
92
+ display: block;
93
+ pointer-events: none;
94
+ width: 6px;
95
+ height: 6px;
96
+ background-color: rgba(111, 66, 193, 0.2);
97
+ border-radius: 50%;
98
+ animation: float 20s infinite ease-in-out;
99
+ }
100
+
101
+ @keyframes float {
102
+ 0%, 100% {
103
+ transform: translateY(0) translateX(0);
104
+ opacity: 0;
105
+ }
106
+ 50% {
107
+ opacity: 0.5;
108
+ }
109
+ 25%, 75% {
110
+ transform: translateY(-100px) translateX(50px);
111
+ }
112
+ }
113
+
114
+ .login-card {
115
+ width: 420px;
116
+ padding: 2.5rem;
117
+ border-radius: 16px;
118
+ background: var(--card-bg);
119
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
120
+ backdrop-filter: blur(8px);
121
+ border: 1px solid rgba(255, 255, 255, 0.1);
122
+ z-index: 10;
123
+ animation: card-fade-in 0.6s ease-out;
124
+ }
125
+
126
+ @keyframes card-fade-in {
127
+ from {
128
+ opacity: 0;
129
+ transform: translateY(20px);
130
+ }
131
+ to {
132
+ opacity: 1;
133
+ transform: translateY(0);
134
+ }
135
+ }
136
+
137
+ .login-header {
138
+ text-align: center;
139
+ margin-bottom: 2rem;
140
+ }
141
+
142
+ .login-header h1 {
143
+ font-size: 2rem;
144
+ font-weight: 600;
145
+ margin-bottom: 0.5rem;
146
+ background: linear-gradient(45deg, #6f42c1, #5181f1);
147
+ -webkit-background-clip: text;
148
+ -webkit-text-fill-color: transparent;
149
+ letter-spacing: 0.5px;
150
+ }
151
+
152
+ .login-header p {
153
+ color: rgba(230, 230, 255, 0.7);
154
+ font-size: 0.95rem;
155
+ }
156
+
157
+ .space-info {
158
+ text-align: center;
159
+ background: rgba(50, 50, 150, 0.2);
160
+ padding: 0.75rem;
161
+ border-radius: 8px;
162
+ margin-bottom: 1.5rem;
163
+ font-size: 0.9rem;
164
+ border: 1px solid rgba(111, 66, 193, 0.3);
165
+ }
166
+
167
+ .space-info a {
168
+ color: var(--primary-color);
169
+ text-decoration: none;
170
+ font-weight: bold;
171
+ transition: all 0.2s;
172
+ }
173
+
174
+ .space-info a:hover {
175
+ text-decoration: underline;
176
+ color: var(--secondary-color);
177
+ }
178
+
179
+ .login-form {
180
+ display: flex;
181
+ flex-direction: column;
182
+ }
183
+
184
+ .form-group {
185
+ margin-bottom: 1.5rem;
186
+ position: relative;
187
+ }
188
+
189
+ .form-group label {
190
+ display: block;
191
+ margin-bottom: 0.5rem;
192
+ font-size: 0.9rem;
193
+ font-weight: 500;
194
+ color: rgba(230, 230, 255, 0.9);
195
+ }
196
+
197
+ .form-control {
198
+ width: 100%;
199
+ padding: 0.75rem 1rem;
200
+ font-size: 1rem;
201
+ line-height: 1.5;
202
+ color: var(--text-color);
203
+ background-color: var(--input-bg);
204
+ border: 1px solid rgba(255, 255, 255, 0.1);
205
+ border-radius: 8px;
206
+ transition: all 0.2s ease;
207
+ outline: none;
208
+ }
209
+
210
+ .form-control:focus {
211
+ border-color: var(--primary-color);
212
+ box-shadow: 0 0 0 3px rgba(111, 66, 193, 0.2);
213
+ }
214
+
215
+ .btn {
216
+ display: inline-block;
217
+ font-weight: 500;
218
+ text-align: center;
219
+ vertical-align: middle;
220
+ cursor: pointer;
221
+ padding: 0.75rem 1rem;
222
+ font-size: 1rem;
223
+ line-height: 1.5;
224
+ border-radius: 8px;
225
+ transition: all 0.15s ease-in-out;
226
+ border: none;
227
+ }
228
+
229
+ .btn-primary {
230
+ color: #fff;
231
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
232
+ box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
233
+ position: relative;
234
+ overflow: hidden;
235
+ }
236
+
237
+ .btn-primary:hover {
238
+ transform: translateY(-2px);
239
+ box-shadow: 0 6px 15px rgba(111, 66, 193, 0.4);
240
+ }
241
+
242
+ .btn-primary:active {
243
+ transform: translateY(0);
244
+ }
245
+
246
+ /* 添加光效效果 */
247
+ .btn-primary::before {
248
+ content: '';
249
+ position: absolute;
250
+ top: -50%;
251
+ left: -50%;
252
+ width: 200%;
253
+ height: 200%;
254
+ background: linear-gradient(
255
+ to bottom right,
256
+ rgba(255, 255, 255, 0) 0%,
257
+ rgba(255, 255, 255, 0.1) 50%,
258
+ rgba(255, 255, 255, 0) 100%
259
+ );
260
+ transform: rotate(45deg);
261
+ animation: btn-shine 3s infinite;
262
+ }
263
+
264
+ @keyframes btn-shine {
265
+ 0% {
266
+ left: -50%;
267
+ }
268
+ 100% {
269
+ left: 150%;
270
+ }
271
+ }
272
+
273
+ .error-message {
274
+ background-color: rgba(248, 114, 114, 0.2);
275
+ color: var(--error-color);
276
+ padding: 0.75rem;
277
+ border-radius: 6px;
278
+ margin-bottom: 1.5rem;
279
+ font-size: 0.9rem;
280
+ border-left: 3px solid var(--error-color);
281
+ display: {{ 'block' if error else 'none' }};
282
+ }
283
+
284
+ .logo {
285
+ margin-bottom: 1rem;
286
+ font-size: 3rem;
287
+ animation: glow 2s infinite alternate;
288
+ }
289
+
290
+ @keyframes glow {
291
+ from {
292
+ text-shadow: 0 0 5px rgba(111, 66, 193, 0.5), 0 0 10px rgba(111, 66, 193, 0.5);
293
+ }
294
+ to {
295
+ text-shadow: 0 0 10px rgba(111, 66, 193, 0.8), 0 0 20px rgba(111, 66, 193, 0.8);
296
+ }
297
+ }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <div class="grid-background"></div>
302
+ <div class="particles">
303
+ <!-- 粒子元素会由JS生成 -->
304
+ </div>
305
+ <div class="web-container">
306
+ <canvas class="web" id="webCanvas"></canvas>
307
+ </div>
308
+
309
+ <div class="login-card">
310
+ <div class="login-header">
311
+ <div class="logo">🤖</div>
312
+ <h1>Abacus Chat Proxy</h1>
313
+ <p>请输入访问密码</p>
314
+ </div>
315
+
316
+ {% if space_url %}
317
+ <div class="space-info">
318
+ api接口为{{ space_url }}/v1,请点击 <a href="{{ space_url }}" target="_blank">{{ space_url }}</a> 来登录并查看使用情况,
319
+ </div>
320
+ {% endif %}
321
+
322
+ <div class="error-message" id="error-message">
323
+ {{ error }}
324
+ </div>
325
+
326
+ <form class="login-form" method="post" action="/login">
327
+ <div class="form-group">
328
+ <label for="password">密码</label>
329
+ <input type="password" class="form-control" id="password" name="password" placeholder="请输入访问密码" required>
330
+ </div>
331
+
332
+ <button type="submit" class="btn btn-primary">登录</button>
333
+ </form>
334
+ </div>
335
+
336
+ <script>
337
+ // 创建浮动粒子
338
+ function createParticles() {
339
+ const particlesContainer = document.querySelector('.particles');
340
+ const particleCount = 20;
341
+
342
+ for (let i = 0; i < particleCount; i++) {
343
+ const particle = document.createElement('div');
344
+ particle.className = 'particle';
345
+
346
+ // 随机位置和大小
347
+ const size = Math.random() * 5 + 2;
348
+ const x = Math.random() * 100;
349
+ const y = Math.random() * 100;
350
+
351
+ particle.style.width = `${size}px`;
352
+ particle.style.height = `${size}px`;
353
+ particle.style.left = `${x}%`;
354
+ particle.style.top = `${y}%`;
355
+
356
+ // 随机动画延迟
357
+ particle.style.animationDelay = `${Math.random() * 10}s`;
358
+ particle.style.animationDuration = `${Math.random() * 10 + 10}s`;
359
+
360
+ // 随机透明度
361
+ particle.style.opacity = Math.random() * 0.5;
362
+
363
+ particlesContainer.appendChild(particle);
364
+ }
365
+ }
366
+
367
+ // 科幻蜘蛛网效果
368
+ function initWebCanvas() {
369
+ const canvas = document.getElementById('webCanvas');
370
+ const ctx = canvas.getContext('2d');
371
+ let width = window.innerWidth;
372
+ let height = window.innerHeight;
373
+
374
+ // 设置canvas尺寸
375
+ canvas.width = width;
376
+ canvas.height = height;
377
+
378
+ // 节点类
379
+ class Node {
380
+ constructor(x, y) {
381
+ this.x = x;
382
+ this.y = y;
383
+ this.vx = (Math.random() - 0.5) * 0.5;
384
+ this.vy = (Math.random() - 0.5) * 0.5;
385
+ this.radius = Math.random() * 2 + 1;
386
+ }
387
+
388
+ update() {
389
+ if (this.x < 0 || this.x > width) this.vx = -this.vx;
390
+ if (this.y < 0 || this.y > height) this.vy = -this.vy;
391
+
392
+ this.x += this.vx;
393
+ this.y += this.vy;
394
+ }
395
+
396
+ draw() {
397
+ ctx.beginPath();
398
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
399
+ ctx.fillStyle = 'rgba(111, 66, 193, 0.4)';
400
+ ctx.fill();
401
+ }
402
+ }
403
+
404
+ // 创建节点
405
+ const nodeCount = Math.floor(width * height / 15000);
406
+ const nodes = [];
407
+
408
+ for (let i = 0; i < nodeCount; i++) {
409
+ nodes.push(new Node(Math.random() * width, Math.random() * height));
410
+ }
411
+
412
+ // 绘制线条
413
+ function drawWeb() {
414
+ ctx.clearRect(0, 0, width, height);
415
+
416
+ // 更新节点
417
+ nodes.forEach(node => {
418
+ node.update();
419
+ node.draw();
420
+ });
421
+
422
+ // 绘制连线
423
+ for (let i = 0; i < nodes.length; i++) {
424
+ for (let j = i + 1; j < nodes.length; j++) {
425
+ const dx = nodes[i].x - nodes[j].x;
426
+ const dy = nodes[i].y - nodes[j].y;
427
+ const distance = Math.sqrt(dx * dx + dy * dy);
428
+
429
+ if (distance < 150) {
430
+ ctx.beginPath();
431
+ ctx.moveTo(nodes[i].x, nodes[i].y);
432
+ ctx.lineTo(nodes[j].x, nodes[j].y);
433
+ ctx.strokeStyle = `rgba(111, 66, 193, ${0.2 * (1 - distance / 150)})`;
434
+ ctx.lineWidth = 0.5;
435
+ ctx.stroke();
436
+ }
437
+ }
438
+ }
439
+
440
+ requestAnimationFrame(drawWeb);
441
+ }
442
+
443
+ // 监听窗口大小变化
444
+ window.addEventListener('resize', () => {
445
+ width = window.innerWidth;
446
+ height = window.innerHeight;
447
+ canvas.width = width;
448
+ canvas.height = height;
449
+ });
450
+
451
+ // 开始动画
452
+ drawWeb();
453
+ }
454
+
455
+ // 页面加载时初始化效果
456
+ window.addEventListener('load', () => {
457
+ createParticles();
458
+ initWebCanvas();
459
+ });
460
+ </script>
461
+ </body>
462
+ </html>