diff --git a/ai/hwaishell.sh b/ai/hwaishell.sh index bdda3bb..c6de670 100644 --- a/ai/hwaishell.sh +++ b/ai/hwaishell.sh @@ -7,250 +7,631 @@ 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}"; } + +# 检查是否有 systemd +has_systemd() { + [ -d /run/systemd/system ] || [ -d /var/run/systemd/system ] || systemctl --version &>/dev/null +} + +# 安全的进程启动(支持 systemd 和 nohup 两种模式) +safe_start_service() { + local name=$1 + local cmd=$2 + local service_file=$3 + + if has_systemd; then + systemctl daemon-reload 2>/dev/null + systemctl enable "$name" --now 2>/dev/null + log_info "$name 已通过 systemd 启动" + else + # 容器/无 systemd 环境:使用 nohup + 简单的 PID 文件管理 + local pid_file="/var/run/${name}.pid" + 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 > "/var/log/${name}.log" 2>&1 & + echo $! > "$pid_file" + log_info "$name 已通过 nohup 启动 (PID: $!)" + fi +} + +# 检查服务状态 +get_service_status() { + local name=$1 + if has_systemd; then + systemctl is-active "$name" 2>/dev/null || echo "unknown" + 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" + [ -f "$pid_file" ] && kill "$(cat "$pid_file")" 2>/dev/null + sleep 1 + fi +} + echo -e "${BLUE}==================================================${PLAIN}" echo -e "${GREEN} 华为云 Token 动态网关 (6小时高能效缓存版) 部署 ${PLAIN}" echo -e "${BLUE}==================================================${PLAIN}" # 1. 检查权限 if [ "$EUID" -ne 0 ]; then - echo -e "${RED}错误:请使用 root 用户运行此脚本!${PLAIN}" + 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 ================= -echo -e "\n${YELLOW}[1/5] 正在检测 frpc 环境...${PLAIN}" -if ! command -v frpc &> /dev/null && [ ! -f "/usr/local/bin/frpc" ] && [ ! -f "/usr/bin/frpc" ]; then - echo -e "${RED}未检测到 frpc!${PLAIN}" +log_step "1/5" "正在检测 frpc 环境..." +if ! command -v frpc &>/dev/null && [ ! -f "/usr/local/bin/frpc" ] && [ ! -f "/usr/bin/frpc" ]; then + log_warn "未检测到 frpc!" read -p "是否需要为您自动下载并安装最新版 frpc (使用国内加速镜像)? (y/n, 默认 y): " INSTALL_FRP INSTALL_FRP=${INSTALL_FRP:-y} - + if [[ "$INSTALL_FRP" == "y" || "$INSTALL_FRP" == "Y" ]]; then ARCH=$(uname -m) - if [ "$ARCH" = "x86_64" ]; then - FRP_ARCH="amd64" - elif [ "$ARCH" = "aarch64" ]; then - FRP_ARCH="arm64" - else - echo -e "${RED}错误:不支持的系统架构 $ARCH,请手动安装 frpc。${PLAIN}" + 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 - - FRP_VERSION="0.58.1" - echo -e "${GREEN}开始通过国内节点下载 frpc v${FRP_VERSION} ($FRP_ARCH)...${PLAIN}" - FRP_URL="https://mirror.ghproxy.com/https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_${FRP_ARCH}.tar.gz" - - wget --no-check-certificate -O frp.tar.gz "$FRP_URL" - if [ $? -ne 0 ]; then echo -e "${RED}下载失败!${PLAIN}"; exit 1; fi - - tar -zxvf frp.tar.gz > /dev/null - cd frp_${FRP_VERSION}_linux_${FRP_ARCH} || { echo -e "${RED}错误:无法进入解压目录!${PLAIN}"; exit 1; } + + 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 << EOF > /etc/frp/frpc.toml + cat << 'FRPCFG' > /etc/frp/frpc.toml serverAddr = "x.x.x.x" serverPort = 7000 -EOF +auth.method = "token" +auth.token = "your_token_here" +FRPCFG + log_warn "已生成 frpc 配置模板,请编辑 /etc/frp/frpc.toml 填入正确的服务器信息" fi - - cat << EOF > /etc/systemd/system/frpc.service + + # 生成 systemd 服务文件(即使当前无 systemd,也保留以便后续使用) + cat << 'FRPSVC' > /etc/systemd/system/frpc.service [Unit] Description=Frp Client Service -After=network.target +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 -EOF - systemctl daemon-reload && systemctl enable frpc --now +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 - echo -e "${GREEN}frpc 安装并注册成功!配置文件位于 /etc/frp/frpc.toml${PLAIN}" + log_info "frpc 安装完成!配置文件位于 /etc/frp/frpc.toml" fi else - echo -e "${GREEN}系统已安装 frpc。${PLAIN}" + log_info "系统已安装 frpc。" fi # ================= 配置网关与映射端口 ================= -echo -e "\n${YELLOW}[2/5] 配置动态网关参数:${PLAIN}" +log_step "2/5" "配置动态网关参数:" read -p "请输入 Python 动态网关的本地监听端口 (默认 8080): " GATEWAY_PORT GATEWAY_PORT=${GATEWAY_PORT:-8080} read -p "请输入你想在 frps 公网端暴露的端口 (默认 18000): " REMOTE_PORT REMOTE_PORT=${REMOTE_PORT:-18000} +# 端口有效性检查 +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 配置文件 ================= -echo -e "\n${YELLOW}[3/5] 检测 frpc 配置文件位置以追加代理规则:${PLAIN}" +log_step "3/5" "检测 frpc 配置文件位置以追加代理规则:" FRPC_CONFIG="" -if [ -f "/etc/frp/frpc.toml" ]; then FRPC_CONFIG="/etc/frp/frpc.toml"; FRPC_TYPE="toml"; -elif [ -f "/etc/frp/frpc.ini" ]; then FRPC_CONFIG="/etc/frp/frpc.ini"; FRPC_TYPE="ini"; -else - FRPC_PATH_SEARCH=$(find /etc /usr/local/etc /opt /home -maxdepth 4 -name "frpc.toml" -o -name "frpc.ini" 2>/dev/null | head -n 1) +FRPC_TYPE="" + +# 按优先级查找配置文件 +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" == *.toml ]] && FRPC_TYPE="toml" || 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" - if [[ "$FRPC_PATH_SEARCH" == *.toml ]]; then FRPC_TYPE="toml"; else FRPC_TYPE="ini"; fi + [[ "$FRPC_PATH_SEARCH" == *.toml ]] && FRPC_TYPE="toml" || FRPC_TYPE="ini" fi fi +APPEND_FRP="n" if [ -n "$FRPC_CONFIG" ]; then - echo -e "${GREEN}识别到 frpc 配置: $FRPC_CONFIG (${FRPC_TYPE})${PLAIN}" + log_info "识别到 frpc 配置: $FRPC_CONFIG (${FRPC_TYPE})" read -p "是否自动将华为云转发规则追加到该文件? (y/n, 默认 y): " APPEND_FRP APPEND_FRP=${APPEND_FRP:-y} else - APPEND_FRP="n" + log_warn "未找到 frpc 配置文件,跳过自动追加" + log_warn "请手动在 frpc 配置中添加以下规则:" + echo -e "${YELLOW}--- TOML 格式 ---${PLAIN}" + echo -e "[[proxies]]" + echo -e "name = \"huawei-dynamic-gateway\"" + echo -e "type = \"tcp\"" + echo -e "local_ip = \"127.0.0.1\"" + echo -e "local_port = ${GATEWAY_PORT}" + echo -e "remote_port = ${REMOTE_PORT}" fi # ================= 安装 Python 依赖 ================= -echo -e "\n${YELLOW}[4/5] 部署 Python 运行环境...${PLAIN}" -if ! command -v python3 &> /dev/null; then - if command -v apt &> /dev/null; then apt update -y && apt install -y python3 python3-pip; - elif command -v yum &> /dev/null; then yum install -y python3 python3-pip; fi -fi -python3 -m pip install flask requests --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple +log_step "4/5" "部署 Python 运行环境..." -# ================= 创建网关核心代码 (核心优化点) ================= -echo -e "\n${YELLOW}[5/5] 生成网关代码并启动服务...${PLAIN}" -cat << 'EOF' > /usr/local/bin/huawei_gateway.py +# 检查 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 + read -p "是否使用虚拟环境安装 Python 依赖? (推荐, y/n, 默认 y): " USE_VENV_INPUT + USE_VENV_INPUT=${USE_VENV_INPUT:-y} + [[ "$USE_VENV_INPUT" == "y" || "$USE_VENV_INPUT" == "Y" ]] && USE_VENV=true +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 --quiet -i https://pypi.tuna.tsinghua.edu.cn/simple + +# ================= 创建网关核心代码 ================= +log_step "5/5" "生成网关代码并启动服务..." +mkdir -p /usr/local/bin /var/log /var/run + +cat << 'PYEOF' > /usr/local/bin/huawei_gateway.py +#!/usr/bin/env python3 +""" +华为云 Token 动态网关 +- 6小时缓存机制 +- 支持内存扫描自动刷新 +- 401 自动重试 +- 兼容生产环境 (Waitress/Gunicorn) +""" import os import re +import sys import time import logging import threading import traceback -import requests +from concurrent.futures import ThreadPoolExecutor, as_completed + from flask import Flask, request, Response -from concurrent.futures import ThreadPoolExecutor -app = Flask(__name__) -logging.basicConfig(level=logging.WARNING, - format='%(asctime)s [%(levelname)s] %(message)s') +# 尝试导入 requests,失败则给出明确提示 +try: + import requests +except ImportError: + print("错误:缺少 requests 模块。请运行: pip install requests") + sys.exit(1) -# 内存缓存字典:6小时有效期,这里设置为安全线 5.5 小时 (19800秒) -cache = {"token": None, "expires_at": 0} -cache_lock = threading.Lock() +# ================= 配置 ================= +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 秒 + + 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() + + 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(f'/proc/{pid}/maps', 'r') as f: + with open(maps_path, 'r') as f: for line in f: parts = line.split() - if len(parts) < 2 or 'r' not in parts[1]: continue - addrs = parts[0].split('-') - if len(addrs) != 2: continue - start, end = int(addrs[0], 16), int(addrs[1], 16) - if (end - start) > 200 * 1024 * 1024: continue - try: - with open(f'/proc/{pid}/mem', 'rb') as mem: - mem.seek(start) - data = mem.read(end - start) - for m in re.finditer(b'Bearer ([A-Za-z0-9+/=_-]+)', data): - val = m.group(1).decode(errors='replace') - if len(val) > 200: - return val - except Exception: + if len(parts) < 2: continue - except Exception: + 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 and token.count('.') >= 2: + return token + + remaining -= len(data) + except (PermissionError, OSError, ValueError): + continue + + except (PermissionError, OSError, ProcessLookupError): pass return None -def force_refresh_token(): - """强制重新从内存扫描 Token 并更新缓存时间""" - pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] - with ThreadPoolExecutor(max_workers=8) as executor: - results = executor.map(scan_pid_mem, pids) - for token in results: - if token: - with cache_lock: - cache["token"] = token - cache["expires_at"] = time.time() + 19800 # 缓存 5.5 小时 - return token - return cache["token"] -def get_latest_token(): - now = time.time() - with cache_lock: - # 如果缓存存在且没过期,直接返回缓存(消耗几乎为 0) - if cache["token"] and now < cache["expires_at"]: - return cache["token"] - # 否则,或者过期了,才去扫描内存 - return force_refresh_token() +def find_token_in_memory(): + """在所有进程中扫描 Token""" + if cache.is_scan_cooldown() and cache.get(): + return cache.get() -@app.route('/v2/', methods=['POST', 'GET', 'OPTIONS']) + 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 + + 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: + cache.set(token) + logger.info(f"Token 已刷新 (来源 PID: {futures[future]})") + return token + except Exception: + continue + + return cache.get() # 返回可能过期的缓存作为兜底 + + +# ================= 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 + }, 200 + + +@app.route('/v2/', methods=['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE']) def proxy(subpath): if request.method == 'OPTIONS': - return Response(), 200 + 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 = get_latest_token() + real_token = find_token_in_memory() if not real_token: - logging.error("未在内存中找到华为云Token") - return {"error": "未在内存中找到华为云Token"}, 500 + 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'): + headers[k] = v - headers = {k: v for k, v in request.headers if k.lower() != 'host'} headers['Authorization'] = f'Bearer {real_token}' - headers['Host'] = 'tokenhub.developer.huaweicloud.com' + headers['Host'] = TARGET_HOST + + target_url = f'https://{TARGET_HOST}/v2/{subpath}' - target_url = f'https://tokenhub.developer.huaweicloud.com/v2/{subpath}' try: resp = requests.request( - method=request.method, url=target_url, headers=headers, - data=request.get_data(), cookies=request.cookies, - allow_redirects=False, timeout=60 + method=request.method, + url=target_url, + headers=headers, + data=request.get_data(), + cookies=request.cookies, + allow_redirects=False, + timeout=60, + stream=False ) - # 兜底保障:如果华为主端提早失效返回了 401,立即强制重新扫描内存并重试一次请求 + # 401 兜底:Token 可能提前过期,强制刷新重试一次 if resp.status_code == 401: - new_token = force_refresh_token() - if new_token: + logger.warning("收到 401,尝试强制刷新 Token 并重试...") + cache.clear() + new_token = find_token_in_memory() + if new_token and new_token != real_token: headers['Authorization'] = f'Bearer {new_token}' resp = requests.request( - method=request.method, url=target_url, headers=headers, - data=request.get_data(), cookies=request.cookies, - allow_redirects=False, timeout=60 + method=request.method, + url=target_url, + headers=headers, + data=request.get_data(), + cookies=request.cookies, + allow_redirects=False, + timeout=60 ) - return Response(resp.content, resp.status_code, resp.headers.items()) + # 构建响应 + response_headers = [] + for k, v in resp.headers.items(): + if k.lower() not in ('transfer-encoding', 'content-encoding', 'content-length'): + response_headers.append((k, v)) + + return Response( + resp.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: - logging.error(f"网关转发失败: {traceback.format_exc()}") + logger.error(f"网关转发失败: {traceback.format_exc()}") return {"error": f"网关转发失败: {str(e)}"}, 500 -if __name__ == '__main__': - import sys + +def main(): port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 - app.run(host='127.0.0.1', port=port, debug=False) + host = sys.argv[2] if len(sys.argv) > 2 else '127.0.0.1' -EOF + # 尝试使用生产级 WSGI 服务器 + try: + import waitress + logger.info(f"使用 Waitress 启动网关 ({host}:{port})") + waitress.serve(app, host=host, port=port, threads=16) + 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) -# 注册网关服务 -cat << EOF > /etc/systemd/system/huawei-gateway.service + +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.target +After=network-online.target +Wants=network-online.target [Service] Type=simple User=root -ExecStart=/usr/bin/python3 /usr/local/bin/huawei_gateway.py $GATEWAY_PORT +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 -systemctl daemon-reload && systemctl enable huawei-gateway --now &>/dev/null +# 启动服务 +safe_start_service "huawei-gateway" "$PYTHON_CMD /usr/local/bin/huawei_gateway.py $GATEWAY_PORT" "/etc/systemd/system/huawei-gateway.service" # ================= 追加 frpc 规则 ================= -if [[ "$APPEND_FRP" == "y" || "$APPEND_FRP" == "Y" ]] && [ -f "$FRPC_CONFIG" ]; then +if [[ "$APPEND_FRP" == "y" || "$APPEND_FRP" == "Y" ]] && [ -n "$FRPC_CONFIG" ] && [ -f "$FRPC_CONFIG" ]; then if grep -q "huawei-dynamic-gateway" "$FRPC_CONFIG" 2>/dev/null; then - echo -e "${YELLOW}检测到 frpc 配置中已存在 huawei-dynamic-gateway 规则,跳过追加。${PLAIN}" + log_warn "frpc 配置中已存在 huawei-dynamic-gateway 规则,跳过追加" else + # 备份原配置 + cp "$FRPC_CONFIG" "${FRPC_CONFIG}.backup.$(date +%Y%m%d%H%M%S)" + if [ "$FRPC_TYPE" = "toml" ]; then cat << EOF >> "$FRPC_CONFIG" @@ -271,18 +652,41 @@ local_port = $GATEWAY_PORT remote_port = $REMOTE_PORT EOF fi - echo -e "${GREEN}frpc 代理规则已追加到 $FRPC_CONFIG${PLAIN}" + log_info "frpc 代理规则已追加到 $FRPC_CONFIG" + log_info "原配置已备份" + fi + + # 重启 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 服务已重启" + fi + else + restart_service "frpc" + # 尝试直接启动 frpc + if command -v frpc &>/dev/null; then + nohup frpc -c "$FRPC_CONFIG" > /var/log/frpc.log 2>&1 & + log_info "frpc 已通过 nohup 启动" + fi fi - if systemctl list-units --type=service | grep -q "frpc"; then systemctl restart frpc; fi fi # ================= 部署总结 ================= echo -e "\n${BLUE}==================================================${PLAIN}" echo -e "${GREEN} 🎉 6小时长效缓存网关部署完成 🎉 ${PLAIN}" echo -e "${BLUE}==================================================${PLAIN}" -echo -e "1. 动态网关状态:$(systemctl is-active huawei-gateway)" -echo -e "2. 您的本地一劳永逸调用地址:" +echo -e "1. 动态网关状态:$(get_service_status huawei-gateway)" +echo -e "2. 健康检查: http://127.0.0.1:${GATEWAY_PORT}/health" +echo -e "3. 您的本地一劳永逸调用地址:" echo -e " - ${GREEN}API Base URL:${PLAIN} http://【你的公网frps_IP】:$REMOTE_PORT/v2" echo -e " - ${GREEN}API Key:${PLAIN} sk-anything" echo -e " - ${GREEN}Model:${PLAIN} glm-5.1" -echo -e "${BLUE}==================================================${PLAIN}\n" \ No newline at end of file + +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"