This commit is contained in:
2026-03-21 18:03:31 +08:00
2 changed files with 134 additions and 47 deletions

View File

@@ -1,6 +1,4 @@
name: Auto Deploy NeoBot (Full Env Secrets) name: Auto Deploy NeoBot (FRP + SSH 密码登录)
# 触发条件推送到main分支 或 手动触发
on: on:
push: push:
branches: [ main ] branches: [ main ]
@@ -8,50 +6,61 @@ on:
jobs: jobs:
deploy-to-server: deploy-to-server:
# 关联你的仓库环境ENV
environment: ENV environment: ENV
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 # 防超时堵塞
steps: steps:
# ========== 1. 检查环境密钥配置 ==========
- name: 检查环境密钥配置 - name: 检查环境密钥配置
run: | run: |
echo "✅ 已关联环境: ${{ github.environment }}" echo "✅ 已关联环境: ${{ github.environment }}"
echo "✅ API_URL 密钥是否存在: ${{ secrets.API_URL != '' }}" # 仅检查密码登录必需的3个密钥
echo "✅ API_TOKEN 密钥是否存在: ${{ secrets.NEOBOT_DEPLOY_TOKEN != '' }}" echo "✅ PROD_SERVER_HOST 密钥是否存在: ${{ secrets.PROD_SERVER_HOST != '' }}"
echo "✅ PROD_SERVER_USER 密钥是否存在: ${{ secrets.PROD_SERVER_USER != '' }}"
echo "✅ PROD_SERVER_PASS 密钥是否存在: ${{ secrets.PROD_SERVER_PASS != '' }}"
- name: 调用部署API # ========== 2. 安装 sshpass密码登录必需 ==========
env: - name: 安装 sshpass 工具
# 从环境密钥中读取API地址和Token均为密文
API_URL: ${{ secrets.API_URL }}
API_TOKEN: ${{ secrets.NEOBOT_DEPLOY_TOKEN }}
run: | run: |
# 安装jq用于解析JSON sudo apt-get update && sudo apt-get install -y sshpass
sudo apt-get update && sudo apt-get install -y jq
# ========== 3. 密码登录服务器 + 执行部署 ==========
# 打印关键信息(脱敏,仅验证是否读取到值) - name: 执行FRP穿透部署用户名+密码登录)
echo "📌 调用的API地址脱敏: $(echo $API_URL | sed 's/http:\/\///; s/\/deploy//')" id: ssh_deploy_step
continue-on-error: true
# 发送POST请求到部署API所有配置均来自密钥 run: |
RESPONSE=$(curl -s -X POST \ # 核心sshpass 实现密码登录,-p 8000 是FRP转发端口
$API_URL \ sshpass -p "${{ secrets.PROD_SERVER_PASS }}" \
-H "Content-Type: application/json" \ ssh -o StrictHostKeyChecking=no -p 8000 ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF'
-H "X-API-Token: $API_TOKEN" \ set -e
-d '{"script_name":"deploy.sh"}') # 适配NeoBot项目更新依赖+重启systemd服务
cd /home/k/NeoBot
# 打印完整响应(便于调试) git pull
echo "📝 API响应详情" pip install -r requirements.txt --upgrade --timeout 300 --only-binary=:all:
echo $RESPONSE | jq . sudo systemctl daemon-reload
sudo systemctl restart neobot
# 解析status字段判断部署结果 # 验证服务状态
STATUS=$(echo $RESPONSE | jq -r '.status') if ! sudo systemctl is-active --quiet neobot; then
if [ "$STATUS" = "success" ]; then echo "❌ NeoBot服务启动失败最后10行日志"
echo "✅ 部署成功!" sudo journalctl -u neobot -n 10 --no-pager
exit 1
fi
echo "✅ NeoBot服务重启成功"
EOF
# ========== 4. 判定最终部署结果 ==========
- name: 判定最终部署结果
run: |
if [ ${{ steps.ssh_deploy_step.outcome }} = 'success' ]; then
echo "✅ 最终部署成功已更新依赖并重启NeoBot systemd服务"
exit 0 exit 0
else else
echo "❌ 部署失败!错误信息:$(echo $RESPONSE | jq -r '.message')" echo "❌ 最终部署失败!核心SSH部署步骤执行出错"
exit 1 exit 1
fi fi
# ========== 5. 部署失败通知(可选) ==========
- name: 部署失败通知(可选) - name: 部署失败通知(可选)
if: failure() if: failure()
run: | run: |
echo "⚠️ 部署失败,可在此添加通知逻辑" echo "⚠️ 部署失败,可在此添加钉钉/企业微信通知逻辑"

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import aiohttp import aiohttp
import asyncio
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from core.utils.logger import logger from core.utils.logger import logger
@@ -24,9 +25,9 @@ class DouyinParser(BaseParser):
# 消息去重缓存 # 消息去重缓存
self.processed_messages: TTLCache[int, bool] = TTLCache(maxsize=100, ttl=10) self.processed_messages: TTLCache[int, bool] = TTLCache(maxsize=100, ttl=10)
async def parse(self, url: str) -> Optional[Dict[str, Any]]: async def _parse_api_xhus(self, url: str) -> Optional[Dict[str, Any]]:
""" """
解析抖音视频信息 使用 xhus API 解析抖音视频
Args: Args:
url (str): 抖音视频URL url (str): 抖音视频URL
@@ -35,31 +36,29 @@ class DouyinParser(BaseParser):
Optional[Dict[str, Any]]: 视频信息字典如果失败则返回None Optional[Dict[str, Any]]: 视频信息字典如果失败则返回None
""" """
try: try:
# 使用第三方API解析抖音视频
api_url = f"http://api.xhus.cn/api/douyin?url={url}" api_url = f"http://api.xhus.cn/api/douyin?url={url}"
session = self.get_session() session = self.get_session()
async with session.get(api_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=10)) as response: async with session.get(api_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=10)) as response:
if response.status != 200: if response.status != 200:
logger.error(f"[{self.name}] API请求失败状态码: {response.status}") logger.error(f"[{self.name}] xhus API请求失败状态码: {response.status}")
return None return None
response_data = await response.json() response_data = await response.json()
if not isinstance(response_data, dict): if not isinstance(response_data, dict):
logger.error(f"[{self.name}] API返回格式错误: {response_data}") logger.error(f"[{self.name}] xhus API返回格式错误: {response_data}")
return None return None
if response_data.get("code") != 200: if response_data.get("code") != 200:
logger.error(f"[{self.name}] API返回错误: {response_data}") logger.error(f"[{self.name}] xhus API返回错误: {response_data}")
return None return None
data = response_data.get("data", {}) data = response_data.get("data", {})
if not data: if not data:
logger.error(f"[{self.name}] API返回数据为空") logger.error(f"[{self.name}] xhus API返回数据为空")
return None return None
# 转换API响应格式
return { return {
"type": "video" if not data.get("images") or not isinstance(data.get("images"), list) else "image", "type": "video" if not data.get("images") or not isinstance(data.get("images"), list) else "image",
"video_url": data.get("url", ""), "video_url": data.get("url", ""),
@@ -74,13 +73,92 @@ class DouyinParser(BaseParser):
"music": data.get("music", {}), "music": data.get("music", {}),
} }
except (aiohttp.ClientError, KeyError, AttributeError, ValueError) as e:
logger.error(f"[{self.name}] 解析抖音视频信息失败: {e}")
logger.debug(f"失败的URL: {url}")
except Exception as e: except Exception as e:
logger.error(f"[{self.name}] 解析抖音视频时发生未知错误: {e}") logger.error(f"[{self.name}] xhus API解析失败: {e}")
logger.debug(f"失败的URL: {url}") return None
async def _parse_api_mmp(self, url: str) -> Optional[Dict[str, Any]]:
"""
使用 mmp API 解析抖音视频
Args:
url (str): 抖音视频URL
Returns:
Optional[Dict[str, Any]]: 视频信息字典如果失败则返回None
"""
try:
api_url = f"https://api.mmp.cc/api/Jiexi?url={url}"
session = self.get_session()
async with session.get(api_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=10)) as response:
if response.status != 200:
logger.error(f"[{self.name}] mmp API请求失败状态码: {response.status}")
return None
response_data = await response.json()
if not isinstance(response_data, dict):
logger.error(f"[{self.name}] mmp API返回格式错误: {response_data}")
return None
if response_data.get("code") != 200:
logger.error(f"[{self.name}] mmp API返回错误: {response_data}")
return None
data = response_data.get("data", {})
if not data:
logger.error(f"[{self.name}] mmp API返回数据为空")
return None
return {
"type": data.get("type", "video"),
"video_url": data.get("video_url", ""),
"video_url_HQ": data.get("video_url_HQ", ""),
"nickname": data.get("nickname", "未知作者"),
"desc": data.get("desc", "无描述"),
"aweme_id": data.get("aweme_id", ""),
"like": data.get("like", 0),
"cover": data.get("cover", ""),
"time": data.get("time", 0),
"author_avatar": data.get("author_avatar", ""),
"music": data.get("music", {}),
}
except Exception as e:
logger.error(f"[{self.name}] mmp API解析失败: {e}")
return None
async def parse(self, url: str) -> Optional[Dict[str, Any]]:
"""
解析抖音视频信息并发请求多个API取最快返回的结果
Args:
url (str): 抖音视频URL
Returns:
Optional[Dict[str, Any]]: 视频信息字典如果失败则返回None
"""
async def try_api(coro, api_name: str) -> tuple:
try:
result = await coro
return (result, api_name)
except Exception as e:
logger.error(f"[{self.name}] {api_name} API异常: {e}")
return (None, api_name)
tasks = [
try_api(self._parse_api_xhus(url), "xhus"),
try_api(self._parse_api_mmp(url), "mmp"),
]
for coro in asyncio.as_completed(tasks):
result, api_name = await coro
if result:
logger.info(f"[{self.name}] 使用 {api_name} API 成功解析")
return result
logger.error(f"[{self.name}] 所有API解析均失败")
return None return None
async def get_real_url(self, short_url: str) -> Optional[str]: async def get_real_url(self, short_url: str) -> Optional[str]: