feat: 更新项目进度,完成 Phase 4 和 Phase 5,添加监控与健康检查功能
This commit is contained in:
68
.dockerignore
Normal file
68
.dockerignore
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
env
|
||||||
|
.eggs
|
||||||
|
*.egg-info
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
.tox
|
||||||
|
.nox
|
||||||
|
|
||||||
|
# Mypy
|
||||||
|
.mypy_cache
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/_build
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Local config
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
config.local.json5
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Logs and data
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
data/
|
||||||
|
|
||||||
|
# PoC files
|
||||||
|
poc/
|
||||||
|
PoC*.md
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
*.tmp
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Tests (optional - include if you want tests in image)
|
||||||
|
# tests/
|
||||||
|
|
||||||
|
# Claude config
|
||||||
|
.claude/
|
||||||
73
Dockerfile
Normal file
73
Dockerfile
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# MineNASAI Dockerfile
|
||||||
|
# 多阶段构建,优化镜像大小
|
||||||
|
|
||||||
|
# ==================== 构建阶段 ====================
|
||||||
|
FROM python:3.13-slim AS builder
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装构建依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 复制项目文件
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
|
||||||
|
# 安装依赖到虚拟环境
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
# ==================== 运行阶段 ====================
|
||||||
|
FROM python:3.13-slim AS runtime
|
||||||
|
|
||||||
|
# 安全设置 - 创建非 root 用户
|
||||||
|
RUN groupadd --gid 1000 minenasai && \
|
||||||
|
useradd --uid 1000 --gid minenasai --shell /bin/bash --create-home minenasai
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PATH="/opt/venv/bin:$PATH" \
|
||||||
|
MINENASAI_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装运行时依赖
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssh-client \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 从构建阶段复制虚拟环境
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY --chown=minenasai:minenasai src/ ./src/
|
||||||
|
COPY --chown=minenasai:minenasai config/ ./config/
|
||||||
|
|
||||||
|
# 创建数据目录
|
||||||
|
RUN mkdir -p /app/data /app/logs && \
|
||||||
|
chown -R minenasai:minenasai /app/data /app/logs
|
||||||
|
|
||||||
|
# 切换到非 root 用户
|
||||||
|
USER minenasai
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8000/health/live || exit 1
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8000 8080
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["python", "-m", "uvicorn", "minenasai.gateway.server:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
155
README.md
155
README.md
@@ -1,16 +1,22 @@
|
|||||||
# MineNASAI
|
# MineNASAI
|
||||||
|
|
||||||
基于 NAS 的智能个人 AI 助理,支持企业微信/飞书通讯,集成 Claude 编程能力。
|
基于 NAS 的智能个人 AI 助理,支持企业微信/飞书通讯,集成多 LLM 编程能力。
|
||||||
|
|
||||||
|
[](https://python.org)
|
||||||
|
[](tests/)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
|
- **多 LLM 支持**: Anthropic Claude、OpenAI、DeepSeek、智谱、MiniMax、Moonshot、Gemini
|
||||||
- **多渠道通讯**: 企业微信、飞书接入
|
- **多渠道通讯**: 企业微信、飞书接入
|
||||||
- **智能路由**: 自动识别任务复杂度,选择最优处理方式
|
- **智能路由**: 自动识别任务复杂度,选择最优处理方式
|
||||||
- **双界面模式**:
|
- **双界面模式**:
|
||||||
- 通讯工具:日常交互、简单任务
|
- 通讯工具:日常交互、简单任务
|
||||||
- Web TUI:深度编程、复杂项目
|
- Web TUI:深度编程、复杂项目
|
||||||
- **安全隔离**: Python 沙箱执行、权限分级
|
- **安全隔离**: Python 沙箱执行、权限分级、确认机制
|
||||||
- **可扩展**: 支持 MCP Server 插件
|
- **生产就绪**: 健康检查、监控指标、Docker 部署
|
||||||
|
- **可扩展**: 工具注册中心、Cron 定时任务
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -24,13 +30,13 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://github.com/minenasai/minenasai.git
|
git clone http://jiulu-gameplay.com.cn:13001/congsh/MineNasAI.git
|
||||||
cd minenasai
|
cd MineNasAI
|
||||||
|
|
||||||
# 创建虚拟环境
|
# 创建虚拟环境
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
source .venv/bin/activate # Linux/macOS
|
.venv\Scripts\activate # Windows
|
||||||
# .venv\Scripts\activate # Windows
|
# source .venv/bin/activate # Linux/macOS
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
@@ -45,39 +51,70 @@ pre-commit install
|
|||||||
# 复制环境变量模板
|
# 复制环境变量模板
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# 编辑 .env 文件,填入 API Key 等配置
|
# 编辑 .env 文件,填入 API Key
|
||||||
# ANTHROPIC_API_KEY=sk-ant-xxxxx
|
# MINENASAI_ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||||
|
# MINENASAI_DEEPSEEK_API_KEY=sk-xxxxx
|
||||||
# 初始化配置文件
|
|
||||||
minenasai config --init
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行
|
### 运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动 Gateway 服务
|
# 启动 Gateway 服务
|
||||||
minenasai server --port 8000
|
python -m uvicorn minenasai.gateway.server:app --port 8000
|
||||||
|
|
||||||
# 启动 Web TUI 服务(另一个终端)
|
# 启动 Web TUI 服务(另一个终端)
|
||||||
minenasai webtui --port 8080
|
python -m uvicorn minenasai.webtui.server:app --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建并启动
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f gateway
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
minenasai/
|
MineNasAI/
|
||||||
├── src/minenasai/
|
├── src/minenasai/
|
||||||
│ ├── core/ # 核心模块(配置、日志)
|
│ ├── core/ # 核心模块
|
||||||
|
│ │ ├── config.py # 配置管理
|
||||||
|
│ │ ├── logging.py # 日志系统
|
||||||
|
│ │ ├── database.py # 数据库
|
||||||
|
│ │ ├── monitoring.py # 监控与健康检查
|
||||||
|
│ │ └── cache.py # 缓存与限流
|
||||||
│ ├── gateway/ # Gateway 服务
|
│ ├── gateway/ # Gateway 服务
|
||||||
│ │ ├── protocol/ # 消息协议
|
│ │ ├── protocol/ # 消息协议
|
||||||
│ │ └── channels/ # 通讯渠道(企业微信、飞书)
|
│ │ ├── router.py # 智能路由
|
||||||
|
│ │ ├── server.py # FastAPI 服务
|
||||||
|
│ │ └── channels/ # 通讯渠道
|
||||||
|
│ ├── llm/ # LLM 集成
|
||||||
|
│ │ ├── base.py # 基础接口
|
||||||
|
│ │ ├── manager.py # LLM 管理器
|
||||||
|
│ │ └── clients/ # 各提供商客户端
|
||||||
│ ├── agent/ # Agent 运行时
|
│ ├── agent/ # Agent 运行时
|
||||||
|
│ │ ├── runtime.py # Agent 执行
|
||||||
|
│ │ ├── permissions.py # 权限管理
|
||||||
|
│ │ ├── tool_registry.py # 工具注册
|
||||||
│ │ └── tools/ # 内置工具
|
│ │ └── tools/ # 内置工具
|
||||||
│ └── webtui/ # Web TUI 界面
|
│ ├── scheduler/ # 定时任务
|
||||||
│ └── static/ # 前端静态文件
|
│ │ └── cron.py # Cron 调度器
|
||||||
├── tests/ # 测试用例
|
│ └── webtui/ # Web TUI
|
||||||
├── config/ # 配置文件模板
|
│ ├── server.py # TUI 服务器
|
||||||
└── docs/ # 文档
|
│ ├── auth.py # 认证管理
|
||||||
|
│ ├── ssh_manager.py # SSH 管理
|
||||||
|
│ └── static/ # 前端文件
|
||||||
|
├── tests/ # 测试用例 (131 tests)
|
||||||
|
├── config/ # 配置模板
|
||||||
|
├── Dockerfile # Docker 构建
|
||||||
|
└── docker-compose.yml # 容器编排
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构概述
|
## 架构概述
|
||||||
@@ -90,7 +127,7 @@ minenasai/
|
|||||||
│
|
│
|
||||||
┌────────────────────────────┴────────────────────────────────┐
|
┌────────────────────────────┴────────────────────────────────┐
|
||||||
│ Gateway 服务 (FastAPI) │
|
│ Gateway 服务 (FastAPI) │
|
||||||
│ WebSocket协议 / 消息队列 / 权限验证 │
|
│ WebSocket协议 / 监控指标 / 健康检查 / CORS │
|
||||||
└────────────────────────────┬────────────────────────────────┘
|
└────────────────────────────┬────────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌────────────────────────────┴────────────────────────────────┐
|
┌────────────────────────────┴────────────────────────────────┐
|
||||||
@@ -101,11 +138,40 @@ minenasai/
|
|||||||
┌───────────────────┼───────────────────┐
|
┌───────────────────┼───────────────────┐
|
||||||
↓ ↓ ↓
|
↓ ↓ ↓
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
│ 快速执行通道 │ │ Anthropic │ │ Web TUI │
|
│ 快速执行通道 │ │ 多 LLM │ │ Web TUI │
|
||||||
│ Python沙箱 │ │ API │ │ SSH+Claude │
|
│ Python沙箱 │ │ API │ │ SSH+Claude │
|
||||||
└─────────────┘ └─────────────┘ └─────────────┘
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
↓ ↓ ↓
|
||||||
|
Anthropic Claude DeepSeek/智谱 OpenAI/Gemini
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 支持的 LLM 提供商
|
||||||
|
|
||||||
|
| 提供商 | 模型示例 | 区域 | 代理 |
|
||||||
|
|--------|----------|------|------|
|
||||||
|
| Anthropic | claude-sonnet-4-20250514 | 境外 | 需要 |
|
||||||
|
| OpenAI | gpt-4o | 境外 | 需要 |
|
||||||
|
| Google | gemini-2.0-flash | 境外 | 需要 |
|
||||||
|
| DeepSeek | deepseek-chat | 国内 | 不需要 |
|
||||||
|
| 智谱 | glm-4-flash | 国内 | 不需要 |
|
||||||
|
| MiniMax | abab6.5s-chat | 国内 | 不需要 |
|
||||||
|
| Moonshot | moonshot-v1-8k | 国内 | 不需要 |
|
||||||
|
|
||||||
|
## API 端点
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/` | GET | 服务状态 |
|
||||||
|
| `/health` | GET | 完整健康检查 |
|
||||||
|
| `/health/live` | GET | 存活检查 (K8s) |
|
||||||
|
| `/health/ready` | GET | 就绪检查 (K8s) |
|
||||||
|
| `/metrics` | GET | 监控指标 |
|
||||||
|
| `/ws` | WebSocket | 消息通道 |
|
||||||
|
| `/api/agents` | GET | Agent 列表 |
|
||||||
|
| `/api/sessions` | GET | 会话列表 |
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
### 代码规范
|
### 代码规范
|
||||||
@@ -124,16 +190,31 @@ mypy src
|
|||||||
### 测试
|
### 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 运行测试
|
# 运行所有测试
|
||||||
pytest
|
pytest
|
||||||
|
|
||||||
# 带覆盖率
|
# 带覆盖率
|
||||||
pytest --cov=minenasai
|
pytest --cov=minenasai
|
||||||
|
|
||||||
|
# 详细输出
|
||||||
|
pytest -v --tb=short
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 当前测试覆盖
|
||||||
|
|
||||||
|
- **test_core.py**: 配置、日志 (9 tests)
|
||||||
|
- **test_gateway.py**: 协议、路由 (14 tests)
|
||||||
|
- **test_llm.py**: LLM 客户端 (10 tests)
|
||||||
|
- **test_monitoring.py**: 监控、健康检查 (17 tests)
|
||||||
|
- **test_cache.py**: 缓存、限流 (21 tests)
|
||||||
|
- **test_permissions.py**: 权限、工具注册 (17 tests)
|
||||||
|
- **test_scheduler.py**: Cron 调度 (15 tests)
|
||||||
|
- **test_tools.py**: 内置工具 (14 tests)
|
||||||
|
- **test_webtui.py**: Web TUI (14 tests)
|
||||||
|
|
||||||
## 配置说明
|
## 配置说明
|
||||||
|
|
||||||
配置文件位置: `~/.config/minenasai/config.json5`
|
配置文件: `config/config.json5`
|
||||||
|
|
||||||
主要配置项:
|
主要配置项:
|
||||||
|
|
||||||
@@ -141,9 +222,29 @@ pytest --cov=minenasai
|
|||||||
|--------|------|--------|
|
|--------|------|--------|
|
||||||
| `gateway.port` | Gateway 端口 | 8000 |
|
| `gateway.port` | Gateway 端口 | 8000 |
|
||||||
| `webtui.port` | Web TUI 端口 | 8080 |
|
| `webtui.port` | Web TUI 端口 | 8080 |
|
||||||
| `agents.default_model` | 默认模型 | claude-sonnet-4-20250514 |
|
| `llm.default_provider` | 默认 LLM 提供商 | anthropic |
|
||||||
|
| `llm.default_model` | 默认模型 | claude-sonnet-4-20250514 |
|
||||||
|
| `proxy.enabled` | 是否启用代理 | false |
|
||||||
| `router.mode` | 路由模式 | agent |
|
| `router.mode` | 路由模式 | agent |
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# LLM API Keys
|
||||||
|
MINENASAI_ANTHROPIC_API_KEY=sk-ant-xxx
|
||||||
|
MINENASAI_OPENAI_API_KEY=sk-xxx
|
||||||
|
MINENASAI_DEEPSEEK_API_KEY=sk-xxx
|
||||||
|
MINENASAI_ZHIPU_API_KEY=xxx
|
||||||
|
MINENASAI_MINIMAX_API_KEY=xxx
|
||||||
|
MINENASAI_MOONSHOT_API_KEY=sk-xxx
|
||||||
|
MINENASAI_GEMINI_API_KEY=xxx
|
||||||
|
|
||||||
|
# 代理设置 (境外 API)
|
||||||
|
MINENASAI_PROXY_ENABLED=true
|
||||||
|
MINENASAI_PROXY_HTTP=http://127.0.0.1:7890
|
||||||
|
MINENASAI_PROXY_HTTPS=http://127.0.0.1:7890
|
||||||
|
```
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|||||||
100
docker-compose.yml
Normal file
100
docker-compose.yml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# MineNASAI Docker Compose 配置
|
||||||
|
# 用于本地开发和生产部署
|
||||||
|
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ==================== Gateway 服务 ====================
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: minenasai-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${GATEWAY_PORT:-8000}:8000"
|
||||||
|
environment:
|
||||||
|
- MINENASAI_ENV=production
|
||||||
|
- MINENASAI_GATEWAY_HOST=0.0.0.0
|
||||||
|
- MINENASAI_GATEWAY_PORT=8000
|
||||||
|
# LLM API Keys (从 .env 读取)
|
||||||
|
- MINENASAI_ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
|
- MINENASAI_OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
|
- MINENASAI_DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-}
|
||||||
|
- MINENASAI_ZHIPU_API_KEY=${ZHIPU_API_KEY:-}
|
||||||
|
- MINENASAI_MINIMAX_API_KEY=${MINIMAX_API_KEY:-}
|
||||||
|
- MINENASAI_MOONSHOT_API_KEY=${MOONSHOT_API_KEY:-}
|
||||||
|
- MINENASAI_GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
||||||
|
# 代理设置
|
||||||
|
- MINENASAI_PROXY_ENABLED=${PROXY_ENABLED:-false}
|
||||||
|
- MINENASAI_PROXY_HTTP=${PROXY_HTTP:-}
|
||||||
|
- MINENASAI_PROXY_HTTPS=${PROXY_HTTPS:-}
|
||||||
|
volumes:
|
||||||
|
- minenasai-data:/app/data
|
||||||
|
- minenasai-logs:/app/logs
|
||||||
|
- ./config:/app/config:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- minenasai-network
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# ==================== Web TUI 服务 ====================
|
||||||
|
webtui:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: minenasai-webtui
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${WEBTUI_PORT:-8080}:8080"
|
||||||
|
environment:
|
||||||
|
- MINENASAI_ENV=production
|
||||||
|
- MINENASAI_WEBTUI_HOST=0.0.0.0
|
||||||
|
- MINENASAI_WEBTUI_PORT=8080
|
||||||
|
volumes:
|
||||||
|
- minenasai-data:/app/data
|
||||||
|
- minenasai-logs:/app/logs
|
||||||
|
command: ["python", "-m", "uvicorn", "minenasai.webtui.server:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
networks:
|
||||||
|
- minenasai-network
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
|
||||||
|
# ==================== Redis (消息队列 & 缓存) ====================
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: minenasai-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- minenasai-network
|
||||||
|
|
||||||
|
# ==================== 数据卷 ====================
|
||||||
|
volumes:
|
||||||
|
minenasai-data:
|
||||||
|
driver: local
|
||||||
|
minenasai-logs:
|
||||||
|
driver: local
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# ==================== 网络 ====================
|
||||||
|
networks:
|
||||||
|
minenasai-network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
"""核心模块
|
"""核心模块
|
||||||
|
|
||||||
提供配置管理、日志系统、数据库等基础功能
|
提供配置管理、日志系统、数据库、监控、缓存等基础功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from minenasai.core.cache import (
|
||||||
|
MemoryCache,
|
||||||
|
RateLimiter,
|
||||||
|
get_rate_limiter,
|
||||||
|
get_response_cache,
|
||||||
|
make_cache_key,
|
||||||
|
)
|
||||||
from minenasai.core.config import Settings, get_settings, load_config, reset_settings
|
from minenasai.core.config import Settings, get_settings, load_config, reset_settings
|
||||||
from minenasai.core.logging import (
|
from minenasai.core.logging import (
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
@@ -10,14 +17,39 @@ from minenasai.core.logging import (
|
|||||||
get_logger,
|
get_logger,
|
||||||
setup_logging,
|
setup_logging,
|
||||||
)
|
)
|
||||||
|
from minenasai.core.monitoring import (
|
||||||
|
ComponentHealth,
|
||||||
|
HealthChecker,
|
||||||
|
HealthStatus,
|
||||||
|
SystemMetrics,
|
||||||
|
get_health_checker,
|
||||||
|
get_metrics,
|
||||||
|
setup_monitoring,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
# 配置
|
||||||
"Settings",
|
"Settings",
|
||||||
"get_settings",
|
"get_settings",
|
||||||
"load_config",
|
"load_config",
|
||||||
"reset_settings",
|
"reset_settings",
|
||||||
|
# 日志
|
||||||
"setup_logging",
|
"setup_logging",
|
||||||
"get_logger",
|
"get_logger",
|
||||||
"AuditLogger",
|
"AuditLogger",
|
||||||
"get_audit_logger",
|
"get_audit_logger",
|
||||||
|
# 监控
|
||||||
|
"setup_monitoring",
|
||||||
|
"get_metrics",
|
||||||
|
"get_health_checker",
|
||||||
|
"SystemMetrics",
|
||||||
|
"HealthChecker",
|
||||||
|
"HealthStatus",
|
||||||
|
"ComponentHealth",
|
||||||
|
# 缓存
|
||||||
|
"MemoryCache",
|
||||||
|
"RateLimiter",
|
||||||
|
"get_response_cache",
|
||||||
|
"get_rate_limiter",
|
||||||
|
"make_cache_key",
|
||||||
]
|
]
|
||||||
|
|||||||
266
src/minenasai/core/cache.py
Normal file
266
src/minenasai/core/cache.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
"""缓存模块
|
||||||
|
|
||||||
|
提供内存缓存和 TTL 管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
|
from minenasai.core.logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CacheEntry(Generic[T]):
|
||||||
|
"""缓存条目"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: T
|
||||||
|
created_at: float
|
||||||
|
expires_at: float
|
||||||
|
hits: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
"""是否过期"""
|
||||||
|
return time.time() > self.expires_at
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ttl_remaining(self) -> float:
|
||||||
|
"""剩余 TTL(秒)"""
|
||||||
|
return max(0, self.expires_at - time.time())
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCache(Generic[T]):
|
||||||
|
"""内存缓存
|
||||||
|
|
||||||
|
支持 TTL、最大容量、LRU 淘汰
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
max_size: int = 1000,
|
||||||
|
default_ttl: float = 300.0, # 5分钟
|
||||||
|
cleanup_interval: float = 60.0, # 1分钟清理一次
|
||||||
|
) -> None:
|
||||||
|
self._cache: dict[str, CacheEntry[T]] = {}
|
||||||
|
self._max_size = max_size
|
||||||
|
self._default_ttl = default_ttl
|
||||||
|
self._cleanup_interval = cleanup_interval
|
||||||
|
self._cleanup_task: asyncio.Task[None] | None = None
|
||||||
|
|
||||||
|
# 统计
|
||||||
|
self._hits = 0
|
||||||
|
self._misses = 0
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""启动后台清理任务"""
|
||||||
|
if self._cleanup_task is None:
|
||||||
|
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
||||||
|
logger.info("cache_cleanup_started", interval=self._cleanup_interval)
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""停止后台清理任务"""
|
||||||
|
if self._cleanup_task:
|
||||||
|
self._cleanup_task.cancel()
|
||||||
|
try:
|
||||||
|
await self._cleanup_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
self._cleanup_task = None
|
||||||
|
|
||||||
|
async def _cleanup_loop(self) -> None:
|
||||||
|
"""清理循环"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(self._cleanup_interval)
|
||||||
|
self._cleanup_expired()
|
||||||
|
|
||||||
|
def _cleanup_expired(self) -> int:
|
||||||
|
"""清理过期条目"""
|
||||||
|
expired_keys = [k for k, v in self._cache.items() if v.is_expired]
|
||||||
|
for key in expired_keys:
|
||||||
|
del self._cache[key]
|
||||||
|
|
||||||
|
if expired_keys:
|
||||||
|
logger.debug("cache_cleanup", removed=len(expired_keys))
|
||||||
|
|
||||||
|
return len(expired_keys)
|
||||||
|
|
||||||
|
def _evict_lru(self) -> None:
|
||||||
|
"""LRU 淘汰"""
|
||||||
|
if len(self._cache) < self._max_size:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 按命中次数和创建时间排序,淘汰最少使用的
|
||||||
|
sorted_entries = sorted(
|
||||||
|
self._cache.items(),
|
||||||
|
key=lambda x: (x[1].hits, x[1].created_at),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 淘汰 10% 的条目
|
||||||
|
evict_count = max(1, len(sorted_entries) // 10)
|
||||||
|
for key, _ in sorted_entries[:evict_count]:
|
||||||
|
del self._cache[key]
|
||||||
|
|
||||||
|
logger.debug("cache_eviction", evicted=evict_count)
|
||||||
|
|
||||||
|
def get(self, key: str) -> T | None:
|
||||||
|
"""获取缓存值"""
|
||||||
|
entry = self._cache.get(key)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
self._misses += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
if entry.is_expired:
|
||||||
|
del self._cache[key]
|
||||||
|
self._misses += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
entry.hits += 1
|
||||||
|
self._hits += 1
|
||||||
|
return entry.value
|
||||||
|
|
||||||
|
def set(self, key: str, value: T, ttl: float | None = None) -> None:
|
||||||
|
"""设置缓存值"""
|
||||||
|
if ttl is None:
|
||||||
|
ttl = self._default_ttl
|
||||||
|
|
||||||
|
# 检查容量
|
||||||
|
if len(self._cache) >= self._max_size:
|
||||||
|
self._evict_lru()
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
self._cache[key] = CacheEntry(
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
created_at=now,
|
||||||
|
expires_at=now + ttl,
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, key: str) -> bool:
|
||||||
|
"""删除缓存"""
|
||||||
|
if key in self._cache:
|
||||||
|
del self._cache[key]
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear(self) -> int:
|
||||||
|
"""清空缓存"""
|
||||||
|
count = len(self._cache)
|
||||||
|
self._cache.clear()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def exists(self, key: str) -> bool:
|
||||||
|
"""检查 key 是否存在且未过期"""
|
||||||
|
entry = self._cache.get(key)
|
||||||
|
if entry is None:
|
||||||
|
return False
|
||||||
|
if entry.is_expired:
|
||||||
|
del self._cache[key]
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_stats(self) -> dict[str, Any]:
|
||||||
|
"""获取统计信息"""
|
||||||
|
total = self._hits + self._misses
|
||||||
|
hit_rate = self._hits / total if total > 0 else 0.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"size": len(self._cache),
|
||||||
|
"max_size": self._max_size,
|
||||||
|
"hits": self._hits,
|
||||||
|
"misses": self._misses,
|
||||||
|
"hit_rate": round(hit_rate * 100, 2),
|
||||||
|
"default_ttl": self._default_ttl,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_cache_key(*args: Any, **kwargs: Any) -> str:
|
||||||
|
"""生成缓存 key"""
|
||||||
|
key_parts = [str(arg) for arg in args]
|
||||||
|
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
||||||
|
key_string = ":".join(key_parts)
|
||||||
|
return hashlib.md5(key_string.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
# 全局缓存实例
|
||||||
|
_response_cache: MemoryCache[dict[str, Any]] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_response_cache() -> MemoryCache[dict[str, Any]]:
|
||||||
|
"""获取响应缓存"""
|
||||||
|
global _response_cache
|
||||||
|
if _response_cache is None:
|
||||||
|
_response_cache = MemoryCache(
|
||||||
|
max_size=500,
|
||||||
|
default_ttl=300.0, # 5分钟
|
||||||
|
)
|
||||||
|
return _response_cache
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RateLimiter:
|
||||||
|
"""速率限制器
|
||||||
|
|
||||||
|
令牌桶算法实现
|
||||||
|
"""
|
||||||
|
|
||||||
|
rate: float # 每秒允许的请求数
|
||||||
|
burst: int # 突发容量
|
||||||
|
|
||||||
|
_tokens: float = field(init=False)
|
||||||
|
_last_update: float = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._tokens = float(self.burst)
|
||||||
|
self._last_update = time.time()
|
||||||
|
|
||||||
|
def _refill(self) -> None:
|
||||||
|
"""补充令牌"""
|
||||||
|
now = time.time()
|
||||||
|
elapsed = now - self._last_update
|
||||||
|
self._tokens = min(self.burst, self._tokens + elapsed * self.rate)
|
||||||
|
self._last_update = now
|
||||||
|
|
||||||
|
def acquire(self, tokens: int = 1) -> bool:
|
||||||
|
"""尝试获取令牌"""
|
||||||
|
self._refill()
|
||||||
|
|
||||||
|
if self._tokens >= tokens:
|
||||||
|
self._tokens -= tokens
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def wait(self, tokens: int = 1) -> None:
|
||||||
|
"""等待获取令牌"""
|
||||||
|
while not self.acquire(tokens):
|
||||||
|
# 计算需要等待的时间
|
||||||
|
needed = tokens - self._tokens
|
||||||
|
wait_time = needed / self.rate
|
||||||
|
await asyncio.sleep(min(wait_time, 0.1))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_tokens(self) -> float:
|
||||||
|
"""可用令牌数"""
|
||||||
|
self._refill()
|
||||||
|
return self._tokens
|
||||||
|
|
||||||
|
|
||||||
|
# 全局速率限制器
|
||||||
|
_rate_limiters: dict[str, RateLimiter] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_rate_limiter(name: str, rate: float = 10.0, burst: int = 20) -> RateLimiter:
|
||||||
|
"""获取或创建速率限制器"""
|
||||||
|
if name not in _rate_limiters:
|
||||||
|
_rate_limiters[name] = RateLimiter(rate=rate, burst=burst)
|
||||||
|
return _rate_limiters[name]
|
||||||
379
src/minenasai/core/monitoring.py
Normal file
379
src/minenasai/core/monitoring.py
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
"""监控与健康检查模块
|
||||||
|
|
||||||
|
提供:
|
||||||
|
- 全局异常处理
|
||||||
|
- 健康检查端点
|
||||||
|
- 监控指标收集
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, Response
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from minenasai.core import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HealthStatus(Enum):
|
||||||
|
"""健康状态"""
|
||||||
|
|
||||||
|
HEALTHY = "healthy"
|
||||||
|
DEGRADED = "degraded"
|
||||||
|
UNHEALTHY = "unhealthy"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ComponentHealth:
|
||||||
|
"""组件健康状态"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
status: HealthStatus
|
||||||
|
message: str = ""
|
||||||
|
latency_ms: float = 0.0
|
||||||
|
last_check: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SystemMetrics:
|
||||||
|
"""系统指标"""
|
||||||
|
|
||||||
|
# 请求计数
|
||||||
|
total_requests: int = 0
|
||||||
|
successful_requests: int = 0
|
||||||
|
failed_requests: int = 0
|
||||||
|
|
||||||
|
# 响应时间
|
||||||
|
total_response_time_ms: float = 0.0
|
||||||
|
min_response_time_ms: float = float("inf")
|
||||||
|
max_response_time_ms: float = 0.0
|
||||||
|
|
||||||
|
# 错误统计
|
||||||
|
errors_by_type: dict[str, int] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# 活跃连接
|
||||||
|
active_connections: int = 0
|
||||||
|
peak_connections: int = 0
|
||||||
|
|
||||||
|
# 启动时间
|
||||||
|
start_time: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avg_response_time_ms(self) -> float:
|
||||||
|
"""平均响应时间"""
|
||||||
|
if self.total_requests == 0:
|
||||||
|
return 0.0
|
||||||
|
return self.total_response_time_ms / self.total_requests
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime_seconds(self) -> float:
|
||||||
|
"""运行时间(秒)"""
|
||||||
|
return (datetime.now() - self.start_time).total_seconds()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_rate(self) -> float:
|
||||||
|
"""成功率"""
|
||||||
|
if self.total_requests == 0:
|
||||||
|
return 1.0
|
||||||
|
return self.successful_requests / self.total_requests
|
||||||
|
|
||||||
|
def record_request(self, response_time_ms: float, success: bool) -> None:
|
||||||
|
"""记录请求"""
|
||||||
|
self.total_requests += 1
|
||||||
|
self.total_response_time_ms += response_time_ms
|
||||||
|
|
||||||
|
if response_time_ms < self.min_response_time_ms:
|
||||||
|
self.min_response_time_ms = response_time_ms
|
||||||
|
if response_time_ms > self.max_response_time_ms:
|
||||||
|
self.max_response_time_ms = response_time_ms
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.successful_requests += 1
|
||||||
|
else:
|
||||||
|
self.failed_requests += 1
|
||||||
|
|
||||||
|
def record_error(self, error_type: str) -> None:
|
||||||
|
"""记录错误"""
|
||||||
|
self.errors_by_type[error_type] = self.errors_by_type.get(error_type, 0) + 1
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""转换为字典"""
|
||||||
|
return {
|
||||||
|
"requests": {
|
||||||
|
"total": self.total_requests,
|
||||||
|
"successful": self.successful_requests,
|
||||||
|
"failed": self.failed_requests,
|
||||||
|
"success_rate": round(self.success_rate * 100, 2),
|
||||||
|
},
|
||||||
|
"response_time_ms": {
|
||||||
|
"avg": round(self.avg_response_time_ms, 2),
|
||||||
|
"min": round(self.min_response_time_ms, 2)
|
||||||
|
if self.min_response_time_ms != float("inf")
|
||||||
|
else 0,
|
||||||
|
"max": round(self.max_response_time_ms, 2),
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"active": self.active_connections,
|
||||||
|
"peak": self.peak_connections,
|
||||||
|
},
|
||||||
|
"errors": self.errors_by_type,
|
||||||
|
"uptime_seconds": round(self.uptime_seconds, 2),
|
||||||
|
"start_time": self.start_time.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HealthChecker:
|
||||||
|
"""健康检查器"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._checks: dict[str, Callable[[], Coroutine[Any, Any, ComponentHealth]]] = {}
|
||||||
|
self._results: dict[str, ComponentHealth] = {}
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
check_func: Callable[[], Coroutine[Any, Any, ComponentHealth]],
|
||||||
|
) -> None:
|
||||||
|
"""注册健康检查"""
|
||||||
|
self._checks[name] = check_func
|
||||||
|
|
||||||
|
async def check_component(self, name: str) -> ComponentHealth:
|
||||||
|
"""检查单个组件"""
|
||||||
|
if name not in self._checks:
|
||||||
|
return ComponentHealth(
|
||||||
|
name=name,
|
||||||
|
status=HealthStatus.UNHEALTHY,
|
||||||
|
message=f"未知组件: {name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
result = await asyncio.wait_for(self._checks[name](), timeout=5.0)
|
||||||
|
result.latency_ms = (time.time() - start_time) * 1000
|
||||||
|
self._results[name] = result
|
||||||
|
return result
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
result = ComponentHealth(
|
||||||
|
name=name,
|
||||||
|
status=HealthStatus.UNHEALTHY,
|
||||||
|
message="检查超时",
|
||||||
|
latency_ms=(time.time() - start_time) * 1000,
|
||||||
|
)
|
||||||
|
self._results[name] = result
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
result = ComponentHealth(
|
||||||
|
name=name,
|
||||||
|
status=HealthStatus.UNHEALTHY,
|
||||||
|
message=str(e),
|
||||||
|
latency_ms=(time.time() - start_time) * 1000,
|
||||||
|
)
|
||||||
|
self._results[name] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def check_all(self) -> dict[str, ComponentHealth]:
|
||||||
|
"""检查所有组件"""
|
||||||
|
tasks = [self.check_component(name) for name in self._checks]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
return {r.name: r for r in results}
|
||||||
|
|
||||||
|
def get_overall_status(self) -> HealthStatus:
|
||||||
|
"""获取总体状态"""
|
||||||
|
if not self._results:
|
||||||
|
return HealthStatus.HEALTHY
|
||||||
|
|
||||||
|
statuses = [r.status for r in self._results.values()]
|
||||||
|
|
||||||
|
if all(s == HealthStatus.HEALTHY for s in statuses):
|
||||||
|
return HealthStatus.HEALTHY
|
||||||
|
elif any(s == HealthStatus.UNHEALTHY for s in statuses):
|
||||||
|
return HealthStatus.UNHEALTHY
|
||||||
|
else:
|
||||||
|
return HealthStatus.DEGRADED
|
||||||
|
|
||||||
|
|
||||||
|
# 全局实例
|
||||||
|
_metrics = SystemMetrics()
|
||||||
|
_health_checker = HealthChecker()
|
||||||
|
|
||||||
|
|
||||||
|
def get_metrics() -> SystemMetrics:
|
||||||
|
"""获取全局指标"""
|
||||||
|
return _metrics
|
||||||
|
|
||||||
|
|
||||||
|
def get_health_checker() -> HealthChecker:
|
||||||
|
"""获取健康检查器"""
|
||||||
|
return _health_checker
|
||||||
|
|
||||||
|
|
||||||
|
def setup_monitoring(app: FastAPI) -> None:
|
||||||
|
"""设置监控中间件和端点"""
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def monitoring_middleware(request: Request, call_next: Any) -> Response:
|
||||||
|
"""监控中间件 - 记录请求指标"""
|
||||||
|
start_time = time.time()
|
||||||
|
success = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await call_next(request)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
success = False
|
||||||
|
_metrics.record_error(f"HTTP_{response.status_code}")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
success = False
|
||||||
|
error_type = type(e).__name__
|
||||||
|
_metrics.record_error(error_type)
|
||||||
|
logger.error(
|
||||||
|
"request_error",
|
||||||
|
path=request.url.path,
|
||||||
|
error_type=error_type,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
response_time_ms = (time.time() - start_time) * 1000
|
||||||
|
_metrics.record_request(response_time_ms, success)
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||||
|
"""全局异常处理器"""
|
||||||
|
error_id = f"ERR-{int(time.time() * 1000)}"
|
||||||
|
error_type = type(exc).__name__
|
||||||
|
|
||||||
|
# 记录详细错误
|
||||||
|
logger.error(
|
||||||
|
"unhandled_exception",
|
||||||
|
error_id=error_id,
|
||||||
|
error_type=error_type,
|
||||||
|
error=str(exc),
|
||||||
|
path=request.url.path,
|
||||||
|
method=request.method,
|
||||||
|
traceback=traceback.format_exc(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回友好错误响应
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"error": "内部服务器错误",
|
||||||
|
"error_id": error_id,
|
||||||
|
"message": "请联系管理员并提供错误ID",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check() -> dict[str, Any]:
|
||||||
|
"""健康检查端点"""
|
||||||
|
results = await _health_checker.check_all()
|
||||||
|
overall_status = _health_checker.get_overall_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": overall_status.value,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"components": {
|
||||||
|
name: {
|
||||||
|
"status": result.status.value,
|
||||||
|
"message": result.message,
|
||||||
|
"latency_ms": round(result.latency_ms, 2),
|
||||||
|
}
|
||||||
|
for name, result in results.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health/live")
|
||||||
|
async def liveness_check() -> dict[str, str]:
|
||||||
|
"""存活检查(Kubernetes liveness probe)"""
|
||||||
|
return {"status": "alive"}
|
||||||
|
|
||||||
|
@app.get("/health/ready")
|
||||||
|
async def readiness_check() -> dict[str, Any]:
|
||||||
|
"""就绪检查(Kubernetes readiness probe)"""
|
||||||
|
results = await _health_checker.check_all()
|
||||||
|
overall_status = _health_checker.get_overall_status()
|
||||||
|
|
||||||
|
if overall_status == HealthStatus.UNHEALTHY:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=503,
|
||||||
|
content={"status": "not_ready", "reason": "依赖服务不可用"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "ready"}
|
||||||
|
|
||||||
|
@app.get("/metrics")
|
||||||
|
async def get_metrics_endpoint() -> dict[str, Any]:
|
||||||
|
"""监控指标端点"""
|
||||||
|
return _metrics.to_dict()
|
||||||
|
|
||||||
|
# 注册默认健康检查
|
||||||
|
async def check_self() -> ComponentHealth:
|
||||||
|
"""自身健康检查"""
|
||||||
|
return ComponentHealth(
|
||||||
|
name="self",
|
||||||
|
status=HealthStatus.HEALTHY,
|
||||||
|
message="服务运行正常",
|
||||||
|
)
|
||||||
|
|
||||||
|
_health_checker.register("self", check_self)
|
||||||
|
|
||||||
|
logger.info("monitoring_setup_complete", endpoints=["/health", "/metrics"])
|
||||||
|
|
||||||
|
|
||||||
|
async def check_database_health() -> ComponentHealth:
|
||||||
|
"""数据库健康检查"""
|
||||||
|
try:
|
||||||
|
from minenasai.core.database import get_database
|
||||||
|
|
||||||
|
db = await get_database()
|
||||||
|
# 简单查询测试
|
||||||
|
await db.execute("SELECT 1")
|
||||||
|
return ComponentHealth(
|
||||||
|
name="database",
|
||||||
|
status=HealthStatus.HEALTHY,
|
||||||
|
message="数据库连接正常",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ComponentHealth(
|
||||||
|
name="database",
|
||||||
|
status=HealthStatus.UNHEALTHY,
|
||||||
|
message=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_llm_health() -> ComponentHealth:
|
||||||
|
"""LLM 服务健康检查"""
|
||||||
|
try:
|
||||||
|
from minenasai.llm import LLMManager
|
||||||
|
|
||||||
|
manager = LLMManager()
|
||||||
|
clients = manager.get_available_providers()
|
||||||
|
|
||||||
|
if not clients:
|
||||||
|
return ComponentHealth(
|
||||||
|
name="llm",
|
||||||
|
status=HealthStatus.DEGRADED,
|
||||||
|
message="无可用的 LLM 提供商",
|
||||||
|
)
|
||||||
|
|
||||||
|
return ComponentHealth(
|
||||||
|
name="llm",
|
||||||
|
status=HealthStatus.HEALTHY,
|
||||||
|
message=f"可用提供商: {', '.join(clients)}",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ComponentHealth(
|
||||||
|
name="llm",
|
||||||
|
status=HealthStatus.UNHEALTHY,
|
||||||
|
message=str(e),
|
||||||
|
)
|
||||||
@@ -12,8 +12,16 @@ from typing import Any, AsyncGenerator
|
|||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from minenasai.core import get_logger, get_settings, setup_logging
|
from minenasai.core import (
|
||||||
|
get_health_checker,
|
||||||
|
get_logger,
|
||||||
|
get_metrics,
|
||||||
|
get_settings,
|
||||||
|
setup_logging,
|
||||||
|
setup_monitoring,
|
||||||
|
)
|
||||||
from minenasai.core.database import close_database, get_database
|
from minenasai.core.database import close_database, get_database
|
||||||
|
from minenasai.core.monitoring import check_database_health, check_llm_health
|
||||||
from minenasai.gateway.protocol import (
|
from minenasai.gateway.protocol import (
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
@@ -38,12 +46,24 @@ class ConnectionManager:
|
|||||||
"""接受新连接"""
|
"""接受新连接"""
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
self.active_connections[client_id] = websocket
|
self.active_connections[client_id] = websocket
|
||||||
|
|
||||||
|
# 更新监控指标
|
||||||
|
metrics = get_metrics()
|
||||||
|
metrics.active_connections = len(self.active_connections)
|
||||||
|
if metrics.active_connections > metrics.peak_connections:
|
||||||
|
metrics.peak_connections = metrics.active_connections
|
||||||
|
|
||||||
logger.info("WebSocket 连接建立", client_id=client_id)
|
logger.info("WebSocket 连接建立", client_id=client_id)
|
||||||
|
|
||||||
def disconnect(self, client_id: str) -> None:
|
def disconnect(self, client_id: str) -> None:
|
||||||
"""断开连接"""
|
"""断开连接"""
|
||||||
if client_id in self.active_connections:
|
if client_id in self.active_connections:
|
||||||
del self.active_connections[client_id]
|
del self.active_connections[client_id]
|
||||||
|
|
||||||
|
# 更新监控指标
|
||||||
|
metrics = get_metrics()
|
||||||
|
metrics.active_connections = len(self.active_connections)
|
||||||
|
|
||||||
logger.info("WebSocket 连接断开", client_id=client_id)
|
logger.info("WebSocket 连接断开", client_id=client_id)
|
||||||
|
|
||||||
async def send_message(self, client_id: str, message: dict[str, Any]) -> None:
|
async def send_message(self, client_id: str, message: dict[str, Any]) -> None:
|
||||||
@@ -66,6 +86,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
setup_logging(settings.logging)
|
setup_logging(settings.logging)
|
||||||
|
|
||||||
|
# 设置监控
|
||||||
|
setup_monitoring(app)
|
||||||
|
|
||||||
|
# 注册健康检查
|
||||||
|
health_checker = get_health_checker()
|
||||||
|
health_checker.register("database", check_database_health)
|
||||||
|
health_checker.register("llm", check_llm_health)
|
||||||
|
|
||||||
# 初始化数据库
|
# 初始化数据库
|
||||||
db = await get_database()
|
db = await get_database()
|
||||||
logger.info("数据库初始化完成")
|
logger.info("数据库初始化完成")
|
||||||
@@ -102,10 +130,8 @@ async def root() -> dict[str, str]:
|
|||||||
return {"service": "MineNASAI Gateway", "status": "running"}
|
return {"service": "MineNASAI Gateway", "status": "running"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
# 注意: /health, /health/live, /health/ready, /metrics 端点
|
||||||
async def health() -> dict[str, str]:
|
# 由 setup_monitoring() 在 lifespan 中自动添加
|
||||||
"""健康检查"""
|
|
||||||
return {"status": "healthy"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/agents")
|
@app.get("/api/agents")
|
||||||
|
|||||||
220
tests/test_cache.py
Normal file
220
tests/test_cache.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"""缓存模块测试"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from minenasai.core.cache import (
|
||||||
|
MemoryCache,
|
||||||
|
RateLimiter,
|
||||||
|
get_rate_limiter,
|
||||||
|
get_response_cache,
|
||||||
|
make_cache_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemoryCache:
|
||||||
|
"""MemoryCache 测试"""
|
||||||
|
|
||||||
|
def test_set_and_get(self):
|
||||||
|
"""测试设置和获取"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
assert cache.get("key1") == "value1"
|
||||||
|
|
||||||
|
def test_get_nonexistent(self):
|
||||||
|
"""测试获取不存在的 key"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
assert cache.get("nonexistent") is None
|
||||||
|
|
||||||
|
def test_ttl_expiration(self):
|
||||||
|
"""测试 TTL 过期"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache(default_ttl=0.1)
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
assert cache.get("key1") == "value1"
|
||||||
|
|
||||||
|
time.sleep(0.15)
|
||||||
|
assert cache.get("key1") is None
|
||||||
|
|
||||||
|
def test_custom_ttl(self):
|
||||||
|
"""测试自定义 TTL"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache(default_ttl=10.0)
|
||||||
|
|
||||||
|
cache.set("key1", "value1", ttl=0.1)
|
||||||
|
|
||||||
|
time.sleep(0.15)
|
||||||
|
assert cache.get("key1") is None
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
"""测试删除"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
assert cache.delete("key1") is True
|
||||||
|
assert cache.get("key1") is None
|
||||||
|
assert cache.delete("key1") is False
|
||||||
|
|
||||||
|
def test_clear(self):
|
||||||
|
"""测试清空"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
cache.set("key2", "value2")
|
||||||
|
|
||||||
|
count = cache.clear()
|
||||||
|
assert count == 2
|
||||||
|
assert cache.get("key1") is None
|
||||||
|
assert cache.get("key2") is None
|
||||||
|
|
||||||
|
def test_exists(self):
|
||||||
|
"""测试存在检查"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
assert cache.exists("key1") is True
|
||||||
|
assert cache.exists("key2") is False
|
||||||
|
|
||||||
|
def test_max_size_eviction(self):
|
||||||
|
"""测试最大容量淘汰"""
|
||||||
|
cache: MemoryCache[int] = MemoryCache(max_size=5)
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
cache.set(f"key{i}", i)
|
||||||
|
|
||||||
|
# 应该只保留部分
|
||||||
|
assert len(cache._cache) <= 5
|
||||||
|
|
||||||
|
def test_hit_tracking(self):
|
||||||
|
"""测试命中跟踪"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache()
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
|
||||||
|
cache.get("key1")
|
||||||
|
cache.get("key1")
|
||||||
|
cache.get("nonexistent")
|
||||||
|
|
||||||
|
stats = cache.get_stats()
|
||||||
|
assert stats["hits"] == 2
|
||||||
|
assert stats["misses"] == 1
|
||||||
|
|
||||||
|
def test_get_stats(self):
|
||||||
|
"""测试获取统计"""
|
||||||
|
cache: MemoryCache[str] = MemoryCache(max_size=100, default_ttl=60.0)
|
||||||
|
|
||||||
|
cache.set("key1", "value1")
|
||||||
|
cache.get("key1")
|
||||||
|
|
||||||
|
stats = cache.get_stats()
|
||||||
|
|
||||||
|
assert stats["size"] == 1
|
||||||
|
assert stats["max_size"] == 100
|
||||||
|
assert stats["default_ttl"] == 60.0
|
||||||
|
assert "hit_rate" in stats
|
||||||
|
|
||||||
|
|
||||||
|
class TestRateLimiter:
|
||||||
|
"""RateLimiter 测试"""
|
||||||
|
|
||||||
|
def test_acquire_within_limit(self):
|
||||||
|
"""测试在限制内获取"""
|
||||||
|
limiter = RateLimiter(rate=10.0, burst=5)
|
||||||
|
|
||||||
|
# 可以获取 burst 数量的令牌
|
||||||
|
for _ in range(5):
|
||||||
|
assert limiter.acquire() is True
|
||||||
|
|
||||||
|
def test_acquire_exceeds_limit(self):
|
||||||
|
"""测试超出限制"""
|
||||||
|
limiter = RateLimiter(rate=10.0, burst=2)
|
||||||
|
|
||||||
|
assert limiter.acquire() is True
|
||||||
|
assert limiter.acquire() is True
|
||||||
|
assert limiter.acquire() is False
|
||||||
|
|
||||||
|
def test_token_refill(self):
|
||||||
|
"""测试令牌补充"""
|
||||||
|
limiter = RateLimiter(rate=100.0, burst=2)
|
||||||
|
|
||||||
|
# 消耗所有令牌
|
||||||
|
limiter.acquire()
|
||||||
|
limiter.acquire()
|
||||||
|
assert limiter.acquire() is False
|
||||||
|
|
||||||
|
# 等待补充
|
||||||
|
time.sleep(0.05)
|
||||||
|
assert limiter.acquire() is True
|
||||||
|
|
||||||
|
def test_available_tokens(self):
|
||||||
|
"""测试可用令牌数"""
|
||||||
|
limiter = RateLimiter(rate=10.0, burst=5)
|
||||||
|
|
||||||
|
assert limiter.available_tokens == pytest.approx(5.0, abs=0.1)
|
||||||
|
|
||||||
|
limiter.acquire(2)
|
||||||
|
assert limiter.available_tokens == pytest.approx(3.0, abs=0.1)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_wait(self):
|
||||||
|
"""测试等待获取"""
|
||||||
|
limiter = RateLimiter(rate=100.0, burst=1)
|
||||||
|
|
||||||
|
limiter.acquire()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
await limiter.wait()
|
||||||
|
elapsed = time.time() - start
|
||||||
|
|
||||||
|
# 应该等待了一小段时间
|
||||||
|
assert elapsed > 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCacheKey:
|
||||||
|
"""make_cache_key 测试"""
|
||||||
|
|
||||||
|
def test_same_args_same_key(self):
|
||||||
|
"""测试相同参数生成相同 key"""
|
||||||
|
key1 = make_cache_key("a", "b", c=1)
|
||||||
|
key2 = make_cache_key("a", "b", c=1)
|
||||||
|
|
||||||
|
assert key1 == key2
|
||||||
|
|
||||||
|
def test_different_args_different_key(self):
|
||||||
|
"""测试不同参数生成不同 key"""
|
||||||
|
key1 = make_cache_key("a", "b")
|
||||||
|
key2 = make_cache_key("a", "c")
|
||||||
|
|
||||||
|
assert key1 != key2
|
||||||
|
|
||||||
|
def test_kwargs_order_independent(self):
|
||||||
|
"""测试 kwargs 顺序无关"""
|
||||||
|
key1 = make_cache_key(a=1, b=2)
|
||||||
|
key2 = make_cache_key(b=2, a=1)
|
||||||
|
|
||||||
|
assert key1 == key2
|
||||||
|
|
||||||
|
|
||||||
|
class TestGlobalInstances:
|
||||||
|
"""全局实例测试"""
|
||||||
|
|
||||||
|
def test_get_response_cache(self):
|
||||||
|
"""测试获取响应缓存"""
|
||||||
|
cache = get_response_cache()
|
||||||
|
assert isinstance(cache, MemoryCache)
|
||||||
|
|
||||||
|
def test_get_rate_limiter(self):
|
||||||
|
"""测试获取速率限制器"""
|
||||||
|
limiter = get_rate_limiter("test", rate=10.0, burst=20)
|
||||||
|
assert isinstance(limiter, RateLimiter)
|
||||||
|
|
||||||
|
def test_get_rate_limiter_reuse(self):
|
||||||
|
"""测试速率限制器复用"""
|
||||||
|
limiter1 = get_rate_limiter("shared")
|
||||||
|
limiter2 = get_rate_limiter("shared")
|
||||||
|
|
||||||
|
assert limiter1 is limiter2
|
||||||
228
tests/test_monitoring.py
Normal file
228
tests/test_monitoring.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"""监控模块测试"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from minenasai.core.monitoring import (
|
||||||
|
ComponentHealth,
|
||||||
|
HealthChecker,
|
||||||
|
HealthStatus,
|
||||||
|
SystemMetrics,
|
||||||
|
get_health_checker,
|
||||||
|
get_metrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthStatus:
|
||||||
|
"""HealthStatus 测试"""
|
||||||
|
|
||||||
|
def test_health_status_values(self):
|
||||||
|
"""测试健康状态值"""
|
||||||
|
assert HealthStatus.HEALTHY.value == "healthy"
|
||||||
|
assert HealthStatus.DEGRADED.value == "degraded"
|
||||||
|
assert HealthStatus.UNHEALTHY.value == "unhealthy"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemMetrics:
|
||||||
|
"""SystemMetrics 测试"""
|
||||||
|
|
||||||
|
def test_initial_metrics(self):
|
||||||
|
"""测试初始指标"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
|
||||||
|
assert metrics.total_requests == 0
|
||||||
|
assert metrics.successful_requests == 0
|
||||||
|
assert metrics.failed_requests == 0
|
||||||
|
assert metrics.active_connections == 0
|
||||||
|
|
||||||
|
def test_record_request(self):
|
||||||
|
"""测试记录请求"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
|
||||||
|
metrics.record_request(100.0, success=True)
|
||||||
|
metrics.record_request(200.0, success=True)
|
||||||
|
metrics.record_request(50.0, success=False)
|
||||||
|
|
||||||
|
assert metrics.total_requests == 3
|
||||||
|
assert metrics.successful_requests == 2
|
||||||
|
assert metrics.failed_requests == 1
|
||||||
|
assert metrics.min_response_time_ms == 50.0
|
||||||
|
assert metrics.max_response_time_ms == 200.0
|
||||||
|
|
||||||
|
def test_avg_response_time(self):
|
||||||
|
"""测试平均响应时间"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
|
||||||
|
metrics.record_request(100.0, success=True)
|
||||||
|
metrics.record_request(200.0, success=True)
|
||||||
|
|
||||||
|
assert metrics.avg_response_time_ms == 150.0
|
||||||
|
|
||||||
|
def test_success_rate(self):
|
||||||
|
"""测试成功率"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
|
||||||
|
metrics.record_request(100.0, success=True)
|
||||||
|
metrics.record_request(100.0, success=True)
|
||||||
|
metrics.record_request(100.0, success=False)
|
||||||
|
|
||||||
|
assert metrics.success_rate == pytest.approx(2 / 3)
|
||||||
|
|
||||||
|
def test_record_error(self):
|
||||||
|
"""测试记录错误"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
|
||||||
|
metrics.record_error("ValueError")
|
||||||
|
metrics.record_error("ValueError")
|
||||||
|
metrics.record_error("TypeError")
|
||||||
|
|
||||||
|
assert metrics.errors_by_type["ValueError"] == 2
|
||||||
|
assert metrics.errors_by_type["TypeError"] == 1
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
"""测试转换为字典"""
|
||||||
|
metrics = SystemMetrics()
|
||||||
|
metrics.record_request(100.0, success=True)
|
||||||
|
|
||||||
|
result = metrics.to_dict()
|
||||||
|
|
||||||
|
assert "requests" in result
|
||||||
|
assert "response_time_ms" in result
|
||||||
|
assert "connections" in result
|
||||||
|
assert "uptime_seconds" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestComponentHealth:
|
||||||
|
"""ComponentHealth 测试"""
|
||||||
|
|
||||||
|
def test_component_health(self):
|
||||||
|
"""测试组件健康状态"""
|
||||||
|
health = ComponentHealth(
|
||||||
|
name="test",
|
||||||
|
status=HealthStatus.HEALTHY,
|
||||||
|
message="OK",
|
||||||
|
latency_ms=10.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert health.name == "test"
|
||||||
|
assert health.status == HealthStatus.HEALTHY
|
||||||
|
assert health.message == "OK"
|
||||||
|
assert health.latency_ms == 10.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthChecker:
|
||||||
|
"""HealthChecker 测试"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""初始化"""
|
||||||
|
self.checker = HealthChecker()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_and_check(self):
|
||||||
|
"""测试注册和检查"""
|
||||||
|
|
||||||
|
async def check_ok() -> ComponentHealth:
|
||||||
|
return ComponentHealth(
|
||||||
|
name="test",
|
||||||
|
status=HealthStatus.HEALTHY,
|
||||||
|
message="OK",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checker.register("test", check_ok)
|
||||||
|
result = await self.checker.check_component("test")
|
||||||
|
|
||||||
|
assert result.status == HealthStatus.HEALTHY
|
||||||
|
assert result.latency_ms > 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_unknown_component(self):
|
||||||
|
"""测试检查未知组件"""
|
||||||
|
result = await self.checker.check_component("unknown")
|
||||||
|
|
||||||
|
assert result.status == HealthStatus.UNHEALTHY
|
||||||
|
assert "未知组件" in result.message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_all(self):
|
||||||
|
"""测试检查所有组件"""
|
||||||
|
|
||||||
|
async def check_a() -> ComponentHealth:
|
||||||
|
return ComponentHealth(name="a", status=HealthStatus.HEALTHY)
|
||||||
|
|
||||||
|
async def check_b() -> ComponentHealth:
|
||||||
|
return ComponentHealth(name="b", status=HealthStatus.HEALTHY)
|
||||||
|
|
||||||
|
self.checker.register("a", check_a)
|
||||||
|
self.checker.register("b", check_b)
|
||||||
|
|
||||||
|
results = await self.checker.check_all()
|
||||||
|
|
||||||
|
assert len(results) == 2
|
||||||
|
assert "a" in results
|
||||||
|
assert "b" in results
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_overall_status_healthy(self):
|
||||||
|
"""测试总体状态 - 健康"""
|
||||||
|
|
||||||
|
async def check_ok() -> ComponentHealth:
|
||||||
|
return ComponentHealth(name="test", status=HealthStatus.HEALTHY)
|
||||||
|
|
||||||
|
self.checker.register("test", check_ok)
|
||||||
|
await self.checker.check_all()
|
||||||
|
|
||||||
|
assert self.checker.get_overall_status() == HealthStatus.HEALTHY
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_overall_status_degraded(self):
|
||||||
|
"""测试总体状态 - 降级"""
|
||||||
|
|
||||||
|
async def check_degraded() -> ComponentHealth:
|
||||||
|
return ComponentHealth(name="test", status=HealthStatus.DEGRADED)
|
||||||
|
|
||||||
|
self.checker.register("test", check_degraded)
|
||||||
|
await self.checker.check_all()
|
||||||
|
|
||||||
|
assert self.checker.get_overall_status() == HealthStatus.DEGRADED
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_overall_status_unhealthy(self):
|
||||||
|
"""测试总体状态 - 不健康"""
|
||||||
|
|
||||||
|
async def check_unhealthy() -> ComponentHealth:
|
||||||
|
return ComponentHealth(name="test", status=HealthStatus.UNHEALTHY)
|
||||||
|
|
||||||
|
self.checker.register("test", check_unhealthy)
|
||||||
|
await self.checker.check_all()
|
||||||
|
|
||||||
|
assert self.checker.get_overall_status() == HealthStatus.UNHEALTHY
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_timeout(self):
|
||||||
|
"""测试检查超时"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def slow_check() -> ComponentHealth:
|
||||||
|
await asyncio.sleep(10) # 超过5秒超时
|
||||||
|
return ComponentHealth(name="slow", status=HealthStatus.HEALTHY)
|
||||||
|
|
||||||
|
self.checker.register("slow", slow_check)
|
||||||
|
result = await self.checker.check_component("slow")
|
||||||
|
|
||||||
|
assert result.status == HealthStatus.UNHEALTHY
|
||||||
|
assert "超时" in result.message
|
||||||
|
|
||||||
|
|
||||||
|
class TestGlobalInstances:
|
||||||
|
"""全局实例测试"""
|
||||||
|
|
||||||
|
def test_get_metrics(self):
|
||||||
|
"""测试获取全局指标"""
|
||||||
|
metrics = get_metrics()
|
||||||
|
assert isinstance(metrics, SystemMetrics)
|
||||||
|
|
||||||
|
def test_get_health_checker(self):
|
||||||
|
"""测试获取健康检查器"""
|
||||||
|
checker = get_health_checker()
|
||||||
|
assert isinstance(checker, HealthChecker)
|
||||||
68
进度.md
68
进度.md
@@ -1,8 +1,8 @@
|
|||||||
# MineNASAI 项目进度跟踪
|
# MineNASAI 项目进度跟踪
|
||||||
|
|
||||||
**更新日期**: 2026-02-04
|
**更新日期**: 2026-02-05
|
||||||
**当前阶段**: Phase 3 完成,准备 Phase 4 开发
|
**当前阶段**: Phase 5 完成,项目基本完成
|
||||||
**整体进度**: 80% (4/5 Phase 完成)
|
**整体进度**: 100% (5/5 Phase 完成)
|
||||||
**技术方案**: Web TUI集成(方案C)+ 多 LLM 支持
|
**技术方案**: Web TUI集成(方案C)+ 多 LLM 支持
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
| Phase 1 | 核心框架(MVP) | ✅ 已完成 | 7/7 | 14-21天 | 1天 |
|
| Phase 1 | 核心框架(MVP) | ✅ 已完成 | 7/7 | 14-21天 | 1天 |
|
||||||
| Phase 2 | Agent与工具系统 | ✅ 已完成 | 6/6 | 14-21天 | 1天 |
|
| Phase 2 | Agent与工具系统 | ✅ 已完成 | 6/6 | 14-21天 | 1天 |
|
||||||
| Phase 3 | Web TUI与Claude集成 | ✅ 已完成 | 5/5 | 7-10天 | 1天 |
|
| Phase 3 | Web TUI与Claude集成 | ✅ 已完成 | 5/5 | 7-10天 | 1天 |
|
||||||
| Phase 4 | 高级特性 | ⏸️ 未开始 | 0/2 | 10-14天 | - |
|
| Phase 4 | 高级特性 | ✅ 已完成 | 2/2 | 10-14天 | 1天 |
|
||||||
| Phase 5 | 生产就绪 | ⏸️ 未开始 | 0/3 | 7-10天 | - |
|
| Phase 5 | 生产就绪 | ✅ 已完成 | 4/4 | 7-10天 | 1天 |
|
||||||
|
|
||||||
**图例**: ⏸️ 未开始 | 🔄 进行中 | ✅ 已完成 | ⚠️ 阻塞
|
**图例**: ⏸️ 未开始 | 🔄 进行中 | ✅ 已完成 | ⚠️ 阻塞
|
||||||
|
|
||||||
@@ -226,9 +226,63 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4-5 (详细任务待展开)
|
## Phase 4: 高级特性 (100%) ✅
|
||||||
|
|
||||||
Phase 4-5 的详细任务清单参见 [开发步骤.md](./开发步骤.md)
|
### 4.1 定时任务调度 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 功能:
|
||||||
|
- [x] Cron 表达式解析 (CronParser)
|
||||||
|
- [x] 预定义表达式支持 (@daily, @hourly 等)
|
||||||
|
- [x] 任务调度器 (CronScheduler)
|
||||||
|
- [x] 异步任务执行
|
||||||
|
|
||||||
|
### 4.2 权限控制与工具管理 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 功能:
|
||||||
|
- [x] 危险级别定义 (DangerLevel)
|
||||||
|
- [x] 权限管理器 (PermissionManager)
|
||||||
|
- [x] 工具注册中心 (ToolRegistry)
|
||||||
|
- [x] @tool 装饰器
|
||||||
|
- [x] 确认机制 (ConfirmationRequest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: 生产就绪 (100%) ✅
|
||||||
|
|
||||||
|
### 5.1 错误处理与监控 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 功能:
|
||||||
|
- [x] 全局异常处理中间件
|
||||||
|
- [x] 健康检查端点 (/health, /health/live, /health/ready)
|
||||||
|
- [x] 监控指标收集 (/metrics)
|
||||||
|
- [x] 组件健康检查 (数据库、LLM)
|
||||||
|
|
||||||
|
### 5.2 性能优化 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 功能:
|
||||||
|
- [x] 内存缓存 (MemoryCache + TTL + LRU)
|
||||||
|
- [x] 速率限制器 (RateLimiter - 令牌桶算法)
|
||||||
|
- [x] 缓存 Key 生成器
|
||||||
|
|
||||||
|
### 5.3 Docker 部署 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 文件:
|
||||||
|
- [x] Dockerfile (多阶段构建)
|
||||||
|
- [x] docker-compose.yml (Gateway + WebTUI + Redis)
|
||||||
|
- [x] .dockerignore
|
||||||
|
|
||||||
|
### 5.4 测试与文档 ✅
|
||||||
|
- 状态: ✅ 已完成
|
||||||
|
- 实际完成: 2026-02-05
|
||||||
|
- 成果:
|
||||||
|
- [x] 131 个测试全部通过
|
||||||
|
- [x] README.md 完善
|
||||||
|
- [x] API 端点文档
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user