新增简易的8080面板😊

This commit is contained in:
Falck
2026-04-17 23:15:15 +08:00
parent c38d2f66d1
commit 9d19d09821
465 changed files with 9235 additions and 35285 deletions

6
.gitignore vendored
View File

@@ -22,3 +22,9 @@ mizukiblog-master.zip
# 日志 # 日志
logs/ logs/
*.log *.log
# 签名验证 - 私钥(绝不要提交!)
data/signature-verifier/keys/private/
# 签名文件(可选,本地开发可能不需要)
# store/**/SIGNATURE

Binary file not shown.

View File

@@ -1,15 +1,62 @@
"""日志系统 - 空壳,由日志插件提供实际功能""" """日志系统 - 彩色日志"""
import sys
class Log:
"""通用彩色日志 - 所有插件可共用"""
_TTY = sys.stdout.isatty()
_C = {
"reset": "\033[0m",
"white": "\033[0;37m",
"yellow": "\033[1;33m",
"blue": "\033[1;34m",
"red": "\033[1;31m",
"green": "\033[0;32m",
}
@classmethod
def _c(cls, text: str, color: str) -> str:
if not cls._TTY:
return text
return f"{cls._C.get(color, '')}{text}{cls._C['reset']}"
@classmethod
def info(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
@classmethod
def warn(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'yellow')} {cls._c('', 'yellow')} {cls._c(msg, 'yellow')}")
@classmethod
def error(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'red')} {cls._c('', 'red')} {cls._c(msg, 'red')}")
@classmethod
def tip(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'blue')} {cls._c('', 'blue')} {cls._c(msg, 'blue')}")
@classmethod
def ok(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
class Logger: class Logger:
"""日志记录器(空壳""" """日志记录器(兼容旧接口"""
def info(self, msg: str, **kwargs): def info(self, msg: str, **kwargs):
print(f"[INFO] {msg}") tag = kwargs.get("tag", "INFO")
Log.info(tag, msg)
def warn(self, msg: str, **kwargs): def warn(self, msg: str, **kwargs):
print(f"[WARN] {msg}") tag = kwargs.get("tag", "WARN")
Log.warn(tag, msg)
def error(self, msg: str, **kwargs): def error(self, msg: str, **kwargs):
print(f"[ERROR] {msg}") tag = kwargs.get("tag", "ERROR")
Log.error(tag, msg)
def debug(self, msg: str, **kwargs): def debug(self, msg: str, **kwargs):
print(f"[DEBUG] {msg}") tag = kwargs.get("tag", "DEBUG")
Log.tip(tag, msg)

Binary file not shown.

View File

@@ -1,3 +1,5 @@
click>=8.0 click>=8.0
pyyaml>=6.0 pyyaml>=6.0
websockets>=12.0 websockets>=12.0
psutil>=5.9.0
cryptography>=41.0

267
start.bat
View File

@@ -3,144 +3,215 @@ chcp 65001 >nul 2>&1
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
:: FutureOSS 启动脚本 Windows :: FutureOSS 智能启动脚本 - Windows
:: 自动检测 Python / 依赖 / 守护 / 崩溃重启 :: 自动检测环境 / 安装依赖 / 进度显示 / 守护重启
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
set "RED=[31m" cd /d "%~dp0"
set "GREEN=[32m"
set "YELLOW=[33m"
set "CYAN=[36m"
set "WHITE=[37m"
set "BOLD=[1m"
set "NC=[0m"
call :color_echo "BOLD" "CYAN" "" :: ── 颜色代码 ──
echo ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
echo ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝ set "DEL=%%a"
echo █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗ )
echo ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║
echo ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝ :: ── 工具函数 ──
echo ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ call :colorEcho 0B "[信息] 环境检测中..."
call :color_echo "BOLD" "WHITE" "" 一切皆为插件 · 零编译热插拔 call :colorEcho 0A "[成功] 检测完成"
call :color_echo "" "WHITE" "" https://gitee.com/starlight-apk/feature-oss call :colorEcho 0E "[警告] 某些组件缺失"
call :colorEcho 0C "[错误] 检测失败"
:: ── Logo ──
echo.
call :colorEcho 0B " ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ "
call :colorEcho 0B " ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝ "
call :colorEcho 0B " █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗"
call :colorEcho 0B " ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║"
call :colorEcho 0B " ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝"
call :colorEcho 0B " ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ "
echo.
call :colorEcho 0F " 开发者通用工具套组 · 一切皆为插件"
call :colorEcho 07 " https://gitee.com/starlight-apk/feature-oss"
echo. echo.
:: ── 目录 ── :: ═══════════════════════════════════════════════════════════
cd /d "%~dp0" :: 1. 检测 Python
:: ═══════════════════════════════════════════════════════════
call :colorEcho 0B "[信息] 检测 Python..."
set "PYTHON_CMD=" set "PYTHON_CMD="
set "PIP_CMD=" for %%p in (python python3 py py3) do (
where %%p >nul 2>&1
:: ═══════════════════════════════════════════════════════════ if !errorlevel! equ 0 (
:: 1. 检查 Python set "PYTHON_CMD=%%p"
:: ═══════════════════════════════════════════════════════════ goto :found_python
call :section "环境检测"
where python 2>nul && set "PYTHON_CMD=python" || (
where python3 2>nul && set "PYTHON_CMD=python3" || (
where py 2>nul && set "PYTHON_CMD=py" || (
call :color_echo "" "YELLOW" "" [!] 未检测到 Python
echo.
echo 请安装 Python 3.10+ :
echo ^> https://www.python.org/downloads/
echo.
echo 安装时请勾选 "Add Python to PATH"
echo.
pause
exit /b 1
)
) )
) )
:found_python
if "%PYTHON_CMD%"=="" (
call :colorEcho 0C "[错误] 未找到 Python请先安装 Python 3.10+"
call :colorEcho 0E "[提示] 下载地址: https://www.python.org/downloads/"
pause
exit /b 1
)
for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set "PY_VER=%%i" for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set "PY_VER=%%i"
call :color_echo "" "GREEN" "" [✓] Python: %PY_VER% call :colorEcho 0A "[成功] %PY_VER%"
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
:: 2. 虚拟环境 & 依赖 :: 2. 虚拟环境
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
call :section "依赖安装" echo.
call :colorEcho 0B "[信息] 配置 Python 环境..."
if not exist ".venv" ( if not exist ".venv" (
call :color_echo "" "CYAN" "" [i] 创建虚拟环境... call :colorEcho 0E "[信息] 创建虚拟环境..."
%PYTHON_CMD% -m venv .venv %PYTHON_CMD% -m venv .venv >nul 2>&1
if errorlevel 1 (
call :colorEcho 0C "[错误] 无法创建虚拟环境"
pause
exit /b 1
)
call :colorEcho 0A "[成功] 虚拟环境已创建"
) else (
call :colorEcho 0A "[成功] 虚拟环境已存在"
) )
set "VENV_PYTHON=.venv\Scripts\python.exe" call .venv\Scripts\activate.bat >nul 2>&1
set "VENV_PIP=.venv\Scripts\pip.exe"
if exist "pyproject.toml" ( :: ═══════════════════════════════════════════════════════════
call :color_echo "" "CYAN" "" [i] 安装项目依赖... :: 3. 安装依赖
%VENV_PIP% install -e . -q 2>nul :: ═══════════════════════════════════════════════════════════
) echo.
call :colorEcho 0B "[信息] 安装 Python 依赖..."
if exist "requirements.txt" ( set "DEPS=click pyyaml websockets psutil cryptography"
call :color_echo "" "CYAN" "" [i] 安装 requirements.txt... set "TOTAL=5"
%VENV_PIP% install -r requirements.txt -q 2>nul set "CURRENT=0"
)
:: 核心依赖兜底 for %%d in (%DEPS%) do (
for %%p in (click pyyaml websockets) do ( set /a CURRENT+=1
%VENV_PYTHON% -c "import %%p" 2>nul || ( call :printProgress !CURRENT! !TOTAL! "安装 %%d"
call :color_echo "" "CYAN" "" [i] 安装 %%p ...
%VENV_PIP% install %%p -q 2>nul %PYTHON_CMD% -c "import %%d" 2>nul
if errorlevel 1 (
pip install %%d -q 2>nul
) )
) )
call :color_echo "" "GREEN" "" [✓] 依赖就绪 echo.
echo.
call :colorEcho 0A "[成功] Python 依赖安装完成"
:: 安装项目依赖
if exist "pyproject.toml" (
call :colorEcho 0E "[信息] 安装项目配置依赖..."
pip install -e . -q 2>nul
)
if exist "requirements.txt" (
call :colorEcho 0E "[信息] 安装 requirements.txt..."
pip install -r requirements.txt -q 2>nul
)
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
:: 3. 确保 data 目录 :: 4. 检查 PHP
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
if not exist "data\html-render" mkdir "data\html-render" echo.
if not exist "data\web-toolkit" mkdir "data\web-toolkit" call :colorEcho 0B "[信息] 检查 PHP..."
if not exist "data\plugin-storage" mkdir "data\plugin-storage"
if not exist "data\DCIM" mkdir "data\DCIM" where php >nul 2>&1
if not exist "data\pkg" mkdir "data\pkg" if errorlevel 1 (
call :colorEcho 0E "[警告] PHP 未安装WebUI 可能无法正常工作"
call :colorEcho 07 "[提示] 安装: choco install php 或从 https://windows.php.net/download/ 下载"
) else (
for /f "tokens=*" %%i in ('php --version 2^>^&1 ^| findstr /r "PHP"') do set "PHP_VER=%%i"
call :colorEcho 0A "[成功] !PHP_VER!"
)
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
:: 4. 启动 :: 5. 创建数据目录
:: ═══════════════════════════════════════════════════════════ :: ═══════════════════════════════════════════════════════════
call :section "启动 FutureOSS" echo.
call :colorEcho 0B "[信息] 初始化数据目录..."
set "DIRS=data data\html-render data\web-toolkit data\plugin-storage data\DCIM data\pkg data\signature-verifier\keys\private data\signature-verifier\keys\public logs"
for %%d in (%DIRS%) do (
if not exist "%%d" mkdir "%%d"
)
call :colorEcho 0A "[成功] 数据目录已就绪"
:: ═══════════════════════════════════════════════════════════
:: 6. 启动服务
:: ═══════════════════════════════════════════════════════════
echo.
call :colorEcho 0B "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
call :colorEcho 0B " 启动 FutureOSS"
call :colorEcho 0B "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo.
if "%1"=="--daemon" goto :daemon_mode
if "%1"=="-d" goto :daemon_mode
:: 前台模式
call :colorEcho 0F "运行中... 按 Ctrl+C 停止"
echo.
set "RESTART_DELAY=3" set "RESTART_DELAY=3"
set "RESTART_COUNT=0" set "RESTART_COUNT=0"
:LOOP :loop
echo. %PYTHON_CMD% -m oss.cli serve
call :color_echo "" "CYAN" "" [i] 启动服务... set "EXIT_CODE=%errorlevel%"
echo.
%VENV_PYTHON% -m oss.cli serve
set "EXIT_CODE=!ERRORLEVEL!"
if !EXIT_CODE! equ 0 ( if %EXIT_CODE% equ 0 (
echo. call :colorEcho 0A "[成功] 服务正常退出"
call :color_echo "" "GREEN" "" [✓] 服务正常退出 goto :end
goto :END )
)
set /a RESTART_COUNT+=1 set /a RESTART_COUNT+=1
echo. call :colorEcho 0E "[警告] 服务异常退出 (code: %EXIT_CODE%)!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次)"
call :color_echo "" "YELLOW" "" [!] 服务异常退出 (code: !EXIT_CODE!)!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次) timeout /t !RESTART_DELAY! /nobreak >nul
timeout /t !RESTART_DELAY! /nobreak >nul
if !RESTART_DELAY! lss 30 set /a RESTART_DELAY=!RESTART_DELAY!*2 :: 指数退避
if !RESTART_DELAY! lss 30 (
set /a RESTART_DELAY=!RESTART_DELAY! * 2
)
goto :LOOP goto :loop
:END :daemon_mode
call :colorEcho 0E "[警告] Windows 守护模式需要额外配置"
call :colorEcho 07 "[提示] 建议使用任务计划程序或 nssm 工具实现"
echo. echo.
%PYTHON_CMD% -m oss.cli serve
goto :end
:end
call .venv\Scripts\deactivate.bat >nul 2>&1
pause pause
exit /b 0 exit /b 0
:: ── 辅助函数 ── :: ── 进度条函数 ──
:color_echo :printProgress
if "%~1" neq "" set /p "=^<ESC>%BOLD%%~2%<ESC>%NC%" <nul set /a "pct=%1 * 100 / %2"
echo. set /a "filled=pct / 2"
goto :eof set /a "empty=50-filled"
set "bar="
for /l %%i in (1,1,%filled%) do set "bar=!bar!█"
for /l %%i in (1,1,%empty%) do set "bar=!bar!░"
echo [!bar!] !pct!%% - %3
exit /b 0
:section :: ── 颜色输出函数 ──
echo. :colorEcho
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════" set "params=%1"
call :color_echo "BOLD" "WHITE" " %~1" set "msg=%~2"
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════" call :colorText %params% "%msg%"
goto :eof exit /b 0
:colorText
echo %~2
exit /b 0

326
start.sh
View File

@@ -1,101 +1,221 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# FutureOSS 启动脚本 Linux / macOS # FutureOSS 智能启动脚本 - Linux
# 自动检测 Python / 依赖 / 守护 / 崩溃重启 # 自动检测环境 / 安装依赖 / 进度显示 / 守护重启
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
set -e PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$PROJECT_DIR"
# ── 颜色 ── # ── 颜色 ──
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' RED='\033[0;31m'
CYAN='\033[0;36m'; WHITE='\033[1;37m'; BOLD='\033[1m'; NC='\033[0m' GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[1;34m'
WHITE='\033[1;37m'
BOLD='\033[1m'
NC='\033[0m'
LOGO=" # ── 工具函数 ──
███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ info() { echo -e "${CYAN}[信息]${NC} $1"; }
██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝ ok() { echo -e "${GREEN}[成功]${NC} $1"; }
█████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗ warn() { echo -e "${YELLOW}[警告]${NC} $1"; }
██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║ err() { echo -e "${RED}[错误]${NC} $1"; }
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝ step() { echo -e "\n${BOLD}${BLUE}$1${NC}"; }
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝"
info() { echo -e "${CYAN} $1${NC}"; } # 进度条
ok() { echo -e "${GREEN}$1${NC}"; } progress_bar() {
warn() { echo -e "${YELLOW}$1${NC}"; } local current=$1
err() { echo -e "${RED}$1${NC}"; } local total=$2
title() { echo -e "\n${BOLD}$1${NC}"; } local label=$3
local pct=$((current * 100 / total))
local filled=$((pct / 2))
local empty=$((50 - filled))
local bar=""
for ((i=0; i<filled; i++)); do bar+="█"; done
for ((i=0; i<empty; i++)); do bar+="░"; done
echo -ne "\r ${GREEN}[${bar}]${NC} ${WHITE}${pct}%${NC} - ${label}"
}
# ── 守护参数 ── # ── Logo ──
echo -e "${BOLD}${CYAN}"
echo " ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ "
echo " ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝ "
echo " █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗"
echo " ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║"
echo " ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝"
echo " ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ "
echo -e "${NC}"
echo -e "${WHITE} 开发者通用工具套组 · 一切皆为插件${NC}"
echo -e "${WHITE} https://gitee.com/starlight-apk/feature-oss${NC}"
echo ""
# ── 守护模式 ──
DAEMON=false DAEMON=false
if [[ "$1" == "--daemon" || "$1" == "-d" ]]; then if [[ "$1" == "--daemon" || "$1" == "-d" ]]; then
DAEMON=true DAEMON=true
fi fi
title "$LOGO"
echo -e "${WHITE} 一切皆为插件 · 零编译热插拔${NC}"
echo -e "${WHITE} https://gitee.com/starlight-apk/feature-oss${NC}"
echo ""
# ── 目录 ──
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$PROJECT_DIR"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 1. 检查 Python # 1. 检测操作系统
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
title "📦 环境检测" step "环境检测"
find_python() { PKG_MANAGER=""
for cmd in python3 python python3.12 python3.11 python3.10; do OS_NAME=""
if command -v "$cmd" &>/dev/null; then
echo "$cmd"
return
fi
done
return 1
}
PYTHON_CMD=$(find_python || true) if [[ -f /etc/os-release ]]; then
OS_NAME=$(. /etc/os-release && echo "$PRETTY_NAME")
if [[ -z "$PYTHON_CMD" ]]; then info "操作系统: $OS_NAME"
warn "未检测到 Python正在自动安装..."
if command -v apt-get &>/dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-pip python3-venv
elif command -v yum &>/dev/null; then
sudo yum install -y python3 python3-pip
elif command -v pacman &>/dev/null; then
sudo pacman -Sy --noconfirm python python-pip
elif command -v brew &>/dev/null; then
brew install python
elif command -v apk &>/dev/null; then
apk add python3 py3-pip
else
err "无法自动安装 Python请手动安装 Python 3.10+"
exit 1
fi
PYTHON_CMD=$(find_python || true)
[[ -z "$PYTHON_CMD" ]] && { err "Python 安装失败"; exit 1; }
fi fi
PY_VER=$($PYTHON_CMD --version 2>&1) if command -v apt-get &>/dev/null; then
ok "Python: $PY_VER ($PYTHON_CMD)" PKG_MANAGER="apt"
ok "包管理器: apt (Debian/Ubuntu)"
elif command -v yum &>/dev/null; then
PKG_MANAGER="yum"
ok "包管理器: yum (CentOS/RHEL)"
elif command -v dnf &>/dev/null; then
PKG_MANAGER="dnf"
ok "包管理器: dnf (Fedora)"
elif command -v pacman &>/dev/null; then
PKG_MANAGER="pacman"
ok "包管理器: pacman (Arch Linux)"
elif command -v apk &>/dev/null; then
PKG_MANAGER="apk"
ok "包管理器: apk (Alpine)"
else
warn "未检测到已知包管理器,将尝试使用系统自带工具"
fi
install_pkg() {
local pkg=$1
case $PKG_MANAGER in
apt) sudo apt-get install -y -qq "$pkg" 2>/dev/null ;;
yum) sudo yum install -y -q "$pkg" 2>/dev/null ;;
dnf) sudo dnf install -y -q "$pkg" 2>/dev/null ;;
pacman) sudo pacman -S --noconfirm "$pkg" 2>/dev/null ;;
apk) sudo apk add --quiet "$pkg" 2>/dev/null ;;
*) warn "无法自动安装 $pkg" ; return 1 ;;
esac
}
update_pkg_cache() {
case $PKG_MANAGER in
apt) sudo apt-get update -qq 2>/dev/null ;;
yum) sudo yum makecache -q 2>/dev/null ;;
dnf) sudo dnf makecache -q 2>/dev/null ;;
pacman) sudo pacman -Sy --quiet 2>/dev/null ;;
apk) sudo apk update --quiet 2>/dev/null ;;
esac
}
TOTAL_STEPS=6
CURRENT_STEP=0
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 2. 虚拟环境 & 依赖 # 2. 安装系统依赖
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
title "📚 依赖安装" step "安装系统依赖"
DEPENDENCIES=("git" "curl" "wget" "php" "php-cli" "php-mbstring" "php-xml" "php-zip" "python3" "python3-pip" "python3-venv")
DEP_TOTAL=${#DEPENDENCIES[@]}
DEP_INSTALLED=0
for dep in "${DEPENDENCIES[@]}"; do
DEP_INSTALLED=$((DEP_INSTALLED + 1))
progress_bar $DEP_INSTALLED $DEP_TOTAL "检测 $dep"
if command -v "$dep" &>/dev/null; then
continue
fi
# 尝试安装
if ! install_pkg "$dep" 2>/dev/null; then
# 更新缓存后重试
update_pkg_cache 2>/dev/null
install_pkg "$dep" 2>/dev/null || true
fi
if command -v "$dep" &>/dev/null; then
continue
fi
done
echo -e "\n"
# 验证关键依赖
if command -v php &>/dev/null; then
ok "PHP: $(php --version 2>&1 | head -n 1)"
else
warn "PHP 未安装WebUI 可能无法正常工作"
fi
if command -v python3 &>/dev/null; then
ok "Python: $(python3 --version 2>&1)"
else
err "Python3 未安装,无法继续"
exit 1
fi
# ═══════════════════════════════════════════════════════════
# 3. Python 虚拟环境
# ═══════════════════════════════════════════════════════════
step "配置 Python 环境"
PYTHON_CMD="python3"
VENV_DIR=".venv" VENV_DIR=".venv"
if [[ ! -d "$VENV_DIR" ]]; then if [[ ! -d "$VENV_DIR" ]]; then
info "创建虚拟环境..." info "创建虚拟环境..."
$PYTHON_CMD -m venv "$VENV_DIR" $PYTHON_CMD -m venv "$VENV_DIR" 2>/dev/null || {
warn "venv 模块缺失,尝试安装..."
case $PKG_MANAGER in
apt) install_pkg "python3-venv" ;;
yum) install_pkg "python3-virtualenv" ;;
dnf) install_pkg "python3-virtualenv" ;;
pacman) ;;
apk) install_pkg "py3-virtualenv" ;;
esac
$PYTHON_CMD -m venv "$VENV_DIR" 2>/dev/null || {
err "无法创建虚拟环境"
exit 1
}
}
ok "虚拟环境已创建"
else
ok "虚拟环境已存在"
fi fi
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
PIP_CMD="$VENV_DIR/bin/pip" PIP_CMD="$VENV_DIR/bin/pip"
# ═══════════════════════════════════════════════════════════
# 4. 安装 Python 依赖
# ═══════════════════════════════════════════════════════════
step "安装 Python 依赖"
CORE_DEPS=("click" "pyyaml" "websockets" "psutil" "cryptography")
DEP_COUNT=${#CORE_DEPS[@]}
DEP_CURRENT=0
for pkg in "${CORE_DEPS[@]}"; do
DEP_CURRENT=$((DEP_CURRENT + 1))
progress_bar $DEP_CURRENT $DEP_COUNT "安装 $pkg"
$PYTHON_CMD -c "import $pkg" 2>/dev/null && continue
$PIP_CMD install "$pkg" -q 2>/dev/null || \
$PIP_CMD install "$pkg" --break-system-packages -q 2>/dev/null || true
done
echo -e "\n"
# 安装项目依赖
if [[ -f "pyproject.toml" ]]; then if [[ -f "pyproject.toml" ]]; then
info "安装项目依赖 (pyproject.toml)..." info "安装项目配置依赖..."
$PIP_CMD install -e . -q 2>/dev/null || $PIP_CMD install -e . --break-system-packages -q 2>/dev/null || true $PIP_CMD install -e . -q 2>/dev/null || true
fi fi
if [[ -f "requirements.txt" ]]; then if [[ -f "requirements.txt" ]]; then
@@ -103,63 +223,85 @@ if [[ -f "requirements.txt" ]]; then
$PIP_CMD install -r requirements.txt -q 2>/dev/null || true $PIP_CMD install -r requirements.txt -q 2>/dev/null || true
fi fi
# 核心依赖兜底 ok "Python 依赖安装完成"
for pkg in click pyyaml websockets; do
$PYTHON_CMD -c "import $pkg" 2>/dev/null || { # ═══════════════════════════════════════════════════════════
info "安装 $pkg ..." # 5. 创建数据目录
$PIP_CMD install "$pkg" -q 2>/dev/null || $PIP_CMD install "$pkg" --break-system-packages -q 2>/dev/null || true # ═══════════════════════════════════════════════════════════
} step "初始化数据目录"
DATA_DIRS=("data" "data/html-render" "data/web-toolkit" "data/plugin-storage" "data/DCIM" "data/pkg" "data/signature-verifier/keys/private" "data/signature-verifier/keys/public" "logs")
DIR_COUNT=${#DATA_DIRS[@]}
DIR_CURRENT=0
for dir in "${DATA_DIRS[@]}"; do
DIR_CURRENT=$((DIR_CURRENT + 1))
progress_bar $DIR_CURRENT $DIR_COUNT "创建 $dir"
mkdir -p "$dir"
done done
ok "依赖就绪" echo -e "\n"
ok "数据目录已就绪"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 3. 确保 data 目录 # 6. 检查 MySQL (可选)
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
mkdir -p data/html-render data/web-toolkit data/plugin-storage data/DCIM data/pkg step "检查数据库 (可选)"
if command -v mysql &>/dev/null; then
ok "MySQL: $(mysql --version 2>&1)"
if pgrep mysqld > /dev/null 2>&1 || pgrep mariadbd > /dev/null 2>&1; then
ok "MySQL 服务运行中"
mysql -u root -e "CREATE DATABASE IF NOT EXISTS futureoss CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && \
ok "数据库 futureoss 已就绪" || \
warn "无法创建数据库,请检查权限"
else
warn "MySQL 服务未运行"
info "启动命令: sudo systemctl start mysql (或 mariadb)"
fi
else
info "MySQL 未安装 (可选功能)"
info "安装: sudo apt install mysql-server (Debian/Ubuntu)"
info " sudo yum install mysql-server (CentOS/RHEL)"
fi
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 4. 启动 # 7. 启动服务
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
title "🚀 启动 FutureOSS" step "启动 FutureOSS"
if $DAEMON; then if $DAEMON; then
title "🔒 守护模式"
LOG_FILE="logs/futureoss.log" LOG_FILE="logs/futureoss.log"
mkdir -p logs
PID_FILE="logs/futureoss.pid" PID_FILE="logs/futureoss.pid"
if [[ -f "$PID_FILE" ]]; then if [[ -f "$PID_FILE" ]]; then
OLD_PID=$(cat "$PID_FILE") OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then if kill -0 "$OLD_PID" 2>/dev/null; then
warn "已有进程运行 (PID: $OLD_PID),正在停止..." warn "已有进程运行 (PID: $OLD_PID)"
kill "$OLD_PID" 2>/dev/null || true info "停止: kill $OLD_PID 或 bash start.sh stop"
sleep 2 exit 0
fi fi
fi fi
nohup $PYTHON_CMD -m oss.cli serve > "$LOG_FILE" 2>&1 & nohup $PYTHON_CMD -m oss.cli serve > "$LOG_FILE" 2>&1 &
NEW_PID=$! NEW_PID=$!
echo "$NEW_PID" > "$PID_FILE" echo "$NEW_PID" > "$PID_FILE"
ok "已启动守护进程 (PID: $NEW_PID)" ok "守护进程已启动 (PID: $NEW_PID)"
info "日志: $LOG_FILE" info "日志文件: $LOG_FILE"
info "停止: kill $(cat $PID_FILE) 或 bash start.sh stop"
sleep 2 sleep 2
curl -s http://localhost:8080/health &>/dev/null && ok "服务就绪: http://localhost:8080" || warn "服务启动中,请稍候..." curl -s http://localhost:8080/health &>/dev/null && ok "服务就绪: http://localhost:8080" || warn "服务启动中,请稍候..."
exit 0 exit 0
fi fi
# ── 前台模式 + 崩溃自动重启 ── # 前台模式 + 崩溃自动重启
echo -e "${WHITE}运行中... 按 Ctrl+C 停止${NC}"
echo ""
RESTART_DELAY=3 RESTART_DELAY=3
MAX_RESTARTS=0
RESTART_COUNT=0 RESTART_COUNT=0
run_server() {
$PYTHON_CMD -m oss.cli serve
}
while true; do while true; do
run_server $PYTHON_CMD -m oss.cli serve
EXIT_CODE=$? EXIT_CODE=$?
if [[ $EXIT_CODE -eq 0 ]]; then if [[ $EXIT_CODE -eq 0 ]]; then
@@ -171,7 +313,7 @@ while true; do
warn "服务异常退出 (code: $EXIT_CODE)${RESTART_DELAY}s 后重启... (第 $RESTART_COUNT 次)" warn "服务异常退出 (code: $EXIT_CODE)${RESTART_DELAY}s 后重启... (第 $RESTART_COUNT 次)"
sleep $RESTART_DELAY sleep $RESTART_DELAY
# 指数退避,最大 30s # 指数退避
if [[ $RESTART_DELAY -lt 30 ]]; then if [[ $RESTART_DELAY -lt 30 ]]; then
RESTART_DELAY=$((RESTART_DELAY * 2)) RESTART_DELAY=$((RESTART_DELAY * 2))
fi fi

View File

@@ -0,0 +1,8 @@
{
"signature": "SizmRKKsPO3WuOYi+GtSOvKwZb5UrwRbSlJNJ26RF7l7811PLQlrBPJ7Awx1SUwy50TLrDpwtqbRIdCnGVqI9yzghBhdkwz7dpaAQ//lZK6SM9ygMMtS4ADJ839/AHTuB4USQM5FlqOwTIBE6QGAMgQw+w4di7Rpyh/6VD4Fg3GoiLJi7Pte0Upuglr4oIfZwpEt1liAi0ZlnE+Qb1GkmEGfQYyNYDYQkLKS0KG113YxqMj7sef9WcRCaKJSm+FZ8rV7dA0pCj1jY5sKOdXO/3PYH9g6O/BdgP0XuAoAUgGWshB0Z/D4WwHyykOIRM3jRHmU8kUB4PjxCzFVoDnkYfvN7wBojMjb0F9POjfbSv40jjC3EDjeDusbAP1FGv+F7QaJyAWhNUBSlRUBcHZZ8icSqRAStwX9MHsBVZa5EGrvHFK4SP8b6X6gm01+3JuKpiSRPGkxyDuxlFLNNDipmUNuHh1byofE/oD48yLNh7nGofVIvaDdOn6bhnc3ZDd54onncDNEBaWAHrLvly1nzkP5VN1bFEax/jZPWbSrcntmQ0Ua+11D0Ot/FVFhhrJo1dBBECM9zkVBUkpYAAf1RN7f9IglBVhi5iK+LmbGXzTSUX695tMvnufwXEJsH4fu3Jkom/PUkEggWNHEgb4qm4IsO2wzMWns+ZbZi3PzXP0=",
"signer": "Falck",
"algorithm": "RSA-SHA256",
"timestamp": 1775964953.1502125,
"plugin_hash": "84d69d65913b62d156e13a22e09dfcc3a5b36e052ae0532c569ced1fb269bb11",
"author": "Falck"
}

View File

@@ -1,9 +1,24 @@
"""HTML 渲染服务 - 通过 config.json 配置,统一文件入口""" """HTML 渲染服务 - 通过 config.json 配置,统一文件入口"""
import json import json
import sys
from pathlib import Path from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response from oss.plugin.types import Plugin, register_plugin_type, Response
class _Log:
_TTY = sys.stdout.isatty()
_C = {"reset": "\033[0m", "white": "\033[0;37m", "yellow": "\033[1;33m", "blue": "\033[1;34m", "red": "\033[1;31m"}
@classmethod
def _c(cls, t, c):
return f"{cls._C.get(c,'')}{t}{cls._C['reset']}" if cls._TTY else t
@classmethod
def info(cls, m): print(f"{cls._c('[html-render]', 'white')} {cls._c(m, 'white')}")
@classmethod
def warn(cls, m): print(f"{cls._c('[html-render]', 'yellow')} {cls._c('', 'yellow')} {cls._c(m, 'yellow')}")
@classmethod
def error(cls, m): print(f"{cls._c('[html-render]', 'red')} {cls._c('', 'red')} {cls._c(m, 'red')}")
class HtmlRenderPlugin(Plugin): class HtmlRenderPlugin(Plugin):
"""HTML 渲染插件 - 渲染服务由 html-render 提供""" """HTML 渲染插件 - 渲染服务由 html-render 提供"""
@@ -16,16 +31,16 @@ class HtmlRenderPlugin(Plugin):
def init(self, deps: dict = None): def init(self, deps: dict = None):
"""初始化 - 读取 config.json 并解析网站根目录""" """初始化 - 读取 config.json 并解析网站根目录"""
self._load_config() self._load_config()
print(f"[html-render] 配置加载完成: root_dir={self.root_dir}") _Log.info(f"配置加载完成: root_dir={self.root_dir}")
def start(self): def start(self):
"""启动 - 注册路由到 http-api共享配置给 web-toolkit""" """启动 - 注册路由到 http-api共享配置给 web-toolkit"""
# 注册首页路由 # 注册首页路由
if self.http_api and hasattr(self.http_api, 'router'): if self.http_api and hasattr(self.http_api, 'router'):
self.http_api.router.get("/", self._serve_html) self.http_api.router.get("/", self._serve_html)
print("[html-render] 已注册路由到 http-api") _Log.info("已注册路由到 http-api")
else: else:
print("[html-render] http-api 未加载") _Log.warn("http-api 未加载")
# 将配置共享给 web-toolkit通过 plugin-storage 的 DCIM 共享存储) # 将配置共享给 web-toolkit通过 plugin-storage 的 DCIM 共享存储)
if self.storage: if self.storage:
@@ -35,7 +50,7 @@ class HtmlRenderPlugin(Plugin):
"index_file": self.config.get("index_file", "index.html"), "index_file": self.config.get("index_file", "index.html"),
"static_prefix": self.config.get("static_prefix", "/static"), "static_prefix": self.config.get("static_prefix", "/static"),
}) })
print("[html-render] 配置已共享到 DCIM") _Log.info("配置已共享到 DCIM")
def stop(self): def stop(self):
"""停止""" """停止"""
@@ -53,7 +68,7 @@ class HtmlRenderPlugin(Plugin):
"""读取 config.json解析根目录""" """读取 config.json解析根目录"""
config_path = Path("./data/html-render/config.json") config_path = Path("./data/html-render/config.json")
if not config_path.exists(): if not config_path.exists():
print("[html-render] 警告: config.json 不存在,使用默认配置") _Log.warn("config.json 不存在,使用默认配置")
self.config = {"root_dir": "../website", "index_file": "index.html"} self.config = {"root_dir": "../website", "index_file": "index.html"}
else: else:
with open(config_path, "r", encoding="utf-8") as f: with open(config_path, "r", encoding="utf-8") as f:
@@ -66,6 +81,11 @@ class HtmlRenderPlugin(Plugin):
def _serve_html(self, request): def _serve_html(self, request):
"""提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径""" """提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径"""
index_file = self.config.get("index_file", "index.html") index_file = self.config.get("index_file", "index.html")
# 安全检查:防止路径穿越
if ".." in index_file or index_file.startswith("/"):
return Response(status=403, body="Forbidden")
if self.storage: if self.storage:
storage = self.storage.get_storage("html-render") storage = self.storage.get_storage("html-render")
if storage.file_exists(index_file): if storage.file_exists(index_file):

View File

@@ -0,0 +1,8 @@
{
"signature": "GYBKpyVNgNFbpeoGlkXNY+wvt5wrJFHeP06At2h3SPsZUX3sXCtUL8RoidfzkqrfphBKAaKYvRnXaZdi3hyaDfXNQ88Ik18U+K7Usx+/o/rrQqzMKqh1pT75UZgZtJpXHu7CiIEjNIQ0pbujRHVfnRFe/4K3E2IClpJLcrziyrvn0fUBcUytt/WCTGBJ8pnyWB+ybcIDTJJQ+l4E69vsy2YmJHZBbBreyOo+TN5AQHDAlZ851dxI1K9euCNtdnlufbW6QSshnQ7DSS94KYZEUgTYFGON4Qi1RiVTFJK4iJEkTExEmohc3AuFJtEoIBBJzbUj/yCmfGcyWrbK7wchdwdGuNxGbexB97FONGm0WFS/z6OM08ljMJUAgvDRZtpInpQHFWJfxBfH+wzBx0AvhkgiJeeUApeofOxlggveOLDYDEH8P858sf0sjHHL0qgE17alvn0Fi8rArOI40wrh420SF7p4VlXE7fufXoue+yAhlSt68zaXOJHAtK5CuMh2ytVFKonRJgF5TAXvXYJeOZgujHyUUTtVqje+thIaBzqtGhEt9xp5N6Ikky2sutKRMgXx34As3hvx0U6a2CHuVykcX9neoB8XtJNlE1+AT24wnWw8LBqm6OjCTeJtAOFWFkliHNID9b1xfq69rZBp/L4Djj1bzy8WNLM7QLbjAvc=",
"signer": "Falck",
"algorithm": "RSA-SHA256",
"timestamp": 1775964953.1846428,
"plugin_hash": "eab1e047be16fe50b9c46f26570924f2975fac71a45af7f6c0b1f9c16ac8b096",
"author": "Falck"
}

View File

@@ -1,5 +1,6 @@
"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)""" """Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)"""
import json import json
import sys
from pathlib import Path from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response from oss.plugin.types import Plugin, register_plugin_type, Response
from .router import WebRouter from .router import WebRouter
@@ -7,6 +8,20 @@ from .static import StaticFileHandler
from .template import TemplateEngine from .template import TemplateEngine
class _Log:
_TTY = sys.stdout.isatty()
_C = {"reset": "\033[0m", "white": "\033[0;37m", "yellow": "\033[1;33m", "blue": "\033[1;34m", "red": "\033[1;31m"}
@classmethod
def _c(cls, t, c):
return f"{cls._C.get(c,'')}{t}{cls._C['reset']}" if cls._TTY else t
@classmethod
def info(cls, m): print(f"{cls._c('[web-toolkit]', 'white')} {cls._c(m, 'white')}")
@classmethod
def warn(cls, m): print(f"{cls._c('[web-toolkit]', 'yellow')} {cls._c('', 'yellow')} {cls._c(m, 'yellow')}")
@classmethod
def error(cls, m): print(f"{cls._c('[web-toolkit]', 'red')} {cls._c('', 'red')} {cls._c(m, 'red')}")
class WebToolkitPlugin(Plugin): class WebToolkitPlugin(Plugin):
"""Web 工具包插件 - 提供网站前端所有服务""" """Web 工具包插件 - 提供网站前端所有服务"""
@@ -26,7 +41,7 @@ class WebToolkitPlugin(Plugin):
self.template_engine = TemplateEngine() self.template_engine = TemplateEngine()
self._load_config() self._load_config()
self.static_handler = StaticFileHandler(root=str(self.root_dir)) self.static_handler = StaticFileHandler(root=str(self.root_dir))
print(f"[web-toolkit] 配置加载完成: root_dir={self.root_dir}") _Log.info(f"配置加载完成: root_dir={self.root_dir}")
def start(self): def start(self):
"""启动""" """启动"""
@@ -65,7 +80,7 @@ class WebToolkitPlugin(Plugin):
self._serve_static self._serve_static
) )
print("[web-toolkit] Web 工具包已启动") _Log.info("Web 工具包已启动")
def stop(self): def stop(self):
"""停止""" """停止"""
@@ -97,7 +112,7 @@ class WebToolkitPlugin(Plugin):
"""读取 config.json解析网站根目录""" """读取 config.json解析网站根目录"""
config_path = Path("./data/web-toolkit/config.json") config_path = Path("./data/web-toolkit/config.json")
if not config_path.exists(): if not config_path.exists():
print("[web-toolkit] 警告: config.json 不存在,使用默认配置") _Log.warn("config.json 不存在,使用默认配置")
self.config = { self.config = {
"root_dir": "../website", "root_dir": "../website",
"index_file": "index.html", "index_file": "index.html",
@@ -146,6 +161,10 @@ class WebToolkitPlugin(Plugin):
else: else:
filename = path.lstrip("/") filename = path.lstrip("/")
# 安全检查:防止路径穿越
if ".." in filename or filename.startswith("/"):
return Response(status=403, body="Forbidden")
if not filename: if not filename:
return self._serve_website_index(request) return self._serve_website_index(request)
return self.static_handler.serve(filename) return self.static_handler.serve(filename)

View File

@@ -43,27 +43,74 @@ class TemplateEngine:
return content return content
def _safe_eval(self, expression: str, context: dict) -> Any: def _safe_eval(self, expression: str, context: dict) -> Any:
"""安全评估表达式(仅允许简单的属性访问和比较""" """安全评估表达式(使用 AST 验证,不使用 eval"""
# 只允许访问 context 中的变量
# 支持的运算符: and, or, not, ==, !=, <, >, <=, >=, in
# 不允许函数调用、导入、属性访问等
# 使用 AST 解析并验证
try: try:
tree = ast.parse(expression, mode='eval') tree = ast.parse(expression, mode='eval')
except SyntaxError: except SyntaxError:
return False return False
# 验证 AST 节点 # 验证 AST 节点
if not self._validate_ast(tree.body[0].value, set(context.keys())): if not self._validate_ast(tree.body[0].value, set(context.keys())):
return False return False
# 在受限环境中评估 # 使用安全的 AST 解释器,不使用 eval
try: try:
return eval(expression, {"__builtins__": {}}, context) return self._eval_ast(tree.body[0].value, context)
except Exception: except Exception:
return False return False
def _eval_ast(self, node: ast.AST, context: dict) -> Any:
"""安全地评估 AST 节点"""
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.Name):
return context.get(node.id, False)
elif isinstance(node, ast.BoolOp):
if isinstance(node.op, ast.And):
return all(self._eval_ast(v, context) for v in node.values)
elif isinstance(node.op, ast.Or):
return any(self._eval_ast(v, context) for v in node.values)
elif isinstance(node, ast.Compare):
return self._eval_compare(node, context)
elif isinstance(node, ast.UnaryOp):
if isinstance(node.op, ast.Not):
return not self._eval_ast(node.operand, context)
elif isinstance(node, ast.Subscript):
return self._eval_subscript(node, context)
return False
def _eval_compare(self, node: ast.Compare, context: dict) -> bool:
"""评估比较表达式"""
left = self._eval_ast(node.left, context)
for op, comp in zip(node.ops, node.comparators):
right = self._eval_ast(comp, context)
if isinstance(op, ast.Eq):
if not (left == right): return False
elif isinstance(op, ast.NotEq):
if not (left != right): return False
elif isinstance(op, ast.Lt):
if not (left < right): return False
elif isinstance(op, ast.Gt):
if not (left > right): return False
elif isinstance(op, ast.LtE):
if not (left <= right): return False
elif isinstance(op, ast.GtE):
if not (left >= right): return False
elif isinstance(op, ast.In):
if not (left in right): return False
elif isinstance(op, ast.NotIn):
if not (left not in right): return False
left = right
return True
def _eval_subscript(self, node: ast.Subscript, context: dict) -> Any:
"""评估下标访问"""
value = self._eval_ast(node.value, context)
key = self._eval_ast(node.slice, context)
if isinstance(value, (dict, list, str)):
return value[key]
return None
def _validate_ast(self, node: ast.AST, allowed_names: set) -> bool: def _validate_ast(self, node: ast.AST, allowed_names: set) -> bool:
"""验证 AST 只包含安全的操作""" """验证 AST 只包含安全的操作"""
if isinstance(node, ast.Name): if isinstance(node, ast.Name):

View File

@@ -1,30 +0,0 @@
# circuit-breaker 熔断器
为插件提供熔断能力,防止级联失败。
## 功能
- 失败计数熔断
- 状态:`closed``open``half-open`
- 可配置失败阈值
- 自动恢复机制
## 状态机
```
closed (正常) → open (熔断) → half-open (半开) → closed (恢复)
```
## 使用
```python
# 检查是否有熔断能力
if "circuit_breaker" in capabilities:
breaker = extensions["_circuit_breaker_provider"]
cb = breaker.create("my-plugin", threshold=5)
try:
result = cb.call(risky_function, arg1, arg2)
except Exception:
print("熔断器已触发")
```

View File

@@ -1,70 +0,0 @@
"""熔断插件 - 为插件提供熔断能力"""
from oss.plugin.types import Plugin, register_plugin_type
class CircuitBreakerProvider:
"""熔断能力提供者"""
def __init__(self):
self.breakers: dict[str, "CircuitBreaker"] = {}
def create(self, name: str, threshold: int = 5) -> "CircuitBreaker":
breaker = CircuitBreaker(name, threshold)
self.breakers[name] = breaker
return breaker
def get(self, name: str):
return self.breakers.get(name)
class CircuitBreaker:
"""熔断器"""
def __init__(self, name: str, threshold: int = 5):
self.name = name
self.threshold = threshold
self.failures = 0
self.state = "closed" # closed, open, half-open
def call(self, func, *args, **kwargs):
if self.state == "open":
raise Exception(f"熔断器 '{self.name}' 已打开")
try:
result = func(*args, **kwargs)
self.failures = 0
self.state = "closed"
return result
except Exception as e:
self.failures += 1
if self.failures >= self.threshold:
self.state = "open"
raise e
class CircuitBreakerPlugin(Plugin):
"""熔断插件"""
def __init__(self):
self.provider = CircuitBreakerProvider()
def init(self, deps: dict = None):
pass
def start(self):
pass
def stop(self):
pass
def get_provider(self):
return self.provider
# 注册类型
register_plugin_type("CircuitBreakerProvider", CircuitBreakerProvider)
register_plugin_type("CircuitBreaker", CircuitBreaker)
def New():
return CircuitBreakerPlugin()

View File

@@ -1,17 +0,0 @@
{
"metadata": {
"name": "circuit-breaker",
"version": "1.0.0",
"author": "FutureOSS",
"description": "熔断器 - 为插件提供熔断能力",
"type": "extension"
},
"config": {
"enabled": true,
"args": {
"default_threshold": 5
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,8 @@
{
"signature": "BRVmR6gX5do7yBsBCtR9jk5/YoE6igio8d3IVNxAtwAtkBdS2Z3LNv9VwMBXeqOE84Dz1+/ypkQO+rdh9VZpGOpAPGxjCyArff9oS3nW6gazMZdLfMKrtsHxVBAL4Ycjb1NmQ3W0kdZa/aS+r2Q/tqVMJ62bqVR5Lbrc2H8eG/i1gPZsEu5tA7KC9pB8oDfaAY/QxeDczg32zWqh9UDD59Hp7TQMZhsWXsH9FgfvKjYKjcsQUEXs6ijUJ6PxHuc2Jx71xhD/IXseOTmnDCMe+8JdPA5aaVN/TEgmT99RXv62wHR+tulyaCYRd/P3sTItSSb1UYfLqEGBumetNAAGdgf33DMijUHKvufuha0JNOm6CCk+8UGbnYnG79HyaBz+pWfiF/pFX+LV7HTJTkBwQc3vXcvXep25UDspSkL+x2w3f1mk9S/oA5mT2go4kSaORxkCb1fAbh74Bn51VRmQV8XLSUOoZvWHjiaMkMdLsyPyTi2+fxqrDD7ehgeQBp3cNSoiGViqYcFcg2xCuHo2P/W441cZMOscfawdLJxg3N4+UC41LTooXN1+IBWzG7jrGTLyeXAFxGeOBo165WoAnsQZ9hh+uj/plv+LIU/mmOBSpJZIb4SuVJfoEcIDGpa7iieVr//8cTnbNTt9zh3GWYuW1NPIm+/WT4YoPfeAs/M=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775964953.1082504,
"plugin_hash": "8894b78ac59c0154acaeb9a976f80588ece406e55079ca633c3b2bd839098d40",
"author": "FutureOSS"
}

View File

@@ -0,0 +1,100 @@
"""质量检查器"""
import ast
class QualityChecker:
"""质量检查器"""
def check(self, filepath: str, content: str) -> list:
"""执行质量检查"""
issues = []
# 检查函数长度
issues.extend(self._check_function_length(filepath, content))
# 检查参数数量
issues.extend(self._check_parameter_count(filepath, content))
# 检查复杂度
issues.extend(self._check_complexity(filepath, content))
return issues
def _check_function_length(self, filepath: str, content: str) -> list:
"""检查函数长度"""
issues = []
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
lines = node.end_lineno - node.lineno
if lines > 100:
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "warning",
"type": "long_function",
"message": f"函数 {node.name} 过长 ({lines} 行)"
})
except:
pass
return issues
def _check_parameter_count(self, filepath: str, content: str) -> list:
"""检查参数数量"""
issues = []
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
args = node.args
count = len(args.args)
if count > 5:
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "info",
"type": "too_many_params",
"message": f"函数 {node.name} 参数过多 ({count} 个)"
})
except:
pass
return issues
def _check_complexity(self, filepath: str, content: str) -> list:
"""检查圈复杂度"""
issues = []
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
complexity = self._calculate_complexity(node)
if complexity > 10:
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "warning",
"type": "high_complexity",
"message": f"函数 {node.name} 复杂度过高 (圈复杂度: {complexity})"
})
except:
pass
return issues
def _calculate_complexity(self, node: ast.AST) -> int:
"""计算圈复杂度"""
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For, ast.Try, ast.With)):
complexity += 1
elif isinstance(child, ast.BoolOp):
complexity += len(child.values) - 1
return complexity

View File

@@ -0,0 +1,323 @@
"""引用检查器 - 检测导入错误、变量错误等"""
import ast
import sys
import os
from pathlib import Path
class ReferenceChecker:
"""引用检查器"""
# Python 标准库模块列表
STD_MODULES = {
'os', 'sys', 'json', 're', 'time', 'datetime', 'pathlib',
'typing', 'collections', 'functools', 'itertools', 'io',
'string', 'math', 'random', 'hashlib', 'hmac', 'secrets',
'urllib', 'http', 'email', 'html', 'xml', 'csv', 'configparser',
'logging', 'warnings', 'traceback', 'inspect', 'importlib',
'threading', 'multiprocessing', 'subprocess', 'socket',
'asyncio', 'concurrent', 'queue', 'contextlib', 'abc',
'enum', 'dataclasses', 'copy', 'pprint', 'textwrap',
'struct', 'codecs', 'locale', 'gettext', 'argparse',
'unittest', 'doctest', 'pdb', 'profile', 'timeit',
'tempfile', 'glob', 'fnmatch', 'stat', 'fileinput',
'shutil', 'pickle', 'shelve', 'sqlite3', 'dbm',
'gzip', 'bz2', 'lzma', 'zipfile', 'tarfile',
'base64', 'binascii', 'quopri', 'uu',
}
# Python 内置函数和类型(不应报告为未定义)
BUILTINS = {
'print', 'len', 'str', 'int', 'float', 'bool', 'list', 'dict',
'set', 'tuple', 'range', 'enumerate', 'zip', 'map', 'filter',
'sorted', 'reversed', 'min', 'max', 'sum', 'abs', 'round',
'isinstance', 'issubclass', 'type', 'id', 'hash', 'repr',
'True', 'False', 'None', 'Exception', 'ValueError', 'TypeError',
'KeyError', 'AttributeError', 'ImportError', 'FileNotFoundError',
'IndexError', 'RuntimeError', 'StopIteration', 'GeneratorExit',
'staticmethod', 'classmethod', 'property', 'super',
'open', 'input', 'format', 'hex', 'oct', 'bin', 'chr', 'ord',
'dir', 'vars', 'locals', 'globals', 'callable', 'getattr',
'setattr', 'hasattr', 'delattr', 'exec', 'eval', 'compile',
'any', 'all', 'slice', 'frozenset', 'bytearray', 'bytes',
'memoryview', 'complex', 'divmod', 'pow', 'object',
'dict', 'list', 'str', 'int', 'float', 'bool', 'set',
'tuple', 'Exception', 'ValueError', 'TypeError', 'KeyError',
'self', 'cls', 'args', 'kwargs',
}
def __init__(self, project_root: str = "."):
self.project_root = Path(project_root)
self._available_modules = set(self.STD_MODULES)
self._scan_project_modules()
def _scan_project_modules(self):
"""扫描项目中的可用模块"""
# 扫描 oss 目录(框架核心)
oss_dir = self.project_root / "oss"
if oss_dir.exists():
self._available_modules.add("oss")
self._scan_module_dir(oss_dir, "oss")
# 扫描 store 目录下的所有插件
store_dir = self.project_root / "store"
if store_dir.exists():
for author_dir in store_dir.iterdir():
if not author_dir.is_dir():
continue
for plugin_dir in author_dir.iterdir():
if not plugin_dir.is_dir():
continue
plugin_name = plugin_dir.name
# 添加插件名作为可用模块
self._available_modules.add(plugin_name)
# 扫描插件内部的子模块
self._scan_plugin_modules(plugin_dir, plugin_name)
def _scan_module_dir(self, dir_path: Path, base_name: str):
"""扫描模块目录"""
if dir_path.exists():
for item in dir_path.iterdir():
if item.is_file() and item.name.endswith(".py") and item.name != "__init__.py":
module_name = item.name[:-3]
full_name = f"{base_name}.{module_name}"
self._available_modules.add(full_name)
elif item.is_dir() and (item / "__init__.py").exists():
full_name = f"{base_name}.{item.name}"
self._available_modules.add(full_name)
self._scan_module_dir(item, full_name)
def _scan_plugin_modules(self, plugin_dir: Path, base_name: str):
"""扫描插件内部的子模块"""
for item in plugin_dir.iterdir():
if item.is_dir() and (item / "__init__.py").exists():
full_name = f"{base_name}.{item.name}"
self._available_modules.add(full_name)
self._scan_module_dir(item, full_name)
elif item.is_file() and item.name.endswith(".py") and item.name != "__init__.py":
module_name = item.name[:-3]
full_name = f"{base_name}.{module_name}"
self._available_modules.add(full_name)
def _add_module_from_dir(self, dir_path: Path, base_name: str):
"""从目录添加模块"""
if dir_path.exists():
for item in dir_path.iterdir():
if item.is_file() and item.name.endswith(".py") and item.name != "__init__.py":
module_name = item.name[:-3]
self._available_modules.add(f"{base_name}.{module_name}")
elif item.is_dir() and (item / "__init__.py").exists():
self._add_module_from_dir(item, f"{base_name}.{item.name}")
def check(self, filepath: str, content: str) -> list:
"""执行引用检查"""
issues = []
try:
tree = ast.parse(content)
except SyntaxError as e:
return [{
"file": filepath,
"line": e.lineno or 0,
"severity": "critical",
"type": "syntax_error",
"message": f"语法错误: {e.msg}"
}]
# 检查导入语句(跳过相对导入)
issues.extend(self._check_imports(filepath, tree))
# 检查属性访问错误
issues.extend(self._check_attribute_access(filepath, tree, content))
# 检查函数调用错误
issues.extend(self._check_function_calls(filepath, tree, content))
return issues
def _check_imports(self, filepath: str, tree: ast.AST) -> list:
"""检查导入语句"""
issues = []
file_path = Path(filepath)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
# 跳过 oss 框架模块(运行时可用)
if alias.name.startswith('oss.') or alias.name == 'oss':
continue
# 跳过 websockets 等第三方库
if alias.name in ('websockets', 'yaml', 'click'):
continue
if not self._is_module_available(alias.name, file_path):
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "critical",
"type": "import_error",
"message": f"无法导入模块: {alias.name}"
})
elif isinstance(node, ast.ImportFrom):
# 跳过相对导入(以 . 开头)
if node.level and node.level > 0:
continue
# 跳过 oss 框架模块
if node.module and (node.module.startswith('oss.') or node.module == 'oss'):
continue
# 跳过第三方库
if node.module and node.module.split('.')[0] in ('websockets', 'yaml', 'click'):
continue
if node.module:
if not self._is_module_available(node.module, file_path):
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "critical",
"type": "import_error",
"message": f"无法导入模块: {node.module}"
})
return issues
def _check_variable_references(self, filepath: str, tree: ast.AST, content: str) -> list:
"""检查变量引用"""
issues = []
lines = content.split('\n')
for node in ast.walk(tree):
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
# 检查是否引用了未定义的变量
if not self._is_name_defined(node.id, tree, node.lineno):
if node.id not in ('True', 'False', 'None', 'self', 'cls'):
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "warning",
"type": "undefined_variable",
"message": f"使用了未定义的变量: {node.id}"
})
return issues
def _check_attribute_access(self, filepath: str, tree: ast.AST, content: str) -> list:
"""检查属性访问"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.Attribute):
# 检查可能的属性错误
if isinstance(node.value, ast.Name):
var_name = node.value.id
if var_name in ('None', 'True', 'False'):
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "critical",
"type": "attribute_error",
"message": f"尝试访问 {var_name} 的属性: {node.attr}"
})
return issues
def _check_function_calls(self, filepath: str, tree: ast.AST, content: str) -> list:
"""检查函数调用"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.Call):
# 检查调用不存在的方法
if isinstance(node.func, ast.Attribute):
if isinstance(node.func.value, ast.Constant) and node.func.value.value is None:
issues.append({
"file": filepath,
"line": node.lineno,
"severity": "critical",
"type": "method_call_on_none",
"message": f"在 None 上调用方法: {node.func.attr}"
})
return issues
def _is_module_available(self, module_name: str, file_path: Path = None) -> bool:
"""检查模块是否可用"""
# 检查是否在已扫描的模块中
if module_name in self._available_modules:
return True
# 检查标准库
base_module = module_name.split('.')[0]
if base_module in self.STD_MODULES:
return True
# 检查是否是 oss 框架模块
if module_name.startswith('oss.') or module_name == 'oss':
return True
# 检查是否是常见第三方库
third_party = {'websockets', 'yaml', 'click', 'requests', 'flask', 'django', 'numpy', 'pandas'}
if module_name.split('.')[0] in third_party:
return True
# 检查是否是当前文件的同目录模块(相对导入的情况)
if file_path:
file_dir = file_path.parent
# 检查同级 .py 文件
sibling_module = file_dir / f"{module_name}.py"
if sibling_module.exists():
return True
# 检查同级包
sibling_pkg = file_dir / module_name
if sibling_pkg.is_dir() and (sibling_pkg / "__init__.py").exists():
return True
# 检查 store 目录下的插件
store_dir = self.project_root / "store"
if store_dir.exists():
for author_dir in store_dir.iterdir():
if author_dir.is_dir():
for plugin_dir in author_dir.iterdir():
if plugin_dir.is_dir() and plugin_dir.name == module_name.split('.')[0]:
return True
return False
def _is_name_defined(self, name: str, tree: ast.AST, line: int) -> bool:
"""检查名称是否已定义"""
# 检查是否是内置函数/类型
if name in self.BUILTINS:
return True
# 检查是否是函数参数
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
for arg in node.args.args:
if arg.arg == name:
return True
# 检查是否是赋值目标
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == name:
return True
# 检查是否是循环变量
elif isinstance(node, ast.For):
if isinstance(node.target, ast.Name) and node.target.id == name:
return True
# 检查是否是导入
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.asname == name or alias.name == name:
return True
elif isinstance(node, ast.ImportFrom):
if node.module:
for alias in node.names:
if alias.asname == name or alias.name == name:
return True
return False

View File

@@ -0,0 +1,85 @@
"""安全检查器"""
class SecurityChecker:
"""安全检查器"""
def check(self, filepath: str, content: str) -> list:
"""执行安全检查"""
issues = []
# 检查硬编码密钥
issues.extend(self._check_secrets(filepath, content))
# 检查危险函数
issues.extend(self._check_dangerous_functions(filepath, content))
# 检查路径穿越
issues.extend(self._check_path_traversal(filepath, content))
return issues
def _check_secrets(self, filepath: str, content: str) -> list:
"""检查硬编码密钥"""
issues = []
patterns = ['password', 'secret', 'token', 'api_key', 'access_token']
for i, line in enumerate(content.split('\n'), 1):
stripped = line.strip()
# 跳过注释和模式定义行
if stripped.startswith('#') or stripped.startswith('patterns') or "'" in stripped[:20]:
continue
for pattern in patterns:
if pattern + ' = "' in line.lower() or pattern + " = '" in line.lower():
issues.append({
"file": filepath,
"line": i,
"severity": "critical",
"type": "hardcoded_secret",
"message": f"发现硬编码密钥: {line.strip()[:50]}"
})
return issues
def _check_dangerous_functions(self, filepath: str, content: str) -> list:
"""检查危险函数"""
issues = []
dangerous = ['eval(', 'exec(', 'os.system(', 'subprocess.call(', 'subprocess.run(']
# 跳过检查安全检查器自身
if 'code-reviewer/checks/security.py' in filepath:
return []
for i, line in enumerate(content.split('\n'), 1):
# 跳过注释和模式定义行
stripped = line.strip()
if stripped.startswith('#') or 'dangerous' in stripped.lower() or "['" in stripped[:30]:
continue
for func in dangerous:
if func in line:
issues.append({
"file": filepath,
"line": i,
"severity": "warning",
"type": "dangerous_function",
"message": f"使用危险函数: {func.strip()}"
})
return issues
def _check_path_traversal(self, filepath: str, content: str) -> list:
"""检查路径穿越风险"""
issues = []
if '../' in content and 'open(' in content:
issues.append({
"file": filepath,
"line": 0,
"severity": "warning",
"type": "path_traversal_risk",
"message": "可能存在路径穿越漏洞"
})
return issues

View File

@@ -0,0 +1,70 @@
"""风格检查器"""
class StyleChecker:
"""风格检查器"""
def check(self, filepath: str, content: str) -> list:
"""执行风格检查"""
issues = []
# 检查行长度
issues.extend(self._check_line_length(filepath, content))
# 检查空行
issues.extend(self._check_blank_lines(filepath, content))
# 检查文件末尾换行
issues.extend(self._check_final_newline(filepath, content))
return issues
def _check_line_length(self, filepath: str, content: str) -> list:
"""检查行长度"""
issues = []
for i, line in enumerate(content.split('\n'), 1):
if len(line) > 120:
issues.append({
"file": filepath,
"line": i,
"severity": "info",
"type": "line_too_long",
"message": f"行过长 ({len(line)} 字符)"
})
return issues
def _check_blank_lines(self, filepath: str, content: str) -> list:
"""检查连续空行"""
issues = []
blank_count = 0
for i, line in enumerate(content.split('\n'), 1):
if line.strip() == '':
blank_count += 1
if blank_count > 2:
issues.append({
"file": filepath,
"line": i,
"severity": "info",
"type": "too_many_blanks",
"message": "连续空行过多"
})
else:
blank_count = 0
return issues
def _check_final_newline(self, filepath: str, content: str) -> list:
"""检查文件末尾换行"""
if content and not content.endswith('\n'):
return [{
"file": filepath,
"line": len(content.split('\n')),
"severity": "info",
"type": "missing_final_newline",
"message": "文件末尾缺少换行符"
}]
return []

View File

@@ -0,0 +1,94 @@
"""代码审查器核心"""
import os
import ast
import json
import time
from pathlib import Path
from typing import Any
from checks.security import SecurityChecker
from checks.quality import QualityChecker
from checks.style import StyleChecker
from checks.references import ReferenceChecker
from report.formatter import ReportFormatter
class CodeReviewer:
"""代码审查器"""
def __init__(self, config: dict):
self.config = config
self.security = SecurityChecker()
self.quality = QualityChecker()
self.style = StyleChecker()
self.references = ReferenceChecker()
self.formatter = ReportFormatter(config.get("report_format", "console"))
def run_check(self, scan_dirs: list) -> dict:
"""执行检查"""
start_time = time.time()
issues = []
files_scanned = 0
for scan_dir in scan_dirs:
if not os.path.exists(scan_dir):
continue
for root, dirs, files in os.walk(scan_dir):
# 排除目录
dirs[:] = [d for d in dirs if d not in self.config.get("exclude_patterns", [])]
for file in files:
if file.endswith('.py'):
filepath = os.path.join(root, file)
file_size = os.path.getsize(filepath)
if file_size > self.config.get("max_file_size", 102400):
continue
issues.extend(self._check_file(filepath))
files_scanned += 1
elapsed = time.time() - start_time
result = {
"status": "completed",
"files_scanned": files_scanned,
"total_issues": len(issues),
"issues": issues,
"scan_time": round(elapsed, 2),
"timestamp": time.time()
}
print(self.formatter.format(result))
return result
def _check_file(self, filepath: str) -> list:
"""检查单个文件"""
issues = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 安全检查
issues.extend(self.security.check(filepath, content))
# 质量检查
issues.extend(self.quality.check(filepath, content))
# 风格检查
issues.extend(self.style.check(filepath, content))
# 引用检查(新增)
issues.extend(self.references.check(filepath, content))
except Exception as e:
issues.append({
"file": filepath,
"line": 0,
"severity": "error",
"type": "parse_error",
"message": f"文件解析失败: {e}"
})
return issues

View File

@@ -0,0 +1,70 @@
"""代码审查器插件"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from oss.logger.logger import Log
from oss.plugin.types import Plugin, register_plugin_type
from core.reviewer import CodeReviewer
class CodeReviewerPlugin(Plugin):
"""代码审查器插件"""
def __init__(self):
self.reviewer = None
self.config = {}
def meta(self):
from oss.plugin.types import Metadata, PluginConfig, Manifest
return Manifest(
metadata=Metadata(
name="code-reviewer",
version="1.0.0",
author="FutureOSS",
description="代码审查器 - 自动扫描代码问题"
),
config=PluginConfig(
enabled=True,
args={
"scan_dirs": ["store", "oss"],
"exclude_patterns": ["__pycache__", "*.pyc"],
"max_file_size": 102400,
"report_format": "console"
}
),
dependencies=[]
)
def init(self, deps: dict = None):
config = {}
if deps:
config = deps.get("config", {})
self.config = {
"scan_dirs": config.get("scan_dirs", ["store", "oss"]),
"exclude_patterns": config.get("exclude_patterns", ["__pycache__"]),
"max_file_size": config.get("max_file_size", 102400),
"report_format": config.get("report_format", "console")
}
self.reviewer = CodeReviewer(self.config)
Log.info("code-reviewer", "初始化完成")
def start(self):
Log.info("code-reviewer", "插件已启动")
def stop(self):
Log.error("code-reviewer", "插件已停止")
def check(self, dirs: list = None) -> dict:
"""执行代码检查"""
scan_dirs = dirs or self.config["scan_dirs"]
return self.reviewer.run_check(scan_dirs)
register_plugin_type("CodeReviewerPlugin", CodeReviewerPlugin)
def New():
return CodeReviewerPlugin()

View File

@@ -0,0 +1,20 @@
{
"metadata": {
"name": "code-reviewer",
"version": "1.0.0",
"author": "FutureOSS",
"description": "代码审查器 - 提供 oss check 功能,自动扫描代码问题",
"type": "tool"
},
"config": {
"enabled": true,
"args": {
"scan_dirs": ["store", "oss"],
"exclude_patterns": ["__pycache__", "*.pyc", "*.pyo"],
"max_file_size": 102400,
"report_format": "console"
}
},
"dependencies": [],
"permissions": ["*"]
}

View File

@@ -0,0 +1,59 @@
"""报告格式化器"""
class ReportFormatter:
"""报告格式化器"""
def __init__(self, format_type: str = "console"):
self.format_type = format_type
def format(self, result: dict) -> str:
"""格式化报告"""
if self.format_type == "console":
return self._format_console(result)
elif self.format_type == "json":
return self._format_json(result)
return str(result)
def _format_console(self, result: dict) -> str:
"""控制台格式"""
lines = []
lines.append("=" * 60)
lines.append("代码审查报告")
lines.append("=" * 60)
lines.append(f"扫描文件: {result['files_scanned']}")
lines.append(f"发现问题: {result['total_issues']}")
lines.append(f"扫描时间: {result['scan_time']}s")
lines.append("")
# 按严重程度分类
critical = [i for i in result['issues'] if i['severity'] == 'critical']
warning = [i for i in result['issues'] if i['severity'] == 'warning']
info = [i for i in result['issues'] if i['severity'] == 'info']
lines.append(f"🔴 严重: {len(critical)}")
lines.append(f"🟡 警告: {len(warning)}")
lines.append(f"🔵 提示: {len(info)}")
lines.append("")
if critical:
lines.append("严重问题:")
for issue in critical:
lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
lines.append("")
if warning:
lines.append("警告:")
for issue in warning[:10]: # 最多显示10个
lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
if len(warning) > 10:
lines.append(f" ... 还有 {len(warning) - 10} 个警告")
lines.append("")
lines.append("=" * 60)
return '\n'.join(lines)
def _format_json(self, result: dict) -> str:
"""JSON 格式"""
import json
return json.dumps(result, indent=2, ensure_ascii=False)

View File

@@ -0,0 +1,8 @@
{
"signature": "vn4hpZQMQTX0d78Wlze2wtTHjN91qn1PIvsRTK7ZFVm8lZ3eQHrZz9X0uDWcKKjxf5FCI/UVKQOqLwYkHiGhcS7d7+v6UKKKIYph+aftHQRrEcOQtrSnrmDQrqSjEdL3mjkl0KTIwqkFySxVNn9ssmL16JCOtWpWpKU5CnKWVrbeEKvs6yZJrmVVr9C7iDGsNq0/aS3oPDI4vg1iaTYgg/2Sh1smJ0jNtE5EsCq78fcyUcSWTziwq8RnJvFsx8LP3cxacC1QuZIP3hTIrpnApAj0KqSTRDLKY7d7rsQAHgDlnbQfYVtA8x94x91R5ybeDpXwYPSwWMpb7P/7XBDJ5GKL56iFUCV0tceHNK9yyjaXdhf2oUTxfoC4ONOTnkmnP2pZ6vRLjd/0WX7qA0XUTmZtewWur1BnZeZwzOjI5K8IYCda5WKXLVyrH64XmBEAwkEu18LIO9xI+DnhbM7rR9/xO+cXHkOYtKgAJMHCzgi6o6tw/UgS9K0myoMeGg58gYaDIVbXpxpf3rHSyFQAwauI67oye7ZxNxJgKnnOtX92cpQLHDfML8psd+sAIuBazxqxe484qzF2k0F5ZZMP17V6Yd3UWUkvWMoKlktq14OwJ2Q67nrmt9OC+9Epzny4gkq/Q7ih85rGwMVxRvkKhxxLLelQLVIni363yOxn7UE=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775967256.7737296,
"plugin_hash": "68f5ab432690beef86da1c167c704fdd6b60512a359e806516dce1c6be27b9c5",
"author": "FutureOSS"
}

View File

@@ -0,0 +1,91 @@
/* Dashboard 仪表盘样式 */
.dashboard {
padding: 20px;
}
.dashboard-header {
margin-bottom: 30px;
}
.dashboard-header h2 {
font-size: 28px;
margin-bottom: 8px;
}
.dashboard-subtitle {
color: #666;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
font-size: 40px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: #1a1a2e;
}
.stat-label {
font-size: 13px;
color: #666;
margin-top: 4px;
}
.dashboard-section {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.dashboard-section h3 {
font-size: 20px;
margin-bottom: 20px;
color: #1a1a2e;
}
.info-table {
display: grid;
gap: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
}
.info-label {
font-weight: 500;
color: #333;
}
.info-value {
color: #666;
}

View File

@@ -0,0 +1,28 @@
{
"refreshInterval": {
"type": "number",
"name": "刷新间隔",
"description": "仪表盘数据自动刷新的间隔时间(秒)",
"default": 2,
"min": 1,
"max": 60,
"order": 1
},
"showDisk": {
"type": "boolean",
"name": "显示磁盘",
"description": "是否在仪表盘显示磁盘使用率",
"default": true,
"order": 2
},
"diskThreshold": {
"type": "number",
"name": "磁盘警告阈值",
"description": "磁盘使用率超过此值时显示警告颜色",
"default": 80,
"min": 50,
"max": 95,
"show_when": { "field": "showDisk", "value": true },
"order": 3
}
}

View File

@@ -0,0 +1,332 @@
"""Dashboard 仪表盘插件"""
import os
import time
import json
import socket
import subprocess
import platform
import psutil
from collections import deque
from oss.logger.logger import Log
from oss.plugin.types import Plugin, Response, register_plugin_type
class DashboardPlugin(Plugin):
"""仪表盘插件 - 依赖 WebUI 容器"""
def __init__(self):
self.webui = None
self.views_dir = os.path.join(os.path.dirname(__file__), 'views')
self._start_time = time.time() # 记录插件启动时间(即项目启动时间)
self._history_len = 60
self._cpu_history = deque(maxlen=self._history_len)
self._ram_history = deque(maxlen=self._history_len)
self._net_recv_history = deque(maxlen=self._history_len)
self._net_sent_history = deque(maxlen=self._history_len)
self._disk_read_history = deque(maxlen=self._history_len)
self._disk_write_history = deque(maxlen=self._history_len)
self._net_latency_history = deque(maxlen=self._history_len)
self._last_net = None
self._last_disk = None
def meta(self):
from oss.plugin.types import Metadata, PluginConfig, Manifest
return Manifest(
metadata=Metadata(
name="dashboard",
version="2.0.0",
author="FutureOSS",
description="WebUI 仪表盘"
),
config=PluginConfig(enabled=True, args={}),
dependencies=["http-api", "webui"]
)
def set_webui(self, webui):
self.webui = webui
def init(self, deps: dict = None):
if self.webui:
Log.info("dashboard", "已获取 WebUI 引用")
self.webui.register_page(
path='/dashboard',
content_provider=self._render_content,
nav_item={'icon': 'ri-dashboard-line', 'text': '仪表盘'}
)
if hasattr(self.webui, 'server') and self.webui.server:
self.webui.server.router.get("/api/dashboard/stats", self._handle_stats_api)
self.webui.server.router.get("/api/dashboard/history", self._handle_history_api)
Log.info("dashboard", "已注册到 WebUI 导航")
else:
Log.warn("dashboard", "警告: 未找到 WebUI 依赖")
def _get_uptime_str(self):
"""计算项目运行时间(从插件启动时算起)"""
elapsed = time.time() - self._start_time
days = int(elapsed // 86400)
hours = int((elapsed % 86400) // 3600)
minutes = int((elapsed % 3600) // 60)
seconds = int(elapsed % 60)
if days > 0:
return f"{days}{hours}{minutes}{seconds}"
elif hours > 0:
return f"{hours}{minutes}{seconds}"
elif minutes > 0:
return f"{minutes}{seconds}"
else:
return f"{seconds}"
def _get_network_stats(self):
try:
net = psutil.net_io_counters()
now = time.time()
if self._last_net is None:
self._last_net = (now, net.bytes_recv, net.bytes_sent)
return {'recv_rate': 0, 'sent_rate': 0, 'total_recv': net.bytes_recv, 'total_sent': net.bytes_sent}
elapsed = now - self._last_net[0]
if elapsed <= 0: elapsed = 1
recv_rate = (net.bytes_recv - self._last_net[1]) / elapsed
sent_rate = (net.bytes_sent - self._last_net[2]) / elapsed
self._last_net = (now, net.bytes_recv, net.bytes_sent)
return {'recv_rate': round(recv_rate, 1), 'sent_rate': round(sent_rate, 1), 'total_recv': net.bytes_recv, 'total_sent': net.bytes_sent}
except Exception:
return {'recv_rate': 0, 'sent_rate': 0, 'total_recv': 0, 'total_sent': 0}
def _get_disk_io_stats(self):
try:
disk_io = psutil.disk_io_counters()
if not disk_io:
return {'read_rate': 0, 'write_rate': 0}
now = time.time()
if self._last_disk is None:
self._last_disk = (now, disk_io.read_bytes, disk_io.write_bytes)
return {'read_rate': 0, 'write_rate': 0}
elapsed = now - self._last_disk[0]
if elapsed <= 0: elapsed = 1
read_rate = (disk_io.read_bytes - self._last_disk[1]) / elapsed
write_rate = (disk_io.write_bytes - self._last_disk[2]) / elapsed
self._last_disk = (now, disk_io.read_bytes, disk_io.write_bytes)
return {'read_rate': round(read_rate, 1), 'write_rate': round(write_rate, 1)}
except Exception:
return {'read_rate': 0, 'write_rate': 0}
def _get_network_latency(self) -> float:
"""测量到公共 DNS 8.8.8.8 的 TCP 连接延迟(真实网络波动)"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
start = time.time()
s.connect(('8.8.8.8', 53))
elapsed = (time.time() - start) * 1000 # 毫秒
s.close()
return round(elapsed, 1)
except Exception:
return 0.0
def _get_network_interfaces(self):
try:
interfaces = []
addrs = psutil.net_if_addrs()
stats = psutil.net_if_stats()
for name, addr_list in addrs.items():
if name == 'lo':
continue
info = {'name': name, 'ip': 'N/A', 'mac': 'N/A', 'is_up': False, 'speed': 0}
for addr in addr_list:
if addr.family == socket.AF_INET:
info['ip'] = addr.address
elif hasattr(psutil, 'AF_LINK') and addr.family == psutil.AF_LINK:
info['mac'] = addr.address
if name in stats:
info['is_up'] = stats[name].isup
info['speed'] = stats[name].speed
interfaces.append(info)
return interfaces
except Exception:
return []
def _get_load_info(self):
try:
load1, load5, load15 = os.getloadavg()
return {'load1': round(load1, 2), 'load5': round(load5, 2), 'load15': round(load15, 2)}
except (OSError, AttributeError):
return {'load1': 0, 'load5': 0, 'load15': 0}
def _handle_stats_api(self, request):
try:
cpu_percent = psutil.cpu_percent(interval=0.3)
mem = psutil.virtual_memory()
disk = psutil.disk_usage('/')
net = self._get_network_stats()
disk_io = self._get_disk_io_stats()
load = self._get_load_info()
latency = self._get_network_latency()
self._cpu_history.append(round(cpu_percent, 1))
self._ram_history.append(round(mem.percent, 1))
self._net_recv_history.append(net['recv_rate'])
self._net_sent_history.append(net['sent_rate'])
self._disk_read_history.append(disk_io['read_rate'])
self._disk_write_history.append(disk_io['write_rate'])
self._net_latency_history.append(latency)
uptime_str = self._get_uptime_str()
data = {
'cpu': {'percent': round(cpu_percent, 1), 'cores': psutil.cpu_count(logical=True)},
'ram': {'percent': round(mem.percent, 1), 'used': round(mem.used / (1024**3), 1), 'total': round(mem.total / (1024**3), 1)},
'disk': {'percent': round(disk.percent, 1), 'used': round(disk.used / (1024**3), 1), 'total': round(disk.total / (1024**3), 1)},
'network': net,
'disk_io': disk_io,
'load': load,
'latency': latency,
'processes': len(psutil.pids()),
'uptime': uptime_str
}
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
except Exception as e:
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
def _handle_history_api(self, request):
try:
data = {
'cpu': list(self._cpu_history),
'ram': list(self._ram_history),
'net_recv': list(self._net_recv_history),
'net_sent': list(self._net_sent_history),
'disk_read': list(self._disk_read_history),
'disk_write': list(self._disk_write_history),
'latency': list(self._net_latency_history)
}
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
except Exception as e:
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
def start(self):
Log.info("dashboard", "仪表盘已启动")
def stop(self):
Log.error("dashboard", "仪表盘已停止")
def _render_content(self) -> str:
try:
php_file = os.path.join(self.views_dir, 'dashboard.php')
if not os.path.exists(php_file):
return "<p>仪表盘视图文件丢失</p>"
cpu_percent = psutil.cpu_percent(interval=0.5)
cpu_cores = psutil.cpu_count(logical=True)
mem = psutil.virtual_memory()
ram_percent = round(mem.percent, 1)
ram_used_gb = round(mem.used / (1024**3), 1)
ram_total_gb = round(mem.total / (1024**3), 1)
disk = psutil.disk_usage('/')
disk_percent = round(disk.percent, 1)
disk_used_gb = round(disk.used / (1024**3), 1)
disk_total_gb = round(disk.total / (1024**3), 1)
net = self._get_network_stats()
disk_io = self._get_disk_io_stats()
load = self._get_load_info()
net_interfaces = self._get_network_interfaces()
processes = len(psutil.pids())
if disk_percent < 50:
disk_color = 'gauge-green'
elif disk_percent < 80:
disk_color = 'gauge-orange'
else:
disk_color = 'gauge-blue'
circumference = 2 * 3.14159 * 52
cpu_dash_offset = round(circumference - (cpu_percent / 100) * circumference, 1)
ram_dash_offset = round(circumference - (ram_percent / 100) * circumference, 1)
disk_dash_offset = round(circumference - (disk_percent / 100) * circumference, 1)
uptime_str = self._get_uptime_str()
def fmt_speed(bps):
if bps >= 1024 * 1024:
return f"{round(bps / (1024*1024), 1)} MB/s"
elif bps >= 1024:
return f"{round(bps / 1024, 1)} KB/s"
else:
return f"{round(bps, 0)} B/s"
variables = {
'cpuPercent': int(cpu_percent),
'cpuDashArray': str(circumference),
'cpuDashOffset': str(cpu_dash_offset),
'cpuCores': str(cpu_cores),
'ramPercent': ram_percent,
'ramDashArray': str(circumference),
'ramDashOffset': str(ram_dash_offset),
'ramUsed': f"{ram_used_gb} GB",
'ramTotal': f"{ram_total_gb} GB",
'diskPercent': disk_percent,
'diskDashArray': str(circumference),
'diskDashOffset': str(disk_dash_offset),
'diskUsed': f"{disk_used_gb} GB",
'diskTotal': f"{disk_total_gb} GB",
'diskColorClass': disk_color,
'uptime': uptime_str,
'osName': f"{platform.system()} {platform.release()}",
'pythonVersion': platform.python_version(),
'phpVersion': self._get_php_version(),
'hostname': platform.node(),
'netRecvSpeed': fmt_speed(net['recv_rate']),
'netSentSpeed': fmt_speed(net['sent_rate']),
'diskReadSpeed': fmt_speed(disk_io['read_rate']),
'diskWriteSpeed': fmt_speed(disk_io['write_rate']),
'load1': str(load['load1']),
'load5': str(load['load5']),
'load15': str(load['load15']),
'processes': str(processes),
'netInterfaces': json.dumps(net_interfaces),
}
return self._execute_php(php_file, variables)
except Exception as e:
return f"<p>仪表盘渲染出错: {e}</p>"
def _execute_php(self, php_file: str, variables: dict) -> str:
php_vars = ""
for key, value in variables.items():
if isinstance(value, str):
escaped = value.replace('\\', '\\\\').replace("'", "\\'").replace("\n", "\\n")
php_vars += f"${key} = '{escaped}';\n"
else:
php_vars += f"${key} = {value};\n"
with open(php_file, 'r', encoding='utf-8') as f:
php_content = f.read()
tmp_file = os.path.join(os.path.dirname(php_file), '.temp_dashboard.php')
try:
with open(tmp_file, 'w', encoding='utf-8') as f:
f.write(f"<?php\n{php_vars}\n?>\n{php_content}")
result = subprocess.run(
["php", "-f", tmp_file],
capture_output=True, text=True, timeout=10
)
return result.stdout if result.returncode == 0 else f"<pre>{result.stderr}</pre>"
finally:
try:
if os.path.exists(tmp_file):
os.unlink(tmp_file)
except Exception:
pass
@staticmethod
def _get_php_version() -> str:
try:
res = subprocess.run(['php', '-r', 'echo phpversion();'], capture_output=True, text=True, timeout=5)
return res.stdout if res.returncode == 0 else 'N/A'
except Exception:
return 'N/A'
register_plugin_type("DashboardPlugin", DashboardPlugin)
def New():
return DashboardPlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "dashboard",
"version": "1.0.0",
"author": "FutureOSS",
"description": "WebUI 仪表盘",
"type": "webui-extension"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": ["http-api", "webui"],
"permissions": ["*"]
}

View File

@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
.dashboard-container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.section-title { font-size: 18px; font-weight: 600; color: #00bcd4; margin-bottom: 16px; padding-left: 12px; border-left: 4px solid #3b82f6; }
.gauges-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 28px; }
.gauge-card { background: #1e293b; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; align-items: center; position: relative; }
.gauge-card .label { font-size: 14px; color: #94a3b8; margin-bottom: 8px; }
.gauge-circle { position: relative; width: 120px; height: 120px; }
.gauge-circle svg { transform: rotate(-90deg); }
.gauge-circle .bg { fill: none; stroke: #334155; stroke-width: 8; }
.gauge-circle .progress { fill: none; stroke: #3b82f6; stroke-width: 8; stroke-linecap: round; transition: stroke-dashoffset 0.8s ease; }
.gauge-circle .value { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; color: #f1f5f9; }
.gauge-circle .unit { font-size: 12px; color: #94a3b8; }
.gauge-card .detail { margin-top: 8px; font-size: 12px; color: #64748b; }
.gauge-green { stroke: #22c55e; }
.gauge-orange { stroke: #f59e0b; }
.gauge-blue { stroke: #3b82f6; }
.gauge-red { stroke: #ef4444; }
.io-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 28px; }
.io-card { background: #1e293b; border-radius: 12px; padding: 20px; }
.io-card .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.io-card .card-header i { font-size: 20px; color: #3b82f6; }
.io-card .card-header span { font-size: 15px; font-weight: 600; color: #e2e8f0; }
.io-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #334155; }
.io-row:last-child { border-bottom: none; }
.io-row .io-label { color: #94a3b8; font-size: 13px; }
.io-row .io-value { color: #f1f5f9; font-size: 14px; font-weight: 500; }
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 28px; }
.info-card { background: #1e293b; border-radius: 12px; padding: 20px; }
.info-card .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.info-card .card-header i { font-size: 20px; color: #3b82f6; }
.info-card .card-header span { font-size: 15px; font-weight: 600; color: #e2e8f0; }
.info-table { width: 100%; }
.info-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #334155; }
.info-row:last-child { border-bottom: none; }
.info-row .info-label { color: #94a3b8; font-size: 13px; }
.info-row .info-value { color: #f1f5f9; font-size: 14px; font-weight: 500; }
.net-ifaces { margin-top: 12px; }
.net-iface { background: #0f172a; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
.net-iface .iface-name { font-weight: 600; color: #e2e8f0; }
.net-iface .iface-info { font-size: 12px; color: #94a3b8; }
.net-iface .iface-status { padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.status-up { background: #064e3b; color: #34d399; }
.status-down { background: #7f1d1d; color: #f87171; }
.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 16px; margin-bottom: 28px; }
.chart-card { background: #1e293b; border-radius: 12px; padding: 20px; }
.chart-card .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.chart-card .card-header i { font-size: 20px; color: #3b82f6; }
.chart-card .card-header span { font-size: 15px; font-weight: 600; color: #e2e8f0; }
.chart-wrapper { position: relative; height: 200px; }
.live-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; display: inline-block; margin-right: 6px; animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.3); } }
</style>
</head>
<body>
<div class="dashboard-container">
<div class="section-title"><span class="live-dot"></span>实时指标</div>
<div class="gauges-grid">
<div class="gauge-card">
<div class="label">CPU 使用率</div>
<div class="gauge-circle">
<svg width="120" height="120"><circle class="bg" cx="60" cy="60" r="52" stroke-dasharray="<?= $cpuDashArray ?>"></circle><circle class="progress gauge-blue" id="cpu-gauge" cx="60" cy="60" r="52" stroke-dasharray="<?= $cpuDashArray ?>" stroke-dashoffset="<?= $cpuDashOffset ?>"></circle></svg>
<div class="value"><span id="cpu-val"><?= $cpuPercent ?></span><span class="unit">%</span></div>
</div>
<div class="detail"><?= $cpuCores ?> 核心</div>
</div>
<div class="gauge-card">
<div class="label">内存使用</div>
<div class="gauge-circle">
<svg width="120" height="120"><circle class="bg" cx="60" cy="60" r="52" stroke-dasharray="<?= $ramDashArray ?>"></circle><circle class="progress gauge-green" id="ram-gauge" cx="60" cy="60" r="52" stroke-dasharray="<?= $ramDashArray ?>" stroke-dashoffset="<?= $ramDashOffset ?>"></circle></svg>
<div class="value"><span id="ram-val"><?= $ramPercent ?></span><span class="unit">%</span></div>
</div>
<div class="detail"><?= $ramUsed ?> / <?= $ramTotal ?></div>
</div>
<div class="gauge-card">
<div class="label">磁盘使用</div>
<div class="gauge-circle">
<svg width="120" height="120"><circle class="bg" cx="60" cy="60" r="52" stroke-dasharray="<?= $diskDashArray ?>"></circle><circle class="progress <?= $diskColorClass ?>" id="disk-gauge" cx="60" cy="60" r="52" stroke-dasharray="<?= $diskDashArray ?>" stroke-dashoffset="<?= $diskDashOffset ?>"></circle></svg>
<div class="value"><span id="disk-val"><?= $diskPercent ?></span><span class="unit">%</span></div>
</div>
<div class="detail"><?= $diskUsed ?> / <?= $diskTotal ?></div>
</div>
<div class="gauge-card">
<div class="label">系统负载</div>
<div class="gauge-circle">
<svg width="120" height="120"><circle class="bg" cx="60" cy="60" r="52" stroke-dasharray="326.73"></circle><circle class="progress gauge-orange" cx="60" cy="60" r="52" stroke-dasharray="326.73" stroke-dashoffset="0"></circle></svg>
<div class="value" style="font-size:16px" id="load-val"><?= $load1 ?></div>
</div>
<div class="detail">1m / 5m / 15m: <?= $load1 ?> / <?= $load5 ?> / <?= $load15 ?></div>
</div>
</div>
<div class="section-title">网络 & 磁盘 I/O</div>
<div class="io-grid">
<div class="io-card">
<div class="card-header"><i class="ri-global-line"></i><span>网络流量</span></div>
<div class="io-row"><span class="io-label">下载速度</span><span class="io-value" id="net-recv"><?= $netRecvSpeed ?></span></div>
<div class="io-row"><span class="io-label">上传速度</span><span class="io-value" id="net-sent"><?= $netSentSpeed ?></span></div>
</div>
<div class="io-card">
<div class="card-header"><i class="ri-hard-drive-3-line"></i><span>磁盘 I/O</span></div>
<div class="io-row"><span class="io-label">读取速度</span><span class="io-value" id="disk-read"><?= $diskReadSpeed ?></span></div>
<div class="io-row"><span class="io-label">写入速度</span><span class="io-value" id="disk-write"><?= $diskWriteSpeed ?></span></div>
</div>
<div class="io-card">
<div class="card-header"><i class="ri-stack-line"></i><span>系统概况</span></div>
<div class="io-row"><span class="io-label">运行进程</span><span class="io-value" id="proc-count"><?= $processes ?></span></div>
<div class="io-row"><span class="io-label">运行时间</span><span class="io-value" id="uptime-val"><?= $uptime ?></span></div>
</div>
</div>
<div class="section-title">历史趋势</div>
<div class="chart-grid">
<div class="chart-card">
<div class="card-header"><i class="ri-cpu-line"></i><span>CPU & 内存趋势</span></div>
<div class="chart-wrapper"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-card">
<div class="card-header"><i class="ri-exchange-line"></i><span>网络吞吐</span></div>
<div class="chart-wrapper"><canvas id="chart-net"></canvas></div>
</div>
<div class="chart-card">
<div class="card-header"><i class="ri-hard-drive-3-line"></i><span>磁盘读写</span></div>
<div class="chart-wrapper"><canvas id="chart-disk-io"></canvas></div>
</div>
<div class="chart-card">
<div class="card-header"><i class="ri-pulse-line"></i><span>网络延迟</span></div>
<div class="chart-wrapper"><canvas id="chart-latency"></canvas></div>
</div>
</div>
<div class="section-title">系统信息</div>
<div class="info-grid">
<div class="info-card">
<div class="card-header"><i class="ri-settings-4-line"></i><span>系统详情</span></div>
<div class="info-table">
<div class="info-row"><span class="info-label">主机名</span><span class="info-value"><?= $hostname ?></span></div>
<div class="info-row"><span class="info-label">操作系统</span><span class="info-value"><?= $osName ?></span></div>
<div class="info-row"><span class="info-label">Python</span><span class="info-value"><?= $pythonVersion ?></span></div>
<div class="info-row"><span class="info-label">PHP</span><span class="info-value"><?= $phpVersion ?></span></div>
<div class="info-row"><span class="info-label">运行时间</span><span class="info-value" id="uptime-info"><?= $uptime ?></span></div>
</div>
</div>
<div class="info-card">
<div class="card-header"><i class="ri-router-line"></i><span>网络接口</span></div>
<div class="net-ifaces" id="net-ifaces">
<script type="application/json" id="ifaces-data"><?= htmlspecialchars($netInterfaces, ENT_QUOTES, 'UTF-8') ?></script>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
(function(){
const $ = id => document.getElementById(id);
const circumference = 2 * Math.PI * 52;
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = '#334155';
Chart.defaults.font.size = 11;
const fmtBytes = v => {
if (v >= 1048576) return (v/1048576).toFixed(1) + ' MB/s';
if (v >= 1024) return (v/1024).toFixed(1) + ' KB/s';
return Math.round(v) + ' B/s';
};
const cpuChart = new Chart($('chart-cpu'), {
type: 'line',
data: { labels: [], datasets: [
{ label: 'CPU %', data: [], borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', tension: 0.4, fill: true, pointRadius: 0 },
{ label: '内存 %', data: [], borderColor: '#22c55e', backgroundColor: 'rgba(34,197,94,0.1)', tension: 0.4, fill: true, pointRadius: 0 }
]},
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 1500, easing: 'easeInOutCubic' },
plugins: { legend: { display: true } },
scales: { x: { display: false }, y: { min: 0, max: 100, grid: { color: '#334155' } } }
}
});
const netChart = new Chart($('chart-net'), {
type: 'line',
data: { labels: [], datasets: [
{ label: '下载', data: [], borderColor: '#06b6d4', tension: 0.4, fill: false, pointRadius: 0 },
{ label: '上传', data: [], borderColor: '#f59e0b', tension: 0.4, fill: false, pointRadius: 0 }
]},
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 1500, easing: 'easeInOutCubic' },
plugins: { legend: { display: true }, tooltip: { callbacks: { label: ctx => ctx.dataset.label + ': ' + fmtBytes(ctx.raw) } } },
scales: { x: { display: false }, y: { grid: { color: '#334155' }, ticks: { callback: v => fmtBytes(v) } } }
}
});
const diskIoChart = new Chart($('chart-disk-io'), {
type: 'line',
data: { labels: [], datasets: [
{ label: '读取', data: [], borderColor: '#8b5cf6', tension: 0.4, fill: false, pointRadius: 0 },
{ label: '写入', data: [], borderColor: '#ec4899', tension: 0.4, fill: false, pointRadius: 0 }
]},
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 1500, easing: 'easeInOutCubic' },
plugins: { legend: { display: true }, tooltip: { callbacks: { label: ctx => ctx.dataset.label + ': ' + fmtBytes(ctx.raw) } } },
scales: { x: { display: false }, y: { grid: { color: '#334155' }, ticks: { callback: v => fmtBytes(v) } } }
}
});
const latencyChart = new Chart($('chart-latency'), {
type: 'line',
data: { labels: [], datasets: [
{ label: '延迟 ms', data: [], borderColor: '#f43f5e', backgroundColor: 'rgba(244,63,94,0.1)', tension: 0.4, fill: true, pointRadius: 0 }
]},
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 1500, easing: 'easeInOutCubic' },
plugins: { legend: { display: true } },
scales: { x: { display: false }, y: { grid: { color: '#334155' }, beginAtZero: true } }
}
});
// 加载历史
const MAX_POINTS = 10;
// 初始化空图表
[cpuChart, netChart, diskIoChart, latencyChart].forEach(c => {
for (let i = 0; i < MAX_POINTS; i++) c.data.labels.push('');
});
fetch('/api/dashboard/history').then(r => r.json()).then(hist => {
const data = {
cpu: hist.cpu, ram: hist.ram,
net_recv: hist.net_recv, net_sent: hist.net_sent,
disk_read: hist.disk_read, disk_write: hist.disk_write,
latency: hist.latency || []
};
const start = Math.max(0, data.cpu.length - MAX_POINTS);
const slice = data.cpu.slice(start);
cpuChart.data.datasets[0].data = data.cpu.slice(start);
cpuChart.data.datasets[1].data = data.ram.slice(start);
netChart.data.datasets[0].data = data.net_recv.slice(start);
netChart.data.datasets[1].data = data.net_sent.slice(start);
diskIoChart.data.datasets[0].data = data.disk_read.slice(start);
diskIoChart.data.datasets[1].data = data.disk_write.slice(start);
latencyChart.data.datasets[0].data = data.latency.slice(start);
// 不足10个补默认值
const pad = (chart, vals) => {
const diff = MAX_POINTS - chart.data.datasets[0].data.length;
for (let i = 0; i < diff; i++) {
chart.data.datasets[0].data.push(vals[0]);
if (chart.data.datasets[1]) chart.data.datasets[1].data.push(vals[1] ?? vals[0]);
}
};
pad(cpuChart, [50, 50]);
pad(netChart, [0, 0]);
pad(diskIoChart, [0, 0]);
pad(latencyChart, [0]);
cpuChart.update(); netChart.update(); diskIoChart.update(); latencyChart.update();
}).catch(() => {
// 加载失败也补默认
[cpuChart, netChart, diskIoChart, latencyChart].forEach(c => {
c.data.datasets.forEach(ds => {
while (ds.data.length < MAX_POINTS) ds.data.push(0);
});
c.update();
});
});
// 渲染网络接口
try {
const el = $('ifaces-data');
if (el) {
const ifaces = JSON.parse(el.textContent);
const container = $('net-ifaces');
if (ifaces.length === 0) {
container.innerHTML = '<div class="net-iface"><div class="iface-info">暂无网络接口</div></div>';
} else {
let html = '';
ifaces.forEach(iface => {
html += `<div class="net-iface"><div><div class="iface-name">${iface.name}</div><div class="iface-info">${iface.ip}</div></div><span class="iface-status ${iface.is_up ? 'status-up' : 'status-down'}">${iface.is_up ? 'UP' : 'DOWN'}</span></div>`;
});
container.innerHTML = html;
}
}
} catch(e) {}
// 定时刷新
setInterval(() => {
fetch('/api/dashboard/stats').then(r => r.json()).then(d => {
const setGauge = (id, pct) => {
const el = $(id);
if (el) el.setAttribute('stroke-dashoffset', circumference - (pct/100)*circumference);
};
setGauge('cpu-gauge', d.cpu.percent);
setGauge('ram-gauge', d.ram.percent);
setGauge('disk-gauge', d.disk.percent);
$('cpu-val').textContent = d.cpu.percent;
$('ram-val').textContent = d.ram.percent;
$('disk-val').textContent = d.disk.percent;
$('load-val').textContent = d.load.load1;
$('net-recv').textContent = fmtBytes(d.network.recv_rate);
$('net-sent').textContent = fmtBytes(d.network.sent_rate);
$('disk-read').textContent = fmtBytes(d.disk_io.read_rate);
$('disk-write').textContent = fmtBytes(d.disk_io.write_rate);
$('proc-count').textContent = d.processes;
$('uptime-val').textContent = d.uptime;
$('uptime-info').textContent = d.uptime;
// 刷新固定10个点数据向左平滑滚动
const pushChart = (chart, v1, v2) => {
// 移除最左边旧数据
chart.data.datasets[0].data.shift();
// 新数据从右边加入
chart.data.datasets[0].data.push(v1);
if (chart.data.datasets[1]) {
chart.data.datasets[1].data.shift();
chart.data.datasets[1].data.push(v2);
}
// 触发 Chart.js 内置过渡动画
chart.update('default');
};
pushChart(cpuChart, d.cpu.percent, d.ram.percent);
pushChart(netChart, d.network.recv_rate, d.network.sent_rate);
pushChart(diskIoChart, d.disk_io.read_rate, d.disk_io.write_rate);
// 网络延迟图
latencyChart.data.datasets[0].data.shift();
latencyChart.data.datasets[0].data.push(d.latency || 0);
latencyChart.update('default');
}).catch(() => {});
}, 2000);
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{
"signature": "JQaw//g6588907vGYH6SyqeXj9qHU5Azb7S/bjYm7rUrVsHqqIsIOEPB7IVsdf/wCnCdCa0LzTrEjmS6lKlEwXVjCCebhzyi64OJIXVOVckd2TJbREH0ZizO4KcEWgOqu56Ln3g8yMPHw5GylLABD5UN0q4F48PwUhram+cECu0SOY/bAHxYwi+nzJ0TcuES/J5cK480xv+NvxnylBhx1Udkkoiz9Y7b3pgglx+h57BuPEeHpJFbXQkXtty5Cf3sXzib0FEhicyIW1u5wmYSLz5yyLd/Pefavjfs6JrDG9J8gfPuestQzazQGsIMiQTy13DL8IDGAZ7AP2/mFQYrXuYLaBTxyhhMAkpfjIANzy+2pobeTZz2Cu4Sr6XMzXS4BkeCRDcHHBnttWVpp1+t5HpRgp3W8eiPcCzmUq6jo1cbd5zWGiR1gDEHePivmJaUi/bxlN0vyc7LjW7T+HuLUYhdSktbxv5BexMwcA7+2UHJzEnTVIc+xqoIT+ApPqqF2hLJFiAUdEJe8FRc/Bwihzh8tfM0xgYoqn8RQQ3eWVwVrK9vx0OZ8INumNZOyKPz8ZlGf3XAJv9UGUQ6Y42raYcDOFrgT+MS82tjAxf2nonm0/c3dhgNFZSy5Cfbvuqd9SYaxXejIcVni3MarVHZX3iKytOdv83cBtwPXRcfloc=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775969851.9656692,
"plugin_hash": "aebef3fd9252245553bc458e4652b094839a5e64bde7cec13435ba1930a8dc0d",
"author": "FutureOSS"
}

View File

@@ -0,0 +1,8 @@
{
"signature": "vBf0JPwb5GjyM9vyp4AuncQKp092RpA07RZh+guhF51OKlVI5PphQEEvtMSy2uBsQ0V0RohRid/gazvB5l02DTuyqt2NcjFyPIZj2wm1gfWtJZWBK+Hp11gIPq13qhxDjdi1bs7H+tTOhVHJHkcoU1TsZuUPU+UYOuONbQhdwB+eqEMbNzVrPBPxb12W1SxRBAo/58q+eGI1QvbTv0FBu4fw10vyySGzd51t0psrBqw9xovKSq47AV96ZJeFEJvbfBTfJTg26VOX0cxLS5dmel9+yMhmidJNvOoL3mlZG2C92Xe9hdZAFxaRhMV3QgNKx3s6C+TQRBNx3ttUtBAzxVcXsGhCE0C+CfvbIpuyGHfgarSPJoiIPyp02numgMztFzAdFc66stULEpB3rHBlosUbDNmeuIMNcbCdKlH6R94xuYMg8E699DO67AGxZwZcaUN/vYmAa2DiffVUFcCFXgzABPzctJTYqTaD51KGlMSMHTeMTN3XCWJ79nkxHvt0Lgb0kWljOhcVaGW2t4JUgfupUD1DIwiZ7AlEC3K3JijsqWS633+Saa/+tOI4/V5VzVtExJt46cM/BSETYlHQtA8eDDl6BhbjtnmMaHSjGF75sgiagtj0DYsOvzKLJUVMT4nFjidzb2sR5lN3/S3ZSmBTUYA5/fDgiMnSfZaK4HQ=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775964953.0432403,
"plugin_hash": "3b226c4e5278ade1ec0997abfd553d4c07724b8e9f69f79acb57e20e0d352817",
"author": "FutureOSS"
}

View File

@@ -5,6 +5,7 @@ import threading
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Callable from typing import Any, Optional, Callable
from oss.logger.logger import Log
from oss.plugin.types import Plugin, register_plugin_type from oss.plugin.types import Plugin, register_plugin_type
@@ -138,7 +139,7 @@ class HotReloadPlugin(Plugin):
elif change_type == "deleted": elif change_type == "deleted":
self.unload_plugin(plugin_name) self.unload_plugin(plugin_name)
except Exception as e: except Exception as e:
print(f"[hot-reload] 处理变化失败: {e}") Log.error("hot-reload", f"处理变化失败: {e}")
def load_plugin(self, plugin_dir: Path) -> bool: def load_plugin(self, plugin_dir: Path) -> bool:
"""运行时加载插件""" """运行时加载插件"""

View File

@@ -0,0 +1,8 @@
{
"signature": "0WK7Njn0KAUP+jfg/uuJxwW0/tWCF+WieK0N0T2crWbvutKQmEOtaNDHnjT6qFz1dcI4+ba3julE4fFi3W3xFiToMEP2VcPXe0WNQ9/kvKNTKSDbwadiBssf43TO1G9E1BxNMxVM91mN8iqybuy+VMdU0Esv2rJ5dcwwwsnT9NWot2RQLez75PRhmMtJpEWRUmrZn2r+u5QnQdjxucONq9Nhwxw0eheTxMCu8IDvIiO6QIWP5ErA/wUz+Hg6IoEZwcVif/lSN2EMqNGqPNR/nIWWVXo9CXWB9qMZZApgEnAZfKYGCAkLzSTwqG64T4iJh4deGxafyMhsONckqRaG82NRTLuzHMReP5+VAichuEGbHI7nxXFOFG7q1mgQQLmHm3LB577usAgCNCh5X3i8SMAj7Sutykxhj0ZyTqMnOfpwnzE2tsNisJF0/8Kw22k7dZChV1obOeLWXjy5InLjdm4hIWTp7wMPjSNWRMZGR+1aZHi9XA1GKd965/30jmo876EXX23xoTAN4ZRhZNlcQg710LhycNohggnQ7qzB9LsV3Ckgh7aY/V/hzND6bpRADCGu62sZtBye2P1yaaAorC8+hRaiJoXlV9Yukg+3yhfKC+qTbn307fI53kgcw1KMSeGGctfTYJUOfK8u0mYsGi50bnM+2Tz45YJiwwdOJJk=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775960645.890869,
"plugin_hash": "ca13c933ffa2c5dd8874e3ad6f7b8dda5dd9a5f9c24be6aeb47228d65097a280",
"author": "FutureOSS"
}

View File

@@ -23,9 +23,12 @@ class CorsMiddleware(Middleware):
class LoggerMiddleware(Middleware): class LoggerMiddleware(Middleware):
"""日志中间件""" """日志中间件"""
# 静默的路由(不打印日志)
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]: def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
req = ctx.get("request") req = ctx.get("request")
if req: if req and req.path not in self._silent_paths:
print(f"[http-api] {req.method} {req.path}") print(f"[http-api] {req.method} {req.path}")
return None return None

View File

@@ -95,14 +95,17 @@ class HttpServer:
self._send_response(resp) self._send_response(resp)
def _send_response(self, resp: Response): def _send_response(self, resp: Response):
self.send_response(resp.status) try:
for k, v in resp.headers.items(): self.send_response(resp.status)
self.send_header(k, v) for k, v in resp.headers.items():
self.end_headers() self.send_header(k, v)
if isinstance(resp.body, str): self.end_headers()
self.wfile.write(resp.body.encode("utf-8")) if isinstance(resp.body, str):
else: self.wfile.write(resp.body.encode("utf-8"))
self.wfile.write(resp.body) else:
self.wfile.write(resp.body)
except BrokenPipeError:
pass # 忽略客户端断开
def log_message(self, format, *args): def log_message(self, format, *args):
pass pass

View File

@@ -0,0 +1,8 @@
{
"signature": "Adt4Pa7dzXVC9LuotOb2hvUREP2sQyInReCfPRVnKLuD2IB+5Uk4BSCjt5EkUUcMiEwIYoefntc1Q0f4k/OL3F4WtKFrwb4G+WJZYuwSbYZ3l4wYtivMFTuP4PjIgz1/sWUfqHdd+jwOquM9a8+uiNaxiz+Ed9UmBCqiJXjbfiP5A5RlkUGO3evwuP51dhfo3BVU+YuVWzSWfVw8Ov9Wx1V0h7fEjPPYof1d9AP+yVnfLLfBeNL1T/VlpkogllRlcqOQm5w+s17sLhR6sQEBHHTsga7Nilh8/BMmXr3vFDrtPbPsOqVGzHvYOFFJf26geFgxowPJ5YxEL9FKp9NtOp0fsDsq6f74mES9nTg7v9uImL8zzYn774fpaIfbOL2CVqsCqzW+kYhNm7fsJD8SfmhwKR8tVEsYvqUiHqpzUwX/J7soD0jlN/ttUUCZREERRKIpumHNNxkcgLuTYsloeSrG935ZOSEt6QuWSg9+dlXgdi84UmE1TbU6Q6HKExopOJitYCUM1p21G5wcFgEn+o7zdkDUdCJEliG1QeqSHdhlo/QyLuH/7mZQOMdprHabggTUrmbrES78nT10XEFWjtUfKxuzQkWwozwYPx6cBdmO4OLYJ+C5u1hwgmVm6if6IbCPm0l/NGy8NUNjH0PxDdmPaUSdnvSLLwa6fwr5/h0=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775960645.9258935,
"plugin_hash": "136d916944b4b1e37134b3b9807a8ea19fc9c4971c62d15cc11e019502de5617",
"author": "FutureOSS"
}

View File

@@ -0,0 +1,8 @@
{
"signature": "N8pwPuJxnjP/hgMG4QLYQy7Z6e1P1KctYLJYoQniALDFT1qb11RDm1w4KUbzNIY82XM56B10zYF88dTQiGMrtbgoExE0gtUvmF3THvEd+aWhQ0m5/2war2w+j02BWH0TvJqxhb5nHCyhA4CknJANWp4wZr9EPjDseb+OhXC3GECKpChVrmM9/DWM6TtjlmGol14kq+jUnrS5EWNSa1hlsLzKIrS3Jf5fLaButDUr6YuQkATRKl6F41M8+JHJwVVw5D1fRSqCZ4xFWwN90Gtdd22JFSeB9iVE2Myb3UurPzTVvJ0B/JE9yxFDhA1B7PtuF/WeWlm060QRWdlwFfO9NjUJOeOGQstn34DUG2xL/q3yF66SjnHcHs67DqVq9lCQ961jQq0QveKunV4u8uBJd4IGH4MTq5W7Be8GDgSZcll5HLG3HBL+9XYf4mJzc7dh88Y0UV+dOabD2SJCwBmMxgzDx+Dx8RwWx7b9IYZvmXz6fxtXhqfV6AFq2oY/+4Xjwn4nq7VOCgx8PxLrUvmuacmCwlar/rXuvHT0YsN/XXmJK9o/3NYsNp/go8Vm0XW0btJ+FnQw4O4OKPvSSd+Ip+tk2rLi7CuZGi0WEVp2o23gUNLXoHkKFrtms02Et6zC9AFwP2gLF+NnaMWImup54owxgDos9s6l2ejTD653rYE=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775964953.002281,
"plugin_hash": "55f90852ff6fbd82bc5a51ea4ebc2725f1316a7a5f9d423ee10a7e571aad339a",
"author": "FutureOSS"
}

View File

@@ -1,6 +1,7 @@
"""i18n 国际化多语言支持插件""" """i18n 国际化多语言支持插件"""
import json import json
from pathlib import Path from pathlib import Path
from oss.logger.logger import Log
from oss.plugin.types import Plugin, register_plugin_type from oss.plugin.types import Plugin, register_plugin_type
from .i18n import I18nEngine from .i18n import I18nEngine
from .middleware import I18nMiddleware from .middleware import I18nMiddleware
@@ -66,8 +67,8 @@ class I18nPlugin(Plugin):
# 初始化中间件 # 初始化中间件
self.middleware_handler = I18nMiddleware(self.engine, config) self.middleware_handler = I18nMiddleware(self.engine, config)
print(f"[i18n] 已加载语言: {', '.join(supported_locales)}") Log.info("i18n", f"已加载语言: {', '.join(supported_locales)}")
print(f"[i18n] 默认语言: {default_locale}") Log.info("i18n", f"默认语言: {default_locale}")
def start(self): def start(self):
"""启动插件 """启动插件
@@ -83,11 +84,11 @@ class I18nPlugin(Plugin):
http_api.router.get("/api/i18n/locales", self._locales_handler) http_api.router.get("/api/i18n/locales", self._locales_handler)
http_api.router.get("/api/i18n/translate", self._translate_handler) http_api.router.get("/api/i18n/translate", self._translate_handler)
http_api.router.post("/api/i18n/locale", self._change_locale_handler) http_api.router.post("/api/i18n/locale", self._change_locale_handler)
print("[i18n] API 路由已注册") Log.info("i18n", "API 路由已注册")
def stop(self): def stop(self):
"""停止插件""" """停止插件"""
print("[i18n] 插件已停止") Log.error("i18n", "插件已停止")
def health(self) -> bool: def health(self) -> bool:
"""健康检查""" """健康检查"""

View File

@@ -0,0 +1,8 @@
{
"signature": "IQ8WAvKno6pRp71kIaxXPb7DzTajPeNOQ0FLZMVovufeyTRMbdSJ8z2zQPBPv9O2a1S9bucyZyhg54fNB2DdLfEnrAbmpepZ3CLrj3cn4KaLNGJjxGHYXWIsFXFvLaYIod/ZuFMYPlzDdwnHJwzHZnkGAmCLrJSR+XvuOqYu/xSZekD/nbMI0fj9VKjaH/S/vopEhq7IFioahVkiSokdYx5qkXYruOVAq3wCnk6O0uCNMfHiIaRhn5pEoQ+VOXcuKX5eOBEph8oXqb+ew1MB917Z1CpaLFuZTyp2Dy8OOmpXjBxfd5VYazH4ZvE9Q7VODHkRDVF2ApkPxTE1k490YvmNOHRamjcf1/mKyu7Myaemtz9oxvZFFiOMOaXBXGfe1wlnsbO832lURTpPu9WXQ6aoDEVp3TNuR/G/xYOXHcWhG1M4tIWW+1ZFcozkVw9cMYvwrVI9JEa89sueXQhJG9foW4nj0DJqmtXaXvcVHnpbFkIxcKFZ0rOMelJ7404XuDb07/sjliJuqCG9Gssmv7/DqNgIrcWUPg24U4UPWW2vWJaJq7HOrGrxFoOxpCT/G4A0WcAWVJrM5NojnfvBNswybSB2IIbspmPRDVtoHQ5a3YJqSLZdgugHh+MbGKlyDvPkQTkPLLE8nrP2F0LwWCq0cYeodE+zU0rZ6CHgAsc=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775964952.836965,
"plugin_hash": "a7f7a20614a2e159e393a95c99b15a0a028724694bda3d089787cb41eceba7c4",
"author": "FutureOSS"
}

View File

@@ -3,6 +3,7 @@ import json
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from datetime import datetime from datetime import datetime
from oss.logger.logger import Log
from oss.plugin.types import Plugin, register_plugin_type from oss.plugin.types import Plugin, register_plugin_type
@@ -127,7 +128,7 @@ class JsonCodecPlugin(Plugin):
def start(self): def start(self):
"""启动""" """启动"""
print("[json-codec] JSON 编解码器已启动") Log.info("json-codec", "JSON 编解码器已启动")
def stop(self): def stop(self):
"""停止""" """停止"""

View File

@@ -0,0 +1,8 @@
{
"signature": "nfM9Sj7VvV+L85zCvVcmIQY4qZ9FDdsk8MZf0LrO/ys1o6FCQ96Ixt1aB+2j6crOvXUBavnSRPk/LNaDs9r3eh49+Zfy5rEK+M0UyGjcawvEY4e/lO20UWy4iLw3JdSBo9nnFQC9eE8D6C9F2oM7YcqmT/sH0wYuyjCsa8tk6P/jy5/IdCwR6bo6AIQSpCnvyNcS9JPU19f603f0nl/siafXVozQxMS3wCLQ5EAoDz7atLevvQK7xAZCIIcCsre/sHTZ3a6O+BFlYYQ5w/giWlrl4aF7W7JJntOwpain39B0ktDRV96msbW744a1BFkcUw91W/2sRU7T9xplARjmhlRPGkdMTlj4PGyy394oaLwhx+uusx28C9+gWxp7pQZNo08LQ6dKmzog4fpUFD3EEyZBtPY2XYsILqKnGQVn3TLAaMmdoHdwoR6moLtR6BfD3ToRFV6vcNRTig8hTiS9GTzZeQtEtVkoSeAZphzxWfB7FunimDRpPxndDmvhervPUJ/uAVLcdorbDFB0RfvR3znUZrQkaw5YQZjP8mhUNyA6avyOBvGdt1i0bhZsc6CUMN4BrC+vOULiykyVGnk3B07XrMHNB8AGuqR8Ai/2DFglomfs/l07mz01HeUotRg3MezqF8aSkofpPTpRieeD9IeQgH03sOGdvXHDgDJB3Xc=",
"signer": "FutureOSS",
"algorithm": "RSA-SHA256",
"timestamp": 1775960646.0212853,
"plugin_hash": "a7d6c6e01a8dc5df868e34777233e33d984d01adedb8adcee24d6892600928a8",
"author": "FutureOSS"
}

Some files were not shown because too many files have changed in this diff Show More