Files
script/ai/hwaishell.sh
T
chaos e9c635dbd0 feat: 网关全面优化 - SSE流式转发/连接池/Token优先级/持久化
- 移除JWT格式过滤(token.count('.')>=2),改为len>200
- HUAWEI_TOKEN环境变量设为最高优先级
- Token持久化到/etc/huawei-gateway.env,重启自动恢复
- SSE流式转发(stream=True + iter_content)
- requests.Session连接池(20连接, 3次重试)
- Waitress线程数16→32
- 过滤hop-by-hop头(Connection/Keep-Alive/Upgrade)
- pip安装增加waitress依赖
- 新增独立huawei_gateway.py文件
2026-07-02 16:23:22 +08:00

922 lines
32 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ================= 颜色定义 =================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;36m'
PLAIN='\033[0m'
# ================= 工具函数 =================
log_info() { echo -e "${GREEN}[INFO]${PLAIN} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${PLAIN} $1"; }
log_error() { echo -e "${RED}[ERROR]${PLAIN} $1"; }
log_step() { echo -e "\n${BLUE}[${1}]${PLAIN} ${2}"; }
# 交互式读取,有默认值可选
read_or_default() {
local prompt_msg="$1"
local default_value="$2"
if [ -t 0 ]; then
read -p "$prompt_msg" input_val
echo "${input_val:-$default_value}"
else
echo "$default_value"
fi
}
# 交互式确认(管道模式下默认返回 true)
confirm_or_default() {
local prompt_msg="$1"
local default="$2"
if [ -t 0 ]; then
read -p "$prompt_msg" answer
answer="${answer:-$default}"
[[ "$answer" == "y" || "$answer" == "Y" ]]
else
[[ "$default" == "y" || "$default" == "Y" ]]
fi
}
# 检查是否有 systemdpid=1 是 systemd 才算真正有 systemd
has_systemd() {
[ -f /proc/1/comm ] && grep -qx "systemd" /proc/1/comm 2>/dev/null
}
# 安全的进程启动(支持 systemd 和 nohup 两种模式)
# 返回 0 表示启动成功
safe_start_service() {
local name=$1
local cmd=$2
if has_systemd; then
systemctl daemon-reload 2>/dev/null
if systemctl enable "$name" --now 2>/dev/null; then
log_info "$name 已通过 systemd 启动"
return 0
fi
# systemd 启动失败,回退到 nohup
log_warn "systemd 启动失败,回退到 nohup 模式"
fi
# 容器/无 systemd 环境:使用 nohup + 简单的 PID 文件管理
local pid_file="/var/run/${name}.pid"
local log_file="/var/log/${name}.log"
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
log_warn "$name 已在运行 (PID: $(cat "$pid_file"))"
return 0
fi
nohup $cmd > "$log_file" 2>&1 &
echo $! > "$pid_file"
sleep 1
if kill -0 "$!" 2>/dev/null; then
log_info "$name 已通过 nohup 启动 (PID: $!)"
return 0
else
log_error "$name 启动失败,查看日志: $log_file"
return 1
fi
}
# 检查服务状态
get_service_status() {
local name=$1
if has_systemd; then
systemctl is-active "$name" 2>/dev/null || echo "inactive"
else
local pid_file="/var/run/${name}.pid"
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
echo "active (nohup, PID: $(cat "$pid_file"))"
else
echo "inactive"
fi
fi
}
# 重启服务
restart_service() {
local name=$1
if has_systemd; then
systemctl restart "$name" 2>/dev/null
else
local pid_file="/var/run/${name}.pid"
if [ -f "$pid_file" ]; then
kill "$(cat "$pid_file")" 2>/dev/null
sleep 1
fi
fi
}
echo -e "${BLUE}==================================================${PLAIN}"
echo -e "${GREEN} 华为云 Token 动态网关 (6小时高能效缓存版) 部署 ${PLAIN}"
echo -e "${BLUE}==================================================${PLAIN}"
# 1. 检查权限
if [ "$EUID" -ne 0 ]; then
log_error "请使用 root 用户运行此脚本!"
exit 1
fi
# 检测环境类型
IS_CONTAINER=false
if [ -f /.dockerenv ] || grep -qE 'docker|containerd|kubepods' /proc/1/cgroup 2>/dev/null; then
IS_CONTAINER=true
log_warn "检测到容器环境,将使用 nohup 模式替代 systemd"
fi
# ================= 检测并安装 frpc =================
log_step "1/5" "正在检测 frpc 环境..."
if ! command -v frpc &>/dev/null && [ ! -f "/usr/local/bin/frpc" ] && [ ! -f "/usr/bin/frpc" ]; then
log_warn "未检测到 frpc"
if confirm_or_default "是否需要为您自动下载并安装最新版 frpc (使用国内加速镜像)? (y/n, 默认 y): " "y"; then
ARCH=$(uname -m)
case "$ARCH" in
x86_64) FRP_ARCH="amd64" ;;
aarch64) FRP_ARCH="arm64" ;;
armv7l) FRP_ARCH="arm" ;;
i386|i686) FRP_ARCH="386" ;;
*)
log_error "不支持的系统架构 $ARCH,请手动安装 frpc。"
exit 1
;;
esac
FRP_VERSION="0.61.0"
log_info "开始通过国内节点下载 frpc v${FRP_VERSION} (${FRP_ARCH})..."
# 使用多个镜像源,自动回退
MIRRORS=(
"https://ghfast.top/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
"https://mirror.ghproxy.com/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
"https://github.moeyy.xyz/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz"
)
DOWNLOADED=false
for url in "${MIRRORS[@]}"; do
log_info "尝试下载: $url"
if wget --timeout=30 --tries=2 --no-check-certificate -O frp.tar.gz "$url" 2>/dev/null; then
DOWNLOADED=true
break
fi
done
if [ "$DOWNLOADED" != "true" ]; then
log_error "所有镜像源下载失败!请检查网络或手动安装 frpc。"
exit 1
fi
tar -zxf frp.tar.gz > /dev/null 2>&1 || { log_error "解压失败"; exit 1; }
cd frp_${FRP_VERSION}_linux_${FRP_ARCH} || { log_error "无法进入解压目录"; exit 1; }
cp frpc /usr/local/bin/ && chmod +x /usr/local/bin/frpc
mkdir -p /etc/frp
if [ ! -f "/etc/frp/frpc.toml" ]; then
cat << 'FRPCFG' > /etc/frp/frpc.toml
serverAddr = "x.x.x.x"
serverPort = 7000
auth.method = "token"
auth.token = "your_token_here"
FRPCFG
log_warn "已生成 frpc 配置模板,请编辑 /etc/frp/frpc.toml 填入正确的服务器信息"
fi
# 生成 systemd 服务文件(即使当前无 systemd,也保留以便后续使用)
cat << 'FRPSVC' > /etc/systemd/system/frpc.service
[Unit]
Description=Frp Client Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s
StartLimitInterval=60s
StartLimitBurst=3
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
ExecReload=/usr/local/bin/frpc reload -c /etc/frp/frpc.toml
[Install]
WantedBy=multi-user.target
FRPSVC
if has_systemd; then
systemctl daemon-reload && systemctl enable frpc --now
fi
cd .. && rm -rf frp_${FRP_VERSION}_linux_${FRP_ARCH} frp.tar.gz
log_info "frpc 安装完成!配置文件位于 /etc/frp/frpc.toml"
fi
else
log_info "系统已安装 frpc。"
fi
# ================= 查找现有 frpc 配置 =================
log_step "2/5" "检测 frpc 配置文件..."
FRPC_CONFIG=""
FRPC_TYPE="toml"
# 按优先级查找现有配置文件
for path in "/etc/frp/frpc.toml" "/etc/frp/frpc.ini" "/usr/local/etc/frp/frpc.toml" "/usr/local/etc/frp/frpc.ini"; do
if [ -f "$path" ]; then
FRPC_CONFIG="$path"
[[ "$path" == *.ini ]] && FRPC_TYPE="ini"
break
fi
done
# 如果没找到,搜索
if [ -z "$FRPC_CONFIG" ]; then
FRPC_PATH_SEARCH=$(find /etc /usr/local/etc /opt /home -maxdepth 3 \( -name "frpc.toml" -o -name "frpc.ini" \) 2>/dev/null | head -n 1)
if [ -n "$FRPC_PATH_SEARCH" ]; then
FRPC_CONFIG="$FRPC_PATH_SEARCH"
[[ "$FRPC_PATH_SEARCH" == *.ini ]] && FRPC_TYPE="ini"
fi
fi
HAS_EXISTING_FRPC=false
if [ -n "$FRPC_CONFIG" ]; then
HAS_EXISTING_FRPC=true
log_info "找到已有 frpc 配置: $FRPC_CONFIG"
fi
# ================= 配置网关与 frpc 代理参数 =================
log_step "3/5" "配置网关与代理参数:"
if $HAS_EXISTING_FRPC; then
# 已有 frpc 配置,只需输入网关端口
log_info "使用已有 frpc 配置,仅需输入网关相关参数"
GATEWAY_PORT="${GATEWAY_PORT:-$(read_or_default "请输入 Python 动态网关的本地监听端口 (默认 8080): " "8080")}"
REMOTE_PORT="${REMOTE_PORT:-$(read_or_default "请输入在 frps 公网端暴露的端口 (默认 18000): " "18000")}"
PROXY_NAME="${PROXY_NAME:-$(read_or_default "请输入代理名称 (默认 huawei-dynamic-gateway): " "huawei-dynamic-gateway")}"
else
# 没有 frpc 配置,需要完整输入 frps 连接信息
log_info "未找到 frpc 配置,需要输入 frps 连接信息"
if [ -t 0 ]; then
while [ -z "$FRPS_ADDR" ]; do
read -p "请输入 frps 服务器地址 (例: 1.2.3.4): " FRPS_ADDR
[ -z "$FRPS_ADDR" ] && log_error "此项为必填,请输入!"
done
read -p "请输入 frps 服务器端口 (默认 7000): " FRPS_PORT
FRPS_PORT=${FRPS_PORT:-7000}
read -p "请输入 frps 认证 token (默认留空): " FRPS_TOKEN
else
if [ -z "$FRPS_ADDR" ]; then
log_error "管道模式下必须通过环境变量提供参数!"
log_error "用法: FRPS_ADDR=1.2.3.4 FRPS_PORT=7000 FRPS_TOKEN=xxx curl -sSL ... | bash"
log_error "或下载后交互式: curl -sSL -o hwaishell.sh <url> && bash hwaishell.sh"
exit 1
fi
FRPS_PORT=${FRPS_PORT:-7000}
FRPS_TOKEN=${FRPS_TOKEN:-}
fi
read -p "请输入代理名称 (默认 huawei-dynamic-gateway): " PROXY_NAME 2>/dev/null || PROXY_NAME=""
PROXY_NAME="${PROXY_NAME:-huawei-dynamic-gateway}"
GATEWAY_PORT="${GATEWAY_PORT:-$(read_or_default "请输入 Python 动态网关的本地监听端口 (默认 8080): " "8080")}"
REMOTE_PORT="${REMOTE_PORT:-$(read_or_default "请输入在 frps 公网端暴露的端口 (默认 18000): " "18000")}"
fi
# 端口有效性检查
if ! [[ "$GATEWAY_PORT" =~ ^[0-9]+$ ]] || [ "$GATEWAY_PORT" -lt 1 ] || [ "$GATEWAY_PORT" -gt 65535 ]; then
log_error "无效的本地端口: $GATEWAY_PORT"
exit 1
fi
if ! [[ "$REMOTE_PORT" =~ ^[0-9]+$ ]] || [ "$REMOTE_PORT" -lt 1 ] || [ "$REMOTE_PORT" -gt 65535 ]; then
log_error "无效的远程端口: $REMOTE_PORT"
exit 1
fi
# ================= 生成/追加 frpc 配置 =================
log_step "4/5" "处理 frpc 代理规则..."
# 构建代理规则块
build_proxy_block() {
local type=$1 # toml | ini
if [ "$type" = "toml" ]; then
cat << BLK
[[proxies]]
name = "${PROXY_NAME}"
type = "tcp"
localIP = "127.0.0.1"
localPort = ${GATEWAY_PORT}
remotePort = ${REMOTE_PORT}
BLK
else
cat << BLK
[${PROXY_NAME}]
type = tcp
local_ip = 127.0.0.1
local_port = ${GATEWAY_PORT}
remote_port = ${REMOTE_PORT}
BLK
fi
}
if $HAS_EXISTING_FRPC; then
# --- 已有 frpc 配置:追加代理规则 ---
if confirm_or_default "是否自动将华为云转发规则追加到该文件? (y/n, 默认 y): " "y"; then
if grep -q "${PROXY_NAME}" "$FRPC_CONFIG" 2>/dev/null; then
log_warn "frpc 配置中已存在 ${PROXY_NAME} 规则,跳过追加"
else
cp "$FRPC_CONFIG" "${FRPC_CONFIG}.backup.$(date +%Y%m%d%H%M%S)"
build_proxy_block "$FRPC_TYPE" >> "$FRPC_CONFIG"
log_info "代理规则已追加到 $FRPC_CONFIG (原配置已备份)"
fi
fi
else
# --- 没有 frpc 配置:自动创建 ---
if confirm_or_default "是否自动创建 frpc 配置文件并添加网关代理规则? (y/n, 默认 y): " "y"; then
FRPC_CONFIG="/etc/frp/frpc.toml"
mkdir -p /etc/frp
# 生成主配置
cat << BLK > "$FRPC_CONFIG"
serverAddr = "${FRPS_ADDR}"
serverPort = ${FRPS_PORT}
auth.method = "token"
auth.token = "${FRPS_TOKEN}"
BLK
# 追加代理规则
build_proxy_block "toml" >> "$FRPC_CONFIG"
log_info "已创建 frpc 配置文件: $FRPC_CONFIG"
fi
fi
# ================= 安装 Python 依赖 =================
log_step "5/5" "部署 Python 运行环境并启动服务..."
# 检查 python3
PYTHON_CMD=""
for cmd in python3 python3.11 python3.10 python3.9 python3.8; do
if command -v "$cmd" &>/dev/null; then
PYTHON_CMD="$cmd"
break
fi
done
if [ -z "$PYTHON_CMD" ]; then
log_warn "未找到 Python3,尝试自动安装..."
if command -v apt &>/dev/null; then
apt-get update -qq && apt-get install -y -qq python3 python3-pip
elif command -v yum &>/dev/null; then
yum install -y -q python3 python3-pip
elif command -v apk &>/dev/null; then
apk add --no-cache python3 py3-pip
else
log_error "无法自动安装 Python,请手动安装 python3 和 pip"
exit 1
fi
PYTHON_CMD="python3"
fi
# 检查 pip 并安装依赖
if ! $PYTHON_CMD -m pip --version &>/dev/null 2>&1; then
log_warn "pip 未安装,尝试安装..."
$PYTHON_CMD -m ensurepip --upgrade 2>/dev/null || {
if command -v apt &>/dev/null; then apt-get install -y -qq python3-pip; fi
}
fi
# 使用虚拟环境避免权限问题(可选)
USE_VENV=false
VENV_PATH="/opt/huawei-gateway/venv"
if [ "$IS_CONTAINER" = true ] || [ "$EUID" -eq 0 ]; then
if confirm_or_default "是否使用虚拟环境安装 Python 依赖? (推荐, y/n, 默认 y): " "y"; then
USE_VENV=true
fi
fi
if [ "$USE_VENV" = true ]; then
mkdir -p /opt/huawei-gateway
$PYTHON_CMD -m venv "$VENV_PATH" 2>/dev/null || {
log_warn "无法创建虚拟环境,将使用系统 Python"
USE_VENV=false
}
fi
PIP_CMD="$PYTHON_CMD -m pip"
if [ "$USE_VENV" = true ]; then
PIP_CMD="$VENV_PATH/bin/pip"
PYTHON_CMD="$VENV_PATH/bin/python"
fi
# 安装依赖(使用国内镜像)
log_info "正在安装 Flask 和 requests..."
$PIP_CMD install --upgrade pip --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple 2>/dev/null
$PIP_CMD install flask requests waitress --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple
# ================= 创建网关核心代码 =================
mkdir -p /usr/local/bin /var/log /var/run
cat << 'PYEOF' > /usr/local/bin/huawei_gateway.py
#!/usr/bin/env python3
"""
华为云 Token 动态网关
- 6小时缓存机制
- 支持内存扫描自动刷新
- HUAWEI_TOKEN 环境变量最高优先级
- Token 持久化到 /etc/huawei-gateway.env
- SSE 流式转发
- 连接池 (20 连接)
- 401 自动重试
- 兼容生产环境 (Waitress 32线程 / Gunicorn)
"""
import os
import re
import sys
import time
import logging
import threading
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, request, Response
# 尝试导入 requests,失败则给出明确提示
try:
import requests
except ImportError:
print("错误:缺少 requests 模块。请运行: pip install requests")
sys.exit(1)
# ================= 配置 =================
CACHE_TTL = 19800 # 5.5 小时(安全线)
MAX_WORKERS = 8 # 内存扫描线程数
MAX_MEM_SEGMENT = 200 * 1024 * 1024 # 单段最大扫描 200MB
TOKEN_PATTERN = re.compile(b'Bearer ([A-Za-z0-9+/=_-]{100,})')
TARGET_HOST = 'tokenhub.developer.huaweicloud.com'
# ================= 日志 =================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('huawei-gateway')
# ================= 缓存 =================
class TokenCache:
def __init__(self):
self._token = None
self._expires_at = 0
self._lock = threading.RLock()
self._last_scan = 0
self._scan_interval = 60 # 扫描间隔最小 60 秒
self._blacklist = set() # 已失效的 token 指纹
def _fingerprint(self, token):
"""取 token 前 16 + 后 16 字符做指纹"""
if len(token) <= 32:
return token
return token[:16] + token[-16:]
def get(self):
with self._lock:
now = time.time()
if self._token and now < self._expires_at:
return self._token
return None
def set(self, token, ttl=CACHE_TTL):
with self._lock:
self._token = token
self._expires_at = time.time() + ttl
self._last_scan = time.time()
self._blacklist.discard(self._fingerprint(token))
def blacklist_current(self):
"""将当前 token 加入黑名单"""
with self._lock:
if self._token:
self._blacklist.add(self._fingerprint(self._token))
self._token = None
self._expires_at = 0
def is_blacklisted(self, token):
with self._lock:
return self._fingerprint(token) in self._blacklist
def is_scan_cooldown(self):
with self._lock:
return (time.time() - self._last_scan) < self._scan_interval
def clear(self):
with self._lock:
self._token = None
self._expires_at = 0
cache = TokenCache()
# ================= 内存扫描 =================
def scan_pid_mem(pid):
"""扫描单个进程的内存寻找 Token"""
maps_path = f'/proc/{pid}/maps'
mem_path = f'/proc/{pid}/mem'
if not os.path.exists(maps_path) or not os.path.exists(mem_path):
return None
try:
with open(maps_path, 'r') as f:
for line in f:
parts = line.split()
if len(parts) < 2:
continue
perms = parts[1]
if 'r' not in perms or 'w' not in perms: # 只扫描可读可写段(更精确)
continue
addrs = parts[0].split('-')
if len(addrs) != 2:
continue
start = int(addrs[0], 16)
end = int(addrs[1], 16)
size = end - start
# 跳过过大或过小的段
if size > MAX_MEM_SEGMENT or size < 1024:
continue
try:
with open(mem_path, 'rb') as mem:
mem.seek(start)
# 分段读取避免大内存占用
chunk_size = 64 * 1024
remaining = size
while remaining > 0:
to_read = min(chunk_size, remaining)
data = mem.read(to_read)
if not data:
break
for match in TOKEN_PATTERN.finditer(data):
token = match.group(1).decode('ascii', errors='replace')
# 验证 token 格式(华为云 token 通常是 JWT 格式)
if len(token) > 200:
return token
remaining -= len(data)
except (PermissionError, OSError, ValueError):
continue
except (PermissionError, OSError, ProcessLookupError):
pass
return None
def find_token_in_memory():
"""在所有进程中扫描 Token"""
# HUAWEI_TOKEN 环境变量优先级最高
env_token = os.environ.get('HUAWEI_TOKEN', '').strip()
if env_token and len(env_token) > 200 and not cache.is_blacklisted(env_token):
cached = cache.get()
if cached != env_token:
cache.set(env_token)
logger.info("Token 从 HUAWEI_TOKEN 环境变量加载")
return env_token
# 从持久化文件加载
env_file = '/etc/huawei-gateway.env'
if os.path.isfile(env_file):
try:
with open(env_file, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('HUAWEI_TOKEN='):
file_token = line.split('=', 1)[1].strip().strip('"').strip("'")
if file_token and len(file_token) > 200 and not cache.is_blacklisted(file_token):
cached = cache.get()
if cached != file_token:
cache.set(file_token)
logger.info("Token 从持久化文件加载")
return file_token
break
except (OSError, IOError):
pass
if cache.is_scan_cooldown() and cache.get():
return cache.get()
try:
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
except OSError:
logger.error("无法访问 /proc 目录")
return None
# 优先扫描常见进程(python, node, java 等可能持有 token 的进程)
priority_pids = []
other_pids = []
for pid in pids:
try:
exe_path = os.readlink(f'/proc/{pid}/exe')
if any(x in exe_path for x in ['python', 'node', 'java', 'chrome', 'electron']):
priority_pids.append(pid)
else:
other_pids.append(pid)
except (OSError, PermissionError):
other_pids.append(pid)
# 先扫描优先级进程
all_pids = priority_pids + other_pids
found_tokens = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {executor.submit(scan_pid_mem, pid): pid for pid in all_pids}
for future in as_completed(futures):
try:
token = future.result(timeout=5)
if token and not cache.is_blacklisted(token):
found_tokens.append((token, futures[future]))
except Exception:
continue
for token, pid in found_tokens:
cache.set(token)
logger.info(f"Token 已刷新 (来源 PID: {pid})")
return token
return cache.get() # 返回可能过期的缓存作为兜底
# ================= HTTP 会话池 =================
http_session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=20,
pool_maxsize=20,
max_retries=3
)
http_session.mount('https://', adapter)
http_session.mount('http://', adapter)
# ================= Flask 应用 =================
app = Flask(__name__)
@app.route('/health')
def health():
"""健康检查端点"""
token = cache.get()
return {
"status": "healthy",
"token_cached": token is not None,
"token_expires_in": max(0, cache._expires_at - time.time()) if hasattr(cache, '_expires_at') else 0,
"blacklisted": len(cache._blacklist) if hasattr(cache, '_blacklist') else 0
}, 200
@app.route('/set_token', methods=['POST'])
def set_token():
"""手动注入有效 Token"""
data = request.get_json(force=True, silent=True) if request.is_json else {}
token = data.get('token', '')
if not token or len(token) < 100:
return {"error": "请提供有效的 token"}, 400
cache.set(token)
# 持久化到文件以便重启后恢复
try:
with open('/etc/huawei-gateway.env', 'w') as f:
f.write(f'HUAWEI_TOKEN={token}\n')
except (OSError, IOError):
pass
logger.info("Token 已手动注入并持久化")
return {"status": "ok", "token_fingerprint": cache._fingerprint(token)}, 200
@app.route('/v2/<path:subpath>', methods=['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE'])
def proxy(subpath):
if request.method == 'OPTIONS':
return Response(status=200, headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
})
real_token = find_token_in_memory()
if not real_token:
logger.error("未在内存中找到华为云 Token")
return {"error": "未在内存中找到华为云Token,请确保华为云相关应用正在运行"}, 500
# 构建请求头
headers = {}
for k, v in request.headers:
kl = k.lower()
if kl not in ('host', 'content-length', 'connection', 'accept-encoding', 'transfer-encoding'):
headers[k] = v
headers['Authorization'] = f'Bearer {real_token}'
headers['Host'] = TARGET_HOST
target_url = f'https://{TARGET_HOST}/v2/{subpath}'
try:
# 使用 stream=True 支持 SSE 流式转发
resp = http_session.request(
method=request.method,
url=target_url,
headers=headers,
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False,
timeout=60,
stream=True
)
# 401 兜底:Token 可能提前过期,加入黑名单后强制刷新重试
if resp.status_code == 401:
resp.close()
logger.warning("收到 401,将当前 Token 加入黑名单并强制刷新...")
cache.blacklist_current()
new_token = find_token_in_memory()
if new_token and new_token != real_token:
headers['Authorization'] = f'Bearer {new_token}'
resp = http_session.request(
method=request.method,
url=target_url,
headers=headers,
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False,
timeout=60,
stream=True
)
# 过滤 hop-by-hop 头和压缩编码头
skip_headers = {'transfer-encoding', 'content-encoding', 'content-length',
'connection', 'keep-alive', 'upgrade'}
response_headers = []
for k, v in resp.headers.items():
if k.lower() not in skip_headers:
response_headers.append((k, v))
# SSE 流式转发
content_type = resp.headers.get('Content-Type', '')
if 'text/event-stream' in content_type or resp.headers.get('Transfer-Encoding', '') == 'chunked':
def sse_stream():
try:
for chunk in resp.iter_content(chunk_size=4096):
if chunk:
yield chunk
finally:
resp.close()
return Response(
sse_stream(),
status=resp.status_code,
headers=response_headers,
direct_passthrough=True
)
else:
# 非流式响应:读取完整内容
content = resp.content
resp.close()
return Response(
content,
status=resp.status_code,
headers=response_headers
)
except requests.exceptions.Timeout:
logger.error("请求华为云 API 超时")
return {"error": "网关超时,请稍后重试"}, 504
except requests.exceptions.ConnectionError:
logger.error("无法连接到华为云 API")
return {"error": "无法连接到华为云服务"}, 502
except Exception as e:
logger.error(f"网关转发失败: {traceback.format_exc()}")
return {"error": f"网关转发失败: {str(e)}"}, 500
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
host = sys.argv[2] if len(sys.argv) > 2 else '127.0.0.1'
# 从持久化文件加载 token
env_file = '/etc/huawei-gateway.env'
if os.path.isfile(env_file):
try:
with open(env_file, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('HUAWEI_TOKEN=') and 'HUAWEI_TOKEN' not in os.environ:
val = line.split('=', 1)[1].strip().strip('"').strip("'")
if val and len(val) > 200:
os.environ['HUAWEI_TOKEN'] = val
logger.info("从持久化文件恢复 Token")
break
except (OSError, IOError):
pass
# 尝试使用生产级 WSGI 服务器
try:
import waitress
logger.info(f"使用 Waitress 启动网关 ({host}:{port})")
waitress.serve(app, host=host, port=port, threads=32)
except ImportError:
try:
import gunicorn.app.wsgiapp
logger.info(f"使用 Gunicorn 启动网关 ({host}:{port})")
os.execlp('gunicorn', 'gunicorn', '-w', '4', '-b', f'{host}:{port}', '--access-logfile', '-', 'huawei_gateway:app')
except (ImportError, OSError):
logger.warning("未安装 Waitress/Gunicorn,使用 Flask 开发服务器(建议生产环境安装 waitress")
logger.info(f"启动网关 ({host}:{port})")
app.run(host=host, port=port, debug=False, threaded=True)
if __name__ == '__main__':
main()
PYEOF
chmod +x /usr/local/bin/huawei_gateway.py
# ================= 注册网关服务 =================
if has_systemd; then
cat << EOF > /etc/systemd/system/huawei-gateway.service
[Unit]
Description=Huawei Dynamic Gateway (6H Cache)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=$PYTHON_CMD /usr/local/bin/huawei_gateway.py $GATEWAY_PORT
Restart=always
RestartSec=3
StartLimitInterval=60s
StartLimitBurst=3
StandardOutput=append:/var/log/huawei-gateway.log
StandardError=append:/var/log/huawei-gateway.log
[Install]
WantedBy=multi-user.target
EOF
fi
# 启动服务
safe_start_service "huawei-gateway" "$PYTHON_CMD /usr/local/bin/huawei_gateway.py $GATEWAY_PORT"
# ================= 启动 frpc =================
if [ -n "$FRPC_CONFIG" ] && [ -f "$FRPC_CONFIG" ]; then
if command -v frpc &>/dev/null; then
# 校验配置基本完整性
if ! grep -q "serverAddr" "$FRPC_CONFIG" 2>/dev/null || ! grep -q "remotePort\|remote_port" "$FRPC_CONFIG" 2>/dev/null; then
log_error "frpc 配置文件不完整: $FRPC_CONFIG"
log_error "请检查配置后手动启动: frpc -c $FRPC_CONFIG"
else
# 重启已运行的 frpc,或首次启动
if has_systemd; then
if systemctl list-units --type=service 2>/dev/null | grep -q "frpc"; then
systemctl restart frpc 2>/dev/null && log_info "frpc 服务已重启"
else
systemctl enable frpc --now 2>/dev/null && log_info "frpc 服务已通过 systemd 启动"
fi
else
restart_service "frpc"
nohup frpc -c "$FRPC_CONFIG" > /var/log/frpc.log 2>&1 &
echo $! > /var/run/frpc.pid
sleep 2
if kill -0 "$(cat /var/run/frpc.pid)" 2>/dev/null; then
log_info "frpc 已通过 nohup 启动 (PID: $(cat /var/run/frpc.pid))"
else
log_error "frpc 启动失败!最后几行日志:"
tail -5 /var/log/frpc.log | while IFS= read -r line; do
echo -e " ${RED}|${PLAIN} $line"
done
fi
fi
fi
else
log_warn "frpc 未安装,跳过启动。配置文件已生成: $FRPC_CONFIG"
fi
fi
# ================= 部署总结 =================
echo -e "\n${BLUE}==================================================${PLAIN}"
echo -e "${GREEN} 🎉 6小时长效缓存网关部署完成 🎉 ${PLAIN}"
echo -e "${BLUE}==================================================${PLAIN}"
echo -e "1. 动态网关状态:$(get_service_status huawei-gateway)"
echo -e "2. frpc 配置: $FRPC_CONFIG"
echo -e "3. 健康检查: http://127.0.0.1:${GATEWAY_PORT}/health"
echo -e "4. 您的本地一劳永逸调用地址:"
echo -e " - ${GREEN}API Base URL:${PLAIN} http://${FRPS_ADDR}:${REMOTE_PORT}/v2"
echo -e " - ${GREEN}API Key:${PLAIN} sk-anything"
echo -e " - ${GREEN}Model:${PLAIN} glm-5.1"
if [ "$IS_CONTAINER" = true ]; then
echo -e "\n${YELLOW}[容器环境提示]${PLAIN}"
echo -e " - 服务通过 nohup 运行,日志位于 /var/log/huawei-gateway.log"
echo -e " - 如需停止服务: kill \$(cat /var/run/huawei-gateway.pid)"
echo -e " - 建议安装 waitress 提升性能: pip install waitress"
fi
echo -e "${BLUE}==================================================${PLAIN}\n"