malt666 commited on
Commit
26263c8
·
verified ·
1 Parent(s): 2eaae2b

Upload 6 files

Browse files
Files changed (5) hide show
  1. Dockerfile +23 -23
  2. README.md +121 -114
  3. app.py +250 -263
  4. templates/dashboard.html +206 -88
  5. templates/login.html +461 -461
Dockerfile CHANGED
@@ -1,24 +1,24 @@
1
- FROM python:3.11-slim
2
-
3
- # 设置用户为root
4
- USER root
5
-
6
- WORKDIR /app
7
-
8
- COPY requirements.txt .
9
- RUN pip install --no-cache-dir -r requirements.txt
10
-
11
- COPY . .
12
-
13
- # 设置环境变量
14
- ENV HOST=0.0.0.0
15
- ENV PORT=7860
16
-
17
- # 删除敏感文件
18
- RUN rm -f config.json password.txt
19
-
20
- # 暴露端口(Hugging Face默认使用7860端口)
21
- EXPOSE 7860
22
-
23
- # 启动命令
24
  CMD ["python", "app.py"]
 
1
+ FROM python:3.11-slim
2
+
3
+ # 设置用户为root
4
+ USER root
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ # 设置环境变量
14
+ ENV HOST=0.0.0.0
15
+ ENV PORT=7860
16
+
17
+ # 删除敏感文件
18
+ RUN rm -f config.json password.txt
19
+
20
+ # 暴露端口(Hugging Face默认使用7860端口)
21
+ EXPOSE 7860
22
+
23
+ # 启动命令
24
  CMD ["python", "app.py"]
README.md CHANGED
@@ -1,114 +1,121 @@
1
- ---
2
- title: Abacus Chat Proxy
3
- emoji: 🤖
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- sdk_version: "3.9"
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
-
14
- # Abacus Chat Proxy
15
-
16
- > 📢 本项目基于 [orbitoo/abacus_chat_proxy](https://github.com/orbitoo/abacus_chat_proxy) 改进
17
- >
18
- > 特别感谢 orbitoo 大佬提供的原始项目!
19
- >
20
- > 本项目增加了:Docker部署支持、Hugging Face一键部署、自动保活功能等
21
-
22
- 一个用于中转API请求的代理服务器。
23
-
24
- [![Deploy to Hugging Face Spaces](https://huggingface.co/datasets/huggingface/badges/raw/main/deploy-to-spaces-lg.svg)](https://huggingface.co/spaces/malt666/abacus_chat_proxy?duplicate=true)
25
-
26
- ## ⚠️ 警告
27
-
28
- **本地部署方式已失效!**为了适配hugging face,本项目的本地部署方式已不再可用。目前只能通过Hugging Face Spaces部署来使用本代理服务。请使用下方的Hugging Face一键部署方法。
29
-
30
- ## 🚀 快速开始
31
-
32
- ### Hugging Face一键部署
33
-
34
- 1. 点击上方的"Deploy to Hugging Face Spaces"按钮
35
- 2. 登录你的Hugging Face账号(如果还没有,需要注册一个)
36
- 3. 在弹出的页面中设置你的Space名称
37
- 4. 创建完Space后,在Space的Settings -> Repository Secrets中添加以下配置:
38
- - `cookie_1`: 你的cookies字符串
39
- - `password`: (可选)访问密码
40
- 5. 等待自动部署完成即可
41
- 6. **获取API链接**:部署成功后,点击右上角的三个点按钮,在弹出的选项卡里面点击"Embed this Space",然后在弹出的"Embed this Space"界面里的"Direct URL"就是你的访问链接,你可以用这个链接调用API和查看使用情况
42
-
43
- ### 本地运行(已失效)
44
-
45
- > ⚠️ 以下本地运行方法已失效,仅作参考。请使用Hugging Face部署方式。
46
-
47
- #### Windows用户
48
-
49
- 1. 双击运行 `start.bat`
50
- 2. 首次运行选择 `0` 进行配置
51
- 3. 配置完成后选择 `Y` 直接启动,或 `N` 返回菜单
52
- 4. 之后可直接选择 `1` 启动代理
53
- 5. 代理服务器默认运行在 `http://127.0.0.1:9876/`
54
-
55
- #### Linux/macOS用户
56
-
57
- ```bash
58
- # 赋予脚本执行权限
59
- chmod +x start.sh
60
-
61
- # 运行脚本
62
- ./start.sh
63
- ```
64
-
65
- 选项说明同Windows。
66
-
67
- ### 🌐 Hugging Face部署
68
-
69
- 1. Fork本仓库到你的GitHub账号
70
- 2. 在Hugging Face上创建新的Space(选择Docker类型)
71
- 3. 在Space的设置中连接你的GitHub仓库
72
- 4. 在Space的设置中添加以下Secrets:
73
- - 第1组配置:
74
- - `cookie_1`: 第1个cookies字符串
75
- - 第2组配置(如果需要):
76
- - `cookie_2`: 第2个cookies字符串
77
- - 更多配置以此类推(`cookie_3`...)
78
- - `password`: (可选)访问密码
79
- 5. Space会自动部署,服务将在 `https://你的空间名-你的用户名.hf.space` 上运行
80
-
81
- ## ⚙️ 环境要求
82
-
83
- - Python 3.8+
84
- - pip
85
-
86
- ## 📦 依赖
87
-
88
- ```bash
89
- Flask==3.1.0
90
- requests==2.32.3
91
- PyJWT==2.8.0
92
- ```
93
-
94
- ## 📝 配置说明
95
-
96
- ### 本地配置
97
-
98
- 首次运行时,请选择 `0` 进行配置,按照提示填写相关信息。配置文件将保存在 `config.json` 中。
99
-
100
- ### 环境变量配置
101
-
102
- 在Docker或云平台部署时,需要配置以下环境变量:
103
-
104
- - 必需的配置(至少需要一组):
105
- - `cookie_1`: 第1组配置
106
- - `cookie_2`: 第2组配置(可选)
107
- - 以此类推...
108
- - 可选配置:
109
- - `password`: 访问密码
110
-
111
- ## 🔒 安全说明
112
-
113
- - 建议在部署到Hugging Face时设置访问密码
114
- - 在Hugging Face上配置时,请使用Secrets来存储敏感信息
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Abacus Chat Proxy
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ sdk_version: "3.9"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ # Abacus Chat Proxy
15
+
16
+ > 📢 本项目基于 [orbitoo/abacus_chat_proxy](https://github.com/orbitoo/abacus_chat_proxy) 改进
17
+ >
18
+ > 特别感谢 orbitoo 大佬提供的原始项目!
19
+ >
20
+ > 本项目增加了:Docker部署支持、Hugging Face一键部署、自动保活功能等
21
+
22
+ 一个用于中转API请求的代理服务器。
23
+
24
+ #### 注意创建space的时候把private改为public,因为不改为public导致的错误别来找我
25
+
26
+ [![Deploy to Hugging Face Spaces](https://huggingface.co/datasets/huggingface/badges/raw/main/deploy-to-spaces-lg.svg)](https://huggingface.co/spaces/malt666/abacus_chat_proxy?duplicate=true)
27
+
28
+ ## ⚠️ 警告
29
+
30
+ **本地部署方式已失效!**为了适配hugging face,本项目的本地部署方式已不再可用。目前只能通过Hugging Face Spaces部署来使用本代理服务。请使用下方的Hugging Face一键部署方法。
31
+
32
+ ## 🚀 快速开始
33
+
34
+ ### Hugging Face一键部署
35
+
36
+ #### 注意创建space的时候把private改为public,因为不改为public导致的错误别来找我
37
+
38
+ 1. 点击上方的"Deploy to Hugging Face Spaces"按钮
39
+ 2. 登录你的Hugging Face账号��如果还没有,需要注册一个)
40
+ 3. 在弹出的页面中设置你的Space名称,注意创建space的时候把private改为public,因为不改为为public导致的错误别来找我
41
+ 4. 创建完Space后,在Space的Settings -> Repository Secrets中添加以下配置:
42
+ - `cookie_1`: 第一个cookie
43
+ - `cookie_2`: 第二个cookie(如果有)
44
+ - 以此类推...
45
+ - `password`: (必填,最好超过8位数,不然被盗用api导致点数用光,损失自负)api调用密码,也是dashboard登录密码
46
+ 5. 等待自动部署完成即可
47
+ 6. 登录dashboard查看使用情况,space里面是没办法登录的,点击弹出的登录页面的小窗口提示“请点击 https://xyz-abacus-chat-proxy.hf.space 来登录并查看使用情况”的链接来登录,登录密码是你设置的password
48
+ 7. **获取API链接**:部署成功后,网络的登录页面会显示api接口链接;或者你点击右上角的三个点按钮,在弹出的选项卡里面点击"Embed this Space",然后在弹出的"Embed this Space"界面里的"Direct URL"就是你的访问链接,你可以用这个链接调用API和查看使用情况
49
+ 8. api调用:api调用的时候注意密钥填写你的password,不然没办法调用
50
+ ### 本地运行(已失效)
51
+
52
+ > ⚠️ 以下本地运行方法已失效,仅作参考。请使用Hugging Face部署方式。
53
+
54
+ #### Windows用户
55
+
56
+ 1. 双击运行 `start.bat`
57
+ 2. 首次运行选择 `0` 进行配置
58
+ 3. 配置完成后选择 `Y` 直接启动,或 `N` 返回菜单
59
+ 4. 之后可直接选择 `1` 启动代理
60
+ 5. 代理服务器默认运行在 `http://127.0.0.1:9876/`
61
+
62
+ #### Linux/macOS用户
63
+
64
+ ```bash
65
+ # 赋予脚本执行权限
66
+ chmod +x start.sh
67
+
68
+ # 运行脚本
69
+ ./start.sh
70
+ ```
71
+
72
+ 选项说明同Windows。
73
+
74
+ ### 🌐 Hugging Face部署
75
+
76
+ 1. Fork本仓库到你的GitHub账号
77
+ 2. 在Hugging Face上创建新的Space(选择Docker类型)
78
+ 3. 在Space的设置中连接你的GitHub仓库
79
+ 4. Space的设置中添加以下Secrets:
80
+ - 第1组配置:
81
+ - `cookie_1`: 第1个cookies字符串
82
+ - 第2组配置(如果需要):
83
+ - `cookie_2`: 第2个cookies字符串
84
+ - 更多配置以此类推(`cookie_3`...)
85
+ - `password`: (可选)访问密码
86
+ 5. Space会自动部署,服务将在 `https://你的空间名-你的用户名.hf.space` 上运行
87
+
88
+ ## ⚙️ 环境要求
89
+
90
+ - Python 3.8+
91
+ - pip
92
+
93
+ ## 📦 依赖
94
+
95
+ ```bash
96
+ Flask==3.1.0
97
+ requests==2.32.3
98
+ PyJWT==2.8.0
99
+ ```
100
+
101
+ ## 📝 配置说明
102
+
103
+ ### 本地配置
104
+
105
+ 首次运行时,请选择 `0` 进行配置,按照提示填写相关信息。配置文件将保存在 `config.json` 中。
106
+
107
+ ### 环境变量配置
108
+
109
+ 在Docker或云平台部署时,需要配置以下环境变量:
110
+
111
+ - 必需的配置(至少需要一组):
112
+ - `cookie_1`: 第1组配置
113
+ - `cookie_2`: 第2组配置(可选)
114
+ - 以此类推...
115
+ - 可选配置:
116
+ - `password`: 访问密码
117
+
118
+ ## 🔒 安全说明
119
+
120
+ - 建议在部署到Hugging Face时设置访问密码
121
+ - 在Hugging Face上配置时,请使用Secrets来存储敏感信息
app.py CHANGED
@@ -39,10 +39,10 @@ USER_NUM = 0
39
  USER_DATA = []
40
  CURRENT_USER = -1
41
  MODELS = set()
42
- # 添加变量跟踪是否删除上一个对话以及上一个对话ID
43
- DELETE_CHAT = True # 默认启用删除功能
44
- PREVIOUS_CONVERSATION_IDS = {} # 用于跟踪每个用户的上一个conversation_id
45
 
 
 
 
46
 
47
  TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
48
  SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
@@ -62,7 +62,7 @@ total_tokens = {
62
 
63
  # 模型调用记录
64
  model_usage_records = [] # 每次调用详细记录
65
- MODEL_USAGE_RECORDS_FILE = "/app/model_usage_records.json" # 调用记录保存文件,使用/app目录确保HF中持久化
66
 
67
  # 计算点信息
68
  compute_points = {
@@ -85,6 +85,7 @@ users_compute_points = []
85
  # 记录启动时间
86
  START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
87
 
 
88
  # 自定义JSON编码器,处理datetime对象
89
  class DateTimeEncoder(json.JSONEncoder):
90
  def default(self, obj):
@@ -93,6 +94,32 @@ class DateTimeEncoder(json.JSONEncoder):
93
  return super(DateTimeEncoder, self).default(obj)
94
 
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def update_conversation_id(user_index, conversation_id):
97
  """更新用户的conversation_id并保存到配置文件"""
98
  try:
@@ -113,59 +140,39 @@ def update_conversation_id(user_index, conversation_id):
113
  print(f"更新conversation_id失败: {e}")
114
 
115
 
116
- def load_config():
117
- """从配置文件加载配置"""
118
- global USER_DATA, CURRENT_USER
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  try:
120
- # 读取配置文件
121
- with open('config.json', 'r') as f:
122
  config = json.load(f)
123
-
124
- # 确保USER_DATA为空列表
125
- USER_DATA = []
126
-
127
- # 如果配置文件中有配置项,则加载
128
- if "config" in config and isinstance(config["config"], list):
129
- for i, user_config in enumerate(config["config"]):
130
- # 获取必要的配置项
131
- cookies = user_config.get("cookies", "")
132
- models = user_config.get("models", {})
133
-
134
- # 不再从配置文件读取conversation_id,确保每次启动都使用新会话
135
- conversation_id = None
136
-
137
- # 创建会话对象
138
- session = requests.Session()
139
-
140
- # 刷新token
141
- session_token = refresh_token(session, cookies)
142
- if not session_token:
143
- print(f"无法获取用户 {i+1} 的token,跳过该用户")
144
- continue
145
-
146
- try:
147
- # 获取模型映射
148
- model_map, models_set = get_model_map(session, cookies, session_token)
149
-
150
- # 添加到USER_DATA
151
- USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
152
- print(f"用户 {i+1} 配置成功,可用模型: {', '.join(models_set)}")
153
- except Exception as e:
154
- print(f"配置用户 {i+1} 失败: {e}")
155
- continue
156
-
157
- print(f"成功加载了 {len(USER_DATA)} 个用户配置")
158
-
159
- # 设置当前用户为第一个用户
160
- CURRENT_USER = 0
161
-
162
- return True
163
- else:
164
- print("配置文件格式错误")
165
- return False
166
- except Exception as e:
167
- print(f"加载配置失败: {e}")
168
- return False
169
 
170
 
171
  def get_password():
@@ -245,10 +252,11 @@ def refresh_token(session, cookies):
245
  }
246
 
247
  try:
248
- response = requests.post(
249
  USER_INFO_URL,
250
  headers=headers,
251
- json={}
 
252
  )
253
 
254
  if response.status_code == 200:
@@ -293,10 +301,11 @@ def get_model_map(session, cookies, session_token):
293
  models_set = set()
294
 
295
  try:
296
- response = requests.post(
297
  MODEL_LIST_URL,
298
  headers=headers,
299
- json={}
 
300
  )
301
 
302
  if response.status_code != 200:
@@ -337,56 +346,43 @@ def get_model_map(session, cookies, session_token):
337
  raise
338
 
339
 
340
- # 获取Hugging Face Space URL
341
- def get_space_url():
342
- # 尝试从环境变量获取
343
- space_url = os.environ.get("SPACE_URL")
344
- if space_url:
345
- return space_url
346
-
347
- # 如果SPACE_URL不存在,尝试从SPACE_ID构建
348
- space_id = os.environ.get("SPACE_ID")
349
- if space_id:
350
- username, space_name = space_id.split("/")
351
- # 将空间名称中的下划线替换为连字符
352
- # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
353
- # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
354
- space_name = space_name.replace("_", "-")
355
- return f"https://{username}-{space_name}.hf.space"
356
-
357
- # 如果以上都不存在,尝试从单独的用户名和空间名构建
358
- username = os.environ.get("SPACE_USERNAME")
359
- space_name = os.environ.get("SPACE_NAME")
360
- if username and space_name:
361
- # 将空间名称中的下划线替换为连字符
362
- # 同上,Hugging Face会自动进行此转换
363
- space_name = space_name.replace("_", "-")
364
- return f"https://{username}-{space_name}.hf.space"
365
-
366
- # 默认返回None
367
- return None
368
-
369
-
370
  def init_session():
371
  get_password()
372
- global USER_NUM, MODELS, USER_DATA
373
 
374
- # 调用load_config代替resolve_config
375
- if not load_config():
376
- print("配置加载失败,退出...")
377
- exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
- # 更新USER_NUM和MODELS
380
  USER_NUM = len(USER_DATA)
381
  if USER_NUM == 0:
382
  print("No user available, exiting...")
383
  exit(1)
384
 
385
- # 获取所有可用模型
386
- all_models = set()
387
- for session, cookies, session_token, conversation_id, model_map, i in USER_DATA:
388
- all_models.update(model_map.keys())
389
-
390
  MODELS = all_models
391
  print(f"启动完成,共配置 {USER_NUM} 个用户")
392
 
@@ -404,8 +400,7 @@ def update_cookie(session, cookies):
404
  return cookies
405
 
406
 
407
- # 初始化会话
408
- init_session()
409
 
410
 
411
  @app.route("/v1/models", methods=["GET"])
@@ -505,7 +500,7 @@ def create_conversation(session, cookies, session_token, external_application_id
505
  }
506
 
507
  try:
508
- response = requests.post(
509
  CREATE_CONVERSATION_URL,
510
  headers=headers,
511
  json=create_payload
@@ -526,10 +521,10 @@ def create_conversation(session, cookies, session_token, external_application_id
526
  return None
527
 
528
 
529
- def delete_conversation(session, cookies, session_token, conversation_id):
530
- """删除指定的会话"""
531
  if not conversation_id:
532
- print("无法删除会话: conversation_id为空")
533
  return False
534
 
535
  headers = {
@@ -545,12 +540,12 @@ def delete_conversation(session, cookies, session_token, conversation_id):
545
  headers["session-token"] = session_token
546
 
547
  delete_payload = {
548
- "deploymentId": "14b2a314cc",
549
  "deploymentConversationId": conversation_id
550
  }
551
 
552
  try:
553
- response = requests.post(
554
  DELETE_CONVERSATION_URL,
555
  headers=headers,
556
  json=delete_payload
@@ -559,13 +554,13 @@ def delete_conversation(session, cookies, session_token, conversation_id):
559
  if response.status_code == 200:
560
  data = response.json()
561
  if data.get("success", False):
562
- print(f"成功删除conversation: {conversation_id}")
563
  return True
564
 
565
- print(f"删除会话失败: {response.status_code} - {response.text[:100]}")
566
  return False
567
  except Exception as e:
568
- print(f"删除会话时出错: {e}")
569
  return False
570
 
571
 
@@ -600,8 +595,7 @@ def is_conversation_valid(session, cookies, session_token, conversation_id, mode
600
  }
601
 
602
  try:
603
- # 直接发送请求
604
- response = requests.post(
605
  CHAT_URL,
606
  headers=headers,
607
  data=json.dumps(payload),
@@ -625,33 +619,42 @@ def is_conversation_valid(session, cookies, session_token, conversation_id, mode
625
 
626
  def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
627
  """获取有效的会话ID,如果无效则创建新会话"""
628
- # 始终创建新会话,不再检查现有会话是否有效
629
- print("正在创建新会话...")
630
-
631
- if model in model_map and len(model_map[model]) >= 2:
632
- external_app_id = model_map[model][0]
633
- # 创建会话时需要deployment_id,我们先使用一个固定值
634
- # 在实际应用中应从API响应中获取
635
- deployment_id = "14b2a314cc" # 这是从您提供的请求中获取的
636
-
637
- new_conversation_id = create_conversation(
638
- session, cookies, session_token,
639
- external_application_id=external_app_id,
640
- deployment_id=deployment_id
641
- )
642
-
643
- if new_conversation_id:
644
- # 更新全局存储的会话ID
645
- global USER_DATA, CURRENT_USER
646
- session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
647
- USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
648
 
649
- # 不再保存到配置文件,确保每次都创建新会话
650
- # update_conversation_id(user_index, new_conversation_id)
 
 
 
651
 
652
- return new_conversation_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
- # 如果创建失败,返回原始ID
655
  return conversation_id
656
 
657
 
@@ -666,14 +669,11 @@ def send_message(message, model, think=False):
666
  """Flua traktado kaj plusendo de mesaĝoj"""
667
  (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
668
 
669
- # 保存当前用户的上一个对话ID用于稍后删除
670
- previous_conversation_id = PREVIOUS_CONVERSATION_IDS.get(user_index)
671
 
672
- # 确保有有效的会话ID,现在总是会创建新的
673
- new_conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
674
-
675
- # 更新当前用户的会话ID记录
676
- PREVIOUS_CONVERSATION_IDS[user_index] = new_conversation_id
677
 
678
  trace_id, sentry_trace = generate_trace_id()
679
 
@@ -702,7 +702,7 @@ def send_message(message, model, think=False):
702
 
703
  payload = {
704
  "requestId": str(uuid.uuid4()),
705
- "deploymentConversationId": new_conversation_id,
706
  "message": message,
707
  "isDesktop": False,
708
  "chatConfig": {
@@ -719,7 +719,7 @@ def send_message(message, model, think=False):
719
  payload["useThinking"] = think
720
 
721
  try:
722
- response = requests.post(
723
  CHAT_URL,
724
  headers=headers,
725
  data=json.dumps(payload),
@@ -789,9 +789,10 @@ def send_message(message, model, think=False):
789
  completion_tokens = num_tokens_from_string(completion_buffer.getvalue())
790
  update_model_stats(model, prompt_tokens, completion_tokens)
791
 
792
- # 在流式传输完成后删除上一个对话(如果存在且启用了删除功能)
793
- if DELETE_CHAT and previous_conversation_id and previous_conversation_id != new_conversation_id:
794
- delete_conversation(session, cookies, session_token, previous_conversation_id)
 
795
 
796
  return Response(generate(), mimetype="text/event-stream")
797
  except requests.exceptions.RequestException as e:
@@ -807,14 +808,11 @@ def send_message_non_stream(message, model, think=False):
807
  """Ne-flua traktado de mesaĝoj"""
808
  (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
809
 
810
- # 保存当前用户的上一个对话ID用于稍后删除
811
- previous_conversation_id = PREVIOUS_CONVERSATION_IDS.get(user_index)
812
-
813
- # 确保有有效的会话ID,现在总是会创建新的
814
- new_conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
815
 
816
- # 更新当前用户的会话ID记录
817
- PREVIOUS_CONVERSATION_IDS[user_index] = new_conversation_id
818
 
819
  trace_id, sentry_trace = generate_trace_id()
820
 
@@ -842,7 +840,7 @@ def send_message_non_stream(message, model, think=False):
842
 
843
  payload = {
844
  "requestId": str(uuid.uuid4()),
845
- "deploymentConversationId": new_conversation_id,
846
  "message": message,
847
  "isDesktop": False,
848
  "chatConfig": {
@@ -859,7 +857,7 @@ def send_message_non_stream(message, model, think=False):
859
  payload["useThinking"] = think
860
 
861
  try:
862
- response = requests.post(
863
  CHAT_URL,
864
  headers=headers,
865
  data=json.dumps(payload),
@@ -920,9 +918,10 @@ def send_message_non_stream(message, model, think=False):
920
  completion_tokens = num_tokens_from_string(think_content + response_content)
921
  update_model_stats(model, prompt_tokens, completion_tokens)
922
 
923
- # 删除上一个对话(如果存在且启用了删除功能)
924
- if DELETE_CHAT and previous_conversation_id and previous_conversation_id != new_conversation_id:
925
- delete_conversation(session, cookies, session_token, previous_conversation_id)
 
926
 
927
  return jsonify({
928
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -957,9 +956,10 @@ def send_message_non_stream(message, model, think=False):
957
  completion_tokens = num_tokens_from_string(response_content)
958
  update_model_stats(model, prompt_tokens, completion_tokens)
959
 
960
- # 删除上一个对话(如果存在且启用了删除功能)
961
- if DELETE_CHAT and previous_conversation_id and previous_conversation_id != new_conversation_id:
962
- delete_conversation(session, cookies, session_token, previous_conversation_id)
 
963
 
964
  return jsonify({
965
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -1053,14 +1053,10 @@ def keep_alive():
1053
  """每20分钟进行一次自我健康检查"""
1054
  while True:
1055
  try:
1056
- # 获取当前端口
1057
- port = int(os.environ.get("PORT", 9876))
1058
- # 请求本地健康检查端点
1059
- requests.get(f"http://127.0.0.1:{port}/health")
1060
  time.sleep(1200) # 20分钟
1061
  except:
1062
- # 忽略错误,保持运行
1063
- time.sleep(1200) # 即使失败也等待20分钟
1064
 
1065
 
1066
  @app.route("/", methods=["GET"])
@@ -1073,61 +1069,64 @@ def index():
1073
  return redirect(url_for('dashboard'))
1074
 
1075
 
1076
- # 使用tiktoken计算字符串的token数量
1077
- def num_tokens_from_string(text):
1078
- """使用tiktoken计算字符串的token数量"""
1079
  try:
1080
- # 使用cl100k_base编码器,这是gpt-4和gpt-3.5-turbo使用的编码器
1081
- enc = tiktoken.get_encoding("cl100k_base")
1082
- tokens = enc.encode(text)
1083
- return len(tokens)
1084
  except Exception as e:
1085
- # 如果出错,使用字符长度作为粗略估计(大约每4个字符1个token)
1086
- print(f"计算token数量出错: {e},使用估算方法")
1087
- return len(text) // 4
 
1088
 
1089
  # 更新模型使用统计
1090
  def update_model_stats(model, prompt_tokens, completion_tokens):
1091
- """更新模型使用统计数据"""
1092
  global model_usage_stats, total_tokens, model_usage_records
1093
 
1094
- # 获取北京时间
1095
- beijing_now = datetime.utcnow() + timedelta(hours=8)
1096
- call_time = beijing_now.strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1097
 
1098
- # 首次使用该模型时初始化统计
 
 
 
1099
  if model not in model_usage_stats:
1100
  model_usage_stats[model] = {
1101
  "count": 0,
1102
  "prompt_tokens": 0,
1103
- "completion_tokens": 0
 
1104
  }
1105
 
1106
- # 更新模型统计
1107
  model_usage_stats[model]["count"] += 1
1108
  model_usage_stats[model]["prompt_tokens"] += prompt_tokens
1109
  model_usage_stats[model]["completion_tokens"] += completion_tokens
 
1110
 
1111
- # 更新总token统计
1112
  total_tokens["prompt"] += prompt_tokens
1113
  total_tokens["completion"] += completion_tokens
1114
  total_tokens["total"] += (prompt_tokens + completion_tokens)
1115
-
1116
- # 添加使用记录
1117
- usage_record = {
1118
- "call_time": call_time,
1119
- "model": model,
1120
- "prompt_tokens": prompt_tokens,
1121
- "completion_tokens": completion_tokens,
1122
- "total_tokens": prompt_tokens + completion_tokens,
1123
- "calculation_method": "tiktoken"
1124
- }
1125
- model_usage_records.append(usage_record)
1126
-
1127
- # 保存记录到文件,确保数据持久化
1128
- save_model_usage_records()
1129
-
1130
- print(f"模型使用记录: {model}, 输入: {prompt_tokens}, 输出: {completion_tokens}, 时间: {call_time}")
1131
 
1132
 
1133
  # 获取计算点信息
@@ -1174,7 +1173,7 @@ def get_compute_points():
1174
  "cookie": cookies
1175
  }
1176
 
1177
- response = requests.get(
1178
  COMPUTE_POINTS_URL,
1179
  headers=headers
1180
  )
@@ -1248,7 +1247,7 @@ def get_compute_points_log(session, cookies, session_token):
1248
  "cookie": cookies
1249
  }
1250
 
1251
- response = requests.post(
1252
  COMPUTE_POINTS_LOG_URL,
1253
  headers=headers,
1254
  json={"byLlm": True}
@@ -1330,24 +1329,60 @@ def dashboard():
1330
  space_url=SPACE_URL, # 传递空间URL
1331
  users_compute_points=users_compute_points, # 传递用户计算点信息
1332
  model_usage_records=model_usage_records, # 传递模型使用记录
1333
- delete_chat=DELETE_CHAT # 传递删除对话开关状态
1334
  )
1335
 
1336
 
1337
- @app.route("/api/set_delete_chat", methods=["POST"])
 
1338
  @require_auth
1339
- def set_delete_chat():
1340
- """设置是否启用自动删除对话功能"""
1341
- global DELETE_CHAT
1342
  try:
1343
  data = request.get_json()
1344
- if "delete_chat" in data:
 
1345
  DELETE_CHAT = bool(data["delete_chat"])
1346
- return jsonify({"success": True, "delete_chat": DELETE_CHAT})
1347
- return jsonify({"success": False, "error": "Missing delete_chat parameter"}), 400
 
 
 
 
 
 
1348
  except Exception as e:
1349
- return jsonify({"success": False, "error": str(e)}), 500
 
 
1350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1351
 
1352
  # 获取空间URL
1353
  SPACE_URL = get_space_url()
@@ -1356,58 +1391,10 @@ if SPACE_URL:
1356
  print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
1357
 
1358
 
1359
- # 定期保存token使用统计
1360
- def auto_save_stats():
1361
- """定期自动保存token使用统计数据的线程函数"""
1362
- while True:
1363
- try:
1364
- # 每10分钟保存一次
1365
- time.sleep(600)
1366
- # 保存模型使用记录
1367
- save_model_usage_records()
1368
- print(f"已自动保存模型使用记录: {datetime.utcnow() + timedelta(hours=8)}")
1369
-
1370
- # 每小时尝试重新加载记录,确保数据不丢失
1371
- if datetime.utcnow().minute < 10: # 每小时的前10分钟执行
1372
- print("尝试重新加载模型使用记录...")
1373
- load_model_usage_records()
1374
- except Exception as e:
1375
- print(f"自动保存/加载模型使用记录出错: {e}")
1376
-
1377
-
1378
- # 加载模型调用记录
1379
- def load_model_usage_records():
1380
- global model_usage_records
1381
- try:
1382
- if os.path.exists(MODEL_USAGE_RECORDS_FILE):
1383
- with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
1384
- records = json.load(f)
1385
- if isinstance(records, list):
1386
- model_usage_records = records
1387
- print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
1388
- else:
1389
- print("调用记录文件格式不正确,初始化为空列表")
1390
- except Exception as e:
1391
- print(f"加载模型调用记录失败: {e}")
1392
- model_usage_records = []
1393
-
1394
- # 保存模型调用记录
1395
- def save_model_usage_records():
1396
- try:
1397
- with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
1398
- json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
1399
- print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
1400
- except Exception as e:
1401
- print(f"保存模型调用记录失败: {e}")
1402
-
1403
-
1404
  if __name__ == "__main__":
1405
  # 启动保活线程
1406
  threading.Thread(target=keep_alive, daemon=True).start()
1407
 
1408
- # 启动自动保存统计数据的线程
1409
- threading.Thread(target=auto_save_stats, daemon=True).start()
1410
-
1411
  # 加载历史模型调用记录
1412
  load_model_usage_records()
1413
 
 
39
  USER_DATA = []
40
  CURRENT_USER = -1
41
  MODELS = set()
 
 
 
42
 
43
+ # 添加记录上一个conversation_id的变量和删除标记
44
+ LAST_CONVERSATION_IDS = [None] * 100 # 为每个用户记录上一个conversation_id
45
+ DELETE_CHAT = True # 是否在对话结束后删除上一个对话
46
 
47
  TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
48
  SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
 
62
 
63
  # 模型调用记录
64
  model_usage_records = [] # 每次调用详细记录
65
+ MODEL_USAGE_RECORDS_FILE = "model_usage_records.json" # 调用记录保存文件
66
 
67
  # 计算点信息
68
  compute_points = {
 
85
  # 记录启动时间
86
  START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
87
 
88
+
89
  # 自定义JSON编码器,处理datetime对象
90
  class DateTimeEncoder(json.JSONEncoder):
91
  def default(self, obj):
 
94
  return super(DateTimeEncoder, self).default(obj)
95
 
96
 
97
+ # 加载模型调用记录
98
+ def load_model_usage_records():
99
+ global model_usage_records
100
+ try:
101
+ if os.path.exists(MODEL_USAGE_RECORDS_FILE):
102
+ with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
103
+ records = json.load(f)
104
+ if isinstance(records, list):
105
+ model_usage_records = records
106
+ print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
107
+ else:
108
+ print("调用记录文件格式不正确,初始化为空列表")
109
+ except Exception as e:
110
+ print(f"加载模型调用记录失败: {e}")
111
+ model_usage_records = []
112
+
113
+ # 保存模型调用记录
114
+ def save_model_usage_records():
115
+ try:
116
+ with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
117
+ json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
118
+ print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
119
+ except Exception as e:
120
+ print(f"保存模型调用记录失败: {e}")
121
+
122
+
123
  def update_conversation_id(user_index, conversation_id):
124
  """更新用户的conversation_id并保存到配置文件"""
125
  try:
 
140
  print(f"更新conversation_id失败: {e}")
141
 
142
 
143
+ def resolve_config():
144
+ # 从环境变量读取多组配置
145
+ config_list = []
146
+ i = 1
147
+ while True:
148
+ cookie = os.environ.get(f"cookie_{i}")
149
+ if not cookie:
150
+ break
151
+
152
+ # 为每个cookie创建一个配置项,conversation_id初始为空
153
+ config_list.append({
154
+ "conversation_id": "", # 初始为空,将通过get_or_create_conversation自动创建
155
+ "cookies": cookie
156
+ })
157
+ i += 1
158
+
159
+ # 如果环境变量存在配置,使用环境变量的配置
160
+ if config_list:
161
+ print(f"从环境变量加载了 {len(config_list)} 个配置")
162
+ return config_list
163
+
164
+ # 如果环境变量不存在,从文件读取
165
  try:
166
+ with open("config.json", "r") as f:
 
167
  config = json.load(f)
168
+ config_list = config.get("config")
169
+ return config_list
170
+ except FileNotFoundError:
171
+ print("未找到config.json文件")
172
+ return []
173
+ except json.JSONDecodeError:
174
+ print("config.json格式错误")
175
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
 
178
  def get_password():
 
252
  }
253
 
254
  try:
255
+ response = session.post(
256
  USER_INFO_URL,
257
  headers=headers,
258
+ json={},
259
+ cookies=None
260
  )
261
 
262
  if response.status_code == 200:
 
301
  models_set = set()
302
 
303
  try:
304
+ response = session.post(
305
  MODEL_LIST_URL,
306
  headers=headers,
307
+ json={},
308
+ cookies=None
309
  )
310
 
311
  if response.status_code != 200:
 
346
  raise
347
 
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  def init_session():
350
  get_password()
351
+ global USER_NUM, MODELS, USER_DATA, DELETE_CHAT
352
 
353
+ # 从环境变量读取是否删除上一个对话的设置
354
+ delete_chat_env = os.environ.get("DELETE_CHAT", "true").lower()
355
+ DELETE_CHAT = delete_chat_env in ["true", "1", "yes", "y"]
356
+ print(f"删除上一个对话设置: {DELETE_CHAT}")
357
+
358
+ config_list = resolve_config()
359
+ user_num = len(config_list)
360
+ all_models = set()
361
+
362
+ for i in range(user_num):
363
+ user = config_list[i]
364
+ cookies = user.get("cookies")
365
+ conversation_id = user.get("conversation_id")
366
+ session = requests.Session()
367
+
368
+ session_token = refresh_token(session, cookies)
369
+ if not session_token:
370
+ print(f"无法获取cookie {i+1}的token")
371
+ continue
372
+
373
+ try:
374
+ model_map, models_set = get_model_map(session, cookies, session_token)
375
+ all_models.update(models_set)
376
+ USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
377
+ except Exception as e:
378
+ print(f"配置用户 {i+1} 失败: {e}")
379
+ continue
380
 
 
381
  USER_NUM = len(USER_DATA)
382
  if USER_NUM == 0:
383
  print("No user available, exiting...")
384
  exit(1)
385
 
 
 
 
 
 
386
  MODELS = all_models
387
  print(f"启动完成,共配置 {USER_NUM} 个用户")
388
 
 
400
  return cookies
401
 
402
 
403
+ user_data = init_session()
 
404
 
405
 
406
  @app.route("/v1/models", methods=["GET"])
 
500
  }
501
 
502
  try:
503
+ response = session.post(
504
  CREATE_CONVERSATION_URL,
505
  headers=headers,
506
  json=create_payload
 
521
  return None
522
 
523
 
524
+ def delete_conversation(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
525
+ """删除指定的对话"""
526
  if not conversation_id:
527
+ print("无法删除对话: 缺少conversation_id")
528
  return False
529
 
530
  headers = {
 
540
  headers["session-token"] = session_token
541
 
542
  delete_payload = {
543
+ "deploymentId": deployment_id,
544
  "deploymentConversationId": conversation_id
545
  }
546
 
547
  try:
548
+ response = session.post(
549
  DELETE_CONVERSATION_URL,
550
  headers=headers,
551
  json=delete_payload
 
554
  if response.status_code == 200:
555
  data = response.json()
556
  if data.get("success", False):
557
+ print(f"成功删除对话: {conversation_id}")
558
  return True
559
 
560
+ print(f"删除对话失败: {response.status_code} - {response.text[:100]}")
561
  return False
562
  except Exception as e:
563
+ print(f"删除对话时出错: {e}")
564
  return False
565
 
566
 
 
595
  }
596
 
597
  try:
598
+ response = session.post(
 
599
  CHAT_URL,
600
  headers=headers,
601
  data=json.dumps(payload),
 
619
 
620
  def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
621
  """获取有效的会话ID,如果无效则创建新会话"""
622
+ # 修改为总是创建新的conversation_id
623
+ print("将为每次对话创建新会话")
624
+ need_create = True
625
+
626
+ # 如果需要创建新会话
627
+ if need_create:
628
+ if model in model_map and len(model_map[model]) >= 2:
629
+ external_app_id = model_map[model][0]
630
+ # 创建会话时需要deployment_id,我们先使用一个固定值
631
+ # 在实际应用中应从API响应中获取
632
+ deployment_id = "14b2a314cc" # 这是从您提供的请求中获取的
 
 
 
 
 
 
 
 
 
633
 
634
+ new_conversation_id = create_conversation(
635
+ session, cookies, session_token,
636
+ external_application_id=external_app_id,
637
+ deployment_id=deployment_id
638
+ )
639
 
640
+ if new_conversation_id:
641
+ # 获取当前用户的上一个conversation_id
642
+ global USER_DATA, CURRENT_USER, LAST_CONVERSATION_IDS, DELETE_CHAT
643
+ last_conversation_id = LAST_CONVERSATION_IDS[user_index]
644
+
645
+ # 更新全局存储的会话ID
646
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
647
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
648
+
649
+ # 保存到配置文件
650
+ update_conversation_id(user_index, new_conversation_id)
651
+
652
+ # 保存新的会话ID为下次调用时的"上一��ID"
653
+ LAST_CONVERSATION_IDS[user_index] = new_conversation_id
654
+
655
+ return new_conversation_id
656
 
657
+ # 如果无法创建,返回原始ID
658
  return conversation_id
659
 
660
 
 
669
  """Flua traktado kaj plusendo de mesaĝoj"""
670
  (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
671
 
672
+ # 获取并保存当前的conversation_id(可能是旧的,用于稍后删除)
673
+ last_conversation_id = conversation_id
674
 
675
+ # 确保有有效的会话ID
676
+ conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
 
 
 
677
 
678
  trace_id, sentry_trace = generate_trace_id()
679
 
 
702
 
703
  payload = {
704
  "requestId": str(uuid.uuid4()),
705
+ "deploymentConversationId": conversation_id,
706
  "message": message,
707
  "isDesktop": False,
708
  "chatConfig": {
 
719
  payload["useThinking"] = think
720
 
721
  try:
722
+ response = session.post(
723
  CHAT_URL,
724
  headers=headers,
725
  data=json.dumps(payload),
 
789
  completion_tokens = num_tokens_from_string(completion_buffer.getvalue())
790
  update_model_stats(model, prompt_tokens, completion_tokens)
791
 
792
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
793
+ global DELETE_CHAT, LAST_CONVERSATION_IDS
794
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
795
+ delete_conversation(session, cookies, session_token, last_conversation_id)
796
 
797
  return Response(generate(), mimetype="text/event-stream")
798
  except requests.exceptions.RequestException as e:
 
808
  """Ne-flua traktado de mesaĝoj"""
809
  (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
810
 
811
+ # 获取并保存当前的conversation_id(可能是旧的,用于稍后删除)
812
+ last_conversation_id = conversation_id
 
 
 
813
 
814
+ # 确保有有效的会话ID
815
+ conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
816
 
817
  trace_id, sentry_trace = generate_trace_id()
818
 
 
840
 
841
  payload = {
842
  "requestId": str(uuid.uuid4()),
843
+ "deploymentConversationId": conversation_id,
844
  "message": message,
845
  "isDesktop": False,
846
  "chatConfig": {
 
857
  payload["useThinking"] = think
858
 
859
  try:
860
+ response = session.post(
861
  CHAT_URL,
862
  headers=headers,
863
  data=json.dumps(payload),
 
918
  completion_tokens = num_tokens_from_string(think_content + response_content)
919
  update_model_stats(model, prompt_tokens, completion_tokens)
920
 
921
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
922
+ global DELETE_CHAT, LAST_CONVERSATION_IDS
923
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
924
+ delete_conversation(session, cookies, session_token, last_conversation_id)
925
 
926
  return jsonify({
927
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
956
  completion_tokens = num_tokens_from_string(response_content)
957
  update_model_stats(model, prompt_tokens, completion_tokens)
958
 
959
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
960
+ global DELETE_CHAT, LAST_CONVERSATION_IDS
961
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
962
+ delete_conversation(session, cookies, session_token, last_conversation_id)
963
 
964
  return jsonify({
965
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
1053
  """每20分钟进行一次自我健康检查"""
1054
  while True:
1055
  try:
1056
+ requests.get("http://127.0.0.1:7860/health")
 
 
 
1057
  time.sleep(1200) # 20分钟
1058
  except:
1059
+ pass # 忽略错误,保持运行
 
1060
 
1061
 
1062
  @app.route("/", methods=["GET"])
 
1069
  return redirect(url_for('dashboard'))
1070
 
1071
 
1072
+ # 获取OpenAI的tokenizer来计算token
1073
+ def num_tokens_from_string(string, model="gpt-3.5-turbo"):
1074
+ """计算文本的token数量"""
1075
  try:
1076
+ encoding = tiktoken.encoding_for_model(model)
1077
+ num_tokens = len(encoding.encode(string))
1078
+ print(f"使用tiktoken计算token数: {num_tokens}")
1079
+ return num_tokens
1080
  except Exception as e:
1081
+ # 如果tiktoken不支持模型或者出错,使用简单的估算
1082
+ estimated_tokens = len(string) // 4 # 粗略估计每个token约4个字符
1083
+ print(f"使用估算方法计算token数: {estimated_tokens} (原因: {str(e)})")
1084
+ return estimated_tokens
1085
 
1086
  # 更新模型使用统计
1087
  def update_model_stats(model, prompt_tokens, completion_tokens):
 
1088
  global model_usage_stats, total_tokens, model_usage_records
1089
 
1090
+ # 添加调用记录
1091
+ # 获取UTC时间
1092
+ utc_now = datetime.utcnow()
1093
+ # 转换为北京时间 (UTC+8)
1094
+ beijing_time = utc_now + timedelta(hours=8)
1095
+ call_time = beijing_time.strftime('%Y-%m-%d %H:%M:%S') # 北京时间
1096
+
1097
+ record = {
1098
+ "model": model,
1099
+ "call_time": call_time,
1100
+ "prompt_tokens": prompt_tokens,
1101
+ "completion_tokens": completion_tokens,
1102
+ "calculation_method": "tiktoken" if any(x in model.lower() for x in ["gpt", "claude"]) or model in ["llama-3", "mistral", "gemma"] else "estimate"
1103
+ }
1104
+ model_usage_records.append(record)
1105
+
1106
+ # 限制记录数量,保留最新的500条
1107
+ if len(model_usage_records) > 500:
1108
+ model_usage_records.pop(0)
1109
 
1110
+ # 保存调用记录到本地文件
1111
+ save_model_usage_records()
1112
+
1113
+ # 更新聚合统计
1114
  if model not in model_usage_stats:
1115
  model_usage_stats[model] = {
1116
  "count": 0,
1117
  "prompt_tokens": 0,
1118
+ "completion_tokens": 0,
1119
+ "total_tokens": 0
1120
  }
1121
 
 
1122
  model_usage_stats[model]["count"] += 1
1123
  model_usage_stats[model]["prompt_tokens"] += prompt_tokens
1124
  model_usage_stats[model]["completion_tokens"] += completion_tokens
1125
+ model_usage_stats[model]["total_tokens"] += (prompt_tokens + completion_tokens)
1126
 
 
1127
  total_tokens["prompt"] += prompt_tokens
1128
  total_tokens["completion"] += completion_tokens
1129
  total_tokens["total"] += (prompt_tokens + completion_tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1130
 
1131
 
1132
  # 获取计算点信息
 
1173
  "cookie": cookies
1174
  }
1175
 
1176
+ response = session.get(
1177
  COMPUTE_POINTS_URL,
1178
  headers=headers
1179
  )
 
1247
  "cookie": cookies
1248
  }
1249
 
1250
+ response = session.post(
1251
  COMPUTE_POINTS_LOG_URL,
1252
  headers=headers,
1253
  json={"byLlm": True}
 
1329
  space_url=SPACE_URL, # 传递空间URL
1330
  users_compute_points=users_compute_points, # 传递用户计算点信息
1331
  model_usage_records=model_usage_records, # 传递模型使用记录
1332
+ delete_chat=DELETE_CHAT # 传递删除对话设置
1333
  )
1334
 
1335
 
1336
+ # 添加更新删除对话设置的路由
1337
+ @app.route("/update_delete_chat_setting", methods=["POST"])
1338
  @require_auth
1339
+ def update_delete_chat_setting():
 
 
1340
  try:
1341
  data = request.get_json()
1342
+ if data and "delete_chat" in data:
1343
+ global DELETE_CHAT
1344
  DELETE_CHAT = bool(data["delete_chat"])
1345
+
1346
+ # 将设置保存到环境变量中,以便重启后保留设置
1347
+ os.environ["DELETE_CHAT"] = "true" if DELETE_CHAT else "false"
1348
+
1349
+ print(f"更新删除对话设置为: {DELETE_CHAT}")
1350
+ return jsonify({"success": True})
1351
+ else:
1352
+ return jsonify({"success": False, "error": "缺少delete_chat参数"})
1353
  except Exception as e:
1354
+ print(f"更新删除对话设置失败: {e}")
1355
+ return jsonify({"success": False, "error": str(e)})
1356
+
1357
 
1358
+ # 获取Hugging Face Space URL
1359
+ def get_space_url():
1360
+ # 尝试从环境变量获取
1361
+ space_url = os.environ.get("SPACE_URL")
1362
+ if space_url:
1363
+ return space_url
1364
+
1365
+ # 如果SPACE_URL不存在,尝试从SPACE_ID构建
1366
+ space_id = os.environ.get("SPACE_ID")
1367
+ if space_id:
1368
+ username, space_name = space_id.split("/")
1369
+ # 将空间名称中的下划线替换为连字符
1370
+ # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
1371
+ # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
1372
+ space_name = space_name.replace("_", "-")
1373
+ return f"https://{username}-{space_name}.hf.space"
1374
+
1375
+ # 如果以上都不存在,尝试从单独的用户名和空间名构建
1376
+ username = os.environ.get("SPACE_USERNAME")
1377
+ space_name = os.environ.get("SPACE_NAME")
1378
+ if username and space_name:
1379
+ # 将空间名称中的下划线替换为连字符
1380
+ # 同上,Hugging Face会自动进行此转换
1381
+ space_name = space_name.replace("_", "-")
1382
+ return f"https://{username}-{space_name}.hf.space"
1383
+
1384
+ # 默认返回None
1385
+ return None
1386
 
1387
  # 获取空间URL
1388
  SPACE_URL = get_space_url()
 
1391
  print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
1392
 
1393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  if __name__ == "__main__":
1395
  # 启动保活线程
1396
  threading.Thread(target=keep_alive, daemon=True).start()
1397
 
 
 
 
1398
  # 加载历史模型调用记录
1399
  load_model_usage_records()
1400
 
templates/dashboard.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Abacus Chat Service - 仪表盘</title>
7
  <style>
8
  :root {
9
  --primary-color: #6f42c1;
@@ -505,62 +505,122 @@
505
  }
506
  }
507
 
508
- /* 开关样式 */
509
- .switch {
 
 
 
 
 
 
 
 
 
 
510
  position: relative;
511
  display: inline-block;
512
  width: 50px;
513
  height: 24px;
514
- margin-right: 10px;
515
- vertical-align: middle;
516
  }
517
 
518
- .switch input {
519
  opacity: 0;
520
  width: 0;
521
  height: 0;
522
  }
523
 
524
- .slider {
525
  position: absolute;
526
  cursor: pointer;
527
  top: 0;
528
  left: 0;
529
  right: 0;
530
  bottom: 0;
531
- background-color: rgba(255, 255, 255, 0.2);
532
  transition: .4s;
 
533
  }
534
 
535
- .slider:before {
536
  position: absolute;
537
  content: "";
538
  height: 18px;
539
  width: 18px;
540
  left: 3px;
541
  bottom: 3px;
542
- background-color: white;
543
  transition: .4s;
 
544
  }
545
 
546
- input:checked + .slider {
547
  background-color: var(--primary-color);
548
  }
549
 
550
- input:focus + .slider {
551
- box-shadow: 0 0 1px var(--primary-color);
552
  }
553
 
554
- input:checked + .slider:before {
555
- transform: translateX(26px);
556
  }
557
 
558
- .slider.round {
559
- border-radius: 24px;
 
560
  }
561
 
562
- .slider.round:before {
563
- border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  }
565
  </style>
566
  </head>
@@ -570,7 +630,7 @@
570
  <nav class="navbar">
571
  <a href="/" class="navbar-brand">
572
  <span class="navbar-logo">🤖</span>
573
- <span class="navbar-title">Abacus Chat Service</span>
574
  </a>
575
  <div class="navbar-actions">
576
  <a href="/logout" class="btn-logout">
@@ -584,42 +644,57 @@
584
  <div class="card">
585
  <div class="card-header">
586
  <h2 class="card-title">
587
- <div class="card-icon">🖥️</div>
588
- 系统信息
589
  </h2>
590
  </div>
591
- <div>
592
- <div class="status-item">
593
- <div class="status-label">系统状态</div>
594
- <div class="status-value success">运行中</div>
595
- </div>
596
- <div class="status-item">
597
- <div class="status-label">运行时间</div>
598
- <div class="status-value">{{ uptime }}</div>
599
- </div>
600
- <div class="status-item">
601
- <div class="status-label">健康检查次数</div>
602
- <div class="status-value">{{ health_checks }}</div>
603
- </div>
604
- <div class="status-item">
605
- <div class="status-label">已配置用户数量</div>
606
- <div class="status-value">{{ user_count }}</div>
607
- </div>
608
- <div class="status-item">
609
- <div class="status-label">可用模型数量</div>
610
- <div class="status-value">{{ models|length }}</div>
 
 
611
  </div>
612
- <div class="status-item">
613
- <div class="status-label">自动删除对话</div>
614
- <div class="status-value">
615
- <label class="switch">
616
- <input type="checkbox" id="deleteChat" {% if delete_chat %}checked{% endif %}>
617
- <span class="slider round"></span>
618
- </label>
619
- <span id="deleteChatStatus">{{ "已启用" if delete_chat else "已禁用" }}</span>
620
- </div>
 
 
 
 
 
 
 
 
 
621
  </div>
622
  </div>
 
 
 
 
623
  </div>
624
 
625
  <div class="grid">
@@ -677,7 +752,7 @@
677
  <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
678
  </div>
679
  <div class="token-note">
680
- <small>* 以上数据仅统计通过本服务使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
681
  </div>
682
  <div class="table-container">
683
  <table class="data-table token-model-table">
@@ -854,7 +929,7 @@
854
  </div>
855
 
856
  <div class="footer">
857
- <p>© {{ year }} Abacus Chat Service. 保持简单,保持可靠。</p>
858
  </div>
859
  </div>
860
 
@@ -880,41 +955,6 @@
880
  // 初始化隐藏回到顶部按钮
881
  document.querySelector('.float-btn').style.opacity = '0';
882
 
883
- // 自动删除对话开关功能
884
- const deleteChatSwitch = document.getElementById('deleteChat');
885
- const deleteChatStatus = document.getElementById('deleteChatStatus');
886
-
887
- if (deleteChatSwitch) {
888
- deleteChatSwitch.addEventListener('change', () => {
889
- // 发送API请求更新设置
890
- fetch('/api/set_delete_chat', {
891
- method: 'POST',
892
- headers: {
893
- 'Content-Type': 'application/json'
894
- },
895
- body: JSON.stringify({
896
- delete_chat: deleteChatSwitch.checked
897
- })
898
- })
899
- .then(response => response.json())
900
- .then(data => {
901
- if (data.success) {
902
- deleteChatStatus.textContent = deleteChatSwitch.checked ? '已启用' : '已禁用';
903
- } else {
904
- alert('更新设置失败: ' + (data.error || '未知错误'));
905
- // 回滚UI状态
906
- deleteChatSwitch.checked = !deleteChatSwitch.checked;
907
- }
908
- })
909
- .catch(error => {
910
- console.error('更新设置出错:', error);
911
- alert('更新设置出错,请查看控制台');
912
- // 回滚UI状态
913
- deleteChatSwitch.checked = !deleteChatSwitch.checked;
914
- });
915
- });
916
- }
917
-
918
  // 模型统计折叠功能
919
  const toggleBtn = document.getElementById('toggleModelStats');
920
  const hiddenModels = document.querySelectorAll('.hidden-model');
@@ -930,6 +970,84 @@
930
  toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
931
  });
932
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
  </script>
934
  </body>
935
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 仪表盘</title>
7
  <style>
8
  :root {
9
  --primary-color: #6f42c1;
 
505
  }
506
  }
507
 
508
+ .token-model-table td, .token-model-table th {
509
+ white-space: nowrap;
510
+ }
511
+
512
+ /* 开关按钮样式 */
513
+ .toggle-switch-container {
514
+ display: flex;
515
+ align-items: center;
516
+ gap: 10px;
517
+ }
518
+
519
+ .toggle-switch {
520
  position: relative;
521
  display: inline-block;
522
  width: 50px;
523
  height: 24px;
 
 
524
  }
525
 
526
+ .toggle-switch input {
527
  opacity: 0;
528
  width: 0;
529
  height: 0;
530
  }
531
 
532
+ .toggle-slider {
533
  position: absolute;
534
  cursor: pointer;
535
  top: 0;
536
  left: 0;
537
  right: 0;
538
  bottom: 0;
539
+ background-color: rgba(100, 100, 150, 0.3);
540
  transition: .4s;
541
+ border-radius: 24px;
542
  }
543
 
544
+ .toggle-slider:before {
545
  position: absolute;
546
  content: "";
547
  height: 18px;
548
  width: 18px;
549
  left: 3px;
550
  bottom: 3px;
551
+ background-color: #e6e6ff;
552
  transition: .4s;
553
+ border-radius: 50%;
554
  }
555
 
556
+ input:checked + .toggle-slider {
557
  background-color: var(--primary-color);
558
  }
559
 
560
+ input:checked + .toggle-slider:before {
561
+ transform: translateX(26px);
562
  }
563
 
564
+ .toggle-status {
565
+ font-weight: 600;
566
  }
567
 
568
+ .info-text {
569
+ font-size: 0.85rem;
570
+ color: rgba(230, 230, 255, 0.7);
571
  }
572
 
573
+ /* 通知样式 */
574
+ .notification {
575
+ position: fixed;
576
+ top: 20px;
577
+ right: 20px;
578
+ padding: 12px 20px;
579
+ border-radius: 8px;
580
+ color: white;
581
+ font-weight: 500;
582
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
583
+ z-index: 1000;
584
+ transform: translateY(-20px);
585
+ opacity: 0;
586
+ transition: all 0.3s ease;
587
+ max-width: 300px;
588
+ }
589
+
590
+ .notification.show {
591
+ transform: translateY(0);
592
+ opacity: 1;
593
+ }
594
+
595
+ .notification.success {
596
+ background-color: var(--success-color);
597
+ }
598
+
599
+ .notification.error {
600
+ background-color: var(--error-color);
601
+ }
602
+
603
+ .notification.info {
604
+ background-color: var(--accent-color);
605
+ }
606
+
607
+ /* 响应式样式 */
608
+ @media (max-width: 768px) {
609
+ .container {
610
+ padding: 1rem;
611
+ }
612
+
613
+ .navbar {
614
+ padding: 1rem;
615
+ }
616
+
617
+ .card {
618
+ padding: 1rem;
619
+ }
620
+
621
+ .grid {
622
+ grid-template-columns: 1fr;
623
+ }
624
  }
625
  </style>
626
  </head>
 
630
  <nav class="navbar">
631
  <a href="/" class="navbar-brand">
632
  <span class="navbar-logo">🤖</span>
633
+ <span class="navbar-title">Abacus Chat Proxy</span>
634
  </a>
635
  <div class="navbar-actions">
636
  <a href="/logout" class="btn-logout">
 
644
  <div class="card">
645
  <div class="card-header">
646
  <h2 class="card-title">
647
+ <span class="card-icon">📊</span>
648
+ 系统状态
649
  </h2>
650
  </div>
651
+ <div class="status-item">
652
+ <span class="status-label">服务状态</span>
653
+ <span class="status-value success">运行中</span>
654
+ </div>
655
+ <div class="status-item">
656
+ <span class="status-label">运行时间</span>
657
+ <span class="status-value">{{ uptime }}</span>
658
+ </div>
659
+ <div class="status-item">
660
+ <span class="status-label">健康检查次数</span>
661
+ <span class="status-value">{{ health_checks }}</span>
662
+ </div>
663
+ <div class="status-item">
664
+ <span class="status-label">已配置用户数</span>
665
+ <span class="status-value">{{ user_count }}</span>
666
+ </div>
667
+ <div class="status-item">
668
+ <span class="status-label">可用模型</span>
669
+ <div class="models-list">
670
+ {% for model in models %}
671
+ <span class="model-tag">{{ model }}</span>
672
+ {% endfor %}
673
  </div>
674
+ </div>
675
+ </div>
676
+
677
+ <div class="card">
678
+ <div class="card-header">
679
+ <h2 class="card-title">
680
+ <span class="card-icon">🗑️</span>
681
+ 对话管理设置
682
+ </h2>
683
+ </div>
684
+ <div class="status-item">
685
+ <span class="status-label">是否自动删除上一个对话</span>
686
+ <div class="toggle-switch-container">
687
+ <label class="toggle-switch">
688
+ <input type="checkbox" id="delete-chat-toggle" {% if delete_chat %}checked{% endif %}>
689
+ <span class="toggle-slider"></span>
690
+ </label>
691
+ <span class="toggle-status" id="delete-chat-status">{{ "开启" if delete_chat else "关闭" }}</span>
692
  </div>
693
  </div>
694
+ <div class="status-item">
695
+ <span class="status-label">设置说明</span>
696
+ <span class="status-value info-text">开启后,系统将在每次对话完成后自动删除上一次对话,只保留最新对话</span>
697
+ </div>
698
  </div>
699
 
700
  <div class="grid">
 
752
  <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
753
  </div>
754
  <div class="token-note">
755
+ <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
756
  </div>
757
  <div class="table-container">
758
  <table class="data-table token-model-table">
 
929
  </div>
930
 
931
  <div class="footer">
932
+ <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
933
  </div>
934
  </div>
935
 
 
955
  // 初始化隐藏回到顶部按钮
956
  document.querySelector('.float-btn').style.opacity = '0';
957
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
  // 模型统计折叠功能
959
  const toggleBtn = document.getElementById('toggleModelStats');
960
  const hiddenModels = document.querySelectorAll('.hidden-model');
 
970
  toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
971
  });
972
  }
973
+
974
+ document.addEventListener('DOMContentLoaded', function() {
975
+ initCharts();
976
+
977
+ // 显示/隐藏更多模型使用记录
978
+ const toggleModelStats = document.getElementById('toggleModelStats');
979
+ if (toggleModelStats) {
980
+ toggleModelStats.addEventListener('click', function() {
981
+ const hiddenRows = document.querySelectorAll('.hidden-model');
982
+ hiddenRows.forEach(row => {
983
+ row.classList.toggle('show-model');
984
+ });
985
+ toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部';
986
+ });
987
+ }
988
+
989
+ // 处理删除对话开关
990
+ const deleteToggle = document.getElementById('delete-chat-toggle');
991
+ const deleteStatus = document.getElementById('delete-chat-status');
992
+
993
+ if (deleteToggle && deleteStatus) {
994
+ deleteToggle.addEventListener('change', function() {
995
+ const isChecked = this.checked;
996
+ deleteStatus.textContent = isChecked ? '开启' : '关闭';
997
+
998
+ // 发送更新请求到后端
999
+ fetch('/update_delete_chat_setting', {
1000
+ method: 'POST',
1001
+ headers: {
1002
+ 'Content-Type': 'application/json',
1003
+ },
1004
+ body: JSON.stringify({ delete_chat: isChecked })
1005
+ })
1006
+ .then(response => response.json())
1007
+ .then(data => {
1008
+ if (data.success) {
1009
+ // 显示成功提示
1010
+ showNotification(isChecked ? '已开启自动删除对话功能' : '已关闭自动删除对话功能', 'success');
1011
+ } else {
1012
+ // 显示错误提示
1013
+ showNotification('设置更新失败: ' + data.error, 'error');
1014
+ // 回滚UI状态
1015
+ deleteToggle.checked = !isChecked;
1016
+ deleteStatus.textContent = !isChecked ? '开启' : '关闭';
1017
+ }
1018
+ })
1019
+ .catch(error => {
1020
+ console.error('更新设置出错:', error);
1021
+ showNotification('更新设置失败,请重试', 'error');
1022
+ // 回滚UI状态
1023
+ deleteToggle.checked = !isChecked;
1024
+ deleteStatus.textContent = !isChecked ? '开启' : '关闭';
1025
+ });
1026
+ });
1027
+ }
1028
+ });
1029
+
1030
+ // 通知函数
1031
+ function showNotification(message, type = 'info') {
1032
+ const notification = document.createElement('div');
1033
+ notification.className = `notification ${type}`;
1034
+ notification.textContent = message;
1035
+
1036
+ document.body.appendChild(notification);
1037
+
1038
+ // 显示动画
1039
+ setTimeout(() => {
1040
+ notification.classList.add('show');
1041
+ }, 10);
1042
+
1043
+ // 3秒后淡出
1044
+ setTimeout(() => {
1045
+ notification.classList.remove('show');
1046
+ setTimeout(() => {
1047
+ notification.remove();
1048
+ }, 300);
1049
+ }, 3000);
1050
+ }
1051
  </script>
1052
  </body>
1053
  </html>
templates/login.html CHANGED
@@ -1,462 +1,462 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Abacus Chat Proxy - 登录</title>
7
- <style>
8
- :root {
9
- --primary-color: #6f42c1;
10
- --secondary-color: #4a32a8;
11
- --bg-color: #0a0a1a;
12
- --text-color: #e6e6ff;
13
- --card-bg: rgba(30, 30, 60, 0.7);
14
- --input-bg: rgba(40, 40, 80, 0.6);
15
- --success-color: #36d399;
16
- --error-color: #f87272;
17
- }
18
-
19
- * {
20
- margin: 0;
21
- padding: 0;
22
- box-sizing: border-box;
23
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
- }
25
-
26
- body {
27
- min-height: 100vh;
28
- display: flex;
29
- align-items: center;
30
- justify-content: center;
31
- background-color: var(--bg-color);
32
- background-image:
33
- radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
34
- radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
35
- color: var(--text-color);
36
- position: relative;
37
- overflow: hidden;
38
- }
39
-
40
- /* 科幻蜘蛛网动画 */
41
- .web-container {
42
- position: absolute;
43
- top: 0;
44
- left: 0;
45
- width: 100%;
46
- height: 100%;
47
- z-index: -2;
48
- opacity: 0.6;
49
- }
50
-
51
- .web {
52
- width: 100%;
53
- height: 100%;
54
- }
55
-
56
- /* 动态背景网格 */
57
- .grid-background {
58
- position: absolute;
59
- top: 0;
60
- left: 0;
61
- width: 100%;
62
- height: 100%;
63
- background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
64
- linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
65
- background-size: 30px 30px;
66
- z-index: -1;
67
- animation: grid-move 20s linear infinite;
68
- }
69
-
70
- @keyframes grid-move {
71
- 0% {
72
- transform: translateY(0);
73
- }
74
- 100% {
75
- transform: translateY(30px);
76
- }
77
- }
78
-
79
- /* 浮动粒子效果 */
80
- .particles {
81
- position: absolute;
82
- top: 0;
83
- left: 0;
84
- width: 100%;
85
- height: 100%;
86
- overflow: hidden;
87
- z-index: -1;
88
- }
89
-
90
- .particle {
91
- position: absolute;
92
- display: block;
93
- pointer-events: none;
94
- width: 6px;
95
- height: 6px;
96
- background-color: rgba(111, 66, 193, 0.2);
97
- border-radius: 50%;
98
- animation: float 20s infinite ease-in-out;
99
- }
100
-
101
- @keyframes float {
102
- 0%, 100% {
103
- transform: translateY(0) translateX(0);
104
- opacity: 0;
105
- }
106
- 50% {
107
- opacity: 0.5;
108
- }
109
- 25%, 75% {
110
- transform: translateY(-100px) translateX(50px);
111
- }
112
- }
113
-
114
- .login-card {
115
- width: 420px;
116
- padding: 2.5rem;
117
- border-radius: 16px;
118
- background: var(--card-bg);
119
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
120
- backdrop-filter: blur(8px);
121
- border: 1px solid rgba(255, 255, 255, 0.1);
122
- z-index: 10;
123
- animation: card-fade-in 0.6s ease-out;
124
- }
125
-
126
- @keyframes card-fade-in {
127
- from {
128
- opacity: 0;
129
- transform: translateY(20px);
130
- }
131
- to {
132
- opacity: 1;
133
- transform: translateY(0);
134
- }
135
- }
136
-
137
- .login-header {
138
- text-align: center;
139
- margin-bottom: 2rem;
140
- }
141
-
142
- .login-header h1 {
143
- font-size: 2rem;
144
- font-weight: 600;
145
- margin-bottom: 0.5rem;
146
- background: linear-gradient(45deg, #6f42c1, #5181f1);
147
- -webkit-background-clip: text;
148
- -webkit-text-fill-color: transparent;
149
- letter-spacing: 0.5px;
150
- }
151
-
152
- .login-header p {
153
- color: rgba(230, 230, 255, 0.7);
154
- font-size: 0.95rem;
155
- }
156
-
157
- .space-info {
158
- text-align: center;
159
- background: rgba(50, 50, 150, 0.2);
160
- padding: 0.75rem;
161
- border-radius: 8px;
162
- margin-bottom: 1.5rem;
163
- font-size: 0.9rem;
164
- border: 1px solid rgba(111, 66, 193, 0.3);
165
- }
166
-
167
- .space-info a {
168
- color: var(--primary-color);
169
- text-decoration: none;
170
- font-weight: bold;
171
- transition: all 0.2s;
172
- }
173
-
174
- .space-info a:hover {
175
- text-decoration: underline;
176
- color: var(--secondary-color);
177
- }
178
-
179
- .login-form {
180
- display: flex;
181
- flex-direction: column;
182
- }
183
-
184
- .form-group {
185
- margin-bottom: 1.5rem;
186
- position: relative;
187
- }
188
-
189
- .form-group label {
190
- display: block;
191
- margin-bottom: 0.5rem;
192
- font-size: 0.9rem;
193
- font-weight: 500;
194
- color: rgba(230, 230, 255, 0.9);
195
- }
196
-
197
- .form-control {
198
- width: 100%;
199
- padding: 0.75rem 1rem;
200
- font-size: 1rem;
201
- line-height: 1.5;
202
- color: var(--text-color);
203
- background-color: var(--input-bg);
204
- border: 1px solid rgba(255, 255, 255, 0.1);
205
- border-radius: 8px;
206
- transition: all 0.2s ease;
207
- outline: none;
208
- }
209
-
210
- .form-control:focus {
211
- border-color: var(--primary-color);
212
- box-shadow: 0 0 0 3px rgba(111, 66, 193, 0.2);
213
- }
214
-
215
- .btn {
216
- display: inline-block;
217
- font-weight: 500;
218
- text-align: center;
219
- vertical-align: middle;
220
- cursor: pointer;
221
- padding: 0.75rem 1rem;
222
- font-size: 1rem;
223
- line-height: 1.5;
224
- border-radius: 8px;
225
- transition: all 0.15s ease-in-out;
226
- border: none;
227
- }
228
-
229
- .btn-primary {
230
- color: #fff;
231
- background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
232
- box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
233
- position: relative;
234
- overflow: hidden;
235
- }
236
-
237
- .btn-primary:hover {
238
- transform: translateY(-2px);
239
- box-shadow: 0 6px 15px rgba(111, 66, 193, 0.4);
240
- }
241
-
242
- .btn-primary:active {
243
- transform: translateY(0);
244
- }
245
-
246
- /* 添加光效效果 */
247
- .btn-primary::before {
248
- content: '';
249
- position: absolute;
250
- top: -50%;
251
- left: -50%;
252
- width: 200%;
253
- height: 200%;
254
- background: linear-gradient(
255
- to bottom right,
256
- rgba(255, 255, 255, 0) 0%,
257
- rgba(255, 255, 255, 0.1) 50%,
258
- rgba(255, 255, 255, 0) 100%
259
- );
260
- transform: rotate(45deg);
261
- animation: btn-shine 3s infinite;
262
- }
263
-
264
- @keyframes btn-shine {
265
- 0% {
266
- left: -50%;
267
- }
268
- 100% {
269
- left: 150%;
270
- }
271
- }
272
-
273
- .error-message {
274
- background-color: rgba(248, 114, 114, 0.2);
275
- color: var(--error-color);
276
- padding: 0.75rem;
277
- border-radius: 6px;
278
- margin-bottom: 1.5rem;
279
- font-size: 0.9rem;
280
- border-left: 3px solid var(--error-color);
281
- display: {{ 'block' if error else 'none' }};
282
- }
283
-
284
- .logo {
285
- margin-bottom: 1rem;
286
- font-size: 3rem;
287
- animation: glow 2s infinite alternate;
288
- }
289
-
290
- @keyframes glow {
291
- from {
292
- text-shadow: 0 0 5px rgba(111, 66, 193, 0.5), 0 0 10px rgba(111, 66, 193, 0.5);
293
- }
294
- to {
295
- text-shadow: 0 0 10px rgba(111, 66, 193, 0.8), 0 0 20px rgba(111, 66, 193, 0.8);
296
- }
297
- }
298
- </style>
299
- </head>
300
- <body>
301
- <div class="grid-background"></div>
302
- <div class="particles">
303
- <!-- 粒子元素会由JS生成 -->
304
- </div>
305
- <div class="web-container">
306
- <canvas class="web" id="webCanvas"></canvas>
307
- </div>
308
-
309
- <div class="login-card">
310
- <div class="login-header">
311
- <div class="logo">🤖</div>
312
- <h1>Abacus Chat Proxy</h1>
313
- <p>请输入访问密码</p>
314
- </div>
315
-
316
- {% if space_url %}
317
- <div class="space-info">
318
- api接口为{{ space_url }}/v1,请点击 <a href="{{ space_url }}" target="_blank">{{ space_url }}</a> 来登录并查看使用情况,
319
- </div>
320
- {% endif %}
321
-
322
- <div class="error-message" id="error-message">
323
- {{ error }}
324
- </div>
325
-
326
- <form class="login-form" method="post" action="/login">
327
- <div class="form-group">
328
- <label for="password">密码</label>
329
- <input type="password" class="form-control" id="password" name="password" placeholder="请输入访问密码" required>
330
- </div>
331
-
332
- <button type="submit" class="btn btn-primary">登录</button>
333
- </form>
334
- </div>
335
-
336
- <script>
337
- // 创建浮动粒子
338
- function createParticles() {
339
- const particlesContainer = document.querySelector('.particles');
340
- const particleCount = 20;
341
-
342
- for (let i = 0; i < particleCount; i++) {
343
- const particle = document.createElement('div');
344
- particle.className = 'particle';
345
-
346
- // 随机位置和大小
347
- const size = Math.random() * 5 + 2;
348
- const x = Math.random() * 100;
349
- const y = Math.random() * 100;
350
-
351
- particle.style.width = `${size}px`;
352
- particle.style.height = `${size}px`;
353
- particle.style.left = `${x}%`;
354
- particle.style.top = `${y}%`;
355
-
356
- // 随机动画延迟
357
- particle.style.animationDelay = `${Math.random() * 10}s`;
358
- particle.style.animationDuration = `${Math.random() * 10 + 10}s`;
359
-
360
- // 随机透明度
361
- particle.style.opacity = Math.random() * 0.5;
362
-
363
- particlesContainer.appendChild(particle);
364
- }
365
- }
366
-
367
- // 科幻蜘蛛网效果
368
- function initWebCanvas() {
369
- const canvas = document.getElementById('webCanvas');
370
- const ctx = canvas.getContext('2d');
371
- let width = window.innerWidth;
372
- let height = window.innerHeight;
373
-
374
- // 设置canvas尺寸
375
- canvas.width = width;
376
- canvas.height = height;
377
-
378
- // 节点类
379
- class Node {
380
- constructor(x, y) {
381
- this.x = x;
382
- this.y = y;
383
- this.vx = (Math.random() - 0.5) * 0.5;
384
- this.vy = (Math.random() - 0.5) * 0.5;
385
- this.radius = Math.random() * 2 + 1;
386
- }
387
-
388
- update() {
389
- if (this.x < 0 || this.x > width) this.vx = -this.vx;
390
- if (this.y < 0 || this.y > height) this.vy = -this.vy;
391
-
392
- this.x += this.vx;
393
- this.y += this.vy;
394
- }
395
-
396
- draw() {
397
- ctx.beginPath();
398
- ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
399
- ctx.fillStyle = 'rgba(111, 66, 193, 0.4)';
400
- ctx.fill();
401
- }
402
- }
403
-
404
- // 创建节点
405
- const nodeCount = Math.floor(width * height / 15000);
406
- const nodes = [];
407
-
408
- for (let i = 0; i < nodeCount; i++) {
409
- nodes.push(new Node(Math.random() * width, Math.random() * height));
410
- }
411
-
412
- // 绘制线条
413
- function drawWeb() {
414
- ctx.clearRect(0, 0, width, height);
415
-
416
- // 更新节点
417
- nodes.forEach(node => {
418
- node.update();
419
- node.draw();
420
- });
421
-
422
- // 绘制连线
423
- for (let i = 0; i < nodes.length; i++) {
424
- for (let j = i + 1; j < nodes.length; j++) {
425
- const dx = nodes[i].x - nodes[j].x;
426
- const dy = nodes[i].y - nodes[j].y;
427
- const distance = Math.sqrt(dx * dx + dy * dy);
428
-
429
- if (distance < 150) {
430
- ctx.beginPath();
431
- ctx.moveTo(nodes[i].x, nodes[i].y);
432
- ctx.lineTo(nodes[j].x, nodes[j].y);
433
- ctx.strokeStyle = `rgba(111, 66, 193, ${0.2 * (1 - distance / 150)})`;
434
- ctx.lineWidth = 0.5;
435
- ctx.stroke();
436
- }
437
- }
438
- }
439
-
440
- requestAnimationFrame(drawWeb);
441
- }
442
-
443
- // 监听窗口大小变化
444
- window.addEventListener('resize', () => {
445
- width = window.innerWidth;
446
- height = window.innerHeight;
447
- canvas.width = width;
448
- canvas.height = height;
449
- });
450
-
451
- // 开始动画
452
- drawWeb();
453
- }
454
-
455
- // 页面加载时初始化效果
456
- window.addEventListener('load', () => {
457
- createParticles();
458
- initWebCanvas();
459
- });
460
- </script>
461
- </body>
462
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 登录</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6f42c1;
10
+ --secondary-color: #4a32a8;
11
+ --bg-color: #0a0a1a;
12
+ --text-color: #e6e6ff;
13
+ --card-bg: rgba(30, 30, 60, 0.7);
14
+ --input-bg: rgba(40, 40, 80, 0.6);
15
+ --success-color: #36d399;
16
+ --error-color: #f87272;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
+ }
25
+
26
+ body {
27
+ min-height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background-color: var(--bg-color);
32
+ background-image:
33
+ radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
34
+ radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
35
+ color: var(--text-color);
36
+ position: relative;
37
+ overflow: hidden;
38
+ }
39
+
40
+ /* 科幻蜘蛛网动画 */
41
+ .web-container {
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ z-index: -2;
48
+ opacity: 0.6;
49
+ }
50
+
51
+ .web {
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+
56
+ /* 动态背景网格 */
57
+ .grid-background {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
64
+ linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
65
+ background-size: 30px 30px;
66
+ z-index: -1;
67
+ animation: grid-move 20s linear infinite;
68
+ }
69
+
70
+ @keyframes grid-move {
71
+ 0% {
72
+ transform: translateY(0);
73
+ }
74
+ 100% {
75
+ transform: translateY(30px);
76
+ }
77
+ }
78
+
79
+ /* 浮动粒子效果 */
80
+ .particles {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ width: 100%;
85
+ height: 100%;
86
+ overflow: hidden;
87
+ z-index: -1;
88
+ }
89
+
90
+ .particle {
91
+ position: absolute;
92
+ display: block;
93
+ pointer-events: none;
94
+ width: 6px;
95
+ height: 6px;
96
+ background-color: rgba(111, 66, 193, 0.2);
97
+ border-radius: 50%;
98
+ animation: float 20s infinite ease-in-out;
99
+ }
100
+
101
+ @keyframes float {
102
+ 0%, 100% {
103
+ transform: translateY(0) translateX(0);
104
+ opacity: 0;
105
+ }
106
+ 50% {
107
+ opacity: 0.5;
108
+ }
109
+ 25%, 75% {
110
+ transform: translateY(-100px) translateX(50px);
111
+ }
112
+ }
113
+
114
+ .login-card {
115
+ width: 420px;
116
+ padding: 2.5rem;
117
+ border-radius: 16px;
118
+ background: var(--card-bg);
119
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
120
+ backdrop-filter: blur(8px);
121
+ border: 1px solid rgba(255, 255, 255, 0.1);
122
+ z-index: 10;
123
+ animation: card-fade-in 0.6s ease-out;
124
+ }
125
+
126
+ @keyframes card-fade-in {
127
+ from {
128
+ opacity: 0;
129
+ transform: translateY(20px);
130
+ }
131
+ to {
132
+ opacity: 1;
133
+ transform: translateY(0);
134
+ }
135
+ }
136
+
137
+ .login-header {
138
+ text-align: center;
139
+ margin-bottom: 2rem;
140
+ }
141
+
142
+ .login-header h1 {
143
+ font-size: 2rem;
144
+ font-weight: 600;
145
+ margin-bottom: 0.5rem;
146
+ background: linear-gradient(45deg, #6f42c1, #5181f1);
147
+ -webkit-background-clip: text;
148
+ -webkit-text-fill-color: transparent;
149
+ letter-spacing: 0.5px;
150
+ }
151
+
152
+ .login-header p {
153
+ color: rgba(230, 230, 255, 0.7);
154
+ font-size: 0.95rem;
155
+ }
156
+
157
+ .space-info {
158
+ text-align: center;
159
+ background: rgba(50, 50, 150, 0.2);
160
+ padding: 0.75rem;
161
+ border-radius: 8px;
162
+ margin-bottom: 1.5rem;
163
+ font-size: 0.9rem;
164
+ border: 1px solid rgba(111, 66, 193, 0.3);
165
+ }
166
+
167
+ .space-info a {
168
+ color: var(--primary-color);
169
+ text-decoration: none;
170
+ font-weight: bold;
171
+ transition: all 0.2s;
172
+ }
173
+
174
+ .space-info a:hover {
175
+ text-decoration: underline;
176
+ color: var(--secondary-color);
177
+ }
178
+
179
+ .login-form {
180
+ display: flex;
181
+ flex-direction: column;
182
+ }
183
+
184
+ .form-group {
185
+ margin-bottom: 1.5rem;
186
+ position: relative;
187
+ }
188
+
189
+ .form-group label {
190
+ display: block;
191
+ margin-bottom: 0.5rem;
192
+ font-size: 0.9rem;
193
+ font-weight: 500;
194
+ color: rgba(230, 230, 255, 0.9);
195
+ }
196
+
197
+ .form-control {
198
+ width: 100%;
199
+ padding: 0.75rem 1rem;
200
+ font-size: 1rem;
201
+ line-height: 1.5;
202
+ color: var(--text-color);
203
+ background-color: var(--input-bg);
204
+ border: 1px solid rgba(255, 255, 255, 0.1);
205
+ border-radius: 8px;
206
+ transition: all 0.2s ease;
207
+ outline: none;
208
+ }
209
+
210
+ .form-control:focus {
211
+ border-color: var(--primary-color);
212
+ box-shadow: 0 0 0 3px rgba(111, 66, 193, 0.2);
213
+ }
214
+
215
+ .btn {
216
+ display: inline-block;
217
+ font-weight: 500;
218
+ text-align: center;
219
+ vertical-align: middle;
220
+ cursor: pointer;
221
+ padding: 0.75rem 1rem;
222
+ font-size: 1rem;
223
+ line-height: 1.5;
224
+ border-radius: 8px;
225
+ transition: all 0.15s ease-in-out;
226
+ border: none;
227
+ }
228
+
229
+ .btn-primary {
230
+ color: #fff;
231
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
232
+ box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
233
+ position: relative;
234
+ overflow: hidden;
235
+ }
236
+
237
+ .btn-primary:hover {
238
+ transform: translateY(-2px);
239
+ box-shadow: 0 6px 15px rgba(111, 66, 193, 0.4);
240
+ }
241
+
242
+ .btn-primary:active {
243
+ transform: translateY(0);
244
+ }
245
+
246
+ /* 添加光效效果 */
247
+ .btn-primary::before {
248
+ content: '';
249
+ position: absolute;
250
+ top: -50%;
251
+ left: -50%;
252
+ width: 200%;
253
+ height: 200%;
254
+ background: linear-gradient(
255
+ to bottom right,
256
+ rgba(255, 255, 255, 0) 0%,
257
+ rgba(255, 255, 255, 0.1) 50%,
258
+ rgba(255, 255, 255, 0) 100%
259
+ );
260
+ transform: rotate(45deg);
261
+ animation: btn-shine 3s infinite;
262
+ }
263
+
264
+ @keyframes btn-shine {
265
+ 0% {
266
+ left: -50%;
267
+ }
268
+ 100% {
269
+ left: 150%;
270
+ }
271
+ }
272
+
273
+ .error-message {
274
+ background-color: rgba(248, 114, 114, 0.2);
275
+ color: var(--error-color);
276
+ padding: 0.75rem;
277
+ border-radius: 6px;
278
+ margin-bottom: 1.5rem;
279
+ font-size: 0.9rem;
280
+ border-left: 3px solid var(--error-color);
281
+ display: {{ 'block' if error else 'none' }};
282
+ }
283
+
284
+ .logo {
285
+ margin-bottom: 1rem;
286
+ font-size: 3rem;
287
+ animation: glow 2s infinite alternate;
288
+ }
289
+
290
+ @keyframes glow {
291
+ from {
292
+ text-shadow: 0 0 5px rgba(111, 66, 193, 0.5), 0 0 10px rgba(111, 66, 193, 0.5);
293
+ }
294
+ to {
295
+ text-shadow: 0 0 10px rgba(111, 66, 193, 0.8), 0 0 20px rgba(111, 66, 193, 0.8);
296
+ }
297
+ }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <div class="grid-background"></div>
302
+ <div class="particles">
303
+ <!-- 粒子元素会由JS生成 -->
304
+ </div>
305
+ <div class="web-container">
306
+ <canvas class="web" id="webCanvas"></canvas>
307
+ </div>
308
+
309
+ <div class="login-card">
310
+ <div class="login-header">
311
+ <div class="logo">🤖</div>
312
+ <h1>Abacus Chat Proxy</h1>
313
+ <p>请输入访问密码</p>
314
+ </div>
315
+
316
+ {% if space_url %}
317
+ <div class="space-info">
318
+ api接口为{{ space_url }}/v1,请点击 <a href="{{ space_url }}" target="_blank">{{ space_url }}</a> 来登录并查看使用情况,
319
+ </div>
320
+ {% endif %}
321
+
322
+ <div class="error-message" id="error-message">
323
+ {{ error }}
324
+ </div>
325
+
326
+ <form class="login-form" method="post" action="/login">
327
+ <div class="form-group">
328
+ <label for="password">密码</label>
329
+ <input type="password" class="form-control" id="password" name="password" placeholder="请输入访问密码" required>
330
+ </div>
331
+
332
+ <button type="submit" class="btn btn-primary">登录</button>
333
+ </form>
334
+ </div>
335
+
336
+ <script>
337
+ // 创建浮动粒子
338
+ function createParticles() {
339
+ const particlesContainer = document.querySelector('.particles');
340
+ const particleCount = 20;
341
+
342
+ for (let i = 0; i < particleCount; i++) {
343
+ const particle = document.createElement('div');
344
+ particle.className = 'particle';
345
+
346
+ // 随机位置和大小
347
+ const size = Math.random() * 5 + 2;
348
+ const x = Math.random() * 100;
349
+ const y = Math.random() * 100;
350
+
351
+ particle.style.width = `${size}px`;
352
+ particle.style.height = `${size}px`;
353
+ particle.style.left = `${x}%`;
354
+ particle.style.top = `${y}%`;
355
+
356
+ // 随机动画延迟
357
+ particle.style.animationDelay = `${Math.random() * 10}s`;
358
+ particle.style.animationDuration = `${Math.random() * 10 + 10}s`;
359
+
360
+ // 随机透明度
361
+ particle.style.opacity = Math.random() * 0.5;
362
+
363
+ particlesContainer.appendChild(particle);
364
+ }
365
+ }
366
+
367
+ // 科幻蜘蛛网效果
368
+ function initWebCanvas() {
369
+ const canvas = document.getElementById('webCanvas');
370
+ const ctx = canvas.getContext('2d');
371
+ let width = window.innerWidth;
372
+ let height = window.innerHeight;
373
+
374
+ // 设置canvas尺寸
375
+ canvas.width = width;
376
+ canvas.height = height;
377
+
378
+ // 节点类
379
+ class Node {
380
+ constructor(x, y) {
381
+ this.x = x;
382
+ this.y = y;
383
+ this.vx = (Math.random() - 0.5) * 0.5;
384
+ this.vy = (Math.random() - 0.5) * 0.5;
385
+ this.radius = Math.random() * 2 + 1;
386
+ }
387
+
388
+ update() {
389
+ if (this.x < 0 || this.x > width) this.vx = -this.vx;
390
+ if (this.y < 0 || this.y > height) this.vy = -this.vy;
391
+
392
+ this.x += this.vx;
393
+ this.y += this.vy;
394
+ }
395
+
396
+ draw() {
397
+ ctx.beginPath();
398
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
399
+ ctx.fillStyle = 'rgba(111, 66, 193, 0.4)';
400
+ ctx.fill();
401
+ }
402
+ }
403
+
404
+ // 创建节点
405
+ const nodeCount = Math.floor(width * height / 15000);
406
+ const nodes = [];
407
+
408
+ for (let i = 0; i < nodeCount; i++) {
409
+ nodes.push(new Node(Math.random() * width, Math.random() * height));
410
+ }
411
+
412
+ // 绘制线条
413
+ function drawWeb() {
414
+ ctx.clearRect(0, 0, width, height);
415
+
416
+ // 更新节点
417
+ nodes.forEach(node => {
418
+ node.update();
419
+ node.draw();
420
+ });
421
+
422
+ // 绘制连线
423
+ for (let i = 0; i < nodes.length; i++) {
424
+ for (let j = i + 1; j < nodes.length; j++) {
425
+ const dx = nodes[i].x - nodes[j].x;
426
+ const dy = nodes[i].y - nodes[j].y;
427
+ const distance = Math.sqrt(dx * dx + dy * dy);
428
+
429
+ if (distance < 150) {
430
+ ctx.beginPath();
431
+ ctx.moveTo(nodes[i].x, nodes[i].y);
432
+ ctx.lineTo(nodes[j].x, nodes[j].y);
433
+ ctx.strokeStyle = `rgba(111, 66, 193, ${0.2 * (1 - distance / 150)})`;
434
+ ctx.lineWidth = 0.5;
435
+ ctx.stroke();
436
+ }
437
+ }
438
+ }
439
+
440
+ requestAnimationFrame(drawWeb);
441
+ }
442
+
443
+ // 监听窗口大小变化
444
+ window.addEventListener('resize', () => {
445
+ width = window.innerWidth;
446
+ height = window.innerHeight;
447
+ canvas.width = width;
448
+ canvas.height = height;
449
+ });
450
+
451
+ // 开始动画
452
+ drawWeb();
453
+ }
454
+
455
+ // 页面加载时初始化效果
456
+ window.addEventListener('load', () => {
457
+ createParticles();
458
+ initWebCanvas();
459
+ });
460
+ </script>
461
+ </body>
462
  </html>