新增简易的8080面板😊
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -22,3 +22,9 @@ mizukiblog-master.zip
|
||||
# 日志
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 签名验证 - 私钥(绝不要提交!)
|
||||
data/signature-verifier/keys/private/
|
||||
|
||||
# 签名文件(可选,本地开发可能不需要)
|
||||
# store/**/SIGNATURE
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
"""日志记录器(空壳)"""
|
||||
"""日志记录器(兼容旧接口)"""
|
||||
|
||||
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):
|
||||
print(f"[WARN] {msg}")
|
||||
tag = kwargs.get("tag", "WARN")
|
||||
Log.warn(tag, msg)
|
||||
|
||||
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):
|
||||
print(f"[DEBUG] {msg}")
|
||||
tag = kwargs.get("tag", "DEBUG")
|
||||
Log.tip(tag, msg)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
oss/shared/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
oss/shared/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
click>=8.0
|
||||
pyyaml>=6.0
|
||||
websockets>=12.0
|
||||
psutil>=5.9.0
|
||||
cryptography>=41.0
|
||||
|
||||
259
start.bat
259
start.bat
@@ -3,144 +3,215 @@ chcp 65001 >nul 2>&1
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: FutureOSS 启动脚本 — Windows
|
||||
:: 自动检测 Python / 依赖 / 守护 / 崩溃重启
|
||||
:: FutureOSS 智能启动脚本 - Windows
|
||||
:: 自动检测环境 / 安装依赖 / 进度显示 / 守护重启
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
|
||||
set "RED=[31m"
|
||||
set "GREEN=[32m"
|
||||
set "YELLOW=[33m"
|
||||
set "CYAN=[36m"
|
||||
set "WHITE=[37m"
|
||||
set "BOLD=[1m"
|
||||
set "NC=[0m"
|
||||
|
||||
call :color_echo "BOLD" "CYAN" ""
|
||||
echo ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗
|
||||
echo ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝
|
||||
echo █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗
|
||||
echo ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║
|
||||
echo ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝
|
||||
echo ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
|
||||
call :color_echo "BOLD" "WHITE" "" 一切皆为插件 · 零编译热插拔
|
||||
call :color_echo "" "WHITE" "" https://gitee.com/starlight-apk/feature-oss
|
||||
echo.
|
||||
|
||||
:: ── 目录 ──
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: ── 颜色代码 ──
|
||||
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
|
||||
set "DEL=%%a"
|
||||
)
|
||||
|
||||
:: ── 工具函数 ──
|
||||
call :colorEcho 0B "[信息] 环境检测中..."
|
||||
call :colorEcho 0A "[成功] 检测完成"
|
||||
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.
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 1. 检测 Python
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
call :colorEcho 0B "[信息] 检测 Python..."
|
||||
|
||||
set "PYTHON_CMD="
|
||||
set "PIP_CMD="
|
||||
for %%p in (python python3 py py3) do (
|
||||
where %%p >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
set "PYTHON_CMD=%%p"
|
||||
goto :found_python
|
||||
)
|
||||
)
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 1. 检查 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.
|
||||
: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"
|
||||
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" (
|
||||
call :color_echo "" "CYAN" "" [i] 创建虚拟环境...
|
||||
%PYTHON_CMD% -m venv .venv
|
||||
call :colorEcho 0E "[信息] 创建虚拟环境..."
|
||||
%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"
|
||||
set "VENV_PIP=.venv\Scripts\pip.exe"
|
||||
call .venv\Scripts\activate.bat >nul 2>&1
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 3. 安装依赖
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
echo.
|
||||
call :colorEcho 0B "[信息] 安装 Python 依赖..."
|
||||
|
||||
set "DEPS=click pyyaml websockets psutil cryptography"
|
||||
set "TOTAL=5"
|
||||
set "CURRENT=0"
|
||||
|
||||
for %%d in (%DEPS%) do (
|
||||
set /a CURRENT+=1
|
||||
call :printProgress !CURRENT! !TOTAL! "安装 %%d"
|
||||
|
||||
%PYTHON_CMD% -c "import %%d" 2>nul
|
||||
if errorlevel 1 (
|
||||
pip install %%d -q 2>nul
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo.
|
||||
call :colorEcho 0A "[成功] Python 依赖安装完成"
|
||||
|
||||
:: 安装项目依赖
|
||||
if exist "pyproject.toml" (
|
||||
call :color_echo "" "CYAN" "" [i] 安装项目依赖...
|
||||
%VENV_PIP% install -e . -q 2>nul
|
||||
call :colorEcho 0E "[信息] 安装项目配置依赖..."
|
||||
pip install -e . -q 2>nul
|
||||
)
|
||||
|
||||
if exist "requirements.txt" (
|
||||
call :color_echo "" "CYAN" "" [i] 安装 requirements.txt...
|
||||
%VENV_PIP% install -r requirements.txt -q 2>nul
|
||||
call :colorEcho 0E "[信息] 安装 requirements.txt..."
|
||||
pip install -r requirements.txt -q 2>nul
|
||||
)
|
||||
|
||||
:: 核心依赖兜底
|
||||
for %%p in (click pyyaml websockets) do (
|
||||
%VENV_PYTHON% -c "import %%p" 2>nul || (
|
||||
call :color_echo "" "CYAN" "" [i] 安装 %%p ...
|
||||
%VENV_PIP% install %%p -q 2>nul
|
||||
)
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 4. 检查 PHP
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
echo.
|
||||
call :colorEcho 0B "[信息] 检查 PHP..."
|
||||
|
||||
where php >nul 2>&1
|
||||
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!"
|
||||
)
|
||||
|
||||
call :color_echo "" "GREEN" "" [✓] 依赖就绪
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 5. 创建数据目录
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
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 "[成功] 数据目录已就绪"
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 3. 确保 data 目录
|
||||
:: 6. 启动服务
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
if not exist "data\html-render" mkdir "data\html-render"
|
||||
if not exist "data\web-toolkit" mkdir "data\web-toolkit"
|
||||
if not exist "data\plugin-storage" mkdir "data\plugin-storage"
|
||||
if not exist "data\DCIM" mkdir "data\DCIM"
|
||||
if not exist "data\pkg" mkdir "data\pkg"
|
||||
echo.
|
||||
call :colorEcho 0B "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
call :colorEcho 0B " 启动 FutureOSS"
|
||||
call :colorEcho 0B "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo.
|
||||
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
:: 4. 启动
|
||||
:: ═══════════════════════════════════════════════════════════
|
||||
call :section "启动 FutureOSS"
|
||||
if "%1"=="--daemon" goto :daemon_mode
|
||||
if "%1"=="-d" goto :daemon_mode
|
||||
|
||||
:: 前台模式
|
||||
call :colorEcho 0F "运行中... 按 Ctrl+C 停止"
|
||||
echo.
|
||||
|
||||
set "RESTART_DELAY=3"
|
||||
set "RESTART_COUNT=0"
|
||||
|
||||
:LOOP
|
||||
echo.
|
||||
call :color_echo "" "CYAN" "" [i] 启动服务...
|
||||
echo.
|
||||
%VENV_PYTHON% -m oss.cli serve
|
||||
set "EXIT_CODE=!ERRORLEVEL!"
|
||||
:loop
|
||||
%PYTHON_CMD% -m oss.cli serve
|
||||
set "EXIT_CODE=%errorlevel%"
|
||||
|
||||
if !EXIT_CODE! equ 0 (
|
||||
echo.
|
||||
call :color_echo "" "GREEN" "" [✓] 服务正常退出
|
||||
goto :END
|
||||
if %EXIT_CODE% equ 0 (
|
||||
call :colorEcho 0A "[成功] 服务正常退出"
|
||||
goto :end
|
||||
)
|
||||
|
||||
set /a RESTART_COUNT+=1
|
||||
echo.
|
||||
call :color_echo "" "YELLOW" "" [!] 服务异常退出 (code: !EXIT_CODE!),!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次)
|
||||
call :colorEcho 0E "[警告] 服务异常退出 (code: %EXIT_CODE%),!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次)"
|
||||
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.
|
||||
%PYTHON_CMD% -m oss.cli serve
|
||||
goto :end
|
||||
|
||||
:end
|
||||
call .venv\Scripts\deactivate.bat >nul 2>&1
|
||||
pause
|
||||
exit /b 0
|
||||
|
||||
:: ── 辅助函数 ──
|
||||
:color_echo
|
||||
if "%~1" neq "" set /p "=^<ESC>%BOLD%%~2%<ESC>%NC%" <nul
|
||||
echo.
|
||||
goto :eof
|
||||
:: ── 进度条函数 ──
|
||||
:printProgress
|
||||
set /a "pct=%1 * 100 / %2"
|
||||
set /a "filled=pct / 2"
|
||||
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.
|
||||
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════"
|
||||
call :color_echo "BOLD" "WHITE" " %~1"
|
||||
call :color_echo "BOLD" "WHITE" "══════════════════════════════════════"
|
||||
goto :eof
|
||||
:: ── 颜色输出函数 ──
|
||||
:colorEcho
|
||||
set "params=%1"
|
||||
set "msg=%~2"
|
||||
call :colorText %params% "%msg%"
|
||||
exit /b 0
|
||||
|
||||
:colorText
|
||||
echo %~2
|
||||
exit /b 0
|
||||
318
start.sh
318
start.sh
@@ -1,101 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FutureOSS 启动脚本 — Linux / macOS
|
||||
# 自动检测 Python / 依赖 / 守护 / 崩溃重启
|
||||
# FutureOSS 智能启动脚本 - Linux
|
||||
# 自动检测环境 / 安装依赖 / 进度显示 / 守护重启
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
set -e
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# ── 颜色 ──
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; WHITE='\033[1;37m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
RED='\033[0;31m'
|
||||
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}"; }
|
||||
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
|
||||
err() { echo -e "${RED}✗ $1${NC}"; }
|
||||
title() { echo -e "\n${BOLD}$1${NC}"; }
|
||||
# 进度条
|
||||
progress_bar() {
|
||||
local current=$1
|
||||
local total=$2
|
||||
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
|
||||
if [[ "$1" == "--daemon" || "$1" == "-d" ]]; then
|
||||
DAEMON=true
|
||||
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() {
|
||||
for cmd in python3 python python3.12 python3.11 python3.10; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
echo "$cmd"
|
||||
return
|
||||
PKG_MANAGER=""
|
||||
OS_NAME=""
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
OS_NAME=$(. /etc/os-release && echo "$PRETTY_NAME")
|
||||
info "操作系统: $OS_NAME"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
|
||||
if command -v apt-get &>/dev/null; then
|
||||
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
|
||||
}
|
||||
|
||||
PYTHON_CMD=$(find_python || true)
|
||||
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
|
||||
}
|
||||
|
||||
if [[ -z "$PYTHON_CMD" ]]; then
|
||||
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
|
||||
TOTAL_STEPS=6
|
||||
CURRENT_STEP=0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 2. 安装系统依赖
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
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
|
||||
err "无法自动安装 Python,请手动安装 Python 3.10+"
|
||||
warn "PHP 未安装,WebUI 可能无法正常工作"
|
||||
fi
|
||||
|
||||
if command -v python3 &>/dev/null; then
|
||||
ok "Python: $(python3 --version 2>&1)"
|
||||
else
|
||||
err "Python3 未安装,无法继续"
|
||||
exit 1
|
||||
fi
|
||||
PYTHON_CMD=$(find_python || true)
|
||||
[[ -z "$PYTHON_CMD" ]] && { err "Python 安装失败"; exit 1; }
|
||||
fi
|
||||
|
||||
PY_VER=$($PYTHON_CMD --version 2>&1)
|
||||
ok "Python: $PY_VER ($PYTHON_CMD)"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 2. 虚拟环境 & 依赖
|
||||
# 3. Python 虚拟环境
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
title "📚 依赖安装"
|
||||
step "配置 Python 环境"
|
||||
|
||||
PYTHON_CMD="python3"
|
||||
VENV_DIR=".venv"
|
||||
|
||||
if [[ ! -d "$VENV_DIR" ]]; then
|
||||
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
|
||||
|
||||
source "$VENV_DIR/bin/activate"
|
||||
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
|
||||
info "安装项目依赖 (pyproject.toml)..."
|
||||
$PIP_CMD install -e . -q 2>/dev/null || $PIP_CMD install -e . --break-system-packages -q 2>/dev/null || true
|
||||
info "安装项目配置依赖..."
|
||||
$PIP_CMD install -e . -q 2>/dev/null || true
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
# 核心依赖兜底
|
||||
for pkg in click pyyaml websockets; do
|
||||
$PYTHON_CMD -c "import $pkg" 2>/dev/null || {
|
||||
info "安装 $pkg ..."
|
||||
$PIP_CMD install "$pkg" -q 2>/dev/null || $PIP_CMD install "$pkg" --break-system-packages -q 2>/dev/null || true
|
||||
}
|
||||
ok "Python 依赖安装完成"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 5. 创建数据目录
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
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
|
||||
|
||||
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
|
||||
title "🔒 守护模式"
|
||||
LOG_FILE="logs/futureoss.log"
|
||||
mkdir -p logs
|
||||
PID_FILE="logs/futureoss.pid"
|
||||
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
OLD_PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$OLD_PID" 2>/dev/null; then
|
||||
warn "已有进程运行 (PID: $OLD_PID),正在停止..."
|
||||
kill "$OLD_PID" 2>/dev/null || true
|
||||
sleep 2
|
||||
warn "已有进程运行 (PID: $OLD_PID)"
|
||||
info "停止: kill $OLD_PID 或 bash start.sh stop"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
nohup $PYTHON_CMD -m oss.cli serve > "$LOG_FILE" 2>&1 &
|
||||
NEW_PID=$!
|
||||
echo "$NEW_PID" > "$PID_FILE"
|
||||
ok "已启动守护进程 (PID: $NEW_PID)"
|
||||
info "日志: $LOG_FILE"
|
||||
info "停止: kill $(cat $PID_FILE) 或 bash start.sh stop"
|
||||
ok "守护进程已启动 (PID: $NEW_PID)"
|
||||
info "日志文件: $LOG_FILE"
|
||||
sleep 2
|
||||
curl -s http://localhost:8080/health &>/dev/null && ok "服务就绪: http://localhost:8080" || warn "服务启动中,请稍候..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 前台模式 + 崩溃自动重启 ──
|
||||
# 前台模式 + 崩溃自动重启
|
||||
echo -e "${WHITE}运行中... 按 Ctrl+C 停止${NC}"
|
||||
echo ""
|
||||
|
||||
RESTART_DELAY=3
|
||||
MAX_RESTARTS=0
|
||||
RESTART_COUNT=0
|
||||
|
||||
run_server() {
|
||||
$PYTHON_CMD -m oss.cli serve
|
||||
}
|
||||
|
||||
while true; do
|
||||
run_server
|
||||
$PYTHON_CMD -m oss.cli serve
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [[ $EXIT_CODE -eq 0 ]]; then
|
||||
@@ -171,7 +313,7 @@ while true; do
|
||||
warn "服务异常退出 (code: $EXIT_CODE),${RESTART_DELAY}s 后重启... (第 $RESTART_COUNT 次)"
|
||||
sleep $RESTART_DELAY
|
||||
|
||||
# 指数退避,最大 30s
|
||||
# 指数退避
|
||||
if [[ $RESTART_DELAY -lt 30 ]]; then
|
||||
RESTART_DELAY=$((RESTART_DELAY * 2))
|
||||
fi
|
||||
|
||||
8
store/@{Falck}/html-render/SIGNATURE
Normal file
8
store/@{Falck}/html-render/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +1,24 @@
|
||||
"""HTML 渲染服务 - 通过 config.json 配置,统一文件入口"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
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):
|
||||
"""HTML 渲染插件 - 渲染服务由 html-render 提供"""
|
||||
|
||||
@@ -16,16 +31,16 @@ class HtmlRenderPlugin(Plugin):
|
||||
def init(self, deps: dict = None):
|
||||
"""初始化 - 读取 config.json 并解析网站根目录"""
|
||||
self._load_config()
|
||||
print(f"[html-render] 配置加载完成: root_dir={self.root_dir}")
|
||||
_Log.info(f"配置加载完成: root_dir={self.root_dir}")
|
||||
|
||||
def start(self):
|
||||
"""启动 - 注册路由到 http-api,共享配置给 web-toolkit"""
|
||||
# 注册首页路由
|
||||
if self.http_api and hasattr(self.http_api, 'router'):
|
||||
self.http_api.router.get("/", self._serve_html)
|
||||
print("[html-render] 已注册路由到 http-api")
|
||||
_Log.info("已注册路由到 http-api")
|
||||
else:
|
||||
print("[html-render] http-api 未加载")
|
||||
_Log.warn("http-api 未加载")
|
||||
|
||||
# 将配置共享给 web-toolkit(通过 plugin-storage 的 DCIM 共享存储)
|
||||
if self.storage:
|
||||
@@ -35,7 +50,7 @@ class HtmlRenderPlugin(Plugin):
|
||||
"index_file": self.config.get("index_file", "index.html"),
|
||||
"static_prefix": self.config.get("static_prefix", "/static"),
|
||||
})
|
||||
print("[html-render] 配置已共享到 DCIM")
|
||||
_Log.info("配置已共享到 DCIM")
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
@@ -53,7 +68,7 @@ class HtmlRenderPlugin(Plugin):
|
||||
"""读取 config.json,解析根目录"""
|
||||
config_path = Path("./data/html-render/config.json")
|
||||
if not config_path.exists():
|
||||
print("[html-render] 警告: config.json 不存在,使用默认配置")
|
||||
_Log.warn("config.json 不存在,使用默认配置")
|
||||
self.config = {"root_dir": "../website", "index_file": "index.html"}
|
||||
else:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
@@ -66,6 +81,11 @@ class HtmlRenderPlugin(Plugin):
|
||||
def _serve_html(self, request):
|
||||
"""提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径"""
|
||||
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:
|
||||
storage = self.storage.get_storage("html-render")
|
||||
if storage.file_exists(index_file):
|
||||
|
||||
8
store/@{Falck}/web-toolkit/SIGNATURE
Normal file
8
store/@{Falck}/web-toolkit/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from oss.plugin.types import Plugin, register_plugin_type, Response
|
||||
from .router import WebRouter
|
||||
@@ -7,6 +8,20 @@ from .static import StaticFileHandler
|
||||
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):
|
||||
"""Web 工具包插件 - 提供网站前端所有服务"""
|
||||
|
||||
@@ -26,7 +41,7 @@ class WebToolkitPlugin(Plugin):
|
||||
self.template_engine = TemplateEngine()
|
||||
self._load_config()
|
||||
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):
|
||||
"""启动"""
|
||||
@@ -65,7 +80,7 @@ class WebToolkitPlugin(Plugin):
|
||||
self._serve_static
|
||||
)
|
||||
|
||||
print("[web-toolkit] Web 工具包已启动")
|
||||
_Log.info("Web 工具包已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
@@ -97,7 +112,7 @@ class WebToolkitPlugin(Plugin):
|
||||
"""读取 config.json,解析网站根目录"""
|
||||
config_path = Path("./data/web-toolkit/config.json")
|
||||
if not config_path.exists():
|
||||
print("[web-toolkit] 警告: config.json 不存在,使用默认配置")
|
||||
_Log.warn("config.json 不存在,使用默认配置")
|
||||
self.config = {
|
||||
"root_dir": "../website",
|
||||
"index_file": "index.html",
|
||||
@@ -146,6 +161,10 @@ class WebToolkitPlugin(Plugin):
|
||||
else:
|
||||
filename = path.lstrip("/")
|
||||
|
||||
# 安全检查:防止路径穿越
|
||||
if ".." in filename or filename.startswith("/"):
|
||||
return Response(status=403, body="Forbidden")
|
||||
|
||||
if not filename:
|
||||
return self._serve_website_index(request)
|
||||
return self.static_handler.serve(filename)
|
||||
|
||||
@@ -43,12 +43,7 @@ class TemplateEngine:
|
||||
return content
|
||||
|
||||
def _safe_eval(self, expression: str, context: dict) -> Any:
|
||||
"""安全评估表达式(仅允许简单的属性访问和比较)"""
|
||||
# 只允许访问 context 中的变量
|
||||
# 支持的运算符: and, or, not, ==, !=, <, >, <=, >=, in
|
||||
# 不允许函数调用、导入、属性访问等
|
||||
|
||||
# 使用 AST 解析并验证
|
||||
"""安全评估表达式(使用 AST 验证,不使用 eval)"""
|
||||
try:
|
||||
tree = ast.parse(expression, mode='eval')
|
||||
except SyntaxError:
|
||||
@@ -58,12 +53,64 @@ class TemplateEngine:
|
||||
if not self._validate_ast(tree.body[0].value, set(context.keys())):
|
||||
return False
|
||||
|
||||
# 在受限环境中评估
|
||||
# 使用安全的 AST 解释器,不使用 eval
|
||||
try:
|
||||
return eval(expression, {"__builtins__": {}}, context)
|
||||
return self._eval_ast(tree.body[0].value, context)
|
||||
except Exception:
|
||||
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:
|
||||
"""验证 AST 只包含安全的操作"""
|
||||
if isinstance(node, ast.Name):
|
||||
|
||||
@@ -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("熔断器已触发")
|
||||
```
|
||||
Binary file not shown.
@@ -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()
|
||||
@@ -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": []
|
||||
}
|
||||
8
store/@{FutureOSS}/code-reviewer.disabled/SIGNATURE
Normal file
8
store/@{FutureOSS}/code-reviewer.disabled/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
100
store/@{FutureOSS}/code-reviewer.disabled/checks/quality.py
Normal file
100
store/@{FutureOSS}/code-reviewer.disabled/checks/quality.py
Normal 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
|
||||
323
store/@{FutureOSS}/code-reviewer.disabled/checks/references.py
Normal file
323
store/@{FutureOSS}/code-reviewer.disabled/checks/references.py
Normal 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
|
||||
85
store/@{FutureOSS}/code-reviewer.disabled/checks/security.py
Normal file
85
store/@{FutureOSS}/code-reviewer.disabled/checks/security.py
Normal 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
|
||||
70
store/@{FutureOSS}/code-reviewer.disabled/checks/style.py
Normal file
70
store/@{FutureOSS}/code-reviewer.disabled/checks/style.py
Normal 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 []
|
||||
Binary file not shown.
Binary file not shown.
94
store/@{FutureOSS}/code-reviewer.disabled/core/reviewer.py
Normal file
94
store/@{FutureOSS}/code-reviewer.disabled/core/reviewer.py
Normal 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
|
||||
70
store/@{FutureOSS}/code-reviewer.disabled/main.py
Normal file
70
store/@{FutureOSS}/code-reviewer.disabled/main.py
Normal 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()
|
||||
20
store/@{FutureOSS}/code-reviewer.disabled/manifest.json
Normal file
20
store/@{FutureOSS}/code-reviewer.disabled/manifest.json
Normal 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": ["*"]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
8
store/@{FutureOSS}/dashboard/SIGNATURE
Normal file
8
store/@{FutureOSS}/dashboard/SIGNATURE
Normal 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"
|
||||
}
|
||||
BIN
store/@{FutureOSS}/dashboard/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/dashboard/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
91
store/@{FutureOSS}/dashboard/assets/css/dashboard.css
Normal file
91
store/@{FutureOSS}/dashboard/assets/css/dashboard.css
Normal 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;
|
||||
}
|
||||
28
store/@{FutureOSS}/dashboard/config.json
Normal file
28
store/@{FutureOSS}/dashboard/config.json
Normal 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
|
||||
}
|
||||
}
|
||||
332
store/@{FutureOSS}/dashboard/main.py
Normal file
332
store/@{FutureOSS}/dashboard/main.py
Normal 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()
|
||||
15
store/@{FutureOSS}/dashboard/manifest.json
Normal file
15
store/@{FutureOSS}/dashboard/manifest.json
Normal 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": ["*"]
|
||||
}
|
||||
350
store/@{FutureOSS}/dashboard/views/dashboard.php
Normal file
350
store/@{FutureOSS}/dashboard/views/dashboard.php
Normal 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>
|
||||
8
store/@{FutureOSS}/dependency/SIGNATURE
Normal file
8
store/@{FutureOSS}/dependency/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
8
store/@{FutureOSS}/hot-reload.disabled/SIGNATURE
Normal file
8
store/@{FutureOSS}/hot-reload.disabled/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Callable
|
||||
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
|
||||
|
||||
@@ -138,7 +139,7 @@ class HotReloadPlugin(Plugin):
|
||||
elif change_type == "deleted":
|
||||
self.unload_plugin(plugin_name)
|
||||
except Exception as e:
|
||||
print(f"[hot-reload] 处理变化失败: {e}")
|
||||
Log.error("hot-reload", f"处理变化失败: {e}")
|
||||
|
||||
def load_plugin(self, plugin_dir: Path) -> bool:
|
||||
"""运行时加载插件"""
|
||||
Binary file not shown.
8
store/@{FutureOSS}/http-api/SIGNATURE
Normal file
8
store/@{FutureOSS}/http-api/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,9 +23,12 @@ class CorsMiddleware(Middleware):
|
||||
|
||||
class LoggerMiddleware(Middleware):
|
||||
"""日志中间件"""
|
||||
# 静默的路由(不打印日志)
|
||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
req = ctx.get("request")
|
||||
if req:
|
||||
if req and req.path not in self._silent_paths:
|
||||
print(f"[http-api] {req.method} {req.path}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ class HttpServer:
|
||||
self._send_response(resp)
|
||||
|
||||
def _send_response(self, resp: Response):
|
||||
try:
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
self.send_header(k, v)
|
||||
@@ -103,6 +104,8 @@ class HttpServer:
|
||||
self.wfile.write(resp.body.encode("utf-8"))
|
||||
else:
|
||||
self.wfile.write(resp.body)
|
||||
except BrokenPipeError:
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
8
store/@{FutureOSS}/http-tcp/SIGNATURE
Normal file
8
store/@{FutureOSS}/http-tcp/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8
store/@{FutureOSS}/i18n.disabled/SIGNATURE
Normal file
8
store/@{FutureOSS}/i18n.disabled/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
"""i18n 国际化多语言支持插件"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
from .i18n import I18nEngine
|
||||
from .middleware import I18nMiddleware
|
||||
@@ -66,8 +67,8 @@ class I18nPlugin(Plugin):
|
||||
# 初始化中间件
|
||||
self.middleware_handler = I18nMiddleware(self.engine, config)
|
||||
|
||||
print(f"[i18n] 已加载语言: {', '.join(supported_locales)}")
|
||||
print(f"[i18n] 默认语言: {default_locale}")
|
||||
Log.info("i18n", f"已加载语言: {', '.join(supported_locales)}")
|
||||
Log.info("i18n", f"默认语言: {default_locale}")
|
||||
|
||||
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/translate", self._translate_handler)
|
||||
http_api.router.post("/api/i18n/locale", self._change_locale_handler)
|
||||
print("[i18n] API 路由已注册")
|
||||
Log.info("i18n", "API 路由已注册")
|
||||
|
||||
def stop(self):
|
||||
"""停止插件"""
|
||||
print("[i18n] 插件已停止")
|
||||
Log.error("i18n", "插件已停止")
|
||||
|
||||
def health(self) -> bool:
|
||||
"""健康检查"""
|
||||
Binary file not shown.
8
store/@{FutureOSS}/json-codec/SIGNATURE
Normal file
8
store/@{FutureOSS}/json-codec/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
@@ -3,6 +3,7 @@ import json
|
||||
from typing import Any, Callable, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
|
||||
|
||||
@@ -127,7 +128,7 @@ class JsonCodecPlugin(Plugin):
|
||||
|
||||
def start(self):
|
||||
"""启动"""
|
||||
print("[json-codec] JSON 编解码器已启动")
|
||||
Log.info("json-codec", "JSON 编解码器已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
|
||||
8
store/@{FutureOSS}/lifecycle.disabled/SIGNATURE
Normal file
8
store/@{FutureOSS}/lifecycle.disabled/SIGNATURE
Normal 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"
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user