update sth at 2026-01-15 17:04:29
Browse files- KEEP_RUNNING.md +35 -0
- app/providers/zai_provider.py +37 -23
- uptime-heartbeat.py +56 -0
KEEP_RUNNING.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Keep HuggingFace Space Alive
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
# 每 5 分钟触发一次
|
| 6 |
+
- cron: '*/5 * * * *'
|
| 7 |
+
workflow_dispatch: # 支持手动触发
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
keep-alive:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
|
| 13 |
+
steps:
|
| 14 |
+
- name: Check Space Health
|
| 15 |
+
run: |
|
| 16 |
+
echo "🚀 发送心跳到 HuggingFace Space"
|
| 17 |
+
curl -s -o /dev/null -w "HTTP 状态码: %{http_code}, 耗时: %{time_total}s\n" \
|
| 18 |
+
https://sanbo1200-zai.hf.space/hf/v1/models || true
|
| 19 |
+
|
| 20 |
+
- name: Test API Endpoint (可选)
|
| 21 |
+
env:
|
| 22 |
+
API_KEY: ${{ secrets.API_KEY }}
|
| 23 |
+
run: |
|
| 24 |
+
# 如果有 API key,测试真实请求
|
| 25 |
+
if [ -n "$API_KEY" ]; then
|
| 26 |
+
echo "测试 API 调用..."
|
| 27 |
+
curl -s -X POST https://sanbo1200-zai.hf.space/hf/v1/chat/completions \
|
| 28 |
+
-H "Content-Type: application/json" \
|
| 29 |
+
-H "Authorization: Bearer $API_KEY" \
|
| 30 |
+
-d '{"model":"GLM-4.5","messages":[{"role":"user","content":"ping"}],"stream":false}' \
|
| 31 |
+
-o /dev/null -w "API 调用状态: %{http_code}\n" || true
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
- name: Log Time
|
| 35 |
+
run: echo "心跳发送完成: $(date -u)"
|
app/providers/zai_provider.py
CHANGED
|
@@ -705,6 +705,7 @@ class ZAIProvider(BaseProvider):
|
|
| 705 |
transformed = await self.transform_request(request)
|
| 706 |
self.logger.debug(f"[chat_completion] 转换后的请求: {transformed['url'][:100]}...")
|
| 707 |
|
|
|
|
| 708 |
# 根据请求类型返回响应
|
| 709 |
if request.stream:
|
| 710 |
# 流式响应
|
|
@@ -713,8 +714,11 @@ class ZAIProvider(BaseProvider):
|
|
| 713 |
# Get proxy configuration
|
| 714 |
proxies = self._get_proxy_config()
|
| 715 |
|
| 716 |
-
# 非流式响应
|
| 717 |
-
|
|
|
|
|
|
|
|
|
|
| 718 |
response = await client.post(
|
| 719 |
transformed["url"],
|
| 720 |
headers=transformed["headers"],
|
|
@@ -731,7 +735,13 @@ class ZAIProvider(BaseProvider):
|
|
| 731 |
# 记录响应状态
|
| 732 |
self.logger.info(f"✅ 上游响应成功: {response.status_code}, Content-Length: {response.headers.get('content-length', 'N/A')}")
|
| 733 |
try:
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
except Exception as transform_error:
|
| 736 |
self.logger.error(f"❌ transform_response 失败: {transform_error}")
|
| 737 |
body_text = response.text[:1000] if response.text else "无响应体"
|
|
@@ -827,21 +837,6 @@ class ZAIProvider(BaseProvider):
|
|
| 827 |
yield "data: [DONE]\n\n"
|
| 828 |
return
|
| 829 |
|
| 830 |
-
async def transform_response(
|
| 831 |
-
self,
|
| 832 |
-
response: httpx.Response,
|
| 833 |
-
request: OpenAIRequest,
|
| 834 |
-
transformed: Dict[str, Any]
|
| 835 |
-
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
|
| 836 |
-
"""转换Z.AI响应为OpenAI格式"""
|
| 837 |
-
chat_id = transformed["chat_id"]
|
| 838 |
-
model = transformed["model"]
|
| 839 |
-
|
| 840 |
-
if request.stream:
|
| 841 |
-
return self._handle_stream_response(response, chat_id, model, request, transformed)
|
| 842 |
-
else:
|
| 843 |
-
return await self._handle_non_stream_response(response, chat_id, model)
|
| 844 |
-
|
| 845 |
async def _handle_stream_response(
|
| 846 |
self,
|
| 847 |
response: httpx.Response,
|
|
@@ -1122,15 +1117,27 @@ class ZAIProvider(BaseProvider):
|
|
| 1122 |
self.logger.info(f"[_handle_non_stream_response] 开始处理响应,Content-Type: {response.headers.get('content-type', '未知')}")
|
| 1123 |
|
| 1124 |
all_lines = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
try:
|
| 1126 |
async for line in response.aiter_lines():
|
| 1127 |
if not line:
|
| 1128 |
continue
|
| 1129 |
|
| 1130 |
line = line.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1131 |
# 收集所有行用于调试
|
| 1132 |
if line:
|
| 1133 |
-
self.logger.debug(f"[_handle_non_stream_response]
|
| 1134 |
all_lines.append(line)
|
| 1135 |
|
| 1136 |
# 仅处理以 data: 开头的 SSE 行,其余行尝试作为错误/JSON 忽略
|
|
@@ -1201,13 +1208,20 @@ class ZAIProvider(BaseProvider):
|
|
| 1201 |
final_content += delta_content
|
| 1202 |
|
| 1203 |
# 循环结束后,记录所有采集的线和内容
|
| 1204 |
-
self.logger.info(f"[_handle_non_stream_response]
|
| 1205 |
-
self.logger.debug(f"[_handle_non_stream_response]
|
|
|
|
| 1206 |
if not final_content and not reasoning_content and len(all_lines) > 0:
|
| 1207 |
-
self.logger.warning(f"[_handle_non_stream_response]
|
| 1208 |
-
self.logger.warning(f"[_handle_non_stream_response]
|
| 1209 |
|
| 1210 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
self.logger.error(f"❌ 非流式响应处理错误: {e}")
|
| 1212 |
import traceback
|
| 1213 |
self.logger.error(traceback.format_exc())
|
|
|
|
| 705 |
transformed = await self.transform_request(request)
|
| 706 |
self.logger.debug(f"[chat_completion] 转换后的请求: {transformed['url'][:100]}...")
|
| 707 |
|
| 708 |
+
|
| 709 |
# 根据请求类型返回响应
|
| 710 |
if request.stream:
|
| 711 |
# 流式响应
|
|
|
|
| 714 |
# Get proxy configuration
|
| 715 |
proxies = self._get_proxy_config()
|
| 716 |
|
| 717 |
+
# 非流式响应 - 增加超时时间到90秒,Z.AI API 有时响应较慢
|
| 718 |
+
# 使用扩展的超时配置:连接5秒,读取85秒
|
| 719 |
+
extended_timeout = httpx.Timeout(5.0, read=85.0, connect=5.0)
|
| 720 |
+
async with httpx.AsyncClient(timeout=extended_timeout, proxy=proxies) as client:
|
| 721 |
+
self.logger.info(f"🔄 发送非流式请求到 Z.AI (超时: 90秒): {transformed['url']}")
|
| 722 |
response = await client.post(
|
| 723 |
transformed["url"],
|
| 724 |
headers=transformed["headers"],
|
|
|
|
| 735 |
# 记录响应状态
|
| 736 |
self.logger.info(f"✅ 上游响应成功: {response.status_code}, Content-Length: {response.headers.get('content-length', 'N/A')}")
|
| 737 |
try:
|
| 738 |
+
# 修正:立即调用正确的处理方法
|
| 739 |
+
if request.stream:
|
| 740 |
+
return self._create_stream_response(request, transformed)
|
| 741 |
+
else:
|
| 742 |
+
chat_id = transformed.get("chat_id", "unknown")
|
| 743 |
+
model = transformed.get("model", "unknown")
|
| 744 |
+
return await self._handle_non_stream_response(response, chat_id, model)
|
| 745 |
except Exception as transform_error:
|
| 746 |
self.logger.error(f"❌ transform_response 失败: {transform_error}")
|
| 747 |
body_text = response.text[:1000] if response.text else "无响应体"
|
|
|
|
| 837 |
yield "data: [DONE]\n\n"
|
| 838 |
return
|
| 839 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
async def _handle_stream_response(
|
| 841 |
self,
|
| 842 |
response: httpx.Response,
|
|
|
|
| 1117 |
self.logger.info(f"[_handle_non_stream_response] 开始处理响应,Content-Type: {response.headers.get('content-type', '未知')}")
|
| 1118 |
|
| 1119 |
all_lines = []
|
| 1120 |
+
line_count = 0
|
| 1121 |
+
max_lines = 1000 # 限制读取行数,防止无限读取
|
| 1122 |
+
read_timeout_count = 0
|
| 1123 |
+
max_timeout_retries = 2 # 最大超时重试次数
|
| 1124 |
+
|
| 1125 |
try:
|
| 1126 |
async for line in response.aiter_lines():
|
| 1127 |
if not line:
|
| 1128 |
continue
|
| 1129 |
|
| 1130 |
line = line.strip()
|
| 1131 |
+
|
| 1132 |
+
# 数量限制:防止无限读取
|
| 1133 |
+
line_count += 1
|
| 1134 |
+
if line_count > max_lines:
|
| 1135 |
+
self.logger.warning(f"⚠️ 行数超过限制 {max_lines},停止读取")
|
| 1136 |
+
break
|
| 1137 |
+
|
| 1138 |
# 收集所有行用于调试
|
| 1139 |
if line:
|
| 1140 |
+
self.logger.debug(f"[_handle_non_stream_response] 原始行 [{line_count}]: {line[:200]}")
|
| 1141 |
all_lines.append(line)
|
| 1142 |
|
| 1143 |
# 仅处理以 data: 开头的 SSE 行,其余行尝试作为错误/JSON 忽略
|
|
|
|
| 1208 |
final_content += delta_content
|
| 1209 |
|
| 1210 |
# 循环结束后,记录所有采集的线和内容
|
| 1211 |
+
self.logger.info(f"[_handle_non_stream_response] 处理完成 - 总行数: {line_count}, 有效SSE行: {len(all_lines)}")
|
| 1212 |
+
self.logger.debug(f"[_handle_non_stream_response] 内容统计 - 答案: {len(final_content)}字, 思考: {len(reasoning_content)}字")
|
| 1213 |
+
|
| 1214 |
if not final_content and not reasoning_content and len(all_lines) > 0:
|
| 1215 |
+
self.logger.warning(f"[_handle_non_stream_response] ⚠️ 未提取到内容,但收到 {len(all_lines)} 行数据")
|
| 1216 |
+
self.logger.warning(f"[_handle_non_stream_response] 调试数据: {all_lines[:5]}")
|
| 1217 |
|
| 1218 |
except Exception as e:
|
| 1219 |
+
# 特殊处理超时异常,提供降级方案
|
| 1220 |
+
if "ReadTimeout" in str(type(e).__name__) or "Timeout" in str(type(e).__name__):
|
| 1221 |
+
self.logger.error(f"⏰ 读取超时异常 - 已处理 {line_count} 行数据,部分结果: {len(final_content)}字")
|
| 1222 |
+
if final_content or reasoning_content:
|
| 1223 |
+
self.logger.warning(f"⚠️ 虽然超时,但返回已收集的内容,用户仍可获得部分响应")
|
| 1224 |
+
# 继续执行后续的清理返回逻辑,不立即返回错误
|
| 1225 |
self.logger.error(f"❌ 非流式响应处理错误: {e}")
|
| 1226 |
import traceback
|
| 1227 |
self.logger.error(traceback.format_exc())
|
uptime-heartbeat.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Uptime Robot 心跳保活脚本
|
| 5 |
+
使用方法:
|
| 6 |
+
1. 在 Uptime Robot 创建监控,类型: HTTP(s),URL: https://{your-space}.hf.space/hf/v1/models
|
| 7 |
+
2. 间隔设置为 5-10 分钟
|
| 8 |
+
3. 或者直接用此脚本本地运行
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import time
|
| 12 |
+
import requests
|
| 13 |
+
import os
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
def send_heartbeat():
|
| 17 |
+
"""发送心跳请求到 HuggingFace Space"""
|
| 18 |
+
|
| 19 |
+
# 你的 Space URL
|
| 20 |
+
# 实际的 HuggingFace Space 域名格式: https://sanbo1200-zai.hf.space
|
| 21 |
+
SPACE_URL = os.getenv("HF_SPACE_URL", "https://sanbo1200-zai.hf.space")
|
| 22 |
+
|
| 23 |
+
# 测试的健康检查端点(使用 /hf/v1/models 轻量级接口)
|
| 24 |
+
HEALTH_URL = f"{SPACE_URL}/hf/v1/models"
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
start_time = datetime.now()
|
| 28 |
+
response = requests.get(
|
| 29 |
+
HEALTH_URL,
|
| 30 |
+
timeout=10,
|
| 31 |
+
headers={
|
| 32 |
+
"User-Agent": "UptimeMonitor/1.0"
|
| 33 |
+
}
|
| 34 |
+
)
|
| 35 |
+
end_time = datetime.now()
|
| 36 |
+
response_time = (end_time - start_time).total_seconds() * 1000
|
| 37 |
+
|
| 38 |
+
if response.status_code == 200:
|
| 39 |
+
print(f"✅ [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 心跳成功 - {response_time:.0f}ms")
|
| 40 |
+
return True
|
| 41 |
+
else:
|
| 42 |
+
print(f"⚠️ [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 心跳失败 - 状态码: {response.status_code}")
|
| 43 |
+
return False
|
| 44 |
+
|
| 45 |
+
except requests.exceptions.RequestException as e:
|
| 46 |
+
print(f"❌ [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 连接错误: {e}")
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
print("🚀 HuggingFace Space 保活服务启动")
|
| 51 |
+
print(f"目标空间: https://sanbo1200-zai.hf.space")
|
| 52 |
+
print("发送间隔: 每 5 分钟 (默认)\n")
|
| 53 |
+
|
| 54 |
+
while True:
|
| 55 |
+
send_heartbeat()
|
| 56 |
+
time.sleep(300) # 5分钟 = 300秒
|