diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,1724 +1,1782 @@
-from flask import Flask, request, jsonify, Response, render_template_string, render_template, redirect, url_for, session as flask_session
-import requests
-import time
-import json
-import uuid
-import random
-import io
-import re
-from functools import wraps
-import hashlib
-import jwt
-import os
-import threading
-from datetime import datetime, timedelta
-
-app = Flask(__name__, template_folder='templates')
-app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
-app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
-
-# 添加tokenizer服务URL
-TOKENIZER_SERVICE_URL = "https://malt666-tokenizer.hf.space/count_tokens"
-
-API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
-MODEL_LIST_URL = "https://abacus.ai/api/v0/listExternalApplications"
-CHAT_URL = "https://apps.abacus.ai/api/_chatLLMSendMessageSSE"
-USER_INFO_URL = "https://abacus.ai/api/v0/_getUserInfo"
-COMPUTE_POINTS_URL = "https://apps.abacus.ai/api/_getOrganizationComputePoints"
-COMPUTE_POINTS_LOG_URL = "https://abacus.ai/api/v0/_getOrganizationComputePointLog"
-CREATE_CONVERSATION_URL = "https://apps.abacus.ai/api/createDeploymentConversation"
-DELETE_CONVERSATION_URL = "https://apps.abacus.ai/api/deleteDeploymentConversation"
-GET_CONVERSATION_URL = "https://apps.abacus.ai/api/getDeploymentConversation"
-COMPUTE_POINT_TOGGLE_URL = "https://abacus.ai/api/v0/_updateOrganizationComputePointToggle"
-
-
-USER_AGENTS = [
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
-]
-
-
-PASSWORD = None
-USER_NUM = 0
-USER_DATA = []
-CURRENT_USER = -1
-MODELS = set()
-
-TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
-SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
-
-
-# 添加一个计数器记录健康检查次数
-health_check_counter = 0
-
-
-# 添加统计变量
-model_usage_stats = {} # 模型使用次数统计
-total_tokens = {
- "prompt": 0, # 输入token统计
- "completion": 0, # 输出token统计
- "total": 0 # 总token统计
-}
-
-# 模型调用记录
-model_usage_records = [] # 每次调用详细记录
-MODEL_USAGE_RECORDS_FILE = "model_usage_records.json" # 调用记录保存文件
-
-# 计算点信息
-compute_points = {
- "left": 0, # 剩余计算点
- "total": 0, # 总计算点
- "used": 0, # 已使用计算点
- "percentage": 0, # 使用百分比
- "last_update": None # 最后更新时间
-}
-
-# 计算点使用日志
-compute_points_log = {
- "columns": {}, # 列名
- "log": [] # 日志数据
-}
-
-# 多用户计算点信息
-users_compute_points = []
-
-# 记录启动时间
-START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
-
-
-# 自定义JSON编码器,处理datetime对象
-class DateTimeEncoder(json.JSONEncoder):
- def default(self, obj):
- if isinstance(obj, datetime):
- return obj.strftime('%Y-%m-%d %H:%M:%S')
- return super(DateTimeEncoder, self).default(obj)
-
-
-# 加载模型调用记录
-def load_model_usage_records():
- global model_usage_records
- try:
- if os.path.exists(MODEL_USAGE_RECORDS_FILE):
- with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
- records = json.load(f)
- if isinstance(records, list):
- model_usage_records = records
- print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
- else:
- print("调用记录文件格式不正确,初始化为空列表")
- except Exception as e:
- print(f"加载模型调用记录失败: {e}")
- model_usage_records = []
-
-# 保存模型调用记录
-def save_model_usage_records():
- try:
- with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
- json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
- print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
- except Exception as e:
- print(f"保存模型调用记录失败: {e}")
-
-
-def update_conversation_id(user_index, conversation_id):
- """更新用户的conversation_id并保存到配置文件"""
- try:
- with open("config.json", "r") as f:
- config = json.load(f)
-
- if "config" in config and user_index < len(config["config"]):
- config["config"][user_index]["conversation_id"] = conversation_id
-
- # 保存到配置文件
- with open("config.json", "w") as f:
- json.dump(config, f, indent=4)
-
- print(f"已将用户 {user_index+1} 的conversation_id更新为: {conversation_id}")
- else:
- print(f"更新conversation_id失败: 配置文件格式错误或用户索引越界")
- except Exception as e:
- print(f"更新conversation_id失败: {e}")
-
-
-def resolve_config():
- # 从环境变量读取多组配置
- config_list = []
- i = 1
- while True:
- cookie = os.environ.get(f"cookie_{i}")
- if not cookie:
- break
-
- # 为每个cookie创建一个配置项,conversation_id初始为空
- config_list.append({
- "conversation_id": "", # 初始为空,将通过get_or_create_conversation自动创建
- "cookies": cookie
- })
- i += 1
-
- # 如果环境变量存在配置,使用环境变量的配置
- if config_list:
- print(f"从环境变量加载了 {len(config_list)} 个配置")
- return config_list
-
- # 如果环境变量不存在,从文件读取
- try:
- with open("config.json", "r") as f:
- config = json.load(f)
- config_list = config.get("config")
- return config_list
- except FileNotFoundError:
- print("未找到config.json文件")
- return []
- except json.JSONDecodeError:
- print("config.json格式错误")
- return []
-
-
-def get_password():
- global PASSWORD
- # 从环境变量读取密码
- env_password = os.environ.get("password")
- if env_password:
- PASSWORD = hashlib.sha256(env_password.encode()).hexdigest()
- return
-
- # 如果环境变量不存在,从文件读取
- try:
- with open("password.txt", "r") as f:
- PASSWORD = f.read().strip()
- except FileNotFoundError:
- with open("password.txt", "w") as f:
- PASSWORD = None
-
-
-def require_auth(f):
- @wraps(f)
- def decorated(*args, **kwargs):
- if not PASSWORD:
- return f(*args, **kwargs)
-
- # 检查Flask会话是否已登录
- if flask_session.get('logged_in'):
- return f(*args, **kwargs)
-
- # 如果是API请求,检查Authorization头
- auth = request.authorization
- if not auth or not check_auth(auth.token):
- # 如果是浏览器请求,重定向到登录页面
- if request.headers.get('Accept', '').find('text/html') >= 0:
- return redirect(url_for('login'))
- return jsonify({"error": "Unauthorized access"}), 401
- return f(*args, **kwargs)
-
- return decorated
-
-
-def check_auth(token):
- return hashlib.sha256(token.encode()).hexdigest() == PASSWORD
-
-
-def is_token_expired(token):
- if not token:
- return True
-
- try:
- # Malkodi tokenon sen validigo de subskribo
- payload = jwt.decode(token, options={"verify_signature": False})
- # Akiru eksvalidiĝan tempon, konsideru eksvalidiĝinta 5 minutojn antaŭe
- return payload.get('exp', 0) - time.time() < 300
- except:
- return True
-
-
-def refresh_token(session, cookies):
- """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-site",
- "x-abacus-org-host": "apps",
- "user-agent": random.choice(USER_AGENTS),
- "origin": "https://apps.abacus.ai",
- "referer": "https://apps.abacus.ai/",
- "cookie": cookies
- }
-
- try:
- response = session.post(
- USER_INFO_URL,
- headers=headers,
- json={},
- cookies=None
- )
-
- if response.status_code == 200:
- response_data = response.json()
- if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
- return response_data['result']['sessionToken']
- else:
- print(f"刷新token失败: {response_data.get('error', '未知错误')}")
- return None
- else:
- print(f"刷新token失败,状态码: {response.status_code}")
- return None
- except Exception as e:
- print(f"刷新token异常: {e}")
- return None
-
-
-def get_model_map(session, cookies, session_token):
- """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-site",
- "x-abacus-org-host": "apps",
- "user-agent": random.choice(USER_AGENTS),
- "origin": "https://apps.abacus.ai",
- "referer": "https://apps.abacus.ai/",
- "cookie": cookies
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- model_map = {}
- models_set = set()
-
- try:
- response = session.post(
- MODEL_LIST_URL,
- headers=headers,
- json={},
- cookies=None
- )
-
- if response.status_code != 200:
- print(f"获取模型列表失败,状态码: {response.status_code}")
- raise Exception("API请求失败")
-
- data = response.json()
- if not data.get('success'):
- print(f"获取模型列表失败: {data.get('error', '未知错误')}")
- raise Exception("API返回错误")
-
- applications = []
- if isinstance(data.get('result'), dict):
- applications = data.get('result', {}).get('externalApplications', [])
- elif isinstance(data.get('result'), list):
- applications = data.get('result', [])
-
- for app in applications:
- app_name = app.get('name', '')
- app_id = app.get('externalApplicationId', '')
- prediction_overrides = app.get('predictionOverrides', {})
- llm_name = prediction_overrides.get('llmName', '') if prediction_overrides else ''
-
- if not (app_name and app_id and llm_name):
- continue
-
- model_name = app_name
- model_map[model_name] = (app_id, llm_name)
- models_set.add(model_name)
-
- if not model_map:
- raise Exception("未找到任何可用模型")
-
- return model_map, models_set
-
- except Exception as e:
- print(f"获取模型列表异常: {e}")
- raise
-
-
-def init_session():
- get_password()
- global USER_NUM, MODELS, USER_DATA
-
- config_list = resolve_config()
- user_num = len(config_list)
- all_models = set()
-
- for i in range(user_num):
- user = config_list[i]
- cookies = user.get("cookies")
- conversation_id = user.get("conversation_id")
- session = requests.Session()
-
- session_token = refresh_token(session, cookies)
- if not session_token:
- print(f"无法获取cookie {i+1}的token")
- continue
-
- try:
- model_map, models_set = get_model_map(session, cookies, session_token)
- all_models.update(models_set)
- USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
-
- # 对第一个成功配置的用户,初始化计算点数记录功能
- if i == 0:
- try:
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-site",
- "x-abacus-org-host": "apps",
- "session-token": session_token
- }
-
- response = session.post(
- COMPUTE_POINT_TOGGLE_URL,
- headers=headers,
- json={"alwaysDisplay": True},
- cookies=None
- )
-
- if response.status_code == 200:
- result = response.json()
- if result.get("success"):
- print("成功初始化计算点数记录功能为开启状态")
- else:
- print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
- else:
- print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
- except Exception as e:
- print(f"初始化计算点数记录功能时出错: {e}")
- except Exception as e:
- print(f"配置用户 {i+1} 失败: {e}")
- continue
-
- USER_NUM = len(USER_DATA)
- if USER_NUM == 0:
- print("No user available, exiting...")
- exit(1)
-
- MODELS = all_models
- print(f"启动完成,共配置 {USER_NUM} 个用户")
-
-
-def update_cookie(session, cookies):
- cookie_jar = {}
- for key, value in session.cookies.items():
- cookie_jar[key] = value
- cookie_dict = {}
- for item in cookies.split(";"):
- key, value = item.strip().split("=", 1)
- cookie_dict[key] = value
- cookie_dict.update(cookie_jar)
- cookies = "; ".join([f"{key}={value}" for key, value in cookie_dict.items()])
- return cookies
-
-
-user_data = init_session()
-
-
-@app.route("/v1/models", methods=["GET"])
-@require_auth
-def get_models():
- if len(MODELS) == 0:
- return jsonify({"error": "No models available"}), 500
- model_list = []
- for model in MODELS:
- model_list.append(
- {
- "id": model,
- "object": "model",
- "created": int(time.time()),
- "owned_by": "Elbert",
- "name": model,
- }
- )
- return jsonify({"object": "list", "data": model_list})
-
-
-@app.route("/v1/chat/completions", methods=["POST"])
-@require_auth
-def chat_completions():
- openai_request = request.get_json()
- stream = openai_request.get("stream", False)
- messages = openai_request.get("messages")
- if messages is None:
- return jsonify({"error": "Messages is required", "status": 400}), 400
- model = openai_request.get("model")
- if model not in MODELS:
- return (
- jsonify(
- {
- "error": "Model not available, check if it is configured properly",
- "status": 404,
- }
- ),
- 404,
- )
- message = format_message(messages)
- think = (
- openai_request.get("think", False) if model == "Claude Sonnet 3.7" else False
- )
- return (
- send_message(message, model, think)
- if stream
- else send_message_non_stream(message, model, think)
- )
-
-
-def get_user_data():
- global CURRENT_USER
- CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
- print(f"使用配置 {CURRENT_USER+1}")
-
- # Akiru uzantajn datumojn
- session, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[CURRENT_USER]
-
- # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
- if is_token_expired(session_token):
- print(f"Cookie {CURRENT_USER+1}的token已过期或即将过期,正在刷新...")
- new_token = refresh_token(session, cookies)
- if new_token:
- # Ĝisdatigu la globale konservitan tokenon
- USER_DATA[CURRENT_USER] = (session, cookies, new_token, conversation_id, model_map, user_index)
- session_token = new_token
- print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
- else:
- print(f"警告:无法刷新Cookie {CURRENT_USER+1}的token,继续使用当前token")
-
- return (session, cookies, session_token, conversation_id, model_map, user_index)
-
-
-def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
- """创建新的会话"""
- if not (external_application_id and deployment_id):
- print("无法创建新会话: 缺少必要参数")
- return None
-
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "cookie": cookies,
- "user-agent": random.choice(USER_AGENTS),
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- create_payload = {
- "deploymentId": deployment_id,
- "name": "New Chat",
- "externalApplicationId": external_application_id
- }
-
- try:
- response = session.post(
- CREATE_CONVERSATION_URL,
- headers=headers,
- json=create_payload
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success", False):
- new_conversation_id = data.get("result", {}).get("deploymentConversationId")
- if new_conversation_id:
- print(f"成功创建新的conversation: {new_conversation_id}")
- return new_conversation_id
-
- print(f"创建会话失败: {response.status_code} - {response.text[:100]}")
- return None
- except Exception as e:
- print(f"创建会话时出错: {e}")
- return None
-
-
-def delete_conversation(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
- """删除指定的对话"""
- if not conversation_id:
- print("无法删除对话: 缺少conversation_id")
- return False
-
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "cookie": cookies,
- "user-agent": random.choice(USER_AGENTS),
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- delete_payload = {
- "deploymentId": deployment_id,
- "deploymentConversationId": conversation_id
- }
-
- try:
- response = session.post(
- DELETE_CONVERSATION_URL,
- headers=headers,
- json=delete_payload
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success", False):
- print(f"成功删除对话: {conversation_id}")
- return True
-
- print(f"删除对话失败: {response.status_code} - {response.text[:100]}")
- return False
- except Exception as e:
- print(f"删除对话时出错: {e}")
- return False
-
-
-def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
- """获取对话ID,如果不存在则创建;返回是否是使用现有会话"""
- print(f"\n----- 获取会话ID (用户 {user_index+1}) -----")
- # 如果有现有的会话ID,直接使用
- if conversation_id:
- print(f"使用现有会话ID: {conversation_id}")
- return conversation_id, True
-
- # 如果没有会话ID,创建新的
- print("没有会话ID,创建新会话...")
- deployment_id = "14b2a314cc"
-
- # 确保模型信息存在
- if model not in model_map or len(model_map[model]) < 2:
- print(f"错误: 无法获取模型 {model} 的信息")
- return None, False
-
- external_app_id = model_map[model][0]
-
- # 创建新会话
- new_conversation_id = create_conversation(
- session, cookies, session_token,
- external_application_id=external_app_id,
- deployment_id=deployment_id
- )
-
- if new_conversation_id:
- print(f"成功创建新会话ID: {new_conversation_id}")
-
- # 更新全局存储的会话ID
- global USER_DATA, CURRENT_USER
- session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
- USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
-
- # 保存到配置文件
- update_conversation_id(user_index, new_conversation_id)
-
- return new_conversation_id, False
-
- print("创建新会话失败")
- return None, False
-
-
-def generate_trace_id():
- """Generu novan trace_id kaj sentry_trace"""
- trace_id = str(uuid.uuid4()).replace('-', '')
- sentry_trace = f"{trace_id}-{str(uuid.uuid4())[:16]}"
- return trace_id, sentry_trace
-
-
-def send_message(message, model, think=False):
- """Flua traktado kaj plusendo de mesaĝoj"""
- print("\n===== 开始处理消息 =====")
- print(f"模型: {model}")
- print(f"思考模式: {think}")
-
- (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
- print(f"使用用户配置: {user_index + 1}")
-
- # 获取会话ID,并判断是否使用现有会话
- conversation_id, is_existing = get_or_create_conversation(
- session, cookies, session_token, conversation_id, model_map, model, user_index
- )
-
- # 如果没有有效的会话ID,返回错误
- if not conversation_id:
- return jsonify({"error": "Failed to get a valid conversation ID"}), 500
-
- print(f"会话ID: {conversation_id} (是否为现有会话: {is_existing})")
-
- trace_id, sentry_trace = generate_trace_id()
-
- # 计算输入token
- print("\n----- 计算输入token -----")
- prompt_tokens, calculation_method = num_tokens_from_string(message, model)
- print(f"输入token数: {prompt_tokens}")
- print(f"计算方法: {calculation_method}")
- completion_buffer = io.StringIO() # 收集所有输出用于计算token
-
- headers = {
- "accept": "text/event-stream",
- "accept-language": "zh-CN,zh;q=0.9",
- "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
- "content-type": "text/plain;charset=UTF-8",
- "cookie": cookies,
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "sentry-trace": sentry_trace,
- "user-agent": random.choice(USER_AGENTS),
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- # 构建基础请求
- payload = {
- "requestId": str(uuid.uuid4()),
- "deploymentConversationId": conversation_id,
- "message": message,
- "isDesktop": False,
- "chatConfig": {
- "timezone": "Asia/Shanghai",
- "language": "zh-CN"
- },
- "llmName": model_map[model][1],
- "externalApplicationId": model_map[model][0]
- }
-
- # 如果是使用现有会话,添加regenerate和editPrompt参数
- if is_existing:
- payload["regenerate"] = True
- payload["editPrompt"] = True
- print("为现有会话添加 regenerate=True 和 editPrompt=True")
-
- if think:
- payload["useThinking"] = think
-
- try:
- response = session.post(
- CHAT_URL,
- headers=headers,
- data=json.dumps(payload),
- stream=True,
- cookies=None
- )
-
- response.raise_for_status()
-
- def extract_segment(line_data):
- try:
- data = json.loads(line_data)
- if "segment" in data:
- if isinstance(data["segment"], str):
- return data["segment"]
- elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
- return data["segment"]["segment"]
- return ""
- except:
- return ""
-
- def generate():
- id = ""
- think_state = 2
-
- yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
-
- for line in response.iter_lines():
- if line:
- decoded_line = line.decode("utf-8")
- try:
- if think:
- data = json.loads(decoded_line)
- if data.get("type") != "text":
- continue
- elif think_state == 2:
- id = data.get("messageId")
- segment = "\n" + data.get("segment", "")
- completion_buffer.write(segment) # 收集输出
- yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
- think_state = 1
- elif think_state == 1:
- if data.get("messageId") != id:
- segment = data.get("segment", "")
- completion_buffer.write(segment) # 收集输出
- yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
- else:
- segment = "\n\n" + data.get("segment", "")
- completion_buffer.write(segment) # 收集输出
- yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
- think_state = 0
- else:
- segment = data.get("segment", "")
- completion_buffer.write(segment) # 收集输出
- yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
- else:
- segment = extract_segment(decoded_line)
- if segment:
- completion_buffer.write(segment) # 收集输出
- yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
- except Exception as e:
- print(f"处理响应出错: {e}")
-
- yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
- yield "data: [DONE]\n\n"
-
- # 在流式传输完成后计算token并更新统计
- completion_result, _ = num_tokens_from_string(completion_buffer.getvalue(), model)
-
- # 保存对话历史并获取计算点数
- _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
-
- # 更新统计信息
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
-
- return Response(generate(), mimetype="text/event-stream")
- except requests.exceptions.RequestException as e:
- error_details = str(e)
- if hasattr(e, 'response') and e.response is not None:
- if hasattr(e.response, 'text'):
- error_details += f" - Response: {e.response.text[:200]}"
- print(f"发送消息失败: {error_details}")
-
- # 如果是使用现有会话失败,尝试创建新会话重试一次
- if is_existing:
- print("使用现有会话失败,尝试创建新会话...")
- # 创建新会话
- deployment_id = "14b2a314cc"
- external_app_id = model_map[model][0] if model in model_map and len(model_map[model]) >= 2 else None
-
- if external_app_id:
- new_conversation_id = create_conversation(
- session, cookies, session_token,
- external_application_id=external_app_id,
- deployment_id=deployment_id
- )
-
- if new_conversation_id:
- print(f"成功创建新会话ID: {new_conversation_id},重试请求")
- # 更新全局存储的会话ID
- global USER_DATA, CURRENT_USER
- session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
- USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
-
- # 保存到配置文件
- update_conversation_id(user_index, new_conversation_id)
-
- # 修改payload使用新会话ID,并移除regenerate和editPrompt
- payload["deploymentConversationId"] = new_conversation_id
- if "regenerate" in payload:
- del payload["regenerate"]
- if "editPrompt" in payload:
- del payload["editPrompt"]
-
- try:
- # 非流式重试逻辑与流式类似,但需要重新提取响应内容
- response = session.post(
- CHAT_URL,
- headers=headers,
- data=json.dumps(payload),
- stream=True,
- cookies=None
- )
-
- response.raise_for_status()
- # 重用现有提取逻辑...
- # 但这里代码重复太多,实际应该重构为共享函数
- buffer = io.StringIO()
-
- for line in response.iter_lines():
- if line:
- decoded_line = line.decode("utf-8")
- segment = extract_segment(decoded_line)
- if segment:
- buffer.write(segment)
-
- response_content = buffer.getvalue()
-
- # 计算输出token并更新统计信息
- completion_result, _ = num_tokens_from_string(response_content, model)
-
- # 保存对话历史并获取计算点数
- _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
-
- # 更新统计信息
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
-
- return jsonify({
- "id": f"chatcmpl-{str(uuid.uuid4())}",
- "object": "chat.completion",
- "created": int(time.time()),
- "model": model,
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": response_content
- },
- "finish_reason": "stop"
- }],
- "usage": {
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_result,
- "total_tokens": prompt_tokens + completion_result
- }
- })
- except Exception as retry_e:
- print(f"重试失败: {retry_e}")
-
- return jsonify({"error": f"Failed to send message: {error_details}"}), 500
-
-
-def send_message_non_stream(message, model, think=False):
- """Ne-flua traktado de mesaĝoj"""
- print("\n===== 开始处理消息(非流式) =====")
- print(f"模型: {model}")
- print(f"思考模式: {think}")
-
- (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
- print(f"使用用户配置: {user_index + 1}")
-
- # 获取会话ID,并判断是否使用现有会话
- conversation_id, is_existing = get_or_create_conversation(
- session, cookies, session_token, conversation_id, model_map, model, user_index
- )
-
- # 如果没有有效的会话ID,返回错误
- if not conversation_id:
- return jsonify({"error": "Failed to get a valid conversation ID"}), 500
-
- print(f"会话ID: {conversation_id} (是否为现有会话: {is_existing})")
-
- trace_id, sentry_trace = generate_trace_id()
-
- # 计算输入token
- print("\n----- 计算输入token -----")
- prompt_tokens, calculation_method = num_tokens_from_string(message, model)
- print(f"输入token数: {prompt_tokens}")
- print(f"计算方法: {calculation_method}")
-
- headers = {
- "accept": "text/event-stream",
- "accept-language": "zh-CN,zh;q=0.9",
- "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
- "content-type": "text/plain;charset=UTF-8",
- "cookie": cookies,
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "sentry-trace": sentry_trace,
- "user-agent": random.choice(USER_AGENTS),
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- # 构建基础请求
- payload = {
- "requestId": str(uuid.uuid4()),
- "deploymentConversationId": conversation_id,
- "message": message,
- "isDesktop": False,
- "chatConfig": {
- "timezone": "Asia/Shanghai",
- "language": "zh-CN"
- },
- "llmName": model_map[model][1],
- "externalApplicationId": model_map[model][0]
- }
-
- # 如果是使用现有会话,添加regenerate和editPrompt参数
- if is_existing:
- payload["regenerate"] = True
- payload["editPrompt"] = True
- print("为现有会话添加 regenerate=True 和 editPrompt=True")
-
- if think:
- payload["useThinking"] = think
-
- try:
- response = session.post(
- CHAT_URL,
- headers=headers,
- data=json.dumps(payload),
- stream=True,
- cookies=None
- )
-
- response.raise_for_status()
- buffer = io.StringIO()
-
- def extract_segment(line_data):
- try:
- data = json.loads(line_data)
- if "segment" in data:
- if isinstance(data["segment"], str):
- return data["segment"]
- elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
- return data["segment"]["segment"]
- return ""
- except:
- return ""
-
- if think:
- id = ""
- think_state = 2
- think_buffer = io.StringIO()
- content_buffer = io.StringIO()
-
- for line in response.iter_lines():
- if line:
- decoded_line = line.decode("utf-8")
- try:
- data = json.loads(decoded_line)
- if data.get("type") != "text":
- continue
- elif think_state == 2:
- id = data.get("messageId")
- segment = data.get("segment", "")
- think_buffer.write(segment)
- think_state = 1
- elif think_state == 1:
- if data.get("messageId") != id:
- segment = data.get("segment", "")
- content_buffer.write(segment)
- else:
- segment = data.get("segment", "")
- think_buffer.write(segment)
- think_state = 0
- else:
- segment = data.get("segment", "")
- content_buffer.write(segment)
- except Exception as e:
- print(f"处理响应出错: {e}")
-
- think_content = think_buffer.getvalue()
- response_content = content_buffer.getvalue()
-
- # 计算输出token并更新统计信息
- completion_result, _ = num_tokens_from_string(think_content + response_content, model)
-
- # 保存对话历史并获取计算点数
- _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
-
- # 更新统计信息
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
-
- return jsonify({
- "id": f"chatcmpl-{str(uuid.uuid4())}",
- "object": "chat.completion",
- "created": int(time.time()),
- "model": model,
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": f"\n{think_content}\n\n{response_content}"
- },
- "finish_reason": "stop"
- }],
- "usage": {
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_result,
- "total_tokens": prompt_tokens + completion_result
- }
- })
- else:
- for line in response.iter_lines():
- if line:
- decoded_line = line.decode("utf-8")
- segment = extract_segment(decoded_line)
- if segment:
- buffer.write(segment)
-
- response_content = buffer.getvalue()
-
- # 计算输出token并更新统计信息
- completion_result, _ = num_tokens_from_string(response_content, model)
-
- # 保存对话历史并获取计算点数
- _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
-
- # 更新统计信息
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
-
- return jsonify({
- "id": f"chatcmpl-{str(uuid.uuid4())}",
- "object": "chat.completion",
- "created": int(time.time()),
- "model": model,
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": response_content
- },
- "finish_reason": "stop"
- }],
- "usage": {
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_result,
- "total_tokens": prompt_tokens + completion_result
- }
- })
- except requests.exceptions.RequestException as e:
- error_details = str(e)
- if hasattr(e, 'response') and e.response is not None:
- if hasattr(e.response, 'text'):
- error_details += f" - Response: {e.response.text[:200]}"
- print(f"发送消息失败: {error_details}")
-
- # 如果是使用现有会话失败,尝试创建新会话重试一次
- if is_existing:
- print("使用现有会话失败,尝试创建新会话...")
- # 创建新会话
- deployment_id = "14b2a314cc"
- external_app_id = model_map[model][0] if model in model_map and len(model_map[model]) >= 2 else None
-
- if external_app_id:
- new_conversation_id = create_conversation(
- session, cookies, session_token,
- external_application_id=external_app_id,
- deployment_id=deployment_id
- )
-
- if new_conversation_id:
- print(f"成功创建新会话ID: {new_conversation_id},重试请求")
- # 更新全局存储的会话ID
- global USER_DATA, CURRENT_USER
- session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
- USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
-
- # 保存到配置文件
- update_conversation_id(user_index, new_conversation_id)
-
- # 修改payload使用新会话ID,并移除regenerate和editPrompt
- payload["deploymentConversationId"] = new_conversation_id
- if "regenerate" in payload:
- del payload["regenerate"]
- if "editPrompt" in payload:
- del payload["editPrompt"]
-
- try:
- # 非流式重试逻辑与流式类似,但需要重新提取响应内容
- response = session.post(
- CHAT_URL,
- headers=headers,
- data=json.dumps(payload),
- stream=True,
- cookies=None
- )
-
- response.raise_for_status()
- # 重用现有提取逻辑...
- # 但这里代码重复太多,实际应该重构为共享函数
- buffer = io.StringIO()
-
- for line in response.iter_lines():
- if line:
- decoded_line = line.decode("utf-8")
- segment = extract_segment(decoded_line)
- if segment:
- buffer.write(segment)
-
- response_content = buffer.getvalue()
-
- # 计算输出token并更新统计信息
- completion_result, _ = num_tokens_from_string(response_content, model)
-
- # 保存对话历史并获取计算点数
- _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
-
- # 更新统计信息
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
-
- return jsonify({
- "id": f"chatcmpl-{str(uuid.uuid4())}",
- "object": "chat.completion",
- "created": int(time.time()),
- "model": model,
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": response_content
- },
- "finish_reason": "stop"
- }],
- "usage": {
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_result,
- "total_tokens": prompt_tokens + completion_result
- }
- })
- except Exception as retry_e:
- print(f"重试失败: {retry_e}")
-
- return jsonify({"error": f"Failed to send message: {error_details}"}), 500
-
-
-def format_message(messages):
- buffer = io.StringIO()
- role_map, prefix, messages = extract_role(messages)
- for message in messages:
- role = message.get("role")
- role = "\b" + role_map[role] if prefix else role_map[role]
- content = message.get("content").replace("\\n", "\n")
- pattern = re.compile(r"<\|removeRole\|>\n")
- if pattern.match(content):
- content = pattern.sub("", content)
- buffer.write(f"{content}\n")
- else:
- buffer.write(f"{role}: {content}\n\n")
- formatted_message = buffer.getvalue()
- return formatted_message
-
-
-def extract_role(messages):
- role_map = {"user": "Human", "assistant": "Assistant", "system": "System"}
- prefix = True # 默认添加前缀
- first_message = messages[0]["content"]
- pattern = re.compile(
- r"""
- \s*
- (?:user:\s*(?P[^\n]*)\s*)? # Make user optional
- (?:assistant:\s*(?P[^\n]*)\s*)? # Make assistant optional
- (?:system:\s*(?P[^\n]*)\s*)? # Make system optional
- (?:prefix:\s*(?P[^\n]*)\s*)? # Make prefix optional
- \n
- """,
- re.VERBOSE,
- )
- match = pattern.search(first_message)
- if match:
- # 更新 role_map 如果提供了值
- user_role = match.group("user")
- assistant_role = match.group("assistant")
- system_role = match.group("system")
- if user_role: role_map["user"] = user_role
- if assistant_role: role_map["assistant"] = assistant_role
- if system_role: role_map["system"] = system_role
-
- # 检查 prefix 值:仅当显式设置为非 "1" 时才将 prefix 设为 False
- prefix_value = match.group("prefix")
- if prefix_value is not None and prefix_value != "1":
- prefix = False
- # 如果 prefix_value 是 None (标签不存在) 或 "1", prefix 保持 True
-
- messages[0]["content"] = pattern.sub("", first_message)
- print(f"Extracted role map:")
- print(
- f"User: {role_map['user']}, Assistant: {role_map['assistant']}, System: {role_map['system']}"
- )
- print(f"Using prefix: {prefix}") # 打印语句保持不变,反映最终结果
- # 如果没有匹配到 ,prefix 保持默认值 True
- return (role_map, prefix, messages)
-
-
-@app.route("/health", methods=["GET"])
-def health_check():
- global health_check_counter
- health_check_counter += 1
- return jsonify({
- "status": "healthy",
- "timestamp": datetime.now().isoformat(),
- "checks": health_check_counter
- })
-
-
-def keep_alive():
- """每20分钟进行一次自我健康检查"""
- while True:
- try:
- requests.get("http://127.0.0.1:7860/health")
- time.sleep(1200) # 20分钟
- except:
- pass # 忽略错误,保持运行
-
-
-@app.route("/", methods=["GET"])
-def index():
- # 如果需要密码且用户未登录,重定向到登录页面
- if PASSWORD and not flask_session.get('logged_in'):
- return redirect(url_for('login'))
-
- # 否则重定向到仪表盘
- return redirect(url_for('dashboard'))
-
-
-def num_tokens_from_string(string, model=""):
- try:
- print("\n===================== 开始计算token =====================")
- print(f"模型: {model}")
- print(f"输入内容长度: {len(string)} 字符")
-
- request_data = {
- "model": model,
- "messages": [{"role": "user", "content": string}]
- }
- print(f"发送请求到tokenizer服务: {TOKENIZER_SERVICE_URL}")
- print(f"请求数据: {json.dumps(request_data, ensure_ascii=False)}")
-
- response = requests.post(
- TOKENIZER_SERVICE_URL,
- json=request_data,
- timeout=10
- )
-
- print(f"\nTokenizer响应状态码: {response.status_code}")
- print(f"Tokenizer响应内容: {response.text}")
-
- if response.status_code == 200:
- result = response.json()
- input_tokens = result.get("input_tokens", 0)
- print(f"\n成功获取token数: {input_tokens}")
- print(f"使用计算方法: 精确")
- print("===================== 计算完成 =====================\n")
- return input_tokens, "精确"
- else:
- estimated_tokens = len(string) // 4
- print(f"\nTokenizer服务错误: {response.status_code}")
- print(f"错误响应: {response.text}")
- print(f"使用估算token数: {estimated_tokens}")
- print(f"使用计算方法: 估算")
- print("===================== 计算完成 =====================\n")
- return estimated_tokens, "估算"
- except Exception as e:
- estimated_tokens = len(string) // 4
- print(f"\n计算token时发生错误: {str(e)}")
- print(f"使用估算token数: {estimated_tokens}")
- print(f"使用计算方法: 估算")
- print("===================== 计算完成 =====================\n")
- return estimated_tokens, "估算"
-
-
-# 更新模型使用统计
-def update_model_stats(model, prompt_tokens, completion_tokens, calculation_method="estimate", compute_points=None):
- global model_usage_stats, total_tokens, model_usage_records
-
- # 添加调用记录
- # 获取UTC时间
- utc_now = datetime.utcnow()
- # 转换为北京时间 (UTC+8)
- beijing_time = utc_now + timedelta(hours=8)
- call_time = beijing_time.strftime('%Y-%m-%d %H:%M:%S') # 北京时间
-
- record = {
- "model": model,
- "call_time": call_time,
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_tokens,
- "calculation_method": calculation_method, # 直接使用传入的值
- "compute_points": compute_points
- }
- model_usage_records.append(record)
-
- # 限制记录数量,保留最新的500条
- if len(model_usage_records) > 500:
- model_usage_records.pop(0)
-
- # 保存调用记录到本地文件
- save_model_usage_records()
-
- # 更新聚合统计
- if model not in model_usage_stats:
- model_usage_stats[model] = {
- "count": 0,
- "prompt_tokens": 0,
- "completion_tokens": 0,
- "total_tokens": 0
- }
-
- model_usage_stats[model]["count"] += 1
- model_usage_stats[model]["prompt_tokens"] += prompt_tokens
- model_usage_stats[model]["completion_tokens"] += completion_tokens
- model_usage_stats[model]["total_tokens"] += (prompt_tokens + completion_tokens)
-
- total_tokens["prompt"] += prompt_tokens
- total_tokens["completion"] += completion_tokens
- total_tokens["total"] += (prompt_tokens + completion_tokens)
-
-
-# 获取计算点信息
-def get_compute_points():
- global compute_points, USER_DATA, users_compute_points
-
- if USER_NUM == 0:
- return
-
- # 清空用户计算点列表
- users_compute_points = []
-
- # 累计总计算点
- total_left = 0
- total_points = 0
-
- # 获取每个用户的计算点信息
- for i, user_data in enumerate(USER_DATA):
- try:
- session, cookies, session_token, _, _, _ = user_data
-
- # 检查token是否有效
- if is_token_expired(session_token):
- session_token = refresh_token(session, cookies)
- if not session_token:
- print(f"用户{i+1}刷新token失败,无法获取计算点信息")
- continue
- USER_DATA[i] = (session, cookies, session_token, user_data[3], user_data[4], i)
-
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "baggage": f"sentry-environment=production,sentry-release=93da8385541a6ce339b1f41b0c94428c70657e22,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "sentry-trace": SENTRY_TRACE,
- "session-token": session_token,
- "x-abacus-org-host": "apps",
- "cookie": cookies
- }
-
- response = session.get(
- COMPUTE_POINTS_URL,
- headers=headers
- )
-
- if response.status_code == 200:
- result = response.json()
- if result.get("success") and "result" in result:
- data = result["result"]
- left = data.get("computePointsLeft", 0)
- total = data.get("totalComputePoints", 0)
- used = total - left
- percentage = round((used / total) * 100, 2) if total > 0 else 0
-
- # 获取北京时间
- beijing_now = datetime.utcnow() + timedelta(hours=8)
-
- # 添加到用户列表
- user_points = {
- "user_id": i + 1, # 用户ID从1开始
- "left": left,
- "total": total,
- "used": used,
- "percentage": percentage,
- "last_update": beijing_now
- }
- users_compute_points.append(user_points)
-
- # 累计总数
- total_left += left
- total_points += total
-
- print(f"用户{i+1}计算点信息更新成功: 剩余 {left}, 总计 {total}")
-
- # 对于第一个用户,获取计算点使用日志
- if i == 0:
- get_compute_points_log(session, cookies, session_token)
- else:
- print(f"获取用户{i+1}计算点信息失败: {result.get('error', '未知错误')}")
- else:
- print(f"获取用户{i+1}计算点信息失败,状态码: {response.status_code}")
- except Exception as e:
- print(f"获取用户{i+1}计算点信息异常: {e}")
-
- # 更新全局计算点信息(所有用户总和)
- if users_compute_points:
- compute_points["left"] = total_left
- compute_points["total"] = total_points
- compute_points["used"] = total_points - total_left
- compute_points["percentage"] = round((compute_points["used"] / compute_points["total"]) * 100, 2) if compute_points["total"] > 0 else 0
- compute_points["last_update"] = datetime.utcnow() + timedelta(hours=8) # 北京时间
- print(f"所有用户计算点总计: 剩余 {total_left}, 总计 {total_points}")
-
-# 获取计算点使用日志
-def get_compute_points_log(session, cookies, session_token):
- global compute_points_log
-
- try:
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-site",
- "session-token": session_token,
- "x-abacus-org-host": "apps",
- "cookie": cookies
- }
-
- response = session.post(
- COMPUTE_POINTS_LOG_URL,
- headers=headers,
- json={"byLlm": True}
- )
-
- if response.status_code == 200:
- result = response.json()
- if result.get("success") and "result" in result:
- data = result["result"]
- compute_points_log["columns"] = data.get("columns", {})
- compute_points_log["log"] = data.get("log", [])
- print(f"计算点使用日志更新成功,获取到 {len(compute_points_log['log'])} 条记录")
- else:
- print(f"获取计算点使用日志失败: {result.get('error', '未知错误')}")
- else:
- print(f"获取计算点使用日志失败,状态码: {response.status_code}")
- except Exception as e:
- print(f"获取计算点使用日志异常: {e}")
-
-
-# 添加登录相关路由
-@app.route("/login", methods=["GET", "POST"])
-def login():
- error = None
- if request.method == "POST":
- password = request.form.get("password")
- if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
- flask_session['logged_in'] = True
- flask_session.permanent = True
- return redirect(url_for('dashboard'))
- else:
- # 密码错误时提示使用环境变量密码
- error = "密码不正确。请使用设置的环境变量 password 或 password.txt 中的值作为密码和API认证密钥。"
-
- # 传递空间URL给模板
- return render_template('login.html', error=error, space_url=SPACE_URL)
-
-
-@app.route("/logout")
-def logout():
- flask_session.clear()
- return redirect(url_for('login'))
-
-
-@app.route("/dashboard")
-@require_auth
-def dashboard():
- # 在每次访问仪表盘时更新计算点信息
- get_compute_points()
-
- # 计算运行时间(使用北京时间)
- beijing_now = datetime.utcnow() + timedelta(hours=8)
- uptime = beijing_now - START_TIME
- days = uptime.days
- hours, remainder = divmod(uptime.seconds, 3600)
- minutes, seconds = divmod(remainder, 60)
-
- if days > 0:
- uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
- elif hours > 0:
- uptime_str = f"{hours}小时 {minutes}分钟"
- else:
- uptime_str = f"{minutes}分钟 {seconds}秒"
-
- # 当前北京年份
- beijing_year = beijing_now.year
-
- return render_template(
- 'dashboard.html',
- uptime=uptime_str,
- health_checks=health_check_counter,
- user_count=USER_NUM,
- models=sorted(list(MODELS)),
- year=beijing_year,
- model_stats=model_usage_stats,
- total_tokens=total_tokens,
- compute_points=compute_points,
- compute_points_log=compute_points_log,
- space_url=SPACE_URL, # 传递空间URL
- users_compute_points=users_compute_points, # 传递用户计算点信息
- model_usage_records=model_usage_records, # 传递模型使用记录
- )
-
-
-# 添加更新计算点数记录设置的路由
-@app.route("/update_compute_point_toggle", methods=["POST"])
-@require_auth
-def update_compute_point_toggle():
- try:
- (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
- data = request.get_json()
- if data and "always_display" in data:
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-site",
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- response = session.post(
- COMPUTE_POINT_TOGGLE_URL,
- headers=headers,
- json={"alwaysDisplay": data["always_display"]},
- cookies=None
- )
-
- if response.status_code == 200:
- result = response.json()
- if result.get("success"):
- print(f"更新计算点数记录设置为: {data['always_display']}")
- return jsonify({"success": True})
-
- return jsonify({"success": False, "error": "API调用失败"})
- else:
- return jsonify({"success": False, "error": "缺少always_display参数"})
- except Exception as e:
- print(f"更新计算点数记录设置失败: {e}")
- return jsonify({"success": False, "error": str(e)})
-
-
-# 获取Hugging Face Space URL
-def get_space_url():
- # 尝试从环境变量获取
- space_url = os.environ.get("SPACE_URL")
- if space_url:
- return space_url
-
- # 如果SPACE_URL不存在,尝试从SPACE_ID构建
- space_id = os.environ.get("SPACE_ID")
- if space_id:
- username, space_name = space_id.split("/")
- # 将空间名称中的下划线替换为连字符
- # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
- # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
- space_name = space_name.replace("_", "-")
- return f"https://{username}-{space_name}.hf.space"
-
- # 如果以上都不存在,尝试从单独的用户名和空间名构建
- username = os.environ.get("SPACE_USERNAME")
- space_name = os.environ.get("SPACE_NAME")
- if username and space_name:
- # 将空间名称中的下划线替换为连字符
- # 同上,Hugging Face会自动进行此转换
- space_name = space_name.replace("_", "-")
- return f"https://{username}-{space_name}.hf.space"
-
- # 默认返回None
- return None
-
-# 获取空间URL
-SPACE_URL = get_space_url()
-if SPACE_URL:
- print(f"Space URL: {SPACE_URL}")
- print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
-
-
-def save_conversation_history(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
- """保存对话历史,返回使用的计算点数"""
- if not conversation_id:
- return False, None
-
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "baggage": f"sentry-environment=production,sentry-release=946244517de08b08598b94f18098411f5a5352d5,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
- "reai-ui": "1",
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "sentry-trace": f"{TRACE_ID}-800cb7f4613dec52",
- "x-abacus-org-host": "apps"
- }
-
- if session_token:
- headers["session-token"] = session_token
-
- params = {
- "deploymentId": deployment_id,
- "deploymentConversationId": conversation_id,
- "skipDocumentBoundingBoxes": "true",
- "filterIntermediateConversationEvents": "false",
- "getUnusedDocumentUploads": "false"
- }
-
- try:
- response = session.get(
- GET_CONVERSATION_URL,
- headers=headers,
- params=params,
- cookies=None
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- # 从最后一条BOT消息中获取计算点数
- history = data.get("result", {}).get("history", [])
- compute_points = None
- for msg in reversed(history):
- if msg.get("role") == "BOT":
- compute_points = msg.get("computePointsUsed")
- break
- print(f"成功保存对话历史: {conversation_id}, 使用计算点: {compute_points}")
- return True, compute_points
- else:
- print(f"保存对话历史失败: {data.get('error', '未知错误')}")
- else:
- print(f"保存对话历史失败,状态码: {response.status_code}")
- return False, None
- except Exception as e:
- print(f"保存对话历史时出错: {e}")
- return False, None
-
-
-if __name__ == "__main__":
- # 启动保活线程
- threading.Thread(target=keep_alive, daemon=True).start()
-
- # 加载历史模型调用记录
- load_model_usage_records()
-
- # 获取初始计算点信息
- get_compute_points()
-
- port = int(os.environ.get("PORT", 9876))
- app.run(port=port, host="0.0.0.0")
+from flask import Flask, request, jsonify, Response, render_template_string, render_template, redirect, url_for, session as flask_session
+import requests
+import time
+import json
+import uuid
+import random
+import io
+import re
+from functools import wraps
+import hashlib
+import jwt
+import os
+import threading
+from datetime import datetime, timedelta
+
+app = Flask(__name__, template_folder='templates')
+app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
+app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
+
+# 添加tokenizer服务URL
+TOKENIZER_SERVICE_URL = "https://malt666-tokenizer.hf.space/count_tokens"
+
+API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
+MODEL_LIST_URL = "https://abacus.ai/api/v0/listExternalApplications"
+CHAT_URL = "https://apps.abacus.ai/api/_chatLLMSendMessageSSE"
+USER_INFO_URL = "https://abacus.ai/api/v0/_getUserInfo"
+COMPUTE_POINTS_URL = "https://apps.abacus.ai/api/_getOrganizationComputePoints"
+COMPUTE_POINTS_LOG_URL = "https://abacus.ai/api/v0/_getOrganizationComputePointLog"
+CREATE_CONVERSATION_URL = "https://apps.abacus.ai/api/createDeploymentConversation"
+DELETE_CONVERSATION_URL = "https://apps.abacus.ai/api/deleteDeploymentConversation"
+GET_CONVERSATION_URL = "https://apps.abacus.ai/api/getDeploymentConversation"
+COMPUTE_POINT_TOGGLE_URL = "https://abacus.ai/api/v0/_updateOrganizationComputePointToggle"
+
+
+USER_AGENTS = [
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
+]
+
+
+PASSWORD = None
+USER_NUM = 0
+USER_DATA = []
+CURRENT_USER = -1
+MODELS = set()
+
+# 添加线程锁用于保护 CURRENT_USER 的访问
+user_selection_lock = threading.Lock()
+
+TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
+SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
+
+
+# 添加一个计数器记录健康检查次数
+health_check_counter = 0
+
+
+# 添加统计变量
+model_usage_stats = {} # 模型使用次数统计
+total_tokens = {
+ "prompt": 0, # 输入token统计
+ "completion": 0, # 输出token统计
+ "total": 0 # 总token统计
+}
+
+# 模型调用记录
+model_usage_records = [] # 每次调用详细记录
+MODEL_USAGE_RECORDS_FILE = "model_usage_records.json" # 调用记录保存文件
+
+# 计算点信息
+compute_points = {
+ "left": 0, # 剩余计算点
+ "total": 0, # 总计算点
+ "used": 0, # 已使用计算点
+ "percentage": 0, # 使用百分比
+ "last_update": None # 最后更新时间
+}
+
+# 计算点使用日志
+compute_points_log = {
+ "columns": {}, # 列名
+ "log": [] # 日志数据
+}
+
+# 多用户计算点信息
+users_compute_points = []
+
+# 记录启动时间
+START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
+
+
+# 自定义JSON编码器,处理datetime对象
+class DateTimeEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, datetime):
+ return obj.strftime('%Y-%m-%d %H:%M:%S')
+ return super(DateTimeEncoder, self).default(obj)
+
+
+# 加载模型调用记录
+def load_model_usage_records():
+ global model_usage_records
+ try:
+ if os.path.exists(MODEL_USAGE_RECORDS_FILE):
+ with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
+ records = json.load(f)
+ if isinstance(records, list):
+ model_usage_records = records
+ print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
+ else:
+ print("调用记录文件格式不正确,初始化为空列表")
+ except Exception as e:
+ print(f"加载模型调用记录失败: {e}")
+ model_usage_records = []
+
+# 保存模型调用记录
+def save_model_usage_records():
+ try:
+ with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
+ json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
+ print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
+ except Exception as e:
+ print(f"保存模型调用记录失败: {e}")
+
+
+def update_conversation_id(user_index, conversation_id):
+ """更新用户的conversation_id并保存到配置文件"""
+ try:
+ with open("config.json", "r") as f:
+ config = json.load(f)
+
+ if "config" in config and user_index < len(config["config"]):
+ config["config"][user_index]["conversation_id"] = conversation_id
+
+ # 保存到配置文件
+ with open("config.json", "w") as f:
+ json.dump(config, f, indent=4)
+
+ print(f"已将用户 {user_index+1} 的conversation_id更新为: {conversation_id}")
+ else:
+ print(f"更新conversation_id失败: 配置文件格式错误或用户索引越界")
+ except Exception as e:
+ print(f"更新conversation_id失败: {e}")
+
+
+def resolve_config():
+ # 从环境变量读取多组配置
+ config_list = []
+ i = 1
+ while True:
+ cookie = os.environ.get(f"cookie_{i}")
+ if not cookie:
+ break
+
+ # 为每个cookie创建一个配置项,conversation_id初始为空
+ config_list.append({
+ "conversation_id": "", # 初始为空,将通过get_or_create_conversation自动创建
+ "cookies": cookie
+ })
+ i += 1
+
+ # 如果环境变量存在配置,使用环境变量的配置
+ if config_list:
+ print(f"从环境变量加载了 {len(config_list)} 个配置")
+ return config_list
+
+ # 如果环境变量不存在,从文件读取
+ try:
+ with open("config.json", "r") as f:
+ config = json.load(f)
+ config_list = config.get("config")
+ return config_list
+ except FileNotFoundError:
+ print("未找到config.json文件")
+ return []
+ except json.JSONDecodeError:
+ print("config.json格式错误")
+ return []
+
+
+def get_password():
+ global PASSWORD
+ # 从环境变量读取密码
+ env_password = os.environ.get("password")
+ if env_password:
+ PASSWORD = hashlib.sha256(env_password.encode()).hexdigest()
+ return
+
+ # 如果环境变量不存在,从文件读取
+ try:
+ with open("password.txt", "r") as f:
+ PASSWORD = f.read().strip()
+ except FileNotFoundError:
+ with open("password.txt", "w") as f:
+ PASSWORD = None
+
+
+def require_auth(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ if not PASSWORD:
+ return f(*args, **kwargs)
+
+ # 检查Flask会话是否已登录
+ if flask_session.get('logged_in'):
+ return f(*args, **kwargs)
+
+ # 如果是API请求,检查Authorization头
+ auth = request.authorization
+ if not auth or not check_auth(auth.token):
+ # 如果是浏览器请求,重定向到登录页面
+ if request.headers.get('Accept', '').find('text/html') >= 0:
+ return redirect(url_for('login'))
+ return jsonify({"error": "Unauthorized access"}), 401
+ return f(*args, **kwargs)
+
+ return decorated
+
+
+def check_auth(token):
+ return hashlib.sha256(token.encode()).hexdigest() == PASSWORD
+
+
+def is_token_expired(token):
+ if not token:
+ return True
+
+ try:
+ # Malkodi tokenon sen validigo de subskribo
+ payload = jwt.decode(token, options={"verify_signature": False})
+ # Akiru eksvalidiĝan tempon, konsideru eksvalidiĝinta 5 minutojn antaŭe
+ return payload.get('exp', 0) - time.time() < 300
+ except:
+ return True
+
+
+def refresh_token(session, cookies):
+ """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-site",
+ "x-abacus-org-host": "apps",
+ "user-agent": random.choice(USER_AGENTS),
+ "origin": "https://apps.abacus.ai",
+ "referer": "https://apps.abacus.ai/",
+ "cookie": cookies
+ }
+
+ try:
+ response = session.post(
+ USER_INFO_URL,
+ headers=headers,
+ json={},
+ cookies=None
+ )
+
+ if response.status_code == 200:
+ response_data = response.json()
+ if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
+ return response_data['result']['sessionToken']
+ else:
+ print(f"刷新token失败: {response_data.get('error', '未知错误')}")
+ return None
+ else:
+ print(f"刷新token失败,状态码: {response.status_code}")
+ return None
+ except Exception as e:
+ print(f"刷新token异常: {e}")
+ return None
+
+
+def get_model_map(session, cookies, session_token):
+ """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-site",
+ "x-abacus-org-host": "apps",
+ "user-agent": random.choice(USER_AGENTS),
+ "origin": "https://apps.abacus.ai",
+ "referer": "https://apps.abacus.ai/",
+ "cookie": cookies
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ model_map = {}
+ models_set = set()
+
+ try:
+ response = session.post(
+ MODEL_LIST_URL,
+ headers=headers,
+ json={},
+ cookies=None
+ )
+
+ if response.status_code != 200:
+ print(f"获取模型列表失败,状态码: {response.status_code}")
+ raise Exception("API请求失败")
+
+ data = response.json()
+ if not data.get('success'):
+ print(f"获取模型列表失败: {data.get('error', '未知错误')}")
+ raise Exception("API返回错误")
+
+ applications = []
+ if isinstance(data.get('result'), dict):
+ applications = data.get('result', {}).get('externalApplications', [])
+ elif isinstance(data.get('result'), list):
+ applications = data.get('result', [])
+
+ for app in applications:
+ app_name = app.get('name', '')
+ app_id = app.get('externalApplicationId', '')
+ prediction_overrides = app.get('predictionOverrides', {})
+ llm_name = prediction_overrides.get('llmName', '') if prediction_overrides else ''
+
+ if not (app_name and app_id and llm_name):
+ continue
+
+ model_name = app_name
+ model_map[model_name] = (app_id, llm_name)
+ models_set.add(model_name)
+
+ if not model_map:
+ raise Exception("未找到任何可用模型")
+
+ return model_map, models_set
+
+ except Exception as e:
+ print(f"获取模型列表异常: {e}")
+ raise
+
+
+def init_session():
+ get_password()
+ global USER_NUM, MODELS, USER_DATA
+
+ config_list = resolve_config()
+ user_num = len(config_list)
+ all_models = set()
+
+ for i in range(user_num):
+ user = config_list[i]
+ cookies = user.get("cookies")
+ conversation_id = user.get("conversation_id")
+ session = requests.Session()
+
+ session_token = refresh_token(session, cookies)
+ if not session_token:
+ print(f"无法获取cookie {i+1}的token")
+ continue
+
+ try:
+ model_map, models_set = get_model_map(session, cookies, session_token)
+ all_models.update(models_set)
+ USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
+
+ # 对第一个成功配置的用户,初始化计算点数记录功能
+ if i == 0:
+ try:
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-site",
+ "x-abacus-org-host": "apps",
+ "session-token": session_token
+ }
+
+ response = session.post(
+ COMPUTE_POINT_TOGGLE_URL,
+ headers=headers,
+ json={"alwaysDisplay": True},
+ cookies=None
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success"):
+ print("成功初始化计算点数记录功能为开启状态")
+ else:
+ print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
+ else:
+ print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
+ except Exception as e:
+ print(f"初始化计算点数记录功能时出错: {e}")
+ except Exception as e:
+ print(f"配置用户 {i+1} 失败: {e}")
+ continue
+
+ USER_NUM = len(USER_DATA)
+ if USER_NUM == 0:
+ print("No user available, exiting...")
+ exit(1)
+
+ MODELS = all_models
+ print(f"启动完成,共配置 {USER_NUM} 个用户")
+
+
+def update_cookie(session, cookies):
+ cookie_jar = {}
+ for key, value in session.cookies.items():
+ cookie_jar[key] = value
+ cookie_dict = {}
+ for item in cookies.split(";"):
+ key, value = item.strip().split("=", 1)
+ cookie_dict[key] = value
+ cookie_dict.update(cookie_jar)
+ cookies = "; ".join([f"{key}={value}" for key, value in cookie_dict.items()])
+ return cookies
+
+
+user_data = init_session()
+
+
+@app.route("/v1/models", methods=["GET"])
+@require_auth
+def get_models():
+ if len(MODELS) == 0:
+ return jsonify({"error": "No models available"}), 500
+ model_list = []
+ for model in MODELS:
+ model_list.append(
+ {
+ "id": model,
+ "object": "model",
+ "created": int(time.time()),
+ "owned_by": "Elbert",
+ "name": model,
+ }
+ )
+ return jsonify({"object": "list", "data": model_list})
+
+
+@app.route("/v1/chat/completions", methods=["POST"])
+@require_auth
+def chat_completions():
+ openai_request = request.get_json()
+ stream = openai_request.get("stream", False)
+ messages = openai_request.get("messages")
+ if messages is None:
+ return jsonify({"error": "Messages is required", "status": 400}), 400
+ model = openai_request.get("model")
+ if model not in MODELS:
+ return (
+ jsonify(
+ {
+ "error": "Model not available, check if it is configured properly",
+ "status": 404,
+ }
+ ),
+ 404,
+ )
+ message = format_message(messages)
+ think = (
+ openai_request.get("think", False) if model == "Claude Sonnet 3.7" else False
+ )
+ return (
+ send_message(message, model, think)
+ if stream
+ else send_message_non_stream(message, model, think)
+ )
+
+
+def get_user_data():
+ global CURRENT_USER
+
+ # 使用锁确保线程安全
+ with user_selection_lock:
+ CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
+ current_user_index_local = CURRENT_USER # 本地副本,避免锁外访问全局变量
+ print(f"使用配置 {current_user_index_local+1}")
+
+ # Akiru uzantajn datumojn (使用本地索引)
+ session, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[current_user_index_local]
+
+ # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
+ if is_token_expired(session_token):
+ print(f"Cookie {current_user_index_local+1}的token已过期或即将过期,正在刷新...")
+ new_token = refresh_token(session, cookies)
+ if new_token:
+ # Ĝisdatigu la globale konservitan tokenon (加锁保护写入)
+ with user_selection_lock:
+ # 重新获取最新的 USER_DATA 状态再更新
+ _session, _cookies, _, _conv_id, _model_map, _user_idx = USER_DATA[current_user_index_local]
+ USER_DATA[current_user_index_local] = (_session, _cookies, new_token, _conv_id, _model_map, _user_idx)
+ session_token = new_token # 更新函数内部使用的token
+ print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
+ else:
+ print(f"警告:无法刷新Cookie {current_user_index_local+1}的token,继续使用当前token")
+
+ # 返回获取到的数据 (使用本地索引)
+ return (session, cookies, session_token, conversation_id, model_map, user_index)
+
+
+def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
+ """创建新的会话"""
+ if not (external_application_id and deployment_id):
+ print("无法创建新会话: 缺少必要参数")
+ return None
+
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "cookie": cookies,
+ "user-agent": random.choice(USER_AGENTS),
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ create_payload = {
+ "deploymentId": deployment_id,
+ "name": "New Chat",
+ "externalApplicationId": external_application_id
+ }
+
+ try:
+ response = session.post(
+ CREATE_CONVERSATION_URL,
+ headers=headers,
+ json=create_payload
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ if data.get("success", False):
+ new_conversation_id = data.get("result", {}).get("deploymentConversationId")
+ if new_conversation_id:
+ print(f"成功创建新的conversation: {new_conversation_id}")
+ return new_conversation_id
+
+ print(f"创建会话失败: {response.status_code} - {response.text[:100]}")
+ return None
+ except Exception as e:
+ print(f"创建会话时出错: {e}")
+ return None
+
+
+def delete_conversation(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
+ """删除指定的对话"""
+ if not conversation_id:
+ print("无法删除对话: 缺少conversation_id")
+ return False
+
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "cookie": cookies,
+ "user-agent": random.choice(USER_AGENTS),
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ delete_payload = {
+ "deploymentId": deployment_id,
+ "deploymentConversationId": conversation_id
+ }
+
+ try:
+ response = session.post(
+ DELETE_CONVERSATION_URL,
+ headers=headers,
+ json=delete_payload
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ if data.get("success", False):
+ print(f"成功删除对话: {conversation_id}")
+ return True
+
+ print(f"删除对话失败: {response.status_code} - {response.text[:100]}")
+ return False
+ except Exception as e:
+ print(f"删除对话时出错: {e}")
+ return False
+
+
+def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
+ """获取对话ID,如果不存在则创建;返回是否是使用现有会话"""
+ print(f"\n----- 获取会话ID (用户 {user_index+1}) -----")
+ # 如果有现有的会话ID,直接使用
+ if conversation_id:
+ print(f"使用现有会话ID: {conversation_id}")
+ return conversation_id, True
+
+ # 如果没有会话ID,创建新的
+ print("没有会话ID,创建新会话...")
+ deployment_id = "14b2a314cc"
+
+ # 确保模型信息存在
+ if model not in model_map or len(model_map[model]) < 2:
+ print(f"错误: 无法获取模型 {model} 的信息")
+ return None, False
+
+ external_app_id = model_map[model][0]
+
+ # 创建新会话
+ new_conversation_id = create_conversation(
+ session, cookies, session_token,
+ external_application_id=external_app_id,
+ deployment_id=deployment_id
+ )
+
+ if new_conversation_id:
+ print(f"成功创建新会话ID: {new_conversation_id}")
+
+ # 更新全局存储的会话ID
+ global USER_DATA, CURRENT_USER
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
+
+ # 保存到配置文件
+ update_conversation_id(user_index, new_conversation_id)
+
+ return new_conversation_id, False
+
+ print("创建新会话失败")
+ return None, False
+
+
+def generate_trace_id():
+ """Generu novan trace_id kaj sentry_trace"""
+ trace_id = str(uuid.uuid4()).replace('-', '')
+ sentry_trace = f"{trace_id}-{str(uuid.uuid4())[:16]}"
+ return trace_id, sentry_trace
+
+
+def send_message(message, model, think=False):
+ """Flua traktado kaj plusendo de mesaĝoj"""
+ print("\n===== 开始处理消息 =====")
+ print(f"模型: {model}")
+ print(f"思考模式: {think}")
+
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
+ print(f"使用用户配置: {user_index + 1}")
+
+ # 获取会话ID,并判断是否使用现有会话
+ conversation_id, is_existing = get_or_create_conversation(
+ session, cookies, session_token, conversation_id, model_map, model, user_index
+ )
+
+ # 如果没有有效的会话ID,返回错误
+ if not conversation_id:
+ return jsonify({"error": "Failed to get a valid conversation ID"}), 500
+
+ print(f"会话ID: {conversation_id} (是否为现有会话: {is_existing})")
+
+ trace_id, sentry_trace = generate_trace_id()
+
+ # 计算输入token
+ completion_buffer = io.StringIO() # 收集所有输出用于计算token
+
+ headers = {
+ "accept": "text/event-stream",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
+ "content-type": "text/plain;charset=UTF-8",
+ "cookie": cookies,
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
+ "sentry-trace": sentry_trace,
+ "user-agent": random.choice(USER_AGENTS),
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ # 构建基础请求
+ payload = {
+ "requestId": str(uuid.uuid4()),
+ "deploymentConversationId": conversation_id,
+ "message": message,
+ "isDesktop": False,
+ "chatConfig": {
+ "timezone": "Asia/Shanghai",
+ "language": "zh-CN"
+ },
+ "llmName": model_map[model][1],
+ "externalApplicationId": model_map[model][0]
+ }
+
+ # 如果是使用现有会话,添加regenerate和editPrompt参数
+ if is_existing:
+ payload["regenerate"] = True
+ payload["editPrompt"] = True
+ print("为现有会话添加 regenerate=True 和 editPrompt=True")
+
+ if think:
+ payload["useThinking"] = think
+
+ try:
+ response = session.post(
+ CHAT_URL,
+ headers=headers,
+ data=json.dumps(payload),
+ stream=True,
+ cookies=None
+ )
+
+ response.raise_for_status()
+
+ def extract_segment(line_data):
+ try:
+ data = json.loads(line_data)
+ if "segment" in data:
+ if isinstance(data["segment"], str):
+ return data["segment"]
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
+ return data["segment"]["segment"]
+ return ""
+ except:
+ return ""
+
+ def generate():
+ id = ""
+ think_state = 2
+
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
+
+ for line in response.iter_lines():
+ if line:
+ decoded_line = line.decode("utf-8")
+ try:
+ if think:
+ data = json.loads(decoded_line)
+ if data.get("type") != "text":
+ continue
+ elif think_state == 2:
+ id = data.get("messageId")
+ segment = "\n" + data.get("segment", "")
+ completion_buffer.write(segment) # 收集输出
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
+ think_state = 1
+ elif think_state == 1:
+ if data.get("messageId") != id:
+ segment = data.get("segment", "")
+ completion_buffer.write(segment)
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
+ else:
+ segment = "\n\n" + data.get("segment", "")
+ completion_buffer.write(segment)
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
+ think_state = 0
+ else:
+ segment = data.get("segment", "")
+ completion_buffer.write(segment)
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
+ else:
+ segment = extract_segment(decoded_line)
+ if segment:
+ completion_buffer.write(segment)
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
+ except Exception as e:
+ print(f"处理响应出错: {e}")
+
+ # ---- Token 计算移到这里 ----
+ print("\n----- 计算 Tokens (流式结束后) -----")
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
+ print(f"输入 token 数: {prompt_tokens} (方法: {calculation_method})")
+
+ completion_content = completion_buffer.getvalue()
+ completion_tokens, comp_calc_method = num_tokens_from_string(completion_content, model)
+ print(f"输出 token 数: {completion_tokens} (方法: {comp_calc_method})")
+
+ # 决定最终使用的计算方法 (优先使用精确)
+ final_calculation_method = "精确" if calculation_method == "精确" and comp_calc_method == "精确" else "估算"
+ # ---- Token 计算结束 ----
+
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
+ yield "data: [DONE]\n\n"
+
+ # 在流式传输完成后计算token并更新统计
+ # 注意: 如果客户端在流结束前断开连接,这里的 completion_content 可能不完整,
+ # 导致 completion_tokens 和 total_tokens 的本地记录不准确。
+ # 但 Abacus 的计算点数扣除通常在其服务端完成,不受此影响。
+
+ # 保存对话历史并获取计算点数
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
+
+ # 更新统计信息
+ update_model_stats(model, prompt_tokens, completion_tokens, final_calculation_method, compute_points_used)
+
+ return Response(generate(), mimetype="text/event-stream")
+ except requests.exceptions.RequestException as e:
+ error_details = str(e)
+ if hasattr(e, 'response') and e.response is not None:
+ if hasattr(e.response, 'text'):
+ error_details += f" - Response: {e.response.text[:200]}"
+ print(f"发送消息失败: {error_details}")
+
+ # 如果是使用现有会话失败,尝试创建新会话重试一次
+ if is_existing:
+ print("使用现有会话失败,尝试创建新会话...")
+ # 创建新会话
+ deployment_id = "14b2a314cc"
+ external_app_id = model_map[model][0] if model in model_map and len(model_map[model]) >= 2 else None
+
+ if external_app_id:
+ new_conversation_id = create_conversation(
+ session, cookies, session_token,
+ external_application_id=external_app_id,
+ deployment_id=deployment_id
+ )
+
+ if new_conversation_id:
+ print(f"成功创建新会话ID: {new_conversation_id},重试请求")
+ # 更新全局存储的会话ID
+ global USER_DATA, CURRENT_USER
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
+
+ # 保存到配置文件
+ update_conversation_id(user_index, new_conversation_id)
+
+ # 修改payload使用新会话ID,并移除regenerate和editPrompt
+ payload["deploymentConversationId"] = new_conversation_id
+ if "regenerate" in payload:
+ del payload["regenerate"]
+ if "editPrompt" in payload:
+ del payload["editPrompt"]
+
+ try:
+ # 非流式重试逻辑与流式类似,但需要重新提取响应内容
+ response = session.post(
+ CHAT_URL,
+ headers=headers,
+ data=json.dumps(payload),
+ stream=True,
+ cookies=None
+ )
+
+ response.raise_for_status()
+ # 重用现有提取逻辑...
+ # 但这里代码重复太多,实际应该重构为共享函数
+ buffer = io.StringIO()
+
+ for line in response.iter_lines():
+ if line:
+ decoded_line = line.decode("utf-8")
+ segment = extract_segment(decoded_line)
+ if segment:
+ buffer.write(segment)
+
+ response_content = buffer.getvalue()
+
+ # ---- 重试逻辑中的 Token 计算 ----
+ print("\n----- 计算 Tokens (重试成功后) -----")
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
+ print(f"输入 token 数: {prompt_tokens} (方法: {calculation_method})")
+
+ # 计算输出token并更新统计信息
+ completion_tokens, comp_calc_method = num_tokens_from_string(response_content, model)
+ print(f"输出 token 数: {completion_tokens} (方法: {comp_calc_method})")
+
+ # 决定最终使用的计算方法
+ final_calculation_method = "精确" if calculation_method == "精确" and comp_calc_method == "精确" else "估算"
+ # ---- Token 计算结束 ----
+
+ # 保存对话历史并获取计算点数
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
+
+ # 更新统计信息
+ # 注意: 重试逻辑。Token 计算准确性依赖于 response 完整性。
+ update_model_stats(model, prompt_tokens, completion_tokens, final_calculation_method, compute_points_used)
+
+ return jsonify({
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
+ "object": "chat.completion",
+ "created": int(time.time()),
+ "model": model,
+ "choices": [{
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": response_content
+ },
+ "finish_reason": "stop"
+ }],
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": prompt_tokens + completion_tokens
+ }
+ })
+ except Exception as retry_e:
+ print(f"重试失败: {retry_e}")
+
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
+
+
+def send_message_non_stream(message, model, think=False):
+ """Ne-flua traktado de mesaĝoj"""
+ print("\n===== 开始处理消息(非流式) =====")
+ print(f"模型: {model}")
+ print(f"思考模式: {think}")
+
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
+ print(f"使用用户配置: {user_index + 1}")
+
+ # 获取会话ID,并判断是否使用现有会话
+ conversation_id, is_existing = get_or_create_conversation(
+ session, cookies, session_token, conversation_id, model_map, model, user_index
+ )
+
+ # 如果没有有效的会话ID,返回错误
+ if not conversation_id:
+ return jsonify({"error": "Failed to get a valid conversation ID"}), 500
+
+ print(f"会话ID: {conversation_id} (是否为现有会话: {is_existing})")
+
+ trace_id, sentry_trace = generate_trace_id()
+
+ headers = {
+ "accept": "text/event-stream",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
+ "content-type": "text/plain;charset=UTF-8",
+ "cookie": cookies,
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
+ "sentry-trace": sentry_trace,
+ "user-agent": random.choice(USER_AGENTS),
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ # 构建基础请求
+ payload = {
+ "requestId": str(uuid.uuid4()),
+ "deploymentConversationId": conversation_id,
+ "message": message,
+ "isDesktop": False,
+ "chatConfig": {
+ "timezone": "Asia/Shanghai",
+ "language": "zh-CN"
+ },
+ "llmName": model_map[model][1],
+ "externalApplicationId": model_map[model][0]
+ }
+
+ # 如果是使用现有会话,添加regenerate和editPrompt参数
+ if is_existing:
+ payload["regenerate"] = True
+ payload["editPrompt"] = True
+ print("为现有会话添加 regenerate=True 和 editPrompt=True")
+
+ if think:
+ payload["useThinking"] = think
+
+ try:
+ response = session.post(
+ CHAT_URL,
+ headers=headers,
+ data=json.dumps(payload),
+ stream=True,
+ cookies=None
+ )
+
+ response.raise_for_status()
+ buffer = io.StringIO()
+
+ def extract_segment(line_data):
+ try:
+ data = json.loads(line_data)
+ if "segment" in data:
+ if isinstance(data["segment"], str):
+ return data["segment"]
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
+ return data["segment"]["segment"]
+ return ""
+ except:
+ return ""
+
+ if think:
+ id = ""
+ think_state = 2
+ think_buffer = io.StringIO()
+ content_buffer = io.StringIO()
+
+ for line in response.iter_lines():
+ if line:
+ decoded_line = line.decode("utf-8")
+ try:
+ data = json.loads(decoded_line)
+ if data.get("type") != "text":
+ continue
+ elif think_state == 2:
+ id = data.get("messageId")
+ segment = data.get("segment", "")
+ think_buffer.write(segment)
+ think_state = 1
+ elif think_state == 1:
+ if data.get("messageId") != id:
+ segment = data.get("segment", "")
+ content_buffer.write(segment)
+ else:
+ segment = data.get("segment", "")
+ think_buffer.write(segment)
+ think_state = 0
+ else:
+ segment = data.get("segment", "")
+ content_buffer.write(segment)
+ except Exception as e:
+ print(f"处理响应出错: {e}")
+
+ think_content = think_buffer.getvalue()
+ response_content = content_buffer.getvalue()
+
+ # ---- Token 计算移到这里 ----
+ print("\n----- 计算 Tokens (非流式, think模式) -----")
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
+ print(f"输入 token 数: {prompt_tokens} (方法: {calculation_method})")
+ # 计算输出token并更新统计信息
+ completion_tokens, comp_calc_method = num_tokens_from_string(think_content + response_content, model)
+ print(f"输出 token 数: {completion_tokens} (方法: {comp_calc_method})")
+ # 决定最终使用的计算方法
+ final_calculation_method = "精确" if calculation_method == "精确" and comp_calc_method == "精确" else "估算"
+ # ---- Token 计算结束 ----
+
+ # 保存对话历史并获取计算点数
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
+
+ # 更新统计信息
+ # 注意: 如果客户端在请求完成前断开连接(理论上非流式不太可能,但网络异常可能发生),
+ # Token 计算的准确性取决于 response 是否完整接收。Abacus 点数扣除不受影响。
+ update_model_stats(model, prompt_tokens, completion_tokens, final_calculation_method, compute_points_used)
+
+ return jsonify({
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
+ "object": "chat.completion",
+ "created": int(time.time()),
+ "model": model,
+ "choices": [{
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": f"\n{think_content}\n\n{response_content}"
+ },
+ "finish_reason": "stop"
+ }],
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": prompt_tokens + completion_tokens
+ }
+ })
+ else:
+ for line in response.iter_lines():
+ if line:
+ decoded_line = line.decode("utf-8")
+ segment = extract_segment(decoded_line)
+ if segment:
+ buffer.write(segment)
+
+ response_content = buffer.getvalue()
+
+ # ---- Token 计算移到这里 ----
+ print("\n----- 计算 Tokens (非流式) -----")
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
+ print(f"输入 token 数: {prompt_tokens} (方法: {calculation_method})")
+ # 计算输出token并更新统计信息
+ completion_tokens, comp_calc_method = num_tokens_from_string(response_content, model)
+ print(f"输出 token 数: {completion_tokens} (方法: {comp_calc_method})")
+ # 决定最终使用的计算方法
+ final_calculation_method = "精确" if calculation_method == "精确" and comp_calc_method == "精确" else "估算"
+ # ---- Token 计算结束 ----
+
+ # 保存对话历史并获取计算点数
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
+
+ # 更新统计信息
+ # 注意: 如果客户端在请求完成前断开连接(理论上非流式不太可能,但网络异常可能发生),
+ # Token 计算的准确性取决于 response 是否完整接收。Abacus 点数扣除不受影响。
+ update_model_stats(model, prompt_tokens, completion_tokens, final_calculation_method, compute_points_used)
+
+ return jsonify({
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
+ "object": "chat.completion",
+ "created": int(time.time()),
+ "model": model,
+ "choices": [{
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": response_content
+ },
+ "finish_reason": "stop"
+ }],
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": prompt_tokens + completion_tokens
+ }
+ })
+ except requests.exceptions.RequestException as e:
+ error_details = str(e)
+ if hasattr(e, 'response') and e.response is not None:
+ if hasattr(e.response, 'text'):
+ error_details += f" - Response: {e.response.text[:200]}"
+ print(f"发送消息失败: {error_details}")
+
+ # 如果是使用现有会话失败,尝试创建新会话重试一次
+ if is_existing:
+ print("使用现有会话失败,尝试创建新会话...")
+ # 创建新会话
+ deployment_id = "14b2a314cc"
+ external_app_id = model_map[model][0] if model in model_map and len(model_map[model]) >= 2 else None
+
+ if external_app_id:
+ new_conversation_id = create_conversation(
+ session, cookies, session_token,
+ external_application_id=external_app_id,
+ deployment_id=deployment_id
+ )
+
+ if new_conversation_id:
+ print(f"成功创建新会话ID: {new_conversation_id},重试请求")
+ # 更新全局存储的会话ID
+ global USER_DATA, CURRENT_USER
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
+
+ # 保存到配置文件
+ update_conversation_id(user_index, new_conversation_id)
+
+ # 修改payload使用新会话ID,并移除regenerate和editPrompt
+ payload["deploymentConversationId"] = new_conversation_id
+ if "regenerate" in payload:
+ del payload["regenerate"]
+ if "editPrompt" in payload:
+ del payload["editPrompt"]
+
+ try:
+ # 非流式重试逻辑与流式类似,但需要重新提取响应内容
+ response = session.post(
+ CHAT_URL,
+ headers=headers,
+ data=json.dumps(payload),
+ stream=True,
+ cookies=None
+ )
+
+ response.raise_for_status()
+ # 重用现有提取逻辑...
+ # 但这里代码重复太多,实际应该重构为共享函数
+ buffer = io.StringIO()
+
+ for line in response.iter_lines():
+ if line:
+ decoded_line = line.decode("utf-8")
+ segment = extract_segment(decoded_line)
+ if segment:
+ buffer.write(segment)
+
+ response_content = buffer.getvalue()
+
+ # ---- 重试逻辑中的 Token 计算 ----
+ print("\n----- 计算 Tokens (重试成功后) -----")
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
+ print(f"输入 token 数: {prompt_tokens} (方法: {calculation_method})")
+
+ # 计算输出token并更新统计信息
+ completion_tokens, comp_calc_method = num_tokens_from_string(response_content, model)
+ print(f"输出 token 数: {completion_tokens} (方法: {comp_calc_method})")
+
+ # 决定最终使用的计算方法
+ final_calculation_method = "精确" if calculation_method == "精确" and comp_calc_method == "精确" else "估算"
+ # ---- Token 计算结束 ----
+
+ # 保存对话历史并获取计算点数
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
+
+ # 更新统计信息
+ # 注意: 重试逻辑。Token 计算准确性依赖于 response 完整性。
+ update_model_stats(model, prompt_tokens, completion_tokens, final_calculation_method, compute_points_used)
+
+ return jsonify({
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
+ "object": "chat.completion",
+ "created": int(time.time()),
+ "model": model,
+ "choices": [{
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": response_content
+ },
+ "finish_reason": "stop"
+ }],
+ "usage": {
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "total_tokens": prompt_tokens + completion_tokens
+ }
+ })
+ except Exception as retry_e:
+ print(f"重试失败: {retry_e}")
+
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
+
+
+def format_message(messages):
+ buffer = io.StringIO()
+ role_map, prefix, messages = extract_role(messages)
+ for message in messages:
+ role = message.get("role")
+ role = "\b" + role_map[role] if prefix else role_map[role]
+ content = message.get("content").replace("\\n", "\n")
+ pattern = re.compile(r"<\|removeRole\|>\n")
+ if pattern.match(content):
+ content = pattern.sub("", content)
+ buffer.write(f"{content}\n")
+ else:
+ buffer.write(f"{role}: {content}\n\n")
+ formatted_message = buffer.getvalue()
+ return formatted_message
+
+
+def extract_role(messages):
+ role_map = {"user": "Human", "assistant": "Assistant", "system": "System"}
+ prefix = True # 默认添加前缀
+ first_message = messages[0]["content"]
+ pattern = re.compile(
+ r"""
+ \s*
+ (?:user:\s*(?P[^\n]*)\s*)? # Make user optional
+ (?:assistant:\s*(?P[^\n]*)\s*)? # Make assistant optional
+ (?:system:\s*(?P[^\n]*)\s*)? # Make system optional
+ (?:prefix:\s*(?P[^\n]*)\s*)? # Make prefix optional
+ \n
+ """,
+ re.VERBOSE,
+ )
+ match = pattern.search(first_message)
+ if match:
+ # 更新 role_map 如果提供了值
+ user_role = match.group("user")
+ assistant_role = match.group("assistant")
+ system_role = match.group("system")
+ if user_role: role_map["user"] = user_role
+ if assistant_role: role_map["assistant"] = assistant_role
+ if system_role: role_map["system"] = system_role
+
+ # 检查 prefix 值:仅当显式设置为非 "1" 时才将 prefix 设为 False
+ prefix_value = match.group("prefix")
+ if prefix_value is not None and prefix_value != "1":
+ prefix = False
+ # 如果 prefix_value 是 None (标签不存在) 或 "1", prefix 保持 True
+
+ messages[0]["content"] = pattern.sub("", first_message)
+ print(f"Extracted role map:")
+ print(
+ f"User: {role_map['user']}, Assistant: {role_map['assistant']}, System: {role_map['system']}"
+ )
+ print(f"Using prefix: {prefix}") # 打印语句保持不变,反映最终结果
+ # 如果没有匹配到 ,prefix 保持默认值 True
+ return (role_map, prefix, messages)
+
+
+@app.route("/health", methods=["GET"])
+def health_check():
+ global health_check_counter
+ health_check_counter += 1
+ return jsonify({
+ "status": "healthy",
+ "timestamp": datetime.now().isoformat(),
+ "checks": health_check_counter
+ })
+
+
+def keep_alive():
+ """每20分钟进行一次自我健康检查"""
+ while True:
+ try:
+ requests.get("http://127.0.0.1:7860/health")
+ time.sleep(1200) # 20分钟
+ except:
+ pass # 忽略错误,保持运行
+
+
+@app.route("/", methods=["GET"])
+def index():
+ # 如果需要密码且用户未登录,重定向到登录页面
+ if PASSWORD and not flask_session.get('logged_in'):
+ return redirect(url_for('login'))
+
+ # 否则重定向到仪表盘
+ return redirect(url_for('dashboard'))
+
+
+def num_tokens_from_string(string, model=""):
+ try:
+ print("\n===================== 开始计算token =====================")
+ print(f"模型: {model}")
+ print(f"输入内容长度: {len(string)} 字符")
+
+ request_data = {
+ "model": model,
+ "messages": [{"role": "user", "content": string}]
+ }
+ print(f"发送请求到tokenizer服务: {TOKENIZER_SERVICE_URL}")
+ print(f"请求数据: {json.dumps(request_data, ensure_ascii=False)}")
+
+ response = requests.post(
+ TOKENIZER_SERVICE_URL,
+ json=request_data,
+ timeout=10
+ )
+
+ print(f"\nTokenizer响应状态码: {response.status_code}")
+ print(f"Tokenizer响应内容: {response.text}")
+
+ if response.status_code == 200:
+ result = response.json()
+ input_tokens = result.get("input_tokens", 0)
+ print(f"\n成功获取token数: {input_tokens}")
+ print(f"使用计算方法: 精确")
+ print("===================== 计算完成 =====================\n")
+ return input_tokens, "精确"
+ else:
+ estimated_tokens = len(string) // 4
+ print(f"\nTokenizer服务错误: {response.status_code}")
+ print(f"错误响应: {response.text}")
+ print(f"使用估算token数: {estimated_tokens}")
+ print(f"使用计算方法: 估算")
+ print("===================== 计算完成 =====================\n")
+ return estimated_tokens, "估算"
+ except Exception as e:
+ estimated_tokens = len(string) // 4
+ print(f"\n计算token时发生错误: {str(e)}")
+ print(f"使用估算token数: {estimated_tokens}")
+ print(f"使用计算方法: 估算")
+ print("===================== 计算完成 =====================\n")
+ return estimated_tokens, "估算"
+
+
+# 更新模型使用统计
+def update_model_stats(model, prompt_tokens, completion_tokens, calculation_method="estimate", compute_points=None):
+ global model_usage_stats, total_tokens, model_usage_records
+
+ # 添加调用记录
+ # 获取UTC时间
+ utc_now = datetime.utcnow()
+ # 转换为北京时间 (UTC+8)
+ beijing_time = utc_now + timedelta(hours=8)
+ call_time = beijing_time.strftime('%Y-%m-%d %H:%M:%S') # 北京时间
+
+ record = {
+ "model": model,
+ "call_time": call_time,
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ "calculation_method": calculation_method, # 直接使用传入的值
+ "compute_points": compute_points
+ }
+ model_usage_records.append(record)
+
+ # 限制记录数量,保留最新的500条
+ if len(model_usage_records) > 500:
+ model_usage_records.pop(0)
+
+ # 保存调用记录到本地文件
+ save_model_usage_records()
+
+ # 更新聚合统计
+ if model not in model_usage_stats:
+ model_usage_stats[model] = {
+ "count": 0,
+ "prompt_tokens": 0,
+ "completion_tokens": 0,
+ "total_tokens": 0
+ }
+
+ model_usage_stats[model]["count"] += 1
+ model_usage_stats[model]["prompt_tokens"] += prompt_tokens
+ model_usage_stats[model]["completion_tokens"] += completion_tokens
+ model_usage_stats[model]["total_tokens"] += (prompt_tokens + completion_tokens)
+
+ total_tokens["prompt"] += prompt_tokens
+ total_tokens["completion"] += completion_tokens
+ total_tokens["total"] += (prompt_tokens + completion_tokens)
+
+
+# 获取计算点信息
+def get_compute_points():
+ global compute_points, USER_DATA, users_compute_points
+
+ if USER_NUM == 0:
+ return
+
+ # 清空用户计算点列表
+ users_compute_points = []
+
+ # 累计总计算点
+ total_left = 0
+ total_points = 0
+
+ # 获取每个用户的计算点信息
+ for i, user_data in enumerate(USER_DATA):
+ try:
+ session, cookies, session_token, _, _, _ = user_data
+
+ # 检查token是否有效
+ if is_token_expired(session_token):
+ session_token = refresh_token(session, cookies)
+ if not session_token:
+ print(f"用户{i+1}刷新token失败,无法获取计算点信息")
+ continue
+ USER_DATA[i] = (session, cookies, session_token, user_data[3], user_data[4], i)
+
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "baggage": f"sentry-environment=production,sentry-release=93da8385541a6ce339b1f41b0c94428c70657e22,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
+ "sentry-trace": SENTRY_TRACE,
+ "session-token": session_token,
+ "x-abacus-org-host": "apps",
+ "cookie": cookies
+ }
+
+ response = session.get(
+ COMPUTE_POINTS_URL,
+ headers=headers
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") and "result" in result:
+ data = result["result"]
+ left = data.get("computePointsLeft", 0)
+ total = data.get("totalComputePoints", 0)
+ used = total - left
+ percentage = round((used / total) * 100, 2) if total > 0 else 0
+
+ # 获取北京时间
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
+
+ # 添加到用户列表
+ user_points = {
+ "user_id": i + 1, # 用户ID从1开始
+ "left": left,
+ "total": total,
+ "used": used,
+ "percentage": percentage,
+ "last_update": beijing_now
+ }
+ users_compute_points.append(user_points)
+
+ # 累计总数
+ total_left += left
+ total_points += total
+
+ print(f"用户{i+1}计算点信息更新成功: 剩余 {left}, 总计 {total}")
+
+ # 对于第一个用户,获取计算点使用日志
+ if i == 0:
+ get_compute_points_log(session, cookies, session_token)
+ else:
+ print(f"获取用户{i+1}计算点信息失败: {result.get('error', '未知错误')}")
+ else:
+ print(f"获取用户{i+1}计算点信息失败,状态码: {response.status_code}")
+ except Exception as e:
+ print(f"获取用户{i+1}计算点信息异常: {e}")
+
+ # 更新全局计算点信息(所有用户总和)
+ if users_compute_points:
+ compute_points["left"] = total_left
+ compute_points["total"] = total_points
+ compute_points["used"] = total_points - total_left
+ compute_points["percentage"] = round((compute_points["used"] / compute_points["total"]) * 100, 2) if compute_points["total"] > 0 else 0
+ compute_points["last_update"] = datetime.utcnow() + timedelta(hours=8) # 北京时间
+ print(f"所有用户计算点总计: 剩余 {total_left}, 总计 {total_points}")
+
+# 获取计算点使用日志
+def get_compute_points_log(session, cookies, session_token):
+ global compute_points_log
+
+ try:
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-site",
+ "session-token": session_token,
+ "x-abacus-org-host": "apps",
+ "cookie": cookies
+ }
+
+ response = session.post(
+ COMPUTE_POINTS_LOG_URL,
+ headers=headers,
+ json={"byLlm": True}
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") and "result" in result:
+ data = result["result"]
+ compute_points_log["columns"] = data.get("columns", {})
+ compute_points_log["log"] = data.get("log", [])
+ print(f"计算点使用日志更新成功,获取到 {len(compute_points_log['log'])} 条记录")
+ else:
+ print(f"获取计算点使用日志失败: {result.get('error', '未知错误')}")
+ else:
+ print(f"获取计算点使用日志失败,状态码: {response.status_code}")
+ except Exception as e:
+ print(f"获取计算点使用日志异常: {e}")
+
+
+# 添加登录相关路由
+@app.route("/login", methods=["GET", "POST"])
+def login():
+ error = None
+ if request.method == "POST":
+ password = request.form.get("password")
+ if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
+ flask_session['logged_in'] = True
+ flask_session.permanent = True
+ return redirect(url_for('dashboard'))
+ else:
+ # 密码错误时提示使用环境变量密码
+ error = "密码不正确。请使用设置的环境变量 password 或 password.txt 中的值作为密码和API认证密钥。"
+
+ # 传递空间URL给模板
+ return render_template('login.html', error=error, space_url=SPACE_URL)
+
+
+@app.route("/logout")
+def logout():
+ flask_session.clear()
+ return redirect(url_for('login'))
+
+
+@app.route("/dashboard")
+@require_auth
+def dashboard():
+ # 在每次访问仪表盘时更新计算点信息
+ get_compute_points()
+
+ # 计算运行时间(使用北京时间)
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
+ uptime = beijing_now - START_TIME
+ days = uptime.days
+ hours, remainder = divmod(uptime.seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+
+ if days > 0:
+ uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
+ elif hours > 0:
+ uptime_str = f"{hours}小时 {minutes}分钟"
+ else:
+ uptime_str = f"{minutes}分钟 {seconds}秒"
+
+ # 当前北京年份
+ beijing_year = beijing_now.year
+
+ return render_template(
+ 'dashboard.html',
+ uptime=uptime_str,
+ health_checks=health_check_counter,
+ user_count=USER_NUM,
+ models=sorted(list(MODELS)),
+ year=beijing_year,
+ model_stats=model_usage_stats,
+ total_tokens=total_tokens,
+ compute_points=compute_points,
+ compute_points_log=compute_points_log,
+ space_url=SPACE_URL, # 传递空间URL
+ users_compute_points=users_compute_points, # 传递用户计算点信息
+ model_usage_records=model_usage_records, # 传递模型使用记录
+ )
+
+
+# 添加更新计算点数记录设置的路由
+@app.route("/update_compute_point_toggle", methods=["POST"])
+@require_auth
+def update_compute_point_toggle():
+ try:
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
+ data = request.get_json()
+ if data and "always_display" in data:
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "content-type": "application/json",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-site",
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ response = session.post(
+ COMPUTE_POINT_TOGGLE_URL,
+ headers=headers,
+ json={"alwaysDisplay": data["always_display"]},
+ cookies=None
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success"):
+ print(f"更新计算点数记录设置为: {data['always_display']}")
+ return jsonify({"success": True})
+
+ return jsonify({"success": False, "error": "API调用失败"})
+ else:
+ return jsonify({"success": False, "error": "缺少always_display参数"})
+ except Exception as e:
+ print(f"更新计算点数记录设置失败: {e}")
+ return jsonify({"success": False, "error": str(e)})
+
+
+# 获取Hugging Face Space URL
+def get_space_url():
+ # 尝试从环境变量获取
+ space_url = os.environ.get("SPACE_URL")
+ if space_url:
+ return space_url
+
+ # 如果SPACE_URL不存在,尝试从SPACE_ID构建
+ space_id = os.environ.get("SPACE_ID")
+ if space_id:
+ username, space_name = space_id.split("/")
+ # 将空间名称中的下划线替换为连字符
+ # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
+ # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
+ space_name = space_name.replace("_", "-")
+ return f"https://{username}-{space_name}.hf.space"
+
+ # 如果以上都不存在,尝试从单独的用户名和空间名构建
+ username = os.environ.get("SPACE_USERNAME")
+ space_name = os.environ.get("SPACE_NAME")
+ if username and space_name:
+ # 将空间名称中的下划线替换为连字符
+ # 同上,Hugging Face会自动进行此转换
+ space_name = space_name.replace("_", "-")
+ return f"https://{username}-{space_name}.hf.space"
+
+ # 默认返回None
+ return None
+
+# 获取空间URL
+SPACE_URL = get_space_url()
+if SPACE_URL:
+ print(f"Space URL: {SPACE_URL}")
+ print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
+
+
+def save_conversation_history(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
+ """保存对话历史,返回使用的计算点数"""
+ if not conversation_id:
+ return False, None
+
+ headers = {
+ "accept": "application/json, text/plain, */*",
+ "accept-language": "zh-CN,zh;q=0.9",
+ "baggage": f"sentry-environment=production,sentry-release=946244517de08b08598b94f18098411f5a5352d5,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
+ "reai-ui": "1",
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"Windows\"",
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
+ "sentry-trace": f"{TRACE_ID}-800cb7f4613dec52",
+ "x-abacus-org-host": "apps"
+ }
+
+ if session_token:
+ headers["session-token"] = session_token
+
+ params = {
+ "deploymentId": deployment_id,
+ "deploymentConversationId": conversation_id,
+ "skipDocumentBoundingBoxes": "true",
+ "filterIntermediateConversationEvents": "false",
+ "getUnusedDocumentUploads": "false"
+ }
+
+ try:
+ response = session.get(
+ GET_CONVERSATION_URL,
+ headers=headers,
+ params=params,
+ cookies=None
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ if data.get("success"):
+ # 从最后一条BOT消息中获取计算点数
+ history = data.get("result", {}).get("history", [])
+ compute_points = None
+ for msg in reversed(history):
+ if msg.get("role") == "BOT":
+ compute_points = msg.get("computePointsUsed")
+ break
+ print(f"成功保存对话历史: {conversation_id}, 使用计算点: {compute_points}")
+ return True, compute_points
+ else:
+ print(f"保存对话历史失败: {data.get('error', '未知错误')}")
+ else:
+ print(f"保存对话历史失败,状态码: {response.status_code}")
+ return False, None
+ except Exception as e:
+ print(f"保存对话历史时出错: {e}")
+ return False, None
+
+
+if __name__ == "__main__":
+ # 启动保活线程
+ threading.Thread(target=keep_alive, daemon=True).start()
+
+ # 加载历史模型调用记录
+ load_model_usage_records()
+
+ # 获取初始计算点信息
+ get_compute_points()
+
+ port = int(os.environ.get("PORT", 9876))
+ app.run(port=port, host="0.0.0.0")